Testing strategy
Calimero Core is tested in layers, each trading scope for speed. Fast in-process unit tests cover application and CRDT logic; a deterministic simulator covers the sync protocol without WASM or a network; merobox workflows cover the real multi-node system end to end. This page maps the layers and gives the exact commands CI enforces, so you can reproduce the gate before opening a PR.
Use both ends of the ladder.
A green cargo test does not prove a networked flow works, and a green E2E does not localize a logic bug.
| Layer | Where | What it proves | Speed |
|---|---|---|---|
App unit tests (TestHost) |
crates/sdk/src/testing.rs |
Your app’s call / view / event / migrate logic, in isolation |
Milliseconds |
| Storage convergence | crates/storage/src/testing.rs |
A Mergeable / #[app::state] type converges under concurrent, reordered ops |
Sub-second |
| Sync simulator | crates/node/tests/sync_sim/ |
The sync protocol converges under loss, latency, reordering, and partitions | Fast, deterministic |
| Fuzzing | fuzz/ |
Parsers of inbound bytes survive hostile input | Coverage-guided, time-boxed |
| Integration / e2e (merobox) | apps/*/workflows/, workflows/ |
The real multi-node system: install, call, sync, govern, migrate | Slow (Docker) |
App unit tests with TestHost
Section titled “App unit tests with TestHost”TestHost
lets you exercise an #[app::state] type as ordinary Rust — no wasm32 build, no
node, no containers. State lives in an in-memory mock store; events and logs are
captured; the executor / context identity is configurable. You drive mutations
with call, reads with view, and inspect events() / logs(). Migrations are
supported too: TestHost::migrate, plus assert_migrate_converges and
assert_absorb_replay_converges for the determinism properties the migration
model rests on.
let mut app = TestHost::new(|| MyApp::init());app.call(|s| s.increment("a".into())).unwrap();let value = app.view(|s| s.get("a".into())).unwrap();assert_eq!(value, 1);assert_eq!(app.events().len(), 1);This is the same harness app developers use; the app-author-facing guide is at /build/testing/.
Storage convergence (property tests)
Section titled “Storage convergence (property tests)”crates/storage/src/testing.rs
provides converge / converge_app: assert in a single #[test] that a state
type converges under concurrent, reordered operations. It drives N in-memory
replicas (each with its own store and executor identity), applies the registered
ops on every replica in a per-replica shuffled order, gossips the resulting
StorageDeltas in shuffled order, and asserts every replica lands on the same
Merkle root hash.
converge_app(TeamMetricsApp::init) .replicas(3) .ops(|s| { let _ = s.record_win("liverpool".into()); }) .assert_all_replicas_equal();The deterministic sync simulator
Section titled “The deterministic sync simulator”crates/node/tests/sync_sim/
is a discrete-event framework for the sync protocol. It reuses the real
calimero-storage Index / Interface::apply_action and Merkle tree, the real
hash computation, and the real select_protocol() — so hash propagation and
protocol selection behave identically to production — while replacing WASM
execution, network I/O, time, and concurrency with a NetworkRouter (with fault
injection), a discrete SimClock, seeded RNG, and sequential event processing.
Seeds make every failure reproducible.
let mut rt = SimRuntime::with_seed(42);Read crates/node/tests/sync_sim/AGENT_GUIDE.md before touching the framework.
The framework itself lives under sync_sim/ (change with review); add new
protocol-behavior tests under sync_scenarios/, and compliance tests under
sync_compliance/.
The neighbouring binaries in crates/node/tests/ (sync_scenarios, network_simulation, dag_*) exercise sync, DAG and readiness convergence across several nodes in one process, without Docker.
They are the fastest way to reproduce a sync or ordering bug.
Fuzzing
Section titled “Fuzzing”fuzz/ holds coverage-guided fuzz targets over inbound bytes: bundles, wire frames, signed ops, CRDT sync payloads and provider records.
Run one with scripts/fuzz.sh <target> [seconds]; the targets and seeds are described in
fuzz/README.md.
Integration and e2e (merobox)
Section titled “Integration and e2e (merobox)”The widest layer runs real merod nodes in Docker via
merobox, driven by declarative
YAML workflows. Each scenario file describes a node topology and a sequence of
install / call / assert steps. These are the
tests that exercise everything TestHost and the simulator leave out — real
networking, cross-context calls, governance across peers, late joiners,
partitions, and multi-node migrations. They run in CI under the
e2e-rust-apps.yml, app-migration-e2e.yml, and sync-regression.yml
workflows, building a local merod:local image plus the WASM apps under
apps/. A scenario joins the e2e-rust-apps.yml suite by an entry in
.github/e2e-scenarios.yml; CI packs those entries into groups that each share
one runner, and cleans every container, network, volume and data directory
between the scenarios of a group.
The scenarios live in:
apps/scaffolding-e2e/workflows/: group membership, key delivery, kick and rejoin, subgroups, leave, late joiners, sync resilience under partition, mesh soak.apps/blobs/workflows/: blob announce, fetch and sizes across nodes.workflows/sync-tests/,workflows/app-migration/andworkflows/fuzzy-tests/: sync regression, v1 to v2 migrations, and fuzzy load.
Run one locally with merobox bootstrap run <scenario.yml>, then merobox stop --all.
The YAML format is described in apps/AGENTS.md.
CI builds the nodes from the PR’s own code (.github/actions/build-local-merod produces merod:local), so a green E2E means that code passed.
A failed group re-runs all its scenarios.
Its step summary names the scenario that failed, and that scenario’s node logs are under docker-logs/<scenario>/ in the group’s logs-* artifact.
Reproducing a CI flake locally
Section titled “Reproducing a CI flake locally”A flake that only shows up in CI is usually timing, so reproduce it with CI’s build and loop it.
- Build
merodthe way CI does:CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256 cargo build -p merod --release. A debug build runs several times slower, and that alone can hide a race: one ordering bug inaccount-pairing-application-scopenever reproduced on a debug build in 17 runs, and failed 2 runs in 11 on this build. - Package it as
merod:localwith.github/workflows/deps/prebuilt.Dockerfile, as.github/actions/build-local-meroddoes. For a scenario on the http registry image, serve the bundles withurl=$(./scripts/e2e-fixture-registry.sh)and stamp the image withscripts/set-registry-mode.sh merod:local http "$url". - Run the scenario from its app directory as
scripts/e2e-run-scenario-group.shdoes:merobox bootstrap run <scenario.yml> --image merod:local --e2e-mode. Embedded-auth scenarios needMERO_AUTH_ADMIN_USERandMERO_AUTH_ADMIN_PASSWORDset, matching the scenario’s login steps. The node logs land underdata/container-logs/in the app directory, and astop_all_nodesscenario removes its containers, so copy that directory before the next run. - Loop it and keep every run’s logs, so a failing run can be compared against a passing one. If it still will not fail, add CPU load (one busy process per core): a gossip-mesh race in the same scenario needed it.
Reading logs
Section titled “Reading logs”merod logs through tracing to stdout.
Target a subsystem with RUST_LOG to see the flow you are debugging:
RUST_LOG=debug merod --node node1 runRUST_LOG=calimero_node::sync=trace,calimero_context=debug merod --node node1 run- In merobox each node is a container, so read its output with
docker logs <container>. To raise verbosity in a scenario, setRUST_LOGon the node in its YAML. - Scenarios assert on log signals by grepping for an expected line; the
sync-resilience-*scenarios, for example, count occurrences of a sync marker. - Panics are logged by a structured hook (message, thread, location, backtrace), so grep for the panic message rather than for
panicked.
Proving a fix
Section titled “Proving a fix”-
Reproduce first, end to end. Write the failing case at the layer a user hits it: a
sync_simor integration test for a logic or ordering bug, a merobox scenario for a networked flow. Watch it fail, and capture the log window around the failure with the specific line or marker that proves the bug. -
Apply the fix and rerun the same test. Keep it as the regression test. The captured before and after is the evidence the PR description shows.
-
Reason about old data. For anything touching on-disk formats or wire encoding, work through the old-data and mixed-version cases explicitly, because E2E will not.
What runs in CI
Section titled “What runs in CI”Workflows are path-filtered:
| Workflow | Triggers on | Runs |
|---|---|---|
e2e-rust-apps.yml |
any crates/** and app dirs |
The main merobox suite (scaffolding-e2e, xcall, blobs, kv-store): the scenarios in .github/e2e-scenarios.yml, several per runner (scripts/e2e-scenario-groups.py) |
sync-regression.yml |
crates/node/src/sync/**, storage sync paths |
workflows/sync-tests/ |
app-migration-e2e.yml |
migration paths | v1 to v2 app-migration scenarios |
sdk-e2e.yml |
SDK paths | SDK end to end |
fuzzy-load-test.yml |
manual, load paths | Fuzzy load |
platforms.yml |
nightly; PRs reaching Windows-only code (scripts/needs-windows-ci.py); label ci:platforms |
Windows tests and e2e, macOS and Windows release builds |
Documentation never starts a code workflow.
Every paths: filter ends with !**/*.md / !**/*.mdx, re-including only the Markdown some code reads (an include_str! target, a manifest’s guide or readme); scripts/tests/ci-paths-test.py fails a filter that lets docs back in.
ci-checks.yml and release.yml carry required checks, so they run on every PR and skip work instead.
scripts/ci-scope.py follows each job’s packages through their dependency closure and skips a job the change cannot reach: the storage cost gate, WASM size gate, cargo-mero e2e and app builds run only when their inputs move, and a docs-only PR builds nothing.
The pre-PR gate
Section titled “The pre-PR gate”These are the exact checks CI enforces in .github/workflows/ci-checks.yml.
CI runs them as parallel jobs behind the one required Rust check: lint, tests, feature-gated tests, the app build, and supply-chain checks.
Run them all locally with ./scripts/check-like-ci.py, which reads that workflow instead of keeping its own copy; --list shows what it will run.
The steps below are the same commands, grouped by job.
-
Check formatting and lint with the no-warnings gate. The
--features calimero-storage/testingflag pulls the storagetestingharness into the same single pass so it is linted too.Terminal window cargo fmt --checkcargo clippy --workspace --all-targets --features calimero-storage/testing -- -D warnings -
Run the tests. CI runs them under cargo-nextest, which runs every test binary in parallel, plus the doctests nextest does not cover. Some networking tests bind real interfaces through a
TEST_HOSTSenv var; set it locally if host-dependent tests fail. Tests that execute an app’s WASM build it themselves when it is missing or stale, so no app build is needed first.Terminal window TEST_HOSTS=127.0.0.1,0.0.0.0 cargo nextest run --workspacecargo test --workspace --doc -
Build every app. CI builds and bundles all in-repo apps in its own job, and the script ends by checking that every wasm embeds its ABI. The result is cached under a key made of the git trees it is built from (every app and the crates it reaches, cargo-mero, the lockfile, the toolchain, the script and the workflows that run it), and only master saves it, so a PR whose change cannot reach the apps restores the set master already built and checked; e2e’s Build Apps restores the same entry (
.github/actions/wasm-apps).Terminal window ./scripts/build-all-apps.sh -
Run the narrow target checks CI keeps green: the no-chain context-config build and the local-only
merodinit binary.Terminal window cargo check -p calimero-context-configcargo check -p merod --no-default-features -
Check the dependency policy - only needed if you changed dependencies. CI pins
cargo-denyto0.19.9and also runscargo machetefor unused dependencies.Terminal window cargo deny check advisories bans licenses sourcescargo machete