Marionette
Marionette is a deterministic simulation testing library for Zig code written against std.Io.
Repo: https://github.com/sb2bg/marionette
Docs: https://sb2bg.github.io/marionette/
Existing code using std.Io can run under Marionette today with no changes.
In tests, your code keeps accepting std.Io, but the backend becomes Marionette’s single-threaded deterministic simulator. Clock, randomness, disk, tasks, futex waits, and a growing std.Io.net subset all run from one seed, so every failure replays identically. Long term the goal is to be the deterministic std.Io for Zig, since the new Io interface makes FoundationDB style simulation testing something a library can offer instead of something you architect your whole system around.
We already run real unmodified third-party code:
xit-vcs/xitdbruns through the simulatedstd.Io.Filebackend with crash and torn-write faults, plus a fuzzer that shrinks failures to a maintainer-readable operation sequence.g41797/mailboxruns on the cooperative scheduler, exercising timed receive, send/wake, and same-deadline timeout ordering.lalinsky/dusty’s realServer.listenaccept loop runs as a simulated task over simulatedstd.Io.netstreams, including cooperative-cancellation shutdown witherror.Canceledlanding in its accept park.
None of these were patched because std.Io allows us to change backends with zero code changes. It also runs an unmodified std.http.Client against a simulated server (http://localhost:<port>/ included, with lookup resolving address literals deterministically), with identical replay.
Test code gets a second surface called control for injecting faults:
try control.disk.crash();
try control.disk.corruptSector(path, offset);
try control.network.partition(&side_a, &side_b);
try control.network.setLossiness(.{ .drop_rate = .percent(20) });
try control.network.heal();
Beyond disk and network there’s deterministic allocation-fault injection (Env.allocator() with seeded fail-after and quota faults via control.allocation) and FoundationDB-style env.buggify hooks. Every run emits a structured trace, and failed runs print it with the seed.
Has it found anything? Yes. Running a small external KV store through an exact model oracle turned up three real bugs, two of them with no fault injection at all. It has found a B+tree leaf insert that overwrote an existing entry’s payload (shrunk to a four-put repro), delete rebalancing that broke separator invariants and made unrelated keys unreachable, and recovery that trusted a corrupt root page.
Details are in FOUND_BUGS.md, which also records the negative and boundary results. Marionette is alpha with no API stability before 1.0, and it requires Zig 0.16.
Boundaries: this is cooperative std.Io concurrency, not preemptive thread or memory-model testing. The simulated std.Io surface is a subset of the full interface, and code that steps outside it won’t run under simulation yet. Windows x86_64 fibers are deliberately disabled until the Win64 entry ABI has execution coverage.
zig fetch --save https://github.com/sb2bg/marionette/archive/refs/tags/v0.5.0.tar.gz
The examples/ directory is the best starting point. If you maintain a library that takes std.Io and you want it run under simulation, I’d love to hear from you. The external validation targets (xitdb, mailbox, dusty, ochi) have been the best forcing function for the simulator so far, and I’m looking for the next one.