Mutare.Sandbox.Command.Output (mutare v0.1.1)

Copy Markdown View Source

Read shapes out of a mix run's captured output.

A mutant's exit code is the primary signal (Mutare.Sandbox.Command decodes it), but three jobs need to look past the code at the human-readable output:

Why these live together

Every pattern here recognises a shape in mix's human-readable output, so they all break together if mix ever changes its format. They are read for different jobs by different modules — compile_error_banner/0 to tell a kill from infra, source_location_regex/0 to map a compile error to a mutant id, test_location_regex/0 to name flaky tests — and the consumers are deliberately not merged (they parse for different ends). But sourcing every pattern from one module gives a mix-output-format change a single home.

Everything here is pure, so each discriminator is unit-testable without spawning mix.

Summary

Types

The remediation class of a Mix dependency-check failure.

Functions

Returns whether output shows the BEAM aborting because the atom table filled.

Returns whether output matches the known boot-failure harness error.

Regex for Mix's == Compilation error in file <path> == banner.

Classify a dependency-check failure in captured Mix output.

Returns the diagnostic block started by a compiler-output line.

Return the last lines lines of captured Mix output.

Regex for a <file>:<line> source reference in Mix output.

Returns whether output reports a Mix compilation error in a test script.

Regex for a <test_file>:<line> reference in Mix output.

Types

dependency_issue()

@type dependency_issue() :: :fetch | :compile | :diverged | :unavailable | :invalid

The remediation class of a Mix dependency-check failure.

Functions

atom_exhausted?(output)

@spec atom_exhausted?(String.t()) :: boolean()

Returns whether output shows the BEAM aborting because the atom table filled.

This is the signature of a mutation that mints unbounded atoms. The runner treats that as a kill, like a timeout, because the suite cannot complete with the mutation active.

This predicate only refines an otherwise-:harness_error exit in Mutare.Sandbox.Command.outcome/2; normal pass, fail, and timeout verdicts take precedence.

boot_failure?(output)

@spec boot_failure?(String.t()) :: boolean()

Returns whether output matches the known boot-failure harness error.

The signature is the emulator's terminating during boot message paired with a secondary :standard_error failure. The original diagnostic is usually gone by then, and the common cause is resource or connection contention while concurrent workers start.

The runner still records this as :harness_error, but it can show a more useful message and use the dedicated boot-failure retry budget. This predicate only refines an otherwise-:harness_error exit; normal pass, fail, and timeout verdicts take precedence.

compile_error_banner()

@spec compile_error_banner() :: Regex.t()

Regex for Mix's == Compilation error in file <path> == banner.

The regex captures <path> and is used by suite_compile_error?/1.

dependency_issue(output)

@spec dependency_issue(String.t()) :: dependency_issue() | nil

Classify a dependency-check failure in captured Mix output.

Returns nil for output unrelated to dependencies. The categories deliberately follow Mix's own recommendations:

  • :fetch — the lock/source state requires mix deps.get;
  • :compile — sources exist but require mix deps.compile;
  • :diverged — dependency declarations conflict;
  • :unavailable — a non-fetchable dependency (normally a local/path dep) is missing from the sandbox's view of the filesystem;
  • :invalid — another status under Mix's unchecked-dependencies banner.

The broad banner proves this is dependency validation, while the narrower recommendation phrases choose remediation. This keeps an arbitrary compiler error that merely mentions mix deps.get from being reclassified.

diagnostic_severity(line)

@spec diagnostic_severity(String.t()) :: :error | :warning | nil

Returns the diagnostic block started by a compiler-output line.

The result is :error for an error: header or raised ** (…Error), :warning for a warning: header, and nil for any other line. Body, footer, and chatter lines inherit the previous header's severity in the caller.

Mutare.Poison uses this to scan only non-warning lines for mutant locations. A failed metamutant compile can include warnings caused by mutations, and those warnings carry the same file:line footer shape as real errors. Separating the diagnostic blocks keeps warning locations from being treated as poison.

output_tail(output, lines \\ 20)

@spec output_tail(String.t(), pos_integer()) :: String.t()

Return the last lines lines of captured Mix output.

Used by baseline, coverage-probe, and Mix-task errors to show enough context without printing an entire suite run.

source_location_regex()

@spec source_location_regex() :: Regex.t()

Regex for a <file>:<line> source reference in Mix output.

It matches .ex and .exs paths such as lib/foo.ex:5 or test/foo_test.exs:42, capturing the file and line. Mutare.Poison uses it to map compile errors back to mutant ids.

suite_compile_error?(output)

@spec suite_compile_error?(String.t()) :: boolean()

Returns whether output reports a Mix compilation error in a test script.

This identifies a mutation that broke test-suite compilation. It matches the compile_error_banner/0 only when the captured path is a .exs file under a test/ directory. Lib-file errors and output without the banner return false.

test_location_regex()

@spec test_location_regex() :: Regex.t()

Regex for a <test_file>:<line> reference in Mix output.

This narrows source_location_regex/0 to _test.exs files. The baseline runner uses it to name tests involved in a flaky run.