JustBash.Fixtures (JustBash v0.4.0)

View Source

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

problem()

@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

canonical(script, files)

@spec canonical(String.t(), map()) :: binary()

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.

content_hash(script, files \\ %{})

@spec content_hash(String.t(), map()) :: String.t()

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

describe(arg)

@spec describe(problem()) :: String.t()

Renders a problem/0 as a single actionable line.

hash_case(test_case)

@spec hash_case(map()) :: String.t()

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.

hash_version()

@spec hash_version() :: pos_integer()

The canonicalization version this module implements.

validate(cases, results)

@spec validate([map()], [map()]) :: [problem()]

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.