-
Notifications
You must be signed in to change notification settings - Fork 10
Description
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.