Skip to content

Notes on porting buf-list's tests from proptest to hegel #148

@sunshowers

Description

@sunshowers

Hi! As promised on Hacker News I've ported over some property-based tests in my buf-list crate from proptest to hegel. Wanted to provide an experience report.

Background

The buf-list crate provides a segmented list of byte buffers. One of buf-list's features is a cursor type which provides io::Read etc on a BufList, just like std::io::Cursor does on contiguous sequences of bytes. The cursor type is very fiddly to get right, with binary searches and +/- 1 offsets scattered all over the place. Luckily it is extremely amenable to model-based testing because we have a very clear model: std::io::Cursor!

The PBT does:

  • Create a segmented list of bytes with various sizes and contents, and wrap that in a buf_list::Cursor. This forms the SUT.
  • Glue the bytes together into a contiguous sequence and wrap that in std::io::Cursor. This forms the oracle.
  • Generate random sequences of operations. This is where the PBT framework (proptest/hegel) is really supposed to help.
  • Run the operations by the SUT and the oracle; ensure behavioral equivalence (the results of each operation are the same) and that invariants aren't violated.

This is a pretty normal way to test this kind of interface. The PBT was also very high value at the time I was writing that code; it found no fewer than six bugs. Neither of these would be a surprise to y'all.

With proptest, the PBT looks like this. This is overall not the worst thing in the world, but the one issue is how proptest::sample::Index has to be used. Because the operations to be generated are data-dependent on the buf-list size, in order to avoid a flat-map one has to use Index fields everywhere. This kind of sucks, and I was hoping that Hegel's imperative model would do a much better job than that.

Porting the PBT

To port this test over to Hegel, my workflow was installing the Claude skill, then telling it:

/hegel port over the existing proptests to Hegel. Note the uses of proptest::sample::Index which can be replaced with imperative draws

Overall, it worked great! Took just a few minutes to do and the result was passing PBTs. This is the final result. I manually introduced bugs in a few spots and Hegel both caught them and provided a pretty good shrunk case.

One note was that Claude generated an integer and then mapped that to a variant. I changed it to writing out a discriminant enum with DefaultGenerator in sunshowers-code/buf-list@a655aa9. Because the values associated with the enum variants are data-dependent, I don't believe we can derive DefaultGenerator on CursorOp directly. In cases like mine, preferring a discriminant enum over an integer is probably preferable -- might be worth updating the Claude skill to tell it to do that.

Anyway, thanks again for all your work! This is really really exciting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions