Skip to content
go-libfossil Testing
Login

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:

  1. Unit tests — standard go test ./... coverage for individual packages under the root module and internal/....
  2. DST simulation tests — deterministic simulation scenarios under ./dst/... that drive multiple repos through a virtual clock with fault injection and invariant checks.
  3. Dual-driver matrix — the whole suite runs against two SQLite drivers (modernc and ncruces) to catch driver-specific behavior.

Running tests

The Makefile wraps the common targets:

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:

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-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:

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:

  1. Process-global, used outside the sync path. simio.Buggify(probability) consults a process-global state toggled by simio.EnableBuggify(seed) / DisableBuggify() (see simio/buggify.go). Example from internal/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
   }

  1. Scoped, passed through session options. BuggifyChecker is an interface (Check(site string, probability float64) bool) defined in internal/sync/session.go and re-exported from the root as libfossil.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