Reads and writes test/passing_tests.json, the regression ratchet registry.
The registry names the tests that must always pass. It has three lists, one per suite, and entries may be literal paths or globs:
{
"description": "...",
"internal_tests": ["test/statifier/**/*_test.exs"],
"last_updated": "2026-08-02",
"scion_tests": [],
"w3c_tests": []
}mix test.regression runs exactly what this file expands to; mix test.baseline is the only supported way to grow it. This module holds the
pure part of both tasks - load, expand, categorize, add, encode - so the
tasks themselves stay thin wrappers around System.cmd/3.
Encoding is deliberately hand-rolled rather than delegated to a JSON pretty printer: keys are sorted and arrays are one entry per line, so ratcheting a test in produces a one-line diff.
Summary
Functions
Adds paths to the registry, dated today.
The suite a test file belongs to, decided by its path.
The conformance suites, the only categories the ratchet grows by hand.
Every test file on disk for a conformance category, sorted.
How much of category's emitted corpus passing covers.
Path of the registry file, relative to the project root.
Encodes registry as JSON with sorted keys, two-space indent, and one array
entry per line, so that ratcheting in a test is a minimal diff.
Expands the patterns held under category into test files.
Expands a list of literal paths and globs into existing test files.
Every test file the registry expands to, across all categories.
The registry key holding category.
Loads the registry from path.
Writes registry back to path, pretty-printed.
One report line per conformance category, plus the denominator caveat.
Directory holding the corpus for a conformance category.
Human-readable name of a conformance suite.
mix test arguments that make paths runnable.
Types
@type category() :: :internal | :scion | :w3c
@type stats() :: %{ passing: non_neg_integer(), total: non_neg_integer(), percent: float() | nil }
Functions
@spec add(registry :: t(), paths :: [String.t()], today :: Date.t()) :: {t(), [String.t()], [String.t()]}
Adds paths to the registry, dated today.
Returns {registry, added, skipped}. Internal tests are skipped: they are
covered by globs already, so listing them individually would only add churn.
Entries are deduplicated and sorted, and the ratchet never removes anything.
The suite a test file belongs to, decided by its path.
Examples
iex> Mix.Statifier.RegressionRegistry.categorize("test/scion_tests/basic/basic0_test.exs")
:scion
iex> Mix.Statifier.RegressionRegistry.categorize("test/statifier/document_test.exs")
:internal
@spec conformance_categories() :: [category()]
The conformance suites, the only categories the ratchet grows by hand.
Every test file on disk for a conformance category, sorted.
Used by mix test.baseline to find candidates the registry does not know
about yet. root prefixes the suite directory; it defaults to the project
root and exists so the tasks can be pointed at a fixture tree under test.
@spec corpus_stats( passing :: [String.t()], category :: category(), root :: String.t() ) :: stats()
How much of category's emitted corpus passing covers.
total counts the test files on disk under the suite directory, which is the
only denominator the ratchet can reach 100% of - cases excluded at generation
time (tools/corpus/*/exclusions.exs) never emit a file. The numerator is the
intersection of passing with those files, never a plain length, because a
registry entry may be a glob or may name a path outside the suite directory.
percent is nil when the corpus is empty, so callers report "no files"
rather than dividing by zero.
@spec default_path() :: String.t()
Path of the registry file, relative to the project root.
Encodes registry as JSON with sorted keys, two-space indent, and one array
entry per line, so that ratcheting in a test is a minimal diff.
Examples
iex> Mix.Statifier.RegressionRegistry.encode(%{"scion_tests" => ["a.exs"], "n" => 1})
~s|{\n "n": 1,\n "scion_tests": [\n "a.exs"\n ]\n}\n|
Expands the patterns held under category into test files.
Returns {files, missing}, where missing lists entries that matched
nothing on disk. A registry entry that no longer exists is a hole in the
ratchet, so callers report it rather than skipping it.
Expands a list of literal paths and globs into existing test files.
Returns {sorted_unique_files, patterns_that_matched_nothing}.
Examples
iex> Mix.Statifier.RegressionRegistry.expand_patterns(["test/no_such_test.exs"])
{[], ["test/no_such_test.exs"]}
Every test file the registry expands to, across all categories.
Returns {files, missing} with the same meaning as expand/2.
The registry key holding category.
Examples
iex> Mix.Statifier.RegressionRegistry.key(:scion)
"scion_tests"
Loads the registry from path.
Returns {:error, reason} rather than raising: both tasks report the reason
and exit non-zero, and a malformed registry must never be mistaken for an
empty one.
Writes registry back to path, pretty-printed.
@spec stats_lines( passing :: [String.t()], categories :: [category()], root :: String.t() ) :: [ String.t() ]
One report line per conformance category, plus the denominator caveat.
Each per-category line reads " LABEL: passing/total (percent%)", with
labels padded so the counts line up. A category whose total is 0 is
omitted entirely. Callers prefix these lines with their own header (the
numerator's meaning differs between a scan and a ratchet run, so the header
text is not this function's job) and print the block as-is otherwise.
Returns [] - no lines, no caveat - when no category has any emitted files,
so a fixture tree with no corpus prints nothing at all.
Directory holding the corpus for a conformance category.
Examples
iex> Mix.Statifier.RegressionRegistry.suite_dir(:w3c)
"test/scxml_tests"
Human-readable name of a conformance suite.
Examples
iex> Mix.Statifier.RegressionRegistry.suite_label(:scion)
"SCION"
iex> Mix.Statifier.RegressionRegistry.suite_label(:internal)
nil
mix test arguments that make paths runnable.
Conformance suites are excluded by default, so their tag has to be included explicitly.
Examples
iex> Mix.Statifier.RegressionRegistry.test_args(["test/scion_tests/basic/basic0_test.exs"])
["--include", "scion"]