JuliaIO / TranscodingStreams.jl

Simple, consistent interfaces for any codec.
https://juliaio.github.io/TranscodingStreams.jl/
Other
85 stars 28 forks source link

Add basic Supposition.jl tests #190

Closed nhz2 closed 5 months ago

nhz2 commented 5 months ago

These tests found a bug with reading data from nested streams if stop_on_end=true is set somewhere in the stack.

To run the fuzz.jl tests run:

cd fuzz
julia --project -e 'using Pkg; pkg"dev .."; pkg"instantiate";'
julia --project fuzz.jl

@Seelengrab thank you for making Supposition.jl, this is my first attempt at using it. I think I'm using the @composed macro correctly, but is there something obvious I missed to make these tests nicer?

Currently, I get the following messages showing counterexample nested streams:

Events occured: 4
    wrapping IOBuffer with:
        nothing
    encoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 1, stop_on_end = true, sharedbuf = false)
    noop
        (bufsize = 1, stop_on_end = false)
┌ Error: Property errored!
│   Description = "read_data"
│   Example = (w = var"#read_wrapper#10"{Vector{Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}}}(Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}[var"#noop_wrapper#7"{@NamedTuple{bufsize::Int64, stop_on_end::Bool}}((bufsize = 1, stop_on_end = false)), var"#r_enc_dec_wrapper#8"{@NamedTuple{bufsize::Int64, stop_on_end::Bool, sharedbuf::Bool}, @NamedTuple{bufsize::Int64, stop_on_end::Bool, sharedbuf::Bool}}((bufsize = 1, stop_on_end = false, sharedbuf = false), (bufsize = 1, stop_on_end = true, sharedbuf = false))]), data = UInt8[])
│   exception =
│    ArgumentError: cannot change the mode from stop to read
│    Stacktrace:
│      [1] changemode!(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}, newmode::Symbol)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:788
│      [2] fillbuffer(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:597
│      [3] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/stream.jl:596 [inlined]
│      [4] fillbuffer(stream::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/noop.jl:179
│      [5] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/noop.jl:173 [inlined]
│      [6] sloweof(stream::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}})
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:210
│      [7] eof(stream::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}})
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:201
│      [8] read_data(w::var"#read_wrapper#10"{Vector{Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}}}, data::Vector{UInt8})
│        @ Main ~/juliadev/TranscodingStreams/fuzz/fuzz.jl:66
│      [9] var"##read_data__run#239"(237::Supposition.TestCase{Xoshiro})
│        @ Main ~/.julia/packages/Supposition/KpGkN/src/api.jl:240
│     [10] macro expansion
│        @ ~/.julia/packages/Supposition/KpGkN/src/teststate.jl:38 [inlined]
└ @ Supposition ~/.julia/packages/Supposition/KpGkN/src/testset.jl:287
Test Summary: | Error  Total   Time
read_data     |     1      1  14.0s
Events occured: 6
    wrapping IOBuffer with:
        nothing
    encoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 1, stop_on_end = true, sharedbuf = false)
    noop
        (bufsize = 1, stop_on_end = false)
    encoder
        (bufsize = 336, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 782, stop_on_end = true)
