JustBash.Fixtures (JustBash v0.4.0)
View SourceContent-addressing and integrity rules for the bash comparison fixture corpus.
Fixture cases live in test/fixtures/bash_cases/<suite>.json; the outputs real
bash produced for them live in test/fixtures/bash_expected/<suite>.json. The
two files are joined by content_hash — a digest of the case's inputs, so a
recording is addressed by the script that produced it rather than by its name.
That join only means anything if the hash is recomputed from the inputs on
every read. A stored hash that is never re-derived degrades into an opaque
label: edit the script, leave the hash, and the case keeps passing while
asserting against output recorded for a different script. content_hash/2 is
the single definition both the recorder (mix bash_fixtures) and the test
(JustBash.FixtureTest) call, so the two cannot disagree.
The digest
content_hash = sha256(canonical(script, files)) |> hex |> first 16 chars
canonical = ~s({"files":) <> compact_json(files) <> ~s(,"script":) <>
compact_json(script) <> ~s(})Compact JSON means no insignificant whitespace, and non-ASCII is emitted as
raw UTF-8 rather than \uXXXX escapes. files keys are sorted by path so the
digest never depends on map iteration order.
Only the fields that feed the recording — script and files — are hashed.
Comparison settings (name, opts.ignore_exit, opts.ignore_stderr) are
deliberately excluded: renaming a case or widening what it tolerates must not
invalidate a recording that is still byte-for-byte correct.
Versioning
@hash_version records the current canonicalization. Changing the digest
invalidates every recorded expectation at once, so it must be a deliberate,
greppable event rather than an accident: bump this, and re-record the corpus.
Summary
Types
An integrity violation found by validate/2.
Functions
The exact bytes content_hash/2 digests.
Digests a case's inputs into its content_hash.
Renders a problem/0 as a single actionable line.
Digests a decoded case map, tolerating an absent or null "files".
The canonicalization version this module implements.
Checks a decoded suite's cases against its decoded recordings.
Types
@type problem() :: {:stale_hash, name :: String.t(), stored :: String.t() | nil, computed :: String.t()} | {:missing_recording, name :: String.t(), computed :: String.t()} | {:orphan_recording, hash :: String.t()} | {:hash_collision, hash :: String.t(), names :: [String.t()]}
An integrity violation found by validate/2.
:stale_hash— the stored hash is not the digest of the current inputs, so the case is joined to a recording made from a different script:missing_recording— the case has no recorded expectation:orphan_recording— a recording whose hash matches no live case:hash_collision— one hash claimed by cases with differing inputs
Functions
The exact bytes content_hash/2 digests.
Exposed for diagnosing a hash mismatch: comparing canonical forms says which input drifted, where comparing digests only says that something did.
Digests a case's inputs into its content_hash.
Examples
iex> JustBash.Fixtures.content_hash("echo 'hello world' | wc")
"d34a7800e7f5b693"
iex> JustBash.Fixtures.content_hash("echo hi") ==
...> JustBash.Fixtures.content_hash("echo hi", %{})
true
Renders a problem/0 as a single actionable line.
Digests a decoded case map, tolerating an absent or null "files".
Accepts the shape found in bash_cases/*.json, so callers that have just
decoded a suite file need not destructure it first.
@spec hash_version() :: pos_integer()
The canonicalization version this module implements.
Checks a decoded suite's cases against its decoded recordings.
Pure, so it runs both offline in mix bash_fixtures.verify and at compile
time in the fixture test. Returns [] when the suite is sound; problems are
returned rather than raised so a caller can report every one at once instead
of one per run.