muharmutation harness
Pages

Spec reference

A spec is one JSON file, passed with -spec. It names the suite command, the positive control, and the mutations to try. A JSON Schema for editors and linters is published at https://muhar.assay.guide/schema/spec.v1.json.

{
  "$schema": "https://muhar.assay.guide/schema/spec.v1.json",
  "root": "/home/dev/limits",
  "test": "go test ./... -count=1 -timeout 60s",
  "control": {
    "name": "CONTROL: never refuse an over-limit withdrawal",
    "file": "limits.go",
    "old": "\t\treturn ErrOverLimit",
    "new": "\t\treturn nil"
  },
  "mutations": [
    {
      "name": "zero is accepted as an amount",
      "file": "limits.go",
      "old": "if amount <= 0 {",
      "new": "if amount < 0 {"
    }
  ]
}

Top-level fields

FieldTypeRequiredMeaning
rootstringnoDirectory the suite runs in and relative file paths resolve against. Empty or absent: the current working directory. A relative root is itself relative to the current directory.
teststringyesSuite command, run as sh -c '<test>' in root. Exit status non-zero means the suite failed. Must be non-empty.
controlmutationyesThe positive control: a mutation the suite must catch. Its name and old must be non-empty.
mutationsarray of mutationnoThe guards under test, run and reported in this order. Empty or absent: only the baseline and control run, and the report says Totals: 0 caught, 0 NOT CAUGHT, 0 could-not-mutate.

Mutation fields

control and every entry of mutations have the same shape.

FieldTypeMeaning
namestringThe label in the report. Name the bug the edit simulates. Not required to be unique, but a duplicate makes the report ambiguous.
filestringFile to edit. Absolute: used as is. Relative: joined to root, or to the current directory when root is empty.
oldstringExact text to replace. Must be non-empty and present in the file.
newstringReplacement text. Must differ from old. May be empty, to delete old.

Exact match, first occurrence

old is matched byte for byte: no regular expressions, no whitespace folding, no trimming. muhar replaces the first occurrence only. If the same text appears more than once and you mean a later one, widen old with surrounding context until it is unique, or until its first occurrence is the one you mean.

The worked example relies on this. The mutation "a withdrawal that lands exactly on the limit is refused" uses old spentToday+amount > limit, which also occurs inside the previous mutation's old. Each mutation is planted on pristine source, so they do not interfere.

Whitespace and line endings

Write tabs and newlines as JSON escapes: \t, \n. A file with Windows line endings needs \r\n in any old that spans a line break; a \n-only old will not match it, and the mutation is reported COULD_NOT_MUTATE with Old text not found. Non-ASCII text is matched as UTF-8 bytes.

What happens to the file

For each mutation muhar reads the file, computes the edit, writes the result with the file's existing permission bits, reads it back to prove the bytes landed, runs the suite, and then writes the original bytes back. Restoration is unconditional once the write has happened, whatever the verdict. A symbolic link is followed: the edit lands in the link's target.

One exception: an interrupt (SIGINT, SIGTERM) during a sequential run ends the process before the restore, so the file under test is left mutated. See How a run works.

Absolute paths and parallel runs

With -j above 1, each worker runs the suite in its own copy of root. A relative file resolves inside that copy. An absolute file does not: every worker edits the same original file while running the suite in its copy, which produces wrong verdicts. Use relative paths in any spec you run with -j above 1 or -j 0. This is tracked as assay#2643.

Decoding rules

muhar decodes the spec with Go's encoding/json. Three consequences:

  • Unknown keys are ignored. That is what makes $schema safe to include, and also means a misspelt key such as "mutation" is silently dropped. The published schema is stricter and rejects unknown keys, so validate against it to catch typos.
  • Key matching is case-insensitive: "Test" and "TEST" both set test.
  • A wrong type is an error: {"test": 1} fails with json: cannot unmarshal number into Go struct field spec.test of type string.

Spec errors

These stop muhar before any suite runs. Each is printed to stderr and exits 1.

MessageCause
muhar: read spec: <error>The -spec file cannot be read.
muhar: parse spec: <error>The file is not valid JSON, or a field has the wrong type.
muhar: spec.test (the suite command) is requiredtest is empty or absent.
muhar: spec.control is required (a mutation the suite MUST catch) — a harness with no failing control is a green light with no bulb (#34)control.name or control.old is empty or absent.

COULD_NOT_MUTATE reasons

When a mutation cannot be planted or proven, the report line carries the reason in parentheses. <name> is the mutation's name, <file> is file as written in the spec, and <path> is the resolved path.

ReasonCause
mutation "<name>": Old is empty — nothing to match, edit cannot landold is empty.
mutation "<name>": Old == New — edit is a no-opold and new are identical.
mutation "<name>": Old text not found in <file> — edit did not landThe file does not contain old. The usual cause is drift: the code changed and the spec did not.
mutation "<name>": content unchanged after replace — silent no-opA final check that the edited content differs from the original.
mutation "<name>": read <path>: <error>The file cannot be read; also stat, write and re-read variants.
mutation "<name>": post-write verify failed — on-disk content does not match the intended mutationThe bytes read back differ from the bytes written.
mutation "<name>": post-write verify failed — file is byte-identical to the originalThe bytes read back equal the original.

The same reasons apply to the control, but there they discard the run: positive control could not be planted (<reason>) — the harness is not mutating; discard the run, exit 2.

JSON Schema

/schema/spec.v1.json is a JSON Schema (draft 2020-12) for this format. It requires test and control, requires a non-empty name, file and old on the control and on every mutation, and rejects unknown keys. It is deliberately stricter than muhar's decoder; a spec that passes it is always accepted by muhar.

To validate in an editor, add "$schema": "https://muhar.assay.guide/schema/spec.v1.json" as the first key. With a command-line validator:

check-jsonschema --schemafile https://muhar.assay.guide/schema/spec.v1.json mutations.json