┌ Error: Property errored!
│   Description = "read_byte_data"
│   Example = (w = var"#read_wrapper#10"{Vector{Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}}}(Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}[var"#r_enc_dec_wrapper#8"{@NamedTuple{bufsize::Int64, stop_on_end::Bool, sharedbuf::Bool}, @NamedTuple{bufsize::Int64, stop_on_end::Bool}}((bufsize = 336, stop_on_end = false, sharedbuf = false), (bufsize = 782, stop_on_end = true)), var"#noop_wrapper#7"{@NamedTuple{bufsize::Int64, stop_on_end::Bool}}((bufsize = 1, stop_on_end = false)), var"#r_enc_dec_wrapper#8"{@NamedTuple{bufsize::Int64, stop_on_end::Bool, sharedbuf::Bool}, @NamedTuple{bufsize::Int64, stop_on_end::Bool, sharedbuf::Bool}}((bufsize = 1, stop_on_end = false, sharedbuf = false), (bufsize = 1, stop_on_end = true, sharedbuf = false))]), data = UInt8[])
│   exception =
│    ArgumentError: cannot change the mode from stop to read
│    Stacktrace:
│      [1] changemode!(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}, newmode::Symbol)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:788
│      [2] fillbuffer(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:597
│      [3] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/stream.jl:596 [inlined]
│      [4] fillbuffer(stream::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/noop.jl:179
│      [5] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/noop.jl:173 [inlined]
│      [6] sloweof(stream::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}})
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:210
│      [7] eof
│        @ ~/juliadev/TranscodingStreams/src/stream.jl:201 [inlined]
│      [8] readdata!(input::NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}, output::TranscodingStreams.Buffer)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:707
│      [9] fillbuffer(stream::TranscodingStream{DoubleFrameEncoder, NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:609
│     [10] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/stream.jl:596 [inlined]
│     [11] readdata!(input::TranscodingStream{DoubleFrameEncoder, NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}}, output::TranscodingStreams.Buffer)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:703
│     [12] fillbuffer(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}}}; eager::Bool)
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:609
│     [13] fillbuffer
│        @ ~/juliadev/TranscodingStreams/src/stream.jl:596 [inlined]
│     [14] sloweof(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}}})
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:210
│     [15] eof(stream::TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, NoopStream{TranscodingStream{DoubleFrameDecoder, TranscodingStream{DoubleFrameEncoder, IOBuffer}}}}})
│        @ TranscodingStreams ~/juliadev/TranscodingStreams/src/stream.jl:201
│     [16] read_byte_data(w::var"#read_wrapper#10"{Vector{Union{var"#noop_wrapper#7", var"#r_enc_dec_wrapper#8"}}}, data::Vector{UInt8})
│        @ Main ~/juliadev/TranscodingStreams/fuzz/fuzz.jl:73
│     [17] var"##read_byte_data__run#262"(260::Supposition.TestCase{Xoshiro})
│        @ Main ~/.julia/packages/Supposition/KpGkN/src/api.jl:240
│     [18] macro expansion
│        @ ~/.julia/packages/Supposition/KpGkN/src/teststate.jl:38 [inlined]
└ @ Supposition ~/.julia/packages/Supposition/KpGkN/src/testset.jl:287
Test Summary:  | Error  Total  Time
read_byte_data |     1      1  9.6s
Test Summary: | Pass  Total     Time
write_data    |    1      1  1m20.2s
Test Summary:   | Pass  Total   Time
write_byte_data |    1      1  35.2s
Seelengrab commented 5 months ago

thank you for making Supposition.jl, this is my first attempt at using it.

No, thank YOU for using the package! Really great to see that it helped you find a bug, that's awesome! If you want to try your hand at some more complex/higher-level encoder/decoder properties, this article by the authors of Hypothesis might be insightful.

I think I'm using the @composed macro correctly, but is there something obvious I missed to make these tests nicer?

Hmm.. nicer in what sense exactly? Do you mean as a nicer printed list of "these steps are needed to wrap an IOBuffer for it to not work"?

