Skip to content

Overview

Marionette is a deterministic I/O and simulation testing library for Zig. The long-term target is to become the deterministic std.Io implementation for Zig.

The core promise: write production-shaped code against std.Io, Recorder, and application-owned protocol seams. Node-scoped std.Io.net runs literal socket-facing code against host I/O in production and Marionette in simulation. Experimental typed endpoints instead model the message-level behavior behind an application-owned transport seam; they preserve protocol/state-machine code, not the production codec or transport implementation.

Marionette is still experimental, but the core replay loop is real: seeded randomness, simulated time, trace logging, twice-and-compare replay, trace summaries, named post-scenario checks, deterministic disk/network authorities with replayable faults, and a deterministic std.Io backend for the current file and network subset. The cooperative scheduler, timed futex waits, and narrow blocking std.Io.net stream path are implemented. Single-future Io.async / Io.concurrent / await, Io.Group, and cooperative cancellation (error.Canceled delivered at futex, sleep, and net suspension points) are scheduler-backed; queue suspension, richer reset/node-down behavior, and richer replay tooling remain planned.

For the precise correctness model, see Architecture. For the replay artifact bytes, see Trace Format. For simulation-only fault hooks, see BUGGIFY. For storage faults, see Disk Fault Model. For the long-term std.Io direction, see std.Io Direction. For a runnable client/server fault example, see Testing std.Io.net Code Deterministically.

What This Solves

Distributed systems bugs often depend on timing, ordering, and failure:

  • A timeout fires just before a response arrives.
  • A write succeeds locally but is lost before replication.
  • A node observes messages in an unlucky order.
  • A retry races with leader election.

Normal tests rarely explore those interleavings systematically. When they do find one, the failure can be hard to reproduce.

DST makes the test environment deterministic. A seed controls simulated choices. The world records a trace. When a bug appears, the seed and trace become the starting point for debugging.

What Marionette Is Not

Marionette is not:

  • A general-purpose testing framework. Use std.testing for unit tests.
  • A fuzzer for pure functions.
  • A consensus library.
  • A production runtime.
  • A syscall interception layer.
  • A tool that makes arbitrary non-deterministic code deterministic.

Users must route time, randomness, disk, and network through Marionette-owned authorities. For storage code, the preferred authority is std.Io; Env is the harness-owned bundle that supplies that I/O backend plus recorders and remaining Marionette capabilities. That discipline is the product.

Marionette does not auto-detect whether code is running in production or simulation. The composition root passes explicit handles into the application; simulation harnesses build those handles with world.simulate.

Why Zig

Marionette fits Zig because Zig already pushes code in the right direction:

  • Explicit allocator passing.
  • No hidden runtime.
  • Comptime specialization.
  • Interface-passing by convention.
  • Systems programmers who care about reproducibility and control.

The design goal is for production builds to pay nothing for simulation code they do not use, while tests get a world that can control and replay every interesting source of non-determinism.

Audience

Marionette is aimed at people building reliability-critical systems:

  • Databases.
  • Queues.
  • Storage engines.
  • Consensus systems.
  • Replicated services.
  • Distributed schedulers.
  • Infrastructure control planes.

It is probably overkill for ordinary CRUD applications.