Testing
This guide describes libfossil’s test strategy for contributors adding tests or
investigating failures. For the module layout referenced throughout, see
architecture.md.
Overview
libfossil’s test suite has three tiers:
- Unit tests — standard
go test ./...coverage for individual packages under the root module andinternal/.... - DST simulation tests — deterministic simulation scenarios under
./dst/...that drive multiple repos through a virtual clock with fault injection and invariant checks. - Dual-driver matrix — the whole suite runs against two SQLite drivers
(
moderncandncruces) to catch driver-specific behavior.
Running tests
The Makefile wraps the common targets:
make test— runsgo test ./... -count=1 -timeout=120swith the defaultmoderncdriver.make test-drivers— runs the full suite twice:go test ./... -count=1 -timeout=120sfollowed bygo test -tags test_ncruces ./... -count=1 -timeout=120s.make test-otel— runs the out-of-workspace OTel submodule withcd observer/otel && GOWORK=off go test ./... -count=1.make test-all— chainstest-drivers,test-otel, andgo build ./cmd/libfossil/. This is the target CI uses and what you should run before opening a PR.
make setup-hooks points core.hooksPath at .githooks/. The installed
pre-commit hook runs both drivers with -short, go vet, the OTel submodule,
and the CLI build in roughly 45 seconds. Skip with git commit --no-verify
only in emergencies.
Driver matrix
libfossil abstracts SQLite behind db/ and two driver submodules. Both must
pass identical tests:
modernc(default) — pure-Go port of SQLite. Used by every build that doesn’t set a tag. Registered viainternal/testdriver/modernc.gounder//go:build !test_ncruces && !test_mattn.ncruces(wasm-capable) — selected with-tags test_ncruces, suitable for WASM targets. Registered viainternal/testdriver/ncruces.gounder//go:build test_ncruces.
GitHub Actions runs test and test-ncruces as parallel jobs (see
.github/workflows/test.yml). The ncruces job excludes cmd/libfossil because
the shipped binary only needs one driver built in; DST and unit tests are
executed under both. For the canonical invocation, use make test-drivers
rather than hand-rolling tag flags.
DST (Deterministic Simulation Testing)
DST is inspired by FoundationDB and TigerBeetle: run many repos in a single
process under a simulated clock, inject faults, and replay a failing seed
deterministically. The harness lives in dst/simulator.go and
dst/invariants.go.
Seeds and reproducibility
dst.SimConfig.Seed drives every random source used by the simulator: the
event queue, network fault decisions, and a SeededBuggify PRNG that gates
fault-injection sites. Any failure on seed N reproduces exactly on seed N.
dst/scenario_test.go exposes three flags driving TestDST:
go test ./dst -run TestDST -seed=42 -level=hostile -steps=10000-seed=<int64>— seed passed toSimConfig.Seed(0 uses test-specific defaults).-level={normal,adversarial,hostile}— picks aseveritystruct settingDropRateandBuggify.normaldisables faults;hostileuses 20% drop and BUGGIFY.-steps=<int>— caps the number of events processed.
Seed-parameterized scenarios (e.g. TestCloneDSTSeedSweep in
dst/clone_test.go) loop over seeds via t.Run(fmt.Sprintf("seed_%d", seed), ...). To reproduce a single sub-seed: go test ./dst -run TestCloneDSTSeedSweep/seed_7 -v.
Invariants
dst/invariants.go defines per-node and cross-node checks. Simulator.CheckSafety
runs the per-node set after every SafetyCheckInterval steps; convergence
checks run once the sim has quiesced:
- Safety (anytime):
CheckBlobIntegrity(UUID matches hash of expanded content),CheckDeltaChains(delta.srcidresolves to a blob),CheckNoOrphanPhantoms,CheckUVIntegrity,CheckTagxrefIntegrity,CheckTableSyncIntegrity. - Convergence (after fault-free sync):
CheckConvergence(leaves match master blob-for-blob),CheckSubsetOf,CheckUVConvergence,CheckTableSyncConvergence,CheckTombstoneConvergence.
Failures return a *InvariantError{Invariant, NodeID, Detail} so the failing
invariant, the offending node, and the seed are all in the log line.
BUGGIFY
BUGGIFY marks code points where faults can be injected — truncated batches, dropped cards, retries, corrupted nonces, byte-flipped content. In production the check is a single boolean read; under DST the seeded PRNG decides whether to fire.
Two styles appear in the tree:
Process-global, used outside the sync path.
simio.Buggify(probability)consults a process-global state toggled bysimio.EnableBuggify(seed)/DisableBuggify()(seesimio/buggify.go). Example frominternal/content/content.go:// BUGGIFY: flip a byte in expanded content to exercise UUID-mismatch detection. if simio.Buggify(0.01) && len(content) > 0 { corrupted := make([]byte, len(content)) copy(corrupted, content) corrupted[0] ^= 0xFF return corrupted, nil }Scoped, passed through session options.
BuggifyCheckeris an interface (Check(site string, probability float64) bool) defined ininternal/sync/session.goand re-exported from the root aslibfossil.BuggifyChecker. Sync code threads a checker through so each site has a stable name:// BUGGIFY: 5% chance drop the last gimme card. if len(gimmes) > 1 && cs.opts.Buggify != nil && cs.opts.Buggify.Check("clone.buildRequest.dropGimme", 0.05) { gimmes = gimmes[:len(gimmes)-1] }
dst.SeededBuggify in dst/simulator.go implements BuggifyChecker with a
seeded *rand.Rand. Simulator.New enables both styles when SimConfig.Buggify
is true, wiring independent PRNG streams into the client, the server-side
MockFossil, and the global simio state.
Writing new tests
- Prefer table-driven tests in unit code; include success and failure rows.
- If you touch sync, merge, clone, or checkin, add a DST scenario under
dst/and run it across a seed range (seeTestCloneDSTSeedSweepfor the pattern). - Mark any new fault-sensitive code point with BUGGIFY. Use the scoped
BuggifyCheckerstyle when the site lives inside the sync session; reservesimio.Buggifyfor code that runs outside an explicit session scope. - Assert invariants. If the new behavior warrants one, add it to
dst/invariants.goand wire it intoCheckSafetyor a convergence helper. - Confirm tests pass under both drivers (
make test-drivers) before requesting review.