The printing of the results is really ugly and I do want to make that prettier (see https://github.com/Seelengrab/Supposition.jl/discussions/23 for the basic target I'm going to aim for), but that's currently a bit blocked by StyledStrings.jl not yet being released. I could expand the printing minimally for now, so that it's not rendered as one big blob and rather have each argument on its own line?

There's also https://github.com/Seelengrab/Supposition.jl/discussions/26, which I think would be a bit of a better fit here; basically everything you have here is an application of either a noop-operation, an enc-dec-operation or a dec-enc-opration on an IO, right? If so, I'd just use those as the operations on an IO directly, similar to how it's done in Stateful testing. Once this interface exists, it'd also get custom printing.

The invariant you'd then test for after applying all "operations" is what you have right now in e.g. read_byte_data. Lacking the interface I linked above, perhaps something like this is suitable for now:

using Supposition, TranscodingStreams

include("../test/codecdoubleframe.jl")

const TS_kwarg = @composed (
    bufsize=Data.Integers(1, 2^10),
    stop_on_end=Data.Booleans(),
    sharedbuf=Data.Booleans(),
) -> (
    if sharedbuf
        # default sharedbuf
        (;bufsize, stop_on_end)
    else
        # sharedbuf = false
        (;bufsize, stop_on_end, sharedbuf)
    end
)

function noop_wrapper(io, kw)
    event!("noop", kw)
    NoopStream(io; kw...)
end

function r_enc_dec_wrapper(io, kw_enc, kw_dec)
    event!("encoder", kw_enc)
    event!("decoder", kw_dec)
    DoubleFrameDecoderStream(DoubleFrameEncoderStream(io; kw_enc...); kw_dec...)
end

const datas = Data.Vectors(Data.Integers{UInt8}())
const read_ops = Data.Vectors(Data.SampledFrom((noop_wrapper, r_enc_dec_wrapper)); max_size=5)

function prepare_kws(ops)
    res = []
    sizehint!(res, length(ops))

    for op in ops
        kw = Data.produce!(TS_kwarg)
        if op === noop_wrapper
            push!(res, (op, (kw,)))
        else
            kw2 = Data.produce!(TS_kwarg)
            push!(res, (op, (kw,kw2)))
        end
    end

    res
end

function prepare_stream(ops, data::Vector{UInt8})::IO
    stream::IO = IOBuffer(data)

    foldl(ops; init=stream) do stream, (op, args)
        op(stream, args...)
    end
end

@time @check db=false function read_data(
        kws=map(prepare_kws, read_ops),
        data=datas)
    stream = prepare_stream(kws, data)
    for i in eachindex(data)
        read(stream, UInt8) == data[i] || return false
    end
    eof(stream)
end;

which is reasonably fast, shrinks well and is still pretty readable when it comes to the example:

Events occured: 4
    encoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 1, stop_on_end = true, sharedbuf = false)
    encoder
        (bufsize = 1, stop_on_end = false)
    decoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
┌ Error: Property errored!
│   Description = "read_data"
│   Example = (kws = Any[(r_enc_dec_wrapper, ((bufsize = 1, stop_on_end = false, sharedbuf = false), (bufsize = 1, stop_on_end = true, sharedbuf = false))), (r_enc_dec_wrapper, ((bufsize = 1, stop_on_end = false), (bufsize = 1, stop_on_end = false, sharedbuf = false)))], data = UInt8[])
│   exception =
│    ArgumentError: cannot change the mode from stop to read
│    Stacktrace:
[...]
Test Summary: | Error  Total  Time
read_data     |     1      1  2.9s
  3.037167 seconds (724.76 k allocations: 84.920 MiB, 0.45% gc time, 6.48% compilation time: 55% of which was recompilation)

(I get varying runtimes of the same order for your original fuzzing code on my machine)

The core difference is that you don't have to generate functions that do the wrapping, but instead have a function that does the wrapping and just call it during generation :) That'll make it much easier to debug, since you can then see in the output which operations led to the result, without having to explicitly call event!.

Seelengrab commented 5 months ago

With a small patch to Supposition.jl, the final output could be made to look like this (omitting the stacktraces here for brevity) for your fuzzer:

Events occured: 4
    wrapping IOBuffer with:
        nothing
    encoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 1, stop_on_end = true, sharedbuf = false)
    noop
        (bufsize = 1, stop_on_end = false, sharedbuf = true)
┌ Error: Property errored!
│   Description = "read_data"
│   w = (::var"#read_wrapper#12"{Vector{Union{var"#noop_wrapper#9", var"#r_enc_dec_wrapper#10"}}}) (generic function with 1 method)
│   data = UInt8[]
│   exception =
│    ArgumentError: cannot change the mode from stop to read
│    Stacktrace:

and like this for the one I posted above:

Events occured: 4
    encoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
    decoder
        (bufsize = 1, stop_on_end = true, sharedbuf = false)
    encoder
        (bufsize = 1, stop_on_end = false)
    decoder
        (bufsize = 1, stop_on_end = false, sharedbuf = false)
┌ Error: Property errored!
│   Description = "read_data"
│   kws =
│    2-element Vector{Any}:
│     (r_enc_dec_wrapper, ((bufsize = 1, stop_on_end = false, sharedbuf = false), (bufsize = 1, stop_on_end = true, sharedbuf = false)))
│     (r_enc_dec_wrapper, ((bufsize = 1, stop_on_end = false), (bufsize = 1, stop_on_end = false, sharedbuf = false)))
│   data = UInt8[]
│   exception =
│    ArgumentError: cannot change the mode from stop to read
│    Stacktrace:
nhz2 commented 5 months ago

Thanks for the suggestions. Using produce! to create a list of operations has much better printing than what I was doing before.