Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/gap-report-output-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/oracles': patch
---

Fix four parts of the `oracle-gaps` output.

- The baseline's replay command now runs the failing test. Before, its `-t` pattern was the test's `>`-joined name in JSON quotes: Vitest 4 matches `-t` against names joined by a space, so a test in a `describe` ran nothing, and a name with regular-expression syntax, `$`, or a backtick broke the pattern or the shell. The pattern is now the suites' and the test's names, escaped, joined to match Vitest 4 and Vitest 5, anchored, and in single quotes. A file that fails with no failing test (it did not load) replays whole, with no `-t`, where before it printed `-t undefined`. A control character in a test's name or failure, such as a newline, prints as its `\uXXXX` escape, in the baseline and in messages, so each stays one line.
- A gap line stays one line: the code of an operator mutant and the intent of an authored mutant have their whitespace collapsed and are cut at 60 characters with `...`, counted by code point so that a cut never splits an emoji. An authored intent prints as a code span. `results.json` keeps the full text.
- A `not-infected` message prints each operand as a code span of a typed value: strings quoted, `-0` as `-0`, bigints with `n`, and `undefined`, `null`, `NaN`, and a value that is not a primitive kept apart. A string longer than 40 characters (code points) prints its first 37 and a digest. Before, `1` and `"1"`, `0` and `-0`, and two long strings with one prefix printed alike.
- A `not-observed` directive also tells the author to add a history where the mutated code computes something different, because the run cannot tell a value nothing observed from a forced branch that computed the original's result.
27 changes: 22 additions & 5 deletions packages/oracles/oracle-gaps/capture-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ function spin() {
// operand that is not a primitive might run code when compared, so it counts
// as a change without a second comparison.
let infected = new Set()
// The first pair of operands each comparison saw in the running test, as
// text (no mutant active: the unmutated runs classify with them). An operand
// that is not a primitive is recorded as its type.
// The first pair of operands each comparison saw in the running test (no
// mutant active: the unmutated runs classify with them), each as `shown`
// records it.
let operands = new Map()
const backgroundInfected = new Set()
// The outcomes of the `if`s that ran outside any test, such as at module
Expand All @@ -294,8 +294,25 @@ const backgroundOutcomes = new Map()
const infect = (id) => (inTest ? infected : backgroundInfected).add(id)
const primitive = (value) =>
value === null || (typeof value !== `object` && typeof value !== `function`)
const shown = (value) =>
primitive(value) ? String(value).slice(0, 40) : typeof value
// An operand as its kind and its text, which classify.mjs prints
// (`operandText`): `-0` for negative zero, a string past 40 characters as
// its first 37 and a digest of the whole, so two different values never
// record alike, and a value that is not a primitive as its type alone.
const shown = (value) => {
if (!primitive(value)) return [typeof value]
// Characters are code points, so a cut never splits an astral-plane one.
const chars = typeof value === `string` ? real.from(value) : undefined
if (chars !== undefined && chars.length > 40)
return [
`string`,
chars.slice(0, 37).join(``),
createHash(`sha1`).update(value).digest(`hex`).slice(0, 8),
]
return [
value === null ? `null` : typeof value,
real.is(value, -0) ? `-0` : String(value),
]
}

// Temporary profiling (ORACLE_GAPS_PROFILE): how many fast-check histories
// each test ran, and in how many of them each site was hit, to measure what
Expand Down
8 changes: 4 additions & 4 deletions packages/oracles/oracle-gaps/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
renderReport,
renderStdout,
} from './report.mjs'
import { baselineLines, gapLine, progressLine } from './stream.mjs'
import { baselineLines, gapLine, progressLine, testNamesOf } from './stream.mjs'
import { analyze, isRunnerCopy } from './analysis.mjs'
import { Refusal } from './ending.mjs'
import {
Expand Down Expand Up @@ -637,6 +637,8 @@ export default async (env) => {
return {
key: `${module.moduleId} > ${testCase.fullName}`,
fullName: testCase.fullName,
// The names a baseline's replay selects the test by (stream.mjs).
names: testNamesOf(testCase),
state: testResult.state,
// fast-check attaches the error its predicate threw as the cause.
message: (testResult.errors ?? [])
Expand Down Expand Up @@ -1345,9 +1347,7 @@ export default async (env) => {
[...oracleFiles].map((file) => ({
file: relative(file),
failure: first.failed.get(file),
fullName: first.units
.find((u) => u.file === file)
?.tests.find((t) => t.state === `failed`)?.fullName,
tests: first.units.find((u) => u.file === file)?.tests ?? [],
})),
)
for (const line of baseline0) log(line)
Expand Down
25 changes: 22 additions & 3 deletions packages/oracles/oracle-gaps/classify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// timed out, and its capture rows (capture-setup.mjs): the sites each test
// reached, the outcomes each `if` took, the hashes of what it asserted on
// and observed, its registered route, and its law verdicts.
import { codeSpan } from './stream.mjs'

// Which kind of test produced a failure: the route the framework registered
// for it through `__oracleRoute` (capture-setup.mjs), never its name. A kill
Expand Down Expand Up @@ -228,14 +229,32 @@ function infectionOf(mutant, targets, baseline) {
return { infected: infected.length > 0, infectedBy: test }
}

// The operand pairs that the owner tests' comparison recorded for a mutant:
// distinct, in test order, up to three. A nondeterministic test's do not count.
/**
* An operand as the capture recorded it (capture-setup.mjs `shown`), as a
* typed text: a string quoted, with a digest after a long one's cut, a bigint
* with `n`, a number as JavaScript prints it but `-0`, and a value that is
* not a primitive as its type in parentheses. Two values the capture records
* differently never print alike.
*/
export function operandText([kind, text, digest]) {
if (text === undefined) return `(${kind})`
if (kind === `string`)
return digest === undefined
? JSON.stringify(text)
: `${JSON.stringify(`${text}...`)}#${digest}`
return kind === `bigint` ? `${text}n` : text
}

// The operand pairs that the owner tests' comparison recorded for a mutant,
// as code spans: distinct, in test order, up to three. A nondeterministic
// test's do not count.
function operandsOf(mutant, targets, baseline) {
const pairs = []
for (const k of targets.oracleKeys)
if (!baseline.nondeterministic.has(k))
for (const [id, pair] of baseline.firstRows.get(k)?.operands ?? [])
if (id === mutant.number) pairs.push(pair.join(` and `))
if (id === mutant.number)
pairs.push(pair.map((o) => codeSpan(operandText(o))).join(` and `))
return [...new Set(pairs)].slice(0, 3)
}

Expand Down
Loading
Loading