The corpus: three layers

  1. Committed synthetic Unity projectsfixtures/projects/{Sample,Avatar,Dynamics}Project. Hand-authored to exercise specific lint rules and component-stats paths; hermetic, run on any machine. Resolved in tests via avatar_testkit::corpus("projects/SampleProject").
  2. In-code synthetic FBXavatar_testkit::fbx::humanoid_skeleton() builds a deterministic binary FBX in memory via the fbxcel writer (feature fbx). No committed .fbx blob; covers the armature + geometry-stats paths.
  3. Env-gated real assetsAVATAR_SAMPLE_FBX, AVATAR_SAMPLE_UNITYPACKAGE, AVATAR_SAMPLE_UNITYPACKAGE_WORLD. Tests self-skip when the variable is unset, so CI without fixtures stays green; on the self-hosted runner they point at real files, so the real-data paths run every push.

Layers 1–2 are hermetic and run everywhere (including forks); layer 3 is the ground-truth pass.

Never commit user assets
User FBX files and Unity projects are never committed (see .gitignore) — the committed corpus is synthetic, and real assets only ever enter tests through the env-var gate.

The harness — avatar-testkit

A publish = false workspace member, added as a dev-dependency by the crates that golden-test:

  • golden::assert_json(path, &value) — serialize to canonical pretty JSON (2-space indent, trailing newline) and compare against the file at path (relative to the consuming crate). On mismatch it panics with a line-located diff; a missing file tells you to regenerate.
  • golden::redact_roots(&mut value) / golden::redact — scrub machine-specific absolute paths (a report's project_root, source) before snapshotting, replacing the workspace-root prefix with <ROOT> so snapshots are identical on every machine.
  • corpus(rel) / workspace_root() — resolve a corpus path from any crate's tests, at runtime.
  • fbx::humanoid_skeleton() — the in-code synthetic FBX.

Snapshots live beside the consuming test, under crates/<crate>/tests/golden/*.json, and are committed. Lists with no guaranteed order (lint diagnostics, the per-avatar PerfReport vec) are sorted in the test before snapshotting so the golden is stable.

Writing a golden test

use avatar_testkit::{corpus, golden};

#[test]
fn golden_my_project() {
    let report = avatar_lint::run(&corpus("projects/SampleProject")).unwrap();
    let mut value = serde_json::to_value(&report).unwrap();
    golden::redact_roots(&mut value);
    golden::assert_json("tests/golden/SampleProject.lint.json", &value);
}

Updating snapshots

After an intentional change to a report shape or a fixture, regenerate and review the diff before committing — the diff is the change-review:

UPDATE_GOLDEN=1 cargo test --workspace # rewrite every snapshot git diff -- '**/tests/golden/**' # review, then commit

UPDATE_GOLDEN is honored for any non-empty, non-0 value. With it unset, a mismatch fails the test — which is the point.

Current golden coverage

CrateSnapshotCovers
avatar-lint{Sample,Avatar,Dynamics}Project.lint.jsonthe full LintReport per corpus project
avatar-stats{Sample,Avatar,Dynamics}Project.project-stats.jsonper-avatar PerfReports (component side)
avatar-statshumanoid_skeleton.fbx-stats.jsonFBX geometry PerfReport
avatar-armaturehumanoid_skeleton.armature.jsonthe full ArmatureReport (humanoid mapping)
avatar-migrateSdk2Project.migrate.json, Sdk2Project.migrated.prefab.txt, Sdk2Project.FX.controller.txt, Sdk2Project.physbones.json, Sdk2Project.physbones.tuned.jsonthe full MigrationReport, the rewritten prefab text, the generated FX controller for the synthetic SDK2 fixture, and the avatar physbone list of the migrated prefab before / after a split + set (curves) + stretch pass
To extend coverage, drop a fixture into the corpus (or add an avatar-testkit::fbx builder) and add a golden test that runs the analysis over it. See also fixtures/README.md and CONTRIBUTING.md.