oxmutate 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +378 -2
- package/binding/oxmutate.darwin-arm64.node +0 -0
- package/binding/oxmutate.darwin-x64.node +0 -0
- package/binding/oxmutate.linux-arm64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-gnu.node +0 -0
- package/binding/oxmutate.linux-x64-musl.node +0 -0
- package/binding/oxmutate.win32-x64-msvc.node +0 -0
- package/dist/bin.js +5 -0
- package/dist/checker.js +122 -0
- package/dist/child.js +125 -0
- package/dist/cli.js +165 -0
- package/dist/command-runner.js +83 -0
- package/dist/config.d.ts +141 -0
- package/dist/config.js +172 -0
- package/dist/coordinator.d.ts +83 -0
- package/dist/coordinator.js +653 -0
- package/dist/files.js +62 -0
- package/dist/frozen.d.ts +19 -0
- package/dist/frozen.js +47 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/lanes.d.ts +38 -0
- package/dist/lanes.js +92 -0
- package/dist/native.d.ts +36 -0
- package/dist/native.js +40 -0
- package/dist/platform.js +27 -0
- package/dist/progress.js +40 -0
- package/dist/reports.d.ts +20 -0
- package/dist/reports.js +225 -0
- package/dist/resolve.js +43 -0
- package/dist/runner.d.ts +49 -0
- package/dist/runner.js +270 -0
- package/dist/runtime.js +24 -0
- package/dist/scheduler.js +170 -0
- package/dist/spec-oracle.d.ts +69 -0
- package/dist/spec-oracle.js +68 -0
- package/dist/spec.d.ts +21 -0
- package/dist/spec.js +75 -0
- package/dist/vite-plugin.js +18 -0
- package/dist/vite.d.ts +16 -0
- package/dist/vite.js +3 -0
- package/package.json +83 -4
package/dist/files.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files to mutate, from Stryker-style globs (`src/**\/*.ts`, `!src/**\/*.test.ts`, `{a,b}`).
|
|
3
|
+
* No dependency: a glob is translated to one regular expression, and the walk skips the
|
|
4
|
+
* directories a project never mutates.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo', '.oxmutate', '.stryker-tmp', 'reports']);
|
|
9
|
+
const escapeRegExp = (s) => s.replace(/[.+^$()|[\]\\]/g, '\\$&');
|
|
10
|
+
/** Translate one glob (without a leading `!`) into an anchored regular expression. */
|
|
11
|
+
export function globToRegExp(glob) {
|
|
12
|
+
let re = '';
|
|
13
|
+
// Stryker disable next-line EqualityOperator: one step past the end reads charAt(length), an empty string, which appends nothing
|
|
14
|
+
for (let i = 0; i < glob.length; i++) {
|
|
15
|
+
const c = glob.charAt(i);
|
|
16
|
+
if (c === '*') {
|
|
17
|
+
if (glob[i + 1] === '*') {
|
|
18
|
+
// `**/` matches zero or more directories; a bare `**` matches anything.
|
|
19
|
+
if (glob[i + 2] === '/') {
|
|
20
|
+
re += '(?:.*/)?';
|
|
21
|
+
i += 2;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
re += '.*';
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
re += '[^/]*';
|
|
30
|
+
}
|
|
31
|
+
else if (c === '?')
|
|
32
|
+
re += '[^/]';
|
|
33
|
+
else if (c === '{') {
|
|
34
|
+
const close = glob.indexOf('}', i);
|
|
35
|
+
re += `(?:${glob.slice(i + 1, close).split(',').map(escapeRegExp).join('|')})`;
|
|
36
|
+
i = close;
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
re += escapeRegExp(c);
|
|
40
|
+
}
|
|
41
|
+
return new RegExp(`^${re}$`);
|
|
42
|
+
}
|
|
43
|
+
/** Repository-relative paths (forward slashes) of files matching the include and exclude globs. */
|
|
44
|
+
export function matchFiles(root, patterns) {
|
|
45
|
+
const include = patterns.filter((p) => !p.startsWith('!')).map(globToRegExp);
|
|
46
|
+
const exclude = patterns.filter((p) => p.startsWith('!')).map((p) => globToRegExp(p.slice(1)));
|
|
47
|
+
const out = [];
|
|
48
|
+
const walk = (dir) => {
|
|
49
|
+
for (const e of readdirSync(path.join(root, dir), { withFileTypes: true })) {
|
|
50
|
+
const rel = dir ? `${dir}/${e.name}` : e.name;
|
|
51
|
+
if (e.isDirectory()) {
|
|
52
|
+
if (!SKIP.has(e.name))
|
|
53
|
+
walk(rel);
|
|
54
|
+
}
|
|
55
|
+
else if (include.some((r) => r.test(rel)) && !exclude.some((r) => r.test(rel)))
|
|
56
|
+
out.push(rel);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk('');
|
|
60
|
+
return out.sort();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=files.js.map
|
package/dist/frozen.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RunResult } from './coordinator.js';
|
|
2
|
+
export interface FrozenOracle {
|
|
3
|
+
base: string;
|
|
4
|
+
/** Test files the change edited or added, repository-relative. */
|
|
5
|
+
changedTests: string[];
|
|
6
|
+
/** Mutation score with the base ref's tests, and with the change's. */
|
|
7
|
+
baseScore: number;
|
|
8
|
+
headScore: number;
|
|
9
|
+
delta: number;
|
|
10
|
+
/** Detected by the base tests, undetected by the change's: the tests got weaker here. */
|
|
11
|
+
weakened: {
|
|
12
|
+
id: string;
|
|
13
|
+
file: string;
|
|
14
|
+
line: number;
|
|
15
|
+
mutatorName: string;
|
|
16
|
+
replacement: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
export declare function frozenOracle(head: RunResult, base: string): Promise<FrozenOracle>;
|
package/dist/frozen.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frozen oracle (spec R14): a change that edits tests is also scored against the base ref's
|
|
3
|
+
* tests. The code under test is the same in both runs, so the mutants and their ids are the same;
|
|
4
|
+
* only the test files differ. A mutant the base tests detect and the changed tests do not is a test
|
|
5
|
+
* the change weakened, which is how a patch that "makes the check pass" by editing the check shows
|
|
6
|
+
* up (the reward-hacking concern in the intent, written as a CI result).
|
|
7
|
+
*
|
|
8
|
+
* The base tests are served to Vitest from memory, the way instrumented sources are: nothing in
|
|
9
|
+
* the working tree is touched.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { ambientRuntime, run as spawn } from 'bellpull';
|
|
13
|
+
import { run } from './coordinator.js';
|
|
14
|
+
const detected = (m) => m?.status === 'Killed' || m?.status === 'Timeout';
|
|
15
|
+
const undetected = (m) => m?.status === 'Survived' || m?.status === 'NoCoverage';
|
|
16
|
+
/**
|
|
17
|
+
* A test file the change added has no base version. It is served as one skipped test, so the base
|
|
18
|
+
* run has the file (Vitest finds it on disk) and none of its tests.
|
|
19
|
+
*/
|
|
20
|
+
const ABSENT_AT_BASE = "import { it } from 'vitest';\nit.skip('absent at the base ref (oxmutate frozen oracle)', () => {});\n";
|
|
21
|
+
async function git(root, args) {
|
|
22
|
+
const r = await spawn('git', args, { cwd: root, runtime: ambientRuntime() });
|
|
23
|
+
return { ok: r.ok, out: r.stdout };
|
|
24
|
+
}
|
|
25
|
+
export async function frozenOracle(head, base) {
|
|
26
|
+
const root = head.root;
|
|
27
|
+
const top = (await git(root, ['rev-parse', '--show-toplevel'])).out.trim() || root;
|
|
28
|
+
const diff = await git(root, ['diff', '--name-only', base, '--']);
|
|
29
|
+
if (!diff.ok)
|
|
30
|
+
throw new Error(`git diff ${base} failed. fix: pass a ref this checkout has (fetch it first in CI)`);
|
|
31
|
+
const changed = new Set(diff.out.split('\n').filter(Boolean).map((f) => path.join(top, f)));
|
|
32
|
+
const testFiles = [...new Set(head.tests.map((t) => t.file))].filter((f) => changed.has(f));
|
|
33
|
+
const result = { base, changedTests: testFiles.map((f) => path.relative(root, f)), baseScore: head.score.mutationScore, headScore: head.score.mutationScore, delta: 0, weakened: [] };
|
|
34
|
+
if (testFiles.length === 0)
|
|
35
|
+
return result;
|
|
36
|
+
const shown = await Promise.all(testFiles.map((f) => git(root, ['show', `${base}:${path.relative(top, f).split(path.sep).join('/')}`])));
|
|
37
|
+
const testOverrides = Object.fromEntries(testFiles.map((f, i) => [f, shown[i]?.ok ? shown[i].out : ABSENT_AT_BASE]));
|
|
38
|
+
const frozen = await run({ ...head.options, testOverrides, incremental: false });
|
|
39
|
+
const byId = new Map(frozen.mutants.map((m) => [m.id, m]));
|
|
40
|
+
result.baseScore = frozen.score.mutationScore;
|
|
41
|
+
result.delta = head.score.mutationScore - frozen.score.mutationScore;
|
|
42
|
+
result.weakened = head.mutants
|
|
43
|
+
.filter((m) => undetected(m) && detected(byId.get(m.id)))
|
|
44
|
+
.map((m) => ({ id: m.id, file: m.file, line: m.start.line, mutatorName: m.mutatorName, replacement: m.replacement }));
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=frozen.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oxmutate's programmatic API. The CLI (`oxmutate run`) is this, declared on burgee.
|
|
3
|
+
*/
|
|
4
|
+
export { loadOptions, type MutateConfig, type Options, type Thresholds } from './config.js';
|
|
5
|
+
export { InstrumentFault, run, type MutantResult, type RunResult, type RunStats, type Status } from './coordinator.js';
|
|
6
|
+
export { changedLines, parseShard, shardOf } from './lanes.js';
|
|
7
|
+
export { native } from './native.js';
|
|
8
|
+
export { sarifReport, strykerReport, textSummary, VERSION } from './reports.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oxmutate's programmatic API. The CLI (`oxmutate run`) is this, declared on burgee.
|
|
3
|
+
*/
|
|
4
|
+
export { loadOptions } from './config.js';
|
|
5
|
+
export { InstrumentFault, run } from './coordinator.js';
|
|
6
|
+
export { changedLines, parseShard, shardOf } from './lanes.js';
|
|
7
|
+
export { native } from './native.js';
|
|
8
|
+
export { sarifReport, strykerReport, textSummary, VERSION } from './reports.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
package/dist/lanes.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type LineRanges = Map<string, [number, number][]>;
|
|
2
|
+
/** Added or changed line ranges per repository-relative path, new side. */
|
|
3
|
+
export declare function changedLines(root: string, base: string): Promise<LineRanges>;
|
|
4
|
+
export declare function parseDiff(diff: string): LineRanges;
|
|
5
|
+
export declare function touches(ranges: LineRanges, file: string, startLine: number, endLine: number): boolean;
|
|
6
|
+
/** FNV-1a 32 over the stable id: the shard a mutant belongs to. */
|
|
7
|
+
export declare function shardOf(id: string, count: number): number;
|
|
8
|
+
export declare function parseShard(value: string): {
|
|
9
|
+
index: number;
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
/** A Stryker `mutate` range: lines 1-based, columns 0-based and inclusive (`src/a.ts:5:4-6:4`). */
|
|
13
|
+
export interface MutateRange {
|
|
14
|
+
start: {
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
};
|
|
18
|
+
end: {
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** A `mutate` entry split into its glob and, when it names one, its range. */
|
|
24
|
+
export declare function parseMutateSpec(spec: string): {
|
|
25
|
+
glob: string;
|
|
26
|
+
range?: MutateRange;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Is the mutant wholly inside the range, as Stryker decides it (`locationIncluded`)? The mutant's
|
|
30
|
+
* columns are the report's, 1-based with the end exclusive; the range's are Stryker's, 0-based.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withinRange(range: MutateRange, start: {
|
|
33
|
+
line: number;
|
|
34
|
+
column: number;
|
|
35
|
+
}, end: {
|
|
36
|
+
line: number;
|
|
37
|
+
column: number;
|
|
38
|
+
}): boolean;
|
package/dist/lanes.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CI lanes' selection rules: which mutants a run owns.
|
|
3
|
+
*
|
|
4
|
+
* - `since <ref>` (the PR lane): mutants on lines the change added or modified, from
|
|
5
|
+
* `git diff -U0 <ref>`, working tree included, so a local run and CI select the same set.
|
|
6
|
+
* - `shard i/n` (a CI matrix): a mutant belongs to shard `fnv(id) mod n`, so a shard's share
|
|
7
|
+
* does not move when unrelated code changes, and the shards partition the run exactly.
|
|
8
|
+
*/
|
|
9
|
+
import { ambientRuntime, run } from 'bellpull';
|
|
10
|
+
const NEW_FILE = '+++ ';
|
|
11
|
+
/** FNV-1a 32-bit parameters. */
|
|
12
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
13
|
+
const FNV_PRIME = 0x01000193;
|
|
14
|
+
/** Added or changed line ranges per repository-relative path, new side. */
|
|
15
|
+
export async function changedLines(root, base) {
|
|
16
|
+
const diff = await run('git', ['diff', '-U0', '--no-color', '--no-ext-diff', base, '--'], { cwd: root, runtime: ambientRuntime() });
|
|
17
|
+
if (!diff.ok)
|
|
18
|
+
throw new Error(`git diff ${base} failed: ${diff.stderr.trim()}. fix: pass a ref this checkout has (fetch it first in CI)`);
|
|
19
|
+
return parseDiff(diff.stdout);
|
|
20
|
+
}
|
|
21
|
+
export function parseDiff(diff) {
|
|
22
|
+
const out = new Map();
|
|
23
|
+
let file;
|
|
24
|
+
for (const line of diff.split('\n')) {
|
|
25
|
+
if (line.startsWith(NEW_FILE)) {
|
|
26
|
+
const p = line.slice(NEW_FILE.length).trim();
|
|
27
|
+
file = p === '/dev/null' ? undefined : p.replace(/^b\//, '');
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
31
|
+
if (!hunk || !file)
|
|
32
|
+
continue;
|
|
33
|
+
const start = Number(hunk[1]);
|
|
34
|
+
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
|
|
35
|
+
if (count === 0)
|
|
36
|
+
continue; // a pure deletion adds no line to mutate
|
|
37
|
+
// Stryker disable next-line OptionalChaining: the get after a set always finds the list; `?.` is only for the type
|
|
38
|
+
(out.get(file) ?? out.set(file, []).get(file))?.push([start, start + count - 1]);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function touches(ranges, file, startLine, endLine) {
|
|
43
|
+
// Stryker disable next-line ArrayDeclaration: a string element destructures to letters, and a number compared with a letter is false, as with no ranges
|
|
44
|
+
return (ranges.get(file) ?? []).some(([a, b]) => startLine <= b && endLine >= a);
|
|
45
|
+
}
|
|
46
|
+
/** FNV-1a 32 over the stable id: the shard a mutant belongs to. */
|
|
47
|
+
export function shardOf(id, count) {
|
|
48
|
+
let h = FNV_OFFSET;
|
|
49
|
+
// Ids are hex (`native/src/stable_id.rs`), so a code point is a code unit and the shards are
|
|
50
|
+
// the ones `charCodeAt` gave.
|
|
51
|
+
for (let i = 0; i < id.length; i++) {
|
|
52
|
+
h ^= id.codePointAt(i) ?? 0;
|
|
53
|
+
h = Math.imul(h, FNV_PRIME) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
return h % count;
|
|
56
|
+
}
|
|
57
|
+
export function parseShard(value) {
|
|
58
|
+
const m = /^(\d+)\/(\d+)$/.exec(value);
|
|
59
|
+
const index = Number(m?.[1]);
|
|
60
|
+
const count = Number(m?.[2]);
|
|
61
|
+
// `count < 1` needs no check of its own: with 1 <= index <= count it cannot hold.
|
|
62
|
+
if (!m || index < 1 || index > count)
|
|
63
|
+
throw new Error(`--shard must be i/n with 1 <= i <= n, got "${value}"`);
|
|
64
|
+
return { index: index - 1, count };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The range end of Stryker's pattern (`mutate-pattern-constants.js`): `<glob>:<line>[:<col>]-<line>[:<col>]`.
|
|
68
|
+
* Only the suffix is matched; the glob is what comes before it. Stryker's lazy `^(.*?):` glob group
|
|
69
|
+
* backtracks polynomially (CodeQL js/polynomial-redos), and the leftmost match is the same split.
|
|
70
|
+
*/
|
|
71
|
+
const RANGE = /:(\d+)(?::(\d+))?-(\d+)(?::(\d+))?$/;
|
|
72
|
+
/** A `mutate` entry split into its glob and, when it names one, its range. */
|
|
73
|
+
export function parseMutateSpec(spec) {
|
|
74
|
+
const m = RANGE.exec(spec);
|
|
75
|
+
if (!m)
|
|
76
|
+
return { glob: spec };
|
|
77
|
+
// Stryker disable next-line StringLiteral: Number('') is 0 as Number('0') is
|
|
78
|
+
const [, startLine, startColumn = '0', endLine, endColumn = String(Number.MAX_SAFE_INTEGER)] = m;
|
|
79
|
+
return { glob: spec.slice(0, m.index), range: { start: { line: Number(startLine), column: Number(startColumn) }, end: { line: Number(endLine), column: Number(endColumn) } } };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Is the mutant wholly inside the range, as Stryker decides it (`locationIncluded`)? The mutant's
|
|
83
|
+
* columns are the report's, 1-based with the end exclusive; the range's are Stryker's, 0-based.
|
|
84
|
+
*/
|
|
85
|
+
export function withinRange(range, start, end) {
|
|
86
|
+
const from = start.column - 1;
|
|
87
|
+
const to = end.column - 1;
|
|
88
|
+
const startIn = range.start.line < start.line || (range.start.line === start.line && range.start.column <= from);
|
|
89
|
+
const endIn = range.end.line > end.line || (range.end.line === end.line && range.end.column >= to);
|
|
90
|
+
return startIn && endIn;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=lanes.js.map
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Location {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
export interface NativeMutant {
|
|
6
|
+
id: string;
|
|
7
|
+
mutatorName: string;
|
|
8
|
+
replacement: string;
|
|
9
|
+
start: Location;
|
|
10
|
+
end: Location;
|
|
11
|
+
/** Set when an ignore rule covers the mutant: reported as Ignored, never placed in the code. */
|
|
12
|
+
ignoredReason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface InstrumentedFile {
|
|
15
|
+
path: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
mutants: NativeMutant[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** A call whose subtree is not mutated (Stryker's ignorers, as data): `console.*`, `**.debug`. */
|
|
21
|
+
export interface IgnoreRule {
|
|
22
|
+
call: string;
|
|
23
|
+
reason?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
interface Binding {
|
|
26
|
+
instrumentFiles(files: {
|
|
27
|
+
path: string;
|
|
28
|
+
source: string;
|
|
29
|
+
}[], options?: {
|
|
30
|
+
strykerCompat?: boolean;
|
|
31
|
+
ignore?: IgnoreRule[];
|
|
32
|
+
}): InstrumentedFile[];
|
|
33
|
+
nativeVersion(): string;
|
|
34
|
+
}
|
|
35
|
+
export declare function native(): Binding;
|
|
36
|
+
export {};
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native core (`native/`, Rust on oxc), loaded for this platform. It ships as one prebuilt
|
|
3
|
+
* `.node` per platform (PRINCIPLES rule 9, `native-engine`): 0.1 bundles all six in the tarball as
|
|
4
|
+
* `binding/oxmutate.<tag>.node` (`platform.ts` names the tag); development builds
|
|
5
|
+
* `binding/oxmutate.<platform>-<arch>.node`; the `oxmutate-<tag>` platform package comes last.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { bindingNames, platformPackage } from './platform.js';
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const target = `${process.platform}-${process.arch}`;
|
|
13
|
+
/** This machine, as `platform.ts` reads it: glibc's runtime version from `process.report` (absent on musl). */
|
|
14
|
+
function machine() {
|
|
15
|
+
const glibc = process.platform === 'linux' ? process.report?.getReport()?.header?.glibcVersionRuntime : undefined;
|
|
16
|
+
return { platform: process.platform, arch: process.arch, glibc };
|
|
17
|
+
}
|
|
18
|
+
let binding;
|
|
19
|
+
export function native() {
|
|
20
|
+
if (binding)
|
|
21
|
+
return binding;
|
|
22
|
+
const self = machine();
|
|
23
|
+
// `binding/` beside `dist/` (published) or `src/` (development), then one level up.
|
|
24
|
+
const candidates = [path.join(here, '..', 'binding'), path.join(here, '..', '..', 'binding')].flatMap((dir) => bindingNames(self).map((name) => path.join(dir, name)));
|
|
25
|
+
try {
|
|
26
|
+
// `process.dlopen` loads a native addon straight from its path, the way `require` does it
|
|
27
|
+
// underneath. Since 0.1 the six release cores ship inside the tarball as
|
|
28
|
+
// `binding/oxmutate.<tag>.node`; the platform package (its `main`, `oxmutate.node`) is last,
|
|
29
|
+
// for when those publish.
|
|
30
|
+
const file = candidates.find((c) => existsSync(c)) ?? fileURLToPath(import.meta.resolve(platformPackage(self)));
|
|
31
|
+
const addon = { exports: {} };
|
|
32
|
+
process.dlopen(addon, file);
|
|
33
|
+
binding = addon.exports;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`oxmutate: no native core for ${target}. fix: install the platform package (npm i ${platformPackage(machine())}) or build it (npm run build:native in packages/oxmutate)`, { cause: error });
|
|
37
|
+
}
|
|
38
|
+
return binding;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=native.js.map
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which native core a machine needs, as napi-rs names it: `<platform>-<arch>[-gnu|-musl|-msvc]`.
|
|
3
|
+
* The tag names the bundled core (`binding/oxmutate.<tag>.node`), the platform package
|
|
4
|
+
* (`oxmutate-<tag>`, `npm/<tag>`) and the release matrix leg, so all three agree by construction.
|
|
5
|
+
* Pure: the machine is passed in, so every branch is testable on any one machine.
|
|
6
|
+
*/
|
|
7
|
+
/** `darwin-arm64`, `linux-x64-gnu`, `linux-x64-musl`, `win32-x64-msvc`, … */
|
|
8
|
+
export function platformTag({ platform, arch, glibc }) {
|
|
9
|
+
if (platform === 'win32')
|
|
10
|
+
return `win32-${arch}-msvc`;
|
|
11
|
+
if (platform === 'linux')
|
|
12
|
+
return `linux-${arch}-${glibc ? 'gnu' : 'musl'}`;
|
|
13
|
+
return `${platform}-${arch}`;
|
|
14
|
+
}
|
|
15
|
+
/** The npm package that carries the tag's core once the platform packages publish. */
|
|
16
|
+
export function platformPackage(machine) {
|
|
17
|
+
return `oxmutate-${platformTag(machine)}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The files to try, in order, under each `binding/` directory: the bundled release core for this
|
|
21
|
+
* tag, then the development build `scripts/copy-binding.mjs` writes (`<platform>-<arch>`, which is
|
|
22
|
+
* the same name on macOS).
|
|
23
|
+
*/
|
|
24
|
+
export function bindingNames(machine) {
|
|
25
|
+
return [...new Set([`oxmutate.${platformTag(machine)}.node`, `oxmutate.${machine.platform}-${machine.arch}.node`])];
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=platform.js.map
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stryker's `progress` reporters, one implementation: the phase, its own count and the time so
|
|
3
|
+
* far, on stderr. On a terminal the line rewrites itself every second (`progress`); anywhere else,
|
|
4
|
+
* or with `progress-append-only` (Stryker's `ci` alias), a new line every few seconds, so a CI log
|
|
5
|
+
* shows the run moving without a carriage-return mess. Off unless a progress reporter is named.
|
|
6
|
+
*/
|
|
7
|
+
const REPORTERS = new Set(['progress', 'progress-append-only', 'ci', 'dots']);
|
|
8
|
+
const TTY_EVERY_MS = 1000;
|
|
9
|
+
const APPEND_EVERY_MS = 10_000;
|
|
10
|
+
const MS_PER_S = 1000;
|
|
11
|
+
export function startProgress(reporters, out, now = () => performance.now()) {
|
|
12
|
+
const enabled = reporters.some((r) => REPORTERS.has(r));
|
|
13
|
+
const rewrite = Boolean(out.isTTY) && reporters.includes('progress');
|
|
14
|
+
const start = now();
|
|
15
|
+
// Stryker disable next-line StringLiteral: line() runs only after phase() has set the name, so the initial value is never printed
|
|
16
|
+
let name = '';
|
|
17
|
+
let detail;
|
|
18
|
+
let timer;
|
|
19
|
+
const line = () => `oxmutate: ${name}${detail ? `, ${detail()}` : ''}, ${((now() - start) / MS_PER_S).toFixed(0)} s`;
|
|
20
|
+
const tick = () => {
|
|
21
|
+
out.write(rewrite ? `\r\u001B[2K${line()}` : `${line()}\n`);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
phase(next, nextDetail) {
|
|
25
|
+
if (!enabled)
|
|
26
|
+
return;
|
|
27
|
+
name = next;
|
|
28
|
+
detail = nextDetail;
|
|
29
|
+
tick();
|
|
30
|
+
timer ??= setInterval(tick, rewrite ? TTY_EVERY_MS : APPEND_EVERY_MS);
|
|
31
|
+
timer.unref();
|
|
32
|
+
},
|
|
33
|
+
stop() {
|
|
34
|
+
clearInterval(timer);
|
|
35
|
+
if (enabled && rewrite)
|
|
36
|
+
out.write('\n');
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MutantResult, RunResult } from './coordinator.js';
|
|
2
|
+
/** The package's own version, read from its manifest so `--version` and the reports never drift from it. */
|
|
3
|
+
export declare const VERSION: string;
|
|
4
|
+
/** `mutation-testing-report-schema` 2, the format Stryker writes (`jsonReporter`). */
|
|
5
|
+
export declare function strykerReport(r: RunResult): object;
|
|
6
|
+
/** SARIF 2.1.0: one result per surviving or uncovered mutant, fingerprinted by stable id. */
|
|
7
|
+
export declare function sarifReport(r: RunResult): object;
|
|
8
|
+
export declare function describe(m: MutantResult): string;
|
|
9
|
+
/** The terminal summary: the score, what to look at first, what the run corrected. */
|
|
10
|
+
export declare function textSummary(r: RunResult, opts?: {
|
|
11
|
+
survivors?: number;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Stryker's HTML report: the JSON report rendered by `mutation-testing-elements`. The component is
|
|
15
|
+
* inlined from the project's own install when it has one (Stryker brings it), so the page works
|
|
16
|
+
* offline as Stryker's does; otherwise the page loads that exact version from a CDN. oxmutate does
|
|
17
|
+
* not depend on it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function htmlReport(r: RunResult): string;
|
|
20
|
+
export declare function writeReports(r: RunResult): string[];
|