Artifact 10c1b397df20e524af118fa08b09779e0d259587f53a50d013bc3baefc30724d:
- File
docs/site/content/docs/testing.md
— part of check-in
[d1016bd2ef]
at
2026-04-26 08:12:25
on branch trunk
— site(landing): SVG architecture diagram + SDK refresh (#7)
* site(landing): SVG architecture diagram + SDK regen
- Replace §04 ASCII diagram with an SVG mirroring the dagnats style: three columns (callers / core / plugins), ember flow arrows with CALLS / DELEGATE / EVENTS / PERSIST labels, bottom box for .fossil (SQLite). Picks up the layout's existing .diagram CSS with no new styles. - Refresh gomarkdoc-generated SDK pages — they had drifted from current Go doc comments since the original generation.
* docs(site): drop redundant body H1s from frontmatter pages
Each of the four moved docs (architecture, extension-points, migration-from-fossil, testing) and the two hand-written driver _index pages had a body '# Title' that duplicated their frontmatter 'title:'. Hextra renders both, producing the visible doubled headline. Removing the body H1 leaves Hextra's frontmatter-driven page heading as the single source.
SDK api.md pages from gomarkdoc are unaffected — they have no frontmatter, so Hextra promotes their body H1 to the page title without duplication.
* site(landing): bump version refs to v0.4.2 (user: [email protected], size: 6834) [annotate] [blame] [check-ins using]
--- title: Testing weight: 50 --- This guide describes libfossil's test strategy for contributors adding tests or investigating failures. For the module layout referenced throughout, see [`architecture.md`](./architecture). ## 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 test` — runs `go test ./... -count=1 -timeout=120s` with the default `modernc` driver. - `make test-drivers` — runs the full suite twice: `go test ./... -count=1 -timeout=120s` followed by `go test -tags test_ncruces ./... -count=1 -timeout=120s`. - `make test-otel` — runs the out-of-workspace OTel submodule with `cd observer/otel && GOWORK=off go test ./... -count=1`. - `make test-all` — chains `test-drivers`, `test-otel`, and `go 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 via `internal/testdriver/modernc.go` under `//go:build !test_ncruces && !test_mattn`. - **`ncruces`** (wasm-capable) — selected with `-tags test_ncruces`, suitable for WASM targets. Registered via `internal/testdriver/ncruces.go` under `//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 to `SimConfig.Seed` (0 uses test-specific defaults). - `-level={normal,adversarial,hostile}` — picks a `severity` struct setting `DropRate` and `Buggify`. `normal` disables faults; `hostile` uses 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.srcid` resolves 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: 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`: ```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 } ``` 2. **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: ```go // 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 (see `TestCloneDSTSeedSweep` for the pattern). - Mark any new fault-sensitive code point with BUGGIFY. Use the scoped `BuggifyChecker` style when the site lives inside the sync session; reserve `simio.Buggify` for code that runs outside an explicit session scope. - Assert invariants. If the new behavior warrants one, add it to `dst/invariants.go` and wire it into `CheckSafety` or a convergence helper. - Confirm tests pass under both drivers (`make test-drivers`) before requesting review.