oxmutate

Spec oracles

Replay a formal model's traces against every mutant, as a second verdict beside the tests: Quint's quint run --mbt traces or any directory of ITF files, and a report of each mutant the model catches and the tests miss.

The tests are one oracle: they say whether a mutant changed behaviour they check. A formal model is another. A spec oracle takes a model's traces (sequences of actions and the states they lead to) and replays each one against the code with a mutant armed. A mutant a replay catches is killed by the spec, whatever the tests say about it.

The interesting result is the disagreement. A mutant the spec kills and the tests let survive is a test-spec gap: behaviour the model pins down and the suite does not. Each one is reported with the trace that caught it.

The config

In oxmutate.config.json (Stryker never reads it):

{
  "specOracles": [
    {
      "name": "save-restore",
      "backend": "quint",
      "model": "models/save-restore.qnt",
      "traces": 200,
      "maxSteps": 20,
      "seed": "7",
      "replay": ["test/cursor-replay.test.ts"],
      "files": ["src/cursor.ts"]
    }
  ]
}
KeyWhat it is
nameThe oracle's name: the key a replay test loads its traces by
backendquint: oxmutate runs quint run --mbt on model. itf: a directory of *.itf.json files any tool wrote, at dir
modelquint: the model file, relative to the project root
traces, maxSteps, seedquint: how many traces (default 100), steps per trace (default 20), and a seed for a run that repeats
quintquint: how to run it (default npx -y @informalsystems/quint@0.33.0)
diritf: the directory of traces. Apalache writes ITF, and a test can pin its traces this way
replayThe replay test files (globs): their tests are this oracle's, scored on a verdict track of their own
filesThe source files the model describes (globs): the denominator of the metrics below

The replay test

A replay test is written once per modelled component. It loads the traces, performs each step's action on the real code, and asserts the model's state after it. Outside an oxmutate run there are no traces, so it skips:

import { loadTraces } from 'oxmutate/spec';
import { expect, it } from 'vitest';

const traces = loadTraces('save-restore');

it.skipIf(traces.length === 0).each(traces)('replays $name', (trace) => {
  const terminal = fakeTerminal();
  for (const step of trace.steps) {
    if (step.action === 'enterRaw') rawMode(terminal);
    expect(terminal.isRaw).toBe(step.state.raw);
  }
});

Each step carries the action the model took (mbt::actionTaken), the nondeterministic choices it made (mbt::nondetPicks, with Some/None unwrapped) and the model's variables after it, with ITF's #bigint, #set, #tup and #map decoded to numbers, arrays and Maps:

t1.itf.json
{"vars":["saved","raw"],"states":[{"#meta":{"index":0},"mbt::actionTaken":"init","mbt::nondetPicks":{},"saved":false,"raw":false},{"#meta":{"index":1},"mbt::actionTaken":"enterRaw","mbt::nondetPicks":{"depth":{"tag":"Some","value":{"#bigint":"1"}}},"saved":true,"raw":true}]}
replay.mjs
import { readTrace } from 'oxmutate/spec';

const trace = readTrace('t1.itf.json');
for (const step of trace.steps) console.log(step.action, step.picks, step.state);
node replay.mjs
init {} { saved: false, raw: false }
enterRaw { depth: 1 } { saved: true, raw: true }

What the report says

Per oracle, over the mutants in its files:

MetricWhat it measures
Spec-kill shareThe share of those mutants a replay kills
Test-spec gapMutants the spec kills and the tests miss, each with the trace that killed it
Spec escapesMutants neither the spec nor the tests kill
Cost per replayMilliseconds per replay with a mutant armed
Mapping coverageThe share of those mutants a replay test reaches at all

On closeout's src/cursor.ts, scored by its own tests and by a 200-trace model of rawMode, the gap was 0: the tests, held to a full line floor, already catch everything the model does. The mapping coverage was 45.5%, because the file also holds code the model says nothing about (m1-results.md, "The spec oracle on a real component").

On this page