@systemfsoftware/stryker-js-cli 3.1.0 → 4.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/CHANGELOG.md +164 -0
- package/README.md +8 -7
- package/dist/main.mjs +1440 -1202
- package/package.json +16 -16
package/dist/main.mjs
CHANGED
|
@@ -1,493 +1,620 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
4
|
+
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
2
5
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
3
6
|
import * as NodeStdio from "@effect/platform-node/NodeStdio";
|
|
7
|
+
import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, readConfig, runMutationTest, strykerEngines, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-platform-node";
|
|
4
8
|
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Exit from "effect/Exit";
|
|
5
10
|
import * as Layer from "effect/Layer";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import * as CliError from "effect/unstable/cli/CliError";
|
|
11
|
+
import * as Logger from "effect/Logger";
|
|
12
|
+
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
13
|
+
import { Heartbeat, HelpRendered, ManifestRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
|
|
14
|
+
import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
|
|
11
15
|
import * as Cause from "effect/Cause";
|
|
12
|
-
import * as Clock from "effect/Clock";
|
|
13
|
-
import * as Deferred from "effect/Deferred";
|
|
14
|
-
import * as Fiber from "effect/Fiber";
|
|
15
|
-
import * as Queue from "effect/Queue";
|
|
16
|
-
import * as Stdio from "effect/Stdio";
|
|
17
|
-
import * as Stream from "effect/Stream";
|
|
18
|
-
import { buildVerdictEnvelope, generateRunId } from "@systemfsoftware/stryker-js-mutation-run/verdict-envelope";
|
|
19
16
|
import * as Console from "effect/Console";
|
|
17
|
+
import * as Fiber from "effect/Fiber";
|
|
20
18
|
import * as FileSystem from "effect/FileSystem";
|
|
19
|
+
import * as Match from "effect/Match";
|
|
21
20
|
import * as Option from "effect/Option";
|
|
22
21
|
import * as Path from "effect/Path";
|
|
22
|
+
import * as Queue from "effect/Queue";
|
|
23
23
|
import * as Ref from "effect/Ref";
|
|
24
|
+
import * as Result from "effect/Result";
|
|
25
|
+
import * as S from "effect/Schema";
|
|
24
26
|
import * as Terminal from "effect/Terminal";
|
|
25
27
|
import * as Argument from "effect/unstable/cli/Argument";
|
|
26
28
|
import * as CliConfig from "effect/unstable/cli/CliConfig";
|
|
29
|
+
import * as CliError from "effect/unstable/cli/CliError";
|
|
27
30
|
import * as Command from "effect/unstable/cli/Command";
|
|
28
31
|
import * as Flag from "effect/unstable/cli/Flag";
|
|
29
32
|
import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
|
|
30
|
-
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
|
|
31
|
-
import { ConfigReader, OptionsValidator, defaultOptions } from "@systemfsoftware/stryker-js-mutation-run/config/config-resolution";
|
|
32
|
-
import { PluginKind } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
33
|
-
import { strykerPlugins } from "@systemfsoftware/stryker-js-mutation-report/stryker-plugins";
|
|
34
|
-
import { createHash } from "node:crypto";
|
|
35
33
|
import { readFileSync } from "node:fs";
|
|
36
34
|
import { resolve } from "node:path";
|
|
35
|
+
import { ExitClass, highestExitClass } from "@systemfsoftware/stryker-js/ExitClass";
|
|
36
|
+
import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
|
|
37
|
+
import * as Clock from "effect/Clock";
|
|
38
|
+
import * as Formatter from "effect/Formatter";
|
|
39
|
+
import * as Predicate from "effect/Predicate";
|
|
37
40
|
import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
38
|
-
import "
|
|
39
|
-
import { noopLogger } from "@systemfsoftware/stryker-js-util";
|
|
40
|
-
import * as Exit from "effect/Exit";
|
|
41
|
+
import * as Context from "effect/Context";
|
|
41
42
|
import { pipe } from "effect/Function";
|
|
42
|
-
import * as
|
|
43
|
-
import * as
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
signal: "agent",
|
|
95
|
-
stdoutIsTTY: true
|
|
43
|
+
import * as Stdio from "effect/Stdio";
|
|
44
|
+
import * as Stream from "effect/Stream";
|
|
45
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
46
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
|
|
47
|
+
//#region src/RunOutcome.workflow.ts
|
|
48
|
+
var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
|
|
49
|
+
succeeded: S.Boolean,
|
|
50
|
+
signal: S.optional(S.Finite),
|
|
51
|
+
interrupted: S.Boolean,
|
|
52
|
+
helpErrorCount: S.optional(S.Finite),
|
|
53
|
+
cliError: S.Boolean,
|
|
54
|
+
unrecognized: S.optional(S.String),
|
|
55
|
+
survivorsReason: S.optional(S.Literals(["no-report", "mismatch"])),
|
|
56
|
+
survivorsDiagnostic: S.optional(S.String),
|
|
57
|
+
schemaError: S.Boolean,
|
|
58
|
+
successExitClass: S.optional(ExitClass),
|
|
59
|
+
highestExitClass: S.optional(ExitClass),
|
|
60
|
+
configDetail: S.optional(S.String),
|
|
61
|
+
diagnostic: S.optional(S.String)
|
|
62
|
+
}) {};
|
|
63
|
+
const CONFIG_CODE$1 = 2;
|
|
64
|
+
const classCode = (exitClass) => Match.value(exitClass).pipe(Match.when("VerdictFail", () => 1), Match.when("ConfigError", () => CONFIG_CODE$1), Match.when("RuntimeError", () => 3), Match.when("InternalError", () => 4), Match.exhaustive);
|
|
65
|
+
var RunOk = class extends S.TaggedClass()("RunOk", { help: S.Boolean }) {};
|
|
66
|
+
var RunInterrupted = class extends S.TaggedError()("RunInterrupted", { code: S.Finite }) {};
|
|
67
|
+
var RunParseFailed = class extends S.TaggedError()("RunParseFailed", { unrecognized: S.optional(S.String) }) {};
|
|
68
|
+
var RunSurvivorsRejected = class extends S.TaggedError()("RunSurvivorsRejected", {
|
|
69
|
+
reason: S.Literals(["no-report", "mismatch"]),
|
|
70
|
+
diagnostic: S.optional(S.String)
|
|
71
|
+
}) {};
|
|
72
|
+
var RunConfigFailed = class extends S.TaggedError()("RunConfigFailed", { detail: S.optional(S.String) }) {};
|
|
73
|
+
var RunFailed$1 = class extends S.TaggedError()("RunFailed", {
|
|
74
|
+
code: S.Finite,
|
|
75
|
+
diagnostic: S.optional(S.String)
|
|
76
|
+
}) {};
|
|
77
|
+
function classify(command) {
|
|
78
|
+
if (command.signal !== void 0) return RunInterrupted.make({ code: 128 + command.signal });
|
|
79
|
+
if (command.succeeded) {
|
|
80
|
+
if (command.successExitClass !== void 0) return RunFailed$1.make({
|
|
81
|
+
code: classCode(command.successExitClass),
|
|
82
|
+
diagnostic: command.diagnostic
|
|
83
|
+
});
|
|
84
|
+
return RunOk.make({ help: false });
|
|
85
|
+
}
|
|
86
|
+
if (command.interrupted) return RunInterrupted.make({ code: 1 });
|
|
87
|
+
if (command.helpErrorCount !== void 0) {
|
|
88
|
+
if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
|
|
89
|
+
return RunOk.make({ help: true });
|
|
90
|
+
}
|
|
91
|
+
if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
|
|
92
|
+
if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
|
|
93
|
+
reason: command.survivorsReason,
|
|
94
|
+
diagnostic: command.survivorsDiagnostic
|
|
96
95
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
|
|
97
|
+
if (command.highestExitClass !== void 0) {
|
|
98
|
+
if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
|
|
99
|
+
return RunFailed$1.make({
|
|
100
|
+
code: classCode(command.highestExitClass),
|
|
101
|
+
diagnostic: command.diagnostic
|
|
103
102
|
});
|
|
104
103
|
}
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
stdoutIsTTY: true
|
|
104
|
+
return RunFailed$1.make({
|
|
105
|
+
code: 1,
|
|
106
|
+
diagnostic: command.diagnostic
|
|
109
107
|
});
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* its control sequences into a pipe, and machine mode keeps stdout clean for
|
|
114
|
-
* the verdict envelope (R5). Decided from the resolved mode's own detection
|
|
115
|
-
* data — never a second `isTTY` probe.
|
|
116
|
-
*/
|
|
117
|
-
function isProgressEnabled(resolved) {
|
|
118
|
-
return resolved.mode === "human" && resolved.stdoutIsTTY;
|
|
109
|
+
function succeedRun(ok) {
|
|
110
|
+
return Result.succeed(ok);
|
|
119
111
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* harness merging `2>&1` is not handed escape sequences it must strip, and
|
|
123
|
-
* `NO_COLOR` is honoured for the human path per the convention: any value
|
|
124
|
-
* other than an unset or empty variable disables colour.
|
|
125
|
-
*/
|
|
126
|
-
function isColorEnabled(resolved, noColor) {
|
|
127
|
-
return resolved.mode === "human" && (noColor === void 0 || noColor.length === 0);
|
|
112
|
+
function failRun(error) {
|
|
113
|
+
return Result.fail(error);
|
|
128
114
|
}
|
|
115
|
+
function runOutcomeDecision(command) {
|
|
116
|
+
return Match.value(classify(command)).pipe(Match.tag("RunOk", succeedRun), Match.tag("RunInterrupted", failRun), Match.tag("RunParseFailed", failRun), Match.tag("RunSurvivorsRejected", failRun), Match.tag("RunConfigFailed", failRun), Match.tag("RunFailed", failRun), Match.exhaustive);
|
|
117
|
+
}
|
|
118
|
+
const runOutcomeWorkflow = Workflow.make(RunOutcomeCommand, runOutcomeDecision);
|
|
129
119
|
//#endregion
|
|
130
|
-
//#region src/
|
|
131
|
-
var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/OutputModeAdapter/OutputModeProbeTag") {};
|
|
132
|
-
const OutputModeProbe = OutputModeProbeTag;
|
|
133
|
-
const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: () => {
|
|
134
|
-
const envMode = process.env["STRYKER_MODE"];
|
|
135
|
-
const agent = process.env["AGENT"];
|
|
136
|
-
return Result.getOrThrow(resolveMode({
|
|
137
|
-
stdoutIsTTY: process.stdout.isTTY === true,
|
|
138
|
-
...envMode !== void 0 ? { envMode } : {},
|
|
139
|
-
...agent !== void 0 ? { agent } : {},
|
|
140
|
-
toolVars: Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]]))
|
|
141
|
-
}));
|
|
142
|
-
} }));
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/StreamProtocol.ts
|
|
120
|
+
//#region src/Survivors.workflow.ts
|
|
145
121
|
/**
|
|
146
|
-
* The
|
|
147
|
-
*
|
|
148
|
-
|
|
122
|
+
* The mutant shape the admission carries, named once because both the decision's
|
|
123
|
+
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
124
|
+
*/
|
|
125
|
+
const MutantShape = S.Struct({
|
|
126
|
+
id: S.String,
|
|
127
|
+
fileName: S.String,
|
|
128
|
+
mutatorName: S.String,
|
|
129
|
+
replacement: S.String,
|
|
130
|
+
location: S.Struct({
|
|
131
|
+
start: S.Struct({
|
|
132
|
+
line: S.Finite,
|
|
133
|
+
column: S.Finite
|
|
134
|
+
}),
|
|
135
|
+
end: S.Struct({
|
|
136
|
+
line: S.Finite,
|
|
137
|
+
column: S.Finite
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
const PriorReportDocument = S.Struct({
|
|
142
|
+
config: S.optional(S.Record(S.String, S.Unknown)),
|
|
143
|
+
framework: S.optional(S.Struct({ version: S.optional(S.String) })),
|
|
144
|
+
files: S.Record(S.String, S.Struct({
|
|
145
|
+
source: S.String,
|
|
146
|
+
mutants: S.Array(S.Struct({
|
|
147
|
+
id: S.String,
|
|
148
|
+
mutatorName: S.String,
|
|
149
|
+
replacement: S.optional(S.String),
|
|
150
|
+
status: S.String,
|
|
151
|
+
location: S.Struct({
|
|
152
|
+
start: S.Struct({
|
|
153
|
+
line: S.Finite,
|
|
154
|
+
column: S.Finite
|
|
155
|
+
}),
|
|
156
|
+
end: S.Struct({
|
|
157
|
+
line: S.Finite,
|
|
158
|
+
column: S.Finite
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
}))
|
|
162
|
+
}))
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
|
|
166
|
+
*
|
|
167
|
+
* The `--survivors` run re-tests exactly the mutants that survived a previous
|
|
168
|
+
* run. Its input is the previous run's mutation report, and the run is
|
|
169
|
+
* admitted only when a single structural hash of the resolved options, the
|
|
170
|
+
* recorded framework version, and the per-file source content all match the
|
|
171
|
+
* current run (KTD6).
|
|
149
172
|
*/
|
|
150
|
-
const TICK_INTERVAL_MS = 1e4;
|
|
151
|
-
//#endregion
|
|
152
|
-
//#region src/RunEventStreamAdapter.ts
|
|
153
|
-
var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/RunEventStreamAdapter/RunEventStreamPortTag") {};
|
|
154
|
-
const RunEventStreamPort = RunEventStreamPortTag;
|
|
155
|
-
const isTerminalEvent = (event) => event.kind === "verdict" || event.kind === "error" || event.kind === "help" || event.kind === "manifest";
|
|
156
173
|
/**
|
|
157
|
-
* The
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
174
|
+
* The decision's helpers reach three language built-ins the purity gate cannot
|
|
175
|
+
* resolve as globals, so each is bound at module scope.
|
|
176
|
+
*
|
|
177
|
+
* These live here, beside the decision, because `make-body-purity` follows the
|
|
178
|
+
* decision's reachable set: a helper `admissionVerdict` calls is checked as part
|
|
179
|
+
* of the body even though it is declared outside it. That is why the properties
|
|
180
|
+
* covering them are in this file's in-source block rather than beside a pure helper -
|
|
181
|
+
* testing a copy the decision does not run is worse than not testing it, because
|
|
182
|
+
* the suite goes green either way.
|
|
161
183
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
end: () => {
|
|
168
|
-
Effect.runSync(Queue.end(queue));
|
|
169
|
-
}
|
|
170
|
-
};
|
|
184
|
+
const isArray = Array.isArray;
|
|
185
|
+
const { fromEntries: objectFromEntries$1, keys: objectKeys } = Object;
|
|
186
|
+
const stringify = JSON.stringify;
|
|
187
|
+
function isRecord(value) {
|
|
188
|
+
return typeof value === "object" && value !== null && !isArray(value);
|
|
171
189
|
}
|
|
190
|
+
const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
|
|
191
|
+
const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
|
|
172
192
|
/**
|
|
173
|
-
* The
|
|
174
|
-
*
|
|
175
|
-
* the sink owns the writable's backpressure, the scoped `'error'` listener a
|
|
176
|
-
* closed consumer raises, and the final `'finish'` wait (`endOnDone`). A
|
|
177
|
-
* write failure surfaces as a `PlatformError` which this catch swallows — a
|
|
178
|
-
* consumer closing the pipe must not replace the run's classed exit code
|
|
179
|
-
* (R31) — and the drain still completes only after every byte was handed to
|
|
180
|
-
* the OS (R30).
|
|
193
|
+
* The resolved options without the survivors-run bookkeeping keys, so both
|
|
194
|
+
* sides of the admission comparison describe the same configuration.
|
|
181
195
|
*/
|
|
182
|
-
|
|
196
|
+
function stripSurvivorsKeys(config) {
|
|
197
|
+
if (!isRecord(config)) return {};
|
|
198
|
+
const rest = { ...config };
|
|
199
|
+
for (const key of SURVIVORS_BOOKKEEPING_KEYS) delete rest[key];
|
|
200
|
+
return rest;
|
|
201
|
+
}
|
|
183
202
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* the adapter never touches the wall clock directly.
|
|
203
|
+
* A report written by a survivors run embeds the bookkeeping key in its
|
|
204
|
+
* `config`. Such a report is never a valid input for another survivors run
|
|
205
|
+
* (KTD7): without this check the second run would either re-read a shrunken set
|
|
206
|
+
* or re-test a stale one.
|
|
189
207
|
*/
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
const state = {
|
|
194
|
-
mode: resolved.mode,
|
|
195
|
-
signal: resolved.signal,
|
|
196
|
-
emit: null,
|
|
197
|
-
headerWritten: false,
|
|
198
|
-
terminalWritten: false,
|
|
199
|
-
progress: {
|
|
200
|
-
completed: 0,
|
|
201
|
-
total: null
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
const registered = yield* Deferred.make();
|
|
205
|
-
const eventStream = Stream.callback((queue) => Effect.sync(() => {
|
|
206
|
-
state.emit = queueEmit(queue);
|
|
207
|
-
}).pipe(Effect.andThen(Deferred.succeed(registered, void 0))));
|
|
208
|
-
const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.filter(() => state.mode === "machine" && state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
|
|
209
|
-
return {
|
|
210
|
-
kind: "tick",
|
|
211
|
-
elapsedMs: (yield* Clock.currentTimeMillis) - startedAt,
|
|
212
|
-
completed: state.progress.completed,
|
|
213
|
-
total: state.progress.total
|
|
214
|
-
};
|
|
215
|
-
})));
|
|
216
|
-
let terminalSeen = false;
|
|
217
|
-
const framed = Stream.merge(eventStream, tickStream, { haltStrategy: "either" }).pipe(Stream.filter((event) => {
|
|
218
|
-
if (terminalSeen) return false;
|
|
219
|
-
if (isTerminalEvent(event)) terminalSeen = true;
|
|
220
|
-
return true;
|
|
221
|
-
}), Stream.map((event) => `${JSON.stringify(event)}\n`));
|
|
222
|
-
const drain = drainOf(stdio, framed);
|
|
223
|
-
let drainFiber = null;
|
|
224
|
-
const sink = (event) => {
|
|
225
|
-
const emit = state.emit;
|
|
226
|
-
if (emit === null || state.terminalWritten || state.mode !== "machine") return;
|
|
227
|
-
if (!state.headerWritten) {
|
|
228
|
-
state.headerWritten = true;
|
|
229
|
-
emit.single({
|
|
230
|
-
kind: "stream",
|
|
231
|
-
schemaVersion: "1.0",
|
|
232
|
-
runId,
|
|
233
|
-
mode: state.mode,
|
|
234
|
-
signal: state.signal
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
switch (event.kind) {
|
|
238
|
-
case "stream":
|
|
239
|
-
case "tick": return;
|
|
240
|
-
case "phase": break;
|
|
241
|
-
case "plan":
|
|
242
|
-
state.progress = {
|
|
243
|
-
...state.progress,
|
|
244
|
-
total: event.total
|
|
245
|
-
};
|
|
246
|
-
break;
|
|
247
|
-
case "mutant":
|
|
248
|
-
state.progress = {
|
|
249
|
-
completed: event.completed,
|
|
250
|
-
total: event.total
|
|
251
|
-
};
|
|
252
|
-
break;
|
|
253
|
-
case "verdict":
|
|
254
|
-
case "error":
|
|
255
|
-
case "help":
|
|
256
|
-
case "manifest":
|
|
257
|
-
emit.single(event);
|
|
258
|
-
state.terminalWritten = true;
|
|
259
|
-
emit.end();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
emit.single(event);
|
|
263
|
-
};
|
|
264
|
-
return {
|
|
265
|
-
sink,
|
|
266
|
-
runId,
|
|
267
|
-
startedAt,
|
|
268
|
-
isOpen: () => state.emit !== null && state.mode === "machine" && !state.terminalWritten,
|
|
269
|
-
ensureOpen: (openResolved) => {
|
|
270
|
-
if (state.headerWritten) return;
|
|
271
|
-
state.mode = openResolved.mode;
|
|
272
|
-
state.signal = openResolved.signal;
|
|
273
|
-
},
|
|
274
|
-
open: Effect.gen(function* () {
|
|
275
|
-
if (drainFiber === null) {
|
|
276
|
-
drainFiber = yield* Effect.forkDetach(drain);
|
|
277
|
-
yield* Effect.race(Deferred.await(registered), Fiber.await(drainFiber));
|
|
278
|
-
}
|
|
279
|
-
}),
|
|
280
|
-
closeAndDrain: Effect.gen(function* () {
|
|
281
|
-
state.terminalWritten = true;
|
|
282
|
-
state.emit?.end();
|
|
283
|
-
if (drainFiber !== null) yield* Fiber.join(drainFiber);
|
|
284
|
-
})
|
|
285
|
-
};
|
|
286
|
-
});
|
|
287
|
-
const RunEventStreamLive = Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
|
|
288
|
-
function isObject(value) {
|
|
289
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
290
|
-
}
|
|
291
|
-
function stringField(node, key) {
|
|
292
|
-
const value = node[key];
|
|
293
|
-
return typeof value === "string" ? value : void 0;
|
|
294
|
-
}
|
|
295
|
-
function stringArrayField(node, key) {
|
|
296
|
-
const value = node[key];
|
|
297
|
-
if (!Array.isArray(value)) return [];
|
|
298
|
-
const strings = [];
|
|
299
|
-
for (const item of value) if (typeof item === "string") strings.push(item);
|
|
300
|
-
return strings;
|
|
301
|
-
}
|
|
302
|
-
/** The compiled shapes discriminate on `_tag`; read it once, off the record. */
|
|
303
|
-
function tagOf(node) {
|
|
304
|
-
return node["_tag"];
|
|
208
|
+
function wasProducedBySurvivorsRun(priorReport) {
|
|
209
|
+
const config = priorReport.config;
|
|
210
|
+
return isRecord(config) && "survivorsPriorReport" in config;
|
|
305
211
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
case "Map":
|
|
313
|
-
case "Transform":
|
|
314
|
-
walkParam(param["param"], isOptional, out);
|
|
315
|
-
return;
|
|
316
|
-
case "Optional":
|
|
317
|
-
walkParam(param["param"], true, out);
|
|
318
|
-
return;
|
|
319
|
-
case "Variadic":
|
|
320
|
-
walkParam(param["param"], isOptional, out);
|
|
321
|
-
return;
|
|
322
|
-
default: return;
|
|
323
|
-
}
|
|
212
|
+
/**
|
|
213
|
+
* Serializes the comparison input with keys sorted at every level, so the result
|
|
214
|
+
* is a function of the data and not of key insertion order.
|
|
215
|
+
*/
|
|
216
|
+
function serializeSurvivorsHashInput(input) {
|
|
217
|
+
return stringify(sortKeys(input));
|
|
324
218
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
FileParse: "file",
|
|
330
|
-
FileSchema: "file",
|
|
331
|
-
FileText: "file",
|
|
332
|
-
Float: "float",
|
|
333
|
-
Integer: "integer",
|
|
334
|
-
KeyValuePair: "key=value",
|
|
335
|
-
None: "none",
|
|
336
|
-
Path: "path",
|
|
337
|
-
Redacted: "redacted",
|
|
338
|
-
String: "text"
|
|
339
|
-
};
|
|
340
|
-
function kindOf(primitive) {
|
|
341
|
-
const tag = stringField(primitive, "_tag");
|
|
342
|
-
return tag === void 0 ? "unknown" : PRIMITIVE_KIND[tag] ?? tag;
|
|
219
|
+
function sortKeys(value) {
|
|
220
|
+
if (isArray(value)) return value.map(sortKeys);
|
|
221
|
+
if (isRecord(value)) return objectFromEntries$1(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
222
|
+
return value;
|
|
343
223
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The prior report's facts the decision reads: its embedded configuration, which carries
|
|
226
|
+
* both the compared options and the survivors-run provenance marker, and the engine
|
|
227
|
+
* version it recorded. The report's files are not here — the survivors and the per-file
|
|
228
|
+
* source hashes derived from them need capabilities the command cannot hold, so they
|
|
229
|
+
* arrive already computed.
|
|
230
|
+
*/
|
|
231
|
+
var PriorReportFacts = class extends S.Class("PriorReportFacts")({
|
|
232
|
+
config: S.Record(S.String, S.Unknown),
|
|
233
|
+
frameworkVersion: S.UndefinedOr(S.String)
|
|
234
|
+
}) {};
|
|
235
|
+
/**
|
|
236
|
+
* The command of the admission workflow: a schema class, because `Workflow.make`
|
|
237
|
+
* constrains its first argument on the class value and a declared interface produces no
|
|
238
|
+
* value to pass. Every field is pure data — the two capabilities the previous shape
|
|
239
|
+
* carried, a digest function and a path resolver, can never be schema fields, so their
|
|
240
|
+
* results arrive precomputed from the decode phase instead.
|
|
241
|
+
*/
|
|
242
|
+
var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
|
|
243
|
+
/**
|
|
244
|
+
* The prior run's report facts, `undefined` when no report exists — the run cannot be
|
|
245
|
+
* admitted without one ('no-report'). Explicitly nullable rather than key-optional: a
|
|
246
|
+
* missing report is a state the edge determined and states, not a key it forgot.
|
|
247
|
+
*/
|
|
248
|
+
priorReport: S.UndefinedOr(PriorReportFacts),
|
|
249
|
+
/** The current run's resolved options (defaults + config file + CLI). */
|
|
250
|
+
currentConfig: S.Record(S.String, S.Unknown),
|
|
251
|
+
/** The current CLI/framework version (`strykerVersion`). */
|
|
252
|
+
frameworkVersion: S.String,
|
|
253
|
+
/**
|
|
254
|
+
* Per-file content hashes of the current source, keyed by the prior report's relative
|
|
255
|
+
* file keys. The prior side is hashed from the sources the report embeds, so an editor
|
|
256
|
+
* save that shifts line ranges — which would silently re-test a different mutant than
|
|
257
|
+
* the one that survived — is caught here.
|
|
258
|
+
*/
|
|
259
|
+
sourceContentHashes: S.Record(S.String, S.String),
|
|
260
|
+
/** The same hashes for the sources the prior report embeds, computed at the edge. */
|
|
261
|
+
priorSourceHashes: S.Record(S.String, S.String),
|
|
262
|
+
/** The prior report's survivors, already converted to the internal mutant shape. */
|
|
263
|
+
priorSurvivors: S.Array(MutantShape)
|
|
264
|
+
}) {};
|
|
265
|
+
const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
|
|
266
|
+
const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
|
|
267
|
+
const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
|
|
268
|
+
/**
|
|
269
|
+
* Whether the admission inputs agree: the prior report's embedded resolved options,
|
|
270
|
+
* framework version and source content against the current run's.
|
|
271
|
+
*
|
|
272
|
+
* The comparison is on the canonical serializations rather than digests of them. Equal
|
|
273
|
+
* serializations are equal runs, so the digest was a lossy restatement of the check that
|
|
274
|
+
* also demanded a capability no command can carry.
|
|
275
|
+
*/
|
|
276
|
+
function hashesMatch(priorReport, input) {
|
|
277
|
+
return serializeSurvivorsHashInput({
|
|
278
|
+
resolvedOptions: stripSurvivorsKeys(priorReport.config),
|
|
279
|
+
frameworkVersion: priorReport.frameworkVersion,
|
|
280
|
+
sourceContentHashes: input.priorSourceHashes
|
|
281
|
+
}) === serializeSurvivorsHashInput({
|
|
282
|
+
resolvedOptions: stripSurvivorsKeys(input.currentConfig),
|
|
283
|
+
frameworkVersion: input.frameworkVersion,
|
|
284
|
+
sourceContentHashes: input.sourceContentHashes
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
|
|
288
|
+
var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
|
|
289
|
+
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
290
|
+
};
|
|
291
|
+
var NoSurvivors = class extends S.TaggedClass()("NoSurvivors", {}) {
|
|
292
|
+
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
293
|
+
};
|
|
294
|
+
S.Union([Admitted, NoSurvivors]);
|
|
295
|
+
var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
|
|
296
|
+
reason: S.Literals(["no-report", "mismatch"]),
|
|
297
|
+
remediation: S.String
|
|
298
|
+
}) {
|
|
299
|
+
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
300
|
+
};
|
|
301
|
+
function reject(reason, detail) {
|
|
302
|
+
return Result.fail(SurvivorsRejection.make({
|
|
303
|
+
reason,
|
|
304
|
+
remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
function decideAdmission(input) {
|
|
308
|
+
const priorReport = input.priorReport;
|
|
309
|
+
if (priorReport === void 0) return reject("no-report", NO_REPORT_DETAIL);
|
|
310
|
+
if (wasProducedBySurvivorsRun(priorReport)) return reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
|
|
311
|
+
if (input.priorSurvivors.length === 0) return Result.succeed(NoSurvivors.make());
|
|
312
|
+
if (!hashesMatch(priorReport, input)) return reject("mismatch", MISMATCH_DETAIL);
|
|
313
|
+
return Result.succeed(Admitted.make({ survivors: input.priorSurvivors }));
|
|
314
|
+
}
|
|
315
|
+
function admissionDecision(command) {
|
|
316
|
+
return decideAdmission(command);
|
|
350
317
|
}
|
|
351
|
-
|
|
352
|
-
|
|
318
|
+
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, admissionDecision);
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/Envelope.ts
|
|
353
321
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
322
|
+
* Envelope — the failure envelope and console capture leaf.
|
|
323
|
+
*
|
|
324
|
+
* Extracted from Cli.ts to break the import cycle Cli <-> Output.
|
|
325
|
+
* Both Cli and Output import from this leaf, so neither depends on the other
|
|
326
|
+
* for these values. This file imports only from external packages and from
|
|
327
|
+
* StreamVersion (leaf) and Survivors.workflow (leaf), never from Cli or Output
|
|
328
|
+
* themselves.
|
|
356
329
|
*/
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
330
|
+
const CONFIG_CODE = 2;
|
|
331
|
+
function runOutcomeCode(result) {
|
|
332
|
+
if (Result.isSuccess(result)) return 0;
|
|
333
|
+
return Match.value(result.failure).pipe(Match.tag("RunInterrupted", (error) => error.code), Match.tag("RunParseFailed", () => CONFIG_CODE), Match.tag("RunSurvivorsRejected", () => CONFIG_CODE), Match.tag("RunConfigFailed", () => CONFIG_CODE), Match.tag("RunFailed", (error) => error.code), Match.exhaustive);
|
|
334
|
+
}
|
|
335
|
+
function isExitClass(value) {
|
|
336
|
+
return S.is(ExitClass)(value);
|
|
337
|
+
}
|
|
338
|
+
function exitClassOf(value) {
|
|
339
|
+
if (typeof value !== "object" || value === null) return;
|
|
340
|
+
if (!("exitClass" in value)) return;
|
|
341
|
+
const raw = Reflect.get(value, "exitClass");
|
|
342
|
+
if (!isExitClass(raw)) return;
|
|
343
|
+
return raw;
|
|
344
|
+
}
|
|
345
|
+
function collectExitClassesFromValue(value, out, seen, depth) {
|
|
346
|
+
if (depth > 10 || value === null || value === void 0) return;
|
|
347
|
+
if (typeof value !== "object") return;
|
|
348
|
+
if (seen.has(value)) return;
|
|
349
|
+
seen.add(value);
|
|
350
|
+
const ec = exitClassOf(value);
|
|
351
|
+
if (ec !== void 0) out.push(ec);
|
|
352
|
+
if ("cause" in value) {
|
|
353
|
+
const causeVal = Reflect.get(value, "cause");
|
|
354
|
+
if (Array.isArray(causeVal)) for (const entry of causeVal) collectExitClassesFromValue(entry, out, seen, depth + 1);
|
|
355
|
+
else collectExitClassesFromValue(causeVal, out, seen, depth + 1);
|
|
366
356
|
}
|
|
367
357
|
}
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
aliases: stringArrayField(single, "aliases"),
|
|
378
|
-
kind,
|
|
379
|
-
required,
|
|
380
|
-
...choices !== void 0 ? { choices } : {},
|
|
381
|
-
description
|
|
382
|
-
};
|
|
383
|
-
if (single["kind"] === "argument") {
|
|
384
|
-
out.args.push({
|
|
385
|
-
name,
|
|
386
|
-
kind,
|
|
387
|
-
required,
|
|
388
|
-
description
|
|
389
|
-
});
|
|
390
|
-
return;
|
|
358
|
+
function collectExitClasses(exit) {
|
|
359
|
+
const out = [];
|
|
360
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
361
|
+
if (Exit.isFailure(exit)) for (const reason of exit.cause.reasons) {
|
|
362
|
+
let candidate;
|
|
363
|
+
if (Cause.isFailReason(reason)) candidate = reason.error;
|
|
364
|
+
else if (Cause.isDieReason(reason)) candidate = reason.defect;
|
|
365
|
+
else candidate = void 0;
|
|
366
|
+
if (candidate !== void 0) collectExitClassesFromValue(candidate, out, seen, 0);
|
|
391
367
|
}
|
|
392
|
-
out
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
function reasonOf(value) {
|
|
371
|
+
if (!("reason" in value)) return;
|
|
372
|
+
const reason = Reflect.get(value, "reason");
|
|
373
|
+
if (typeof reason !== "string" || reason.length === 0) return;
|
|
374
|
+
const detail = causeTextOf(value);
|
|
375
|
+
if (detail === void 0) return reason;
|
|
376
|
+
return `${reason}: ${detail}`;
|
|
377
|
+
}
|
|
378
|
+
function causeTextOf(value, depth = 0) {
|
|
379
|
+
if (depth > 4 || !("cause" in value)) return;
|
|
380
|
+
const cause = Reflect.get(value, "cause");
|
|
381
|
+
return causeText(cause, depth + 1);
|
|
382
|
+
}
|
|
383
|
+
function configDetailOf(value) {
|
|
384
|
+
if (!("reason" in value) && !("message" in value)) return;
|
|
385
|
+
const reason = Reflect.get(value, "reason");
|
|
386
|
+
if (typeof reason === "string" && reason.length > 0) return reason;
|
|
387
|
+
const message = Reflect.get(value, "message");
|
|
388
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
389
|
+
}
|
|
390
|
+
function shouldVisitConfigValue(value, depth, seen) {
|
|
391
|
+
if (depth > 10) return false;
|
|
392
|
+
if (value === null || value === void 0) return false;
|
|
393
|
+
if (typeof value !== "object") return false;
|
|
394
|
+
if (seen.has(value)) return false;
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
function pushConfigCauses(value, depth, stack) {
|
|
398
|
+
if (!("cause" in value)) return;
|
|
399
|
+
const causeVal = Reflect.get(value, "cause");
|
|
400
|
+
if (Array.isArray(causeVal)) for (let index = causeVal.length - 1; index >= 0; index--) stack.push({
|
|
401
|
+
value: causeVal[index],
|
|
402
|
+
depth: depth + 1
|
|
403
|
+
});
|
|
404
|
+
else stack.push({
|
|
405
|
+
value: causeVal,
|
|
406
|
+
depth: depth + 1
|
|
407
|
+
});
|
|
393
408
|
}
|
|
394
|
-
function
|
|
395
|
-
if (!
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
409
|
+
function firstConfigErrorDetail(exit) {
|
|
410
|
+
if (!Exit.isFailure(exit)) return;
|
|
411
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
412
|
+
const stack = [];
|
|
413
|
+
for (const reason of exit.cause.reasons) {
|
|
414
|
+
let candidate;
|
|
415
|
+
if (Cause.isFailReason(reason)) candidate = reason.error;
|
|
416
|
+
else if (Cause.isDieReason(reason)) candidate = reason.defect;
|
|
417
|
+
else candidate = void 0;
|
|
418
|
+
if (candidate !== void 0) stack.push({
|
|
419
|
+
value: candidate,
|
|
420
|
+
depth: 0
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
while (stack.length > 0) {
|
|
424
|
+
const entry = stack.pop();
|
|
425
|
+
if (entry === void 0) continue;
|
|
426
|
+
const { value, depth } = entry;
|
|
427
|
+
if (!shouldVisitConfigValue(value, depth, seen)) continue;
|
|
428
|
+
seen.add(value);
|
|
429
|
+
if (exitClassOf(value) === "ConfigError") {
|
|
430
|
+
const detail = configDetailOf(value);
|
|
431
|
+
if (detail !== void 0) return detail;
|
|
402
432
|
}
|
|
403
|
-
|
|
404
|
-
if (Array.isArray(node["children"])) for (const child of node["children"]) walkConfigNode(child, orderedParams, out);
|
|
405
|
-
return;
|
|
406
|
-
case "Nested":
|
|
407
|
-
if (isObject(node["tree"])) walkConfigTree(node["tree"], orderedParams, out);
|
|
408
|
-
return;
|
|
409
|
-
default: return;
|
|
433
|
+
pushConfigCauses(value, depth, stack);
|
|
410
434
|
}
|
|
411
435
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const orderedParams = Array.isArray(config["orderedParams"]) ? config["orderedParams"] : [];
|
|
424
|
-
walkConfigTree(config["tree"], orderedParams, out);
|
|
436
|
+
function describeFailure(exit) {
|
|
437
|
+
if (!Exit.isFailure(exit)) return "Unknown failure";
|
|
438
|
+
const value = failureValue(exit);
|
|
439
|
+
if (value !== void 0) {
|
|
440
|
+
if (S.is(SurvivorsRejection)(value)) return value.remediation;
|
|
441
|
+
if (typeof value === "object" && value !== null) {
|
|
442
|
+
const reason = reasonOf(value);
|
|
443
|
+
if (reason !== void 0) return reason;
|
|
444
|
+
}
|
|
445
|
+
if (value instanceof Error && value.message.length > 0) return value.message;
|
|
446
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
|
|
425
447
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
448
|
+
if (collectExitClasses(exit).includes("ConfigError")) {
|
|
449
|
+
const detail = firstConfigErrorDetail(exit);
|
|
450
|
+
if (detail !== void 0) return detail;
|
|
451
|
+
}
|
|
452
|
+
const rendered = Cause.pretty(exit.cause);
|
|
453
|
+
if (rendered.length > 0) return rendered;
|
|
454
|
+
return "Unknown failure";
|
|
455
|
+
}
|
|
456
|
+
function unrecognizedArgumentOf(exit, argv) {
|
|
457
|
+
if (!Exit.isFailure(exit)) return;
|
|
458
|
+
const value = failureValue(exit);
|
|
459
|
+
if (value === void 0 || !CliError.isCliError(value)) return;
|
|
460
|
+
const errors = (() => {
|
|
461
|
+
if (S.is(CliError.ShowHelp)(value)) return value.errors;
|
|
462
|
+
return [value];
|
|
463
|
+
})();
|
|
464
|
+
for (const error of errors) {
|
|
465
|
+
if (S.is(CliError.UnrecognizedOption)(error)) {
|
|
466
|
+
const at = argv.indexOf(error.option);
|
|
467
|
+
const next = (() => {
|
|
468
|
+
if (at >= 0) return argv[at + 1];
|
|
469
|
+
})();
|
|
470
|
+
if (next !== void 0 && !next.startsWith("-")) return next;
|
|
471
|
+
return error.option;
|
|
433
472
|
}
|
|
473
|
+
if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
|
|
474
|
+
if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
|
|
434
475
|
}
|
|
435
|
-
return {
|
|
436
|
-
name: stringField(node, "name") ?? "",
|
|
437
|
-
description: typeof node["description"] === "string" ? node["description"] : "",
|
|
438
|
-
options: out.flags,
|
|
439
|
-
args: out.args,
|
|
440
|
-
subcommands
|
|
441
|
-
};
|
|
442
476
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
477
|
+
function failureValue(exit) {
|
|
478
|
+
if (!Exit.isFailure(exit)) return;
|
|
479
|
+
const failure = Cause.findErrorOption(exit.cause);
|
|
480
|
+
if (Option.isSome(failure)) return failure.value;
|
|
481
|
+
}
|
|
482
|
+
const SIGNAL_REMEDIATION = "the run was interrupted by a signal; re-run it to continue";
|
|
483
|
+
const PARSE_REMEDIATION = "re-run with --help to see the full usage";
|
|
484
|
+
const DEFAULT_REMEDIATION = "see --reportFile or the verdict envelope on stdout";
|
|
485
|
+
function successExitClassOf(exit) {
|
|
486
|
+
if (!Exit.isSuccess(exit)) return;
|
|
487
|
+
const value = exit.value;
|
|
488
|
+
if (!Predicate.hasProperty(value, "verdict")) return;
|
|
489
|
+
const candidate = value.verdict;
|
|
490
|
+
if (!isExitClass(candidate)) return;
|
|
491
|
+
return candidate;
|
|
492
|
+
}
|
|
493
|
+
function helpErrorCountOf(value) {
|
|
494
|
+
if (value === void 0) return;
|
|
495
|
+
if (!S.is(CliError.ShowHelp)(value)) return;
|
|
496
|
+
return value.errors.length;
|
|
497
|
+
}
|
|
498
|
+
function survivorsRejectionOf(value) {
|
|
499
|
+
if (value === void 0) return;
|
|
500
|
+
if (!S.is(SurvivorsRejection)(value)) return;
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
function present(value) {
|
|
504
|
+
if (value === null) return;
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
function survivorsReasonOf(survivors) {
|
|
508
|
+
if (survivors === void 0) return;
|
|
509
|
+
return survivors.reason;
|
|
510
|
+
}
|
|
511
|
+
function survivorsDiagnosticOf(survivors) {
|
|
512
|
+
if (survivors === void 0) return;
|
|
513
|
+
return survivors.remediation;
|
|
514
|
+
}
|
|
515
|
+
function omitUnknownFailure(diagnostic) {
|
|
516
|
+
if (diagnostic === "Unknown failure") return;
|
|
517
|
+
return diagnostic;
|
|
518
|
+
}
|
|
519
|
+
function capturedOrUnknown(captured) {
|
|
520
|
+
if (captured.length > 0) return captured;
|
|
521
|
+
return "Unknown failure";
|
|
522
|
+
}
|
|
523
|
+
function gatherRunOutcome(exit, signal, argv) {
|
|
524
|
+
const value = failureValue(exit);
|
|
525
|
+
const survivors = survivorsRejectionOf(value);
|
|
526
|
+
return RunOutcomeCommand.make({
|
|
527
|
+
succeeded: Exit.isSuccess(exit),
|
|
528
|
+
signal: present(signal),
|
|
529
|
+
interrupted: Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause),
|
|
530
|
+
helpErrorCount: helpErrorCountOf(value),
|
|
531
|
+
cliError: value !== void 0 && CliError.isCliError(value),
|
|
532
|
+
unrecognized: unrecognizedArgumentOf(exit, argv),
|
|
533
|
+
survivorsReason: survivorsReasonOf(survivors),
|
|
534
|
+
survivorsDiagnostic: survivorsDiagnosticOf(survivors),
|
|
535
|
+
schemaError: value !== void 0 && S.isSchemaError(value),
|
|
536
|
+
successExitClass: successExitClassOf(exit),
|
|
537
|
+
highestExitClass: present(highestExitClass(collectExitClasses(exit))),
|
|
538
|
+
configDetail: firstConfigErrorDetail(exit),
|
|
539
|
+
diagnostic: omitUnknownFailure(describeFailure(exit))
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function errorText(error, captured) {
|
|
543
|
+
return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => {
|
|
544
|
+
if (failed.unrecognized !== void 0) return `Received unknown argument: '${failed.unrecognized}'`;
|
|
545
|
+
return capturedOrUnknown(captured);
|
|
546
|
+
}), Match.tag("RunSurvivorsRejected", (failed) => {
|
|
547
|
+
if (failed.diagnostic !== void 0) return failed.diagnostic;
|
|
548
|
+
return "Unknown failure";
|
|
549
|
+
}), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => {
|
|
550
|
+
if (captured.length > 0) return captured;
|
|
551
|
+
if (failed.detail !== void 0) return failed.detail;
|
|
552
|
+
return "Unknown failure";
|
|
553
|
+
}), Match.tag("RunFailed", (failed) => {
|
|
554
|
+
if (captured.length > 0) return captured;
|
|
555
|
+
if (failed.diagnostic !== void 0) return failed.diagnostic;
|
|
556
|
+
return "Unknown failure";
|
|
557
|
+
}), Match.exhaustive);
|
|
558
|
+
}
|
|
559
|
+
function remediationText(error) {
|
|
560
|
+
return Match.value(error).pipe(Match.tag("RunInterrupted", (failed) => {
|
|
561
|
+
if (failed.code > 128) return SIGNAL_REMEDIATION;
|
|
562
|
+
return DEFAULT_REMEDIATION;
|
|
563
|
+
}), Match.tag("RunParseFailed", () => PARSE_REMEDIATION), Match.tag("RunSurvivorsRejected", (failed) => {
|
|
564
|
+
if (failed.diagnostic !== void 0) return failed.diagnostic;
|
|
565
|
+
return DEFAULT_REMEDIATION;
|
|
566
|
+
}), Match.tag("RunConfigFailed", (failed) => {
|
|
567
|
+
if (failed.detail !== void 0) return `check the config file: ${failed.detail}`;
|
|
568
|
+
return "check the config file";
|
|
569
|
+
}), Match.tag("RunFailed", () => DEFAULT_REMEDIATION), Match.exhaustive);
|
|
570
|
+
}
|
|
571
|
+
function shapeEnvelope(error, captured) {
|
|
456
572
|
return {
|
|
457
573
|
schemaVersion: "1.0",
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
574
|
+
code: runOutcomeCode(Result.fail(error)),
|
|
575
|
+
error: errorText(error, captured),
|
|
576
|
+
remediation: remediationText(error)
|
|
461
577
|
};
|
|
462
578
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return JSON.stringify(buildLLMSManifest(command, version));
|
|
579
|
+
function classifyRunOutcome(exit, signal, argv) {
|
|
580
|
+
return runOutcomeWorkflow(gatherRunOutcome(exit, signal, argv));
|
|
466
581
|
}
|
|
467
|
-
//#endregion
|
|
468
|
-
//#region src/OutputModeConsoleState.ts
|
|
469
|
-
/**
|
|
470
|
-
* U6 — the machine-mode `Console` layer (KTD3, R7).
|
|
471
|
-
*
|
|
472
|
-
* The v4 CLI renders help and errors through an ANSI renderer and prints them
|
|
473
|
-
* with the `Console` reference — and there is no seam to intercept: the
|
|
474
|
-
* document is written *before* the failure propagates. Machine mode therefore
|
|
475
|
-
* replaces the `Console` reference itself with a capturing implementation:
|
|
476
|
-
* every write lands in an in-memory buffer instead of the real stdout/stderr,
|
|
477
|
-
* and the terminating bootstrap (StrykerCliHandler.ts) emits the buffer as
|
|
478
|
-
* one JSON envelope at teardown. Human mode keeps the default console so the
|
|
479
|
-
* framework's prose rendering is untouched.
|
|
480
|
-
*
|
|
481
|
-
* The v4 `Console.Console` interface is the sync `globalThis.console` shape
|
|
482
|
-
* (the v3 service's effect-returning surface became the module-level
|
|
483
|
-
* wrapper functions), so the capture implementation needs no `unsafe`
|
|
484
|
-
* mirror — every method stores into the buffer directly.
|
|
485
|
-
*/
|
|
486
582
|
const capturedConsoleChunks = [];
|
|
487
583
|
const countByLabel = /* @__PURE__ */ new Map();
|
|
488
584
|
const timeByLabel = /* @__PURE__ */ new Map();
|
|
585
|
+
function inspectValue(value) {
|
|
586
|
+
if (typeof value === "string") return value;
|
|
587
|
+
return Formatter.format(value);
|
|
588
|
+
}
|
|
489
589
|
function formatArgs(args) {
|
|
490
|
-
|
|
590
|
+
if (args.length === 0) return "";
|
|
591
|
+
const first = args[0];
|
|
592
|
+
if (typeof first === "string") {
|
|
593
|
+
let index = 1;
|
|
594
|
+
let result = first.replace(/%[sdijfopO%]/g, (match) => {
|
|
595
|
+
if (match === "%%") return "%";
|
|
596
|
+
if (index >= args.length) return match;
|
|
597
|
+
const arg = args[index++];
|
|
598
|
+
switch (match) {
|
|
599
|
+
case "%s": return String(arg);
|
|
600
|
+
case "%d":
|
|
601
|
+
case "%i":
|
|
602
|
+
case "%f": return Number(arg).toString();
|
|
603
|
+
case "%j": try {
|
|
604
|
+
return String(JSON.stringify(arg));
|
|
605
|
+
} catch {
|
|
606
|
+
return "[Circular]";
|
|
607
|
+
}
|
|
608
|
+
case "%o":
|
|
609
|
+
case "%O":
|
|
610
|
+
case "%p": return inspectValue(arg);
|
|
611
|
+
default: return match;
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
for (; index < args.length; index++) result += ` ${inspectValue(args[index])}`;
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
return args.map(inspectValue).join(" ");
|
|
491
618
|
}
|
|
492
619
|
function captureSync(args) {
|
|
493
620
|
capturedConsoleChunks.push(formatArgs(args));
|
|
@@ -501,282 +628,453 @@ function captureCount(label) {
|
|
|
501
628
|
countByLabel.set(key, next);
|
|
502
629
|
capturedConsoleChunks.push(`${key}: ${next}`);
|
|
503
630
|
}
|
|
504
|
-
function captureTimeEnd(label,
|
|
631
|
+
function captureTimeEnd(label, nowNanos) {
|
|
505
632
|
const key = label ?? "default";
|
|
506
633
|
const started = timeByLabel.get(key);
|
|
507
634
|
if (started !== void 0) {
|
|
508
635
|
timeByLabel.delete(key);
|
|
509
|
-
|
|
636
|
+
const diffMs = Number(nowNanos - started) / 1e6;
|
|
637
|
+
capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
|
|
510
638
|
}
|
|
511
639
|
}
|
|
512
640
|
function captureTrace(args) {
|
|
513
641
|
capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
|
|
514
642
|
}
|
|
515
|
-
const
|
|
643
|
+
const makeCapturingConsole = (clock) => ({
|
|
516
644
|
assert: (condition, ...args) => captureAssert(condition, args),
|
|
517
645
|
clear: () => {},
|
|
518
646
|
count: (label) => captureCount(label),
|
|
519
647
|
countReset: (label) => countByLabel.delete(label ?? "default"),
|
|
520
648
|
debug: (...args) => captureSync(args),
|
|
521
|
-
dir: (item,
|
|
522
|
-
dirxml: (item) => capturedConsoleChunks.push(
|
|
649
|
+
dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
650
|
+
dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
523
651
|
error: (...args) => captureSync(args),
|
|
524
652
|
group: () => {},
|
|
525
653
|
groupCollapsed: () => {},
|
|
526
654
|
groupEnd: () => {},
|
|
527
655
|
info: (...args) => captureSync(args),
|
|
528
656
|
log: (...args) => captureSync(args),
|
|
529
|
-
table: (tabularData) => capturedConsoleChunks.push(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
})),
|
|
533
|
-
time: (label) => timeByLabel.set(label ?? "default", performance.now()),
|
|
534
|
-
timeEnd: (label) => captureTimeEnd(label, performance.now()),
|
|
657
|
+
table: (tabularData) => capturedConsoleChunks.push(Formatter.format(tabularData)),
|
|
658
|
+
time: (label) => timeByLabel.set(label ?? "default", clock.monotonicTimeNanosUnsafe()),
|
|
659
|
+
timeEnd: (label) => captureTimeEnd(label, clock.monotonicTimeNanosUnsafe()),
|
|
535
660
|
timeLog: (label, ...args) => {
|
|
536
661
|
const key = label ?? "default";
|
|
537
662
|
const started = timeByLabel.get(key);
|
|
538
|
-
if (started
|
|
663
|
+
if (started === void 0) return;
|
|
664
|
+
const diffMs = Number(clock.monotonicTimeNanosUnsafe() - started) / 1e6;
|
|
665
|
+
if (args.length === 0) {
|
|
666
|
+
capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
capturedConsoleChunks.push(`${key}: ${diffMs}ms ${formatArgs(args)}`);
|
|
539
670
|
},
|
|
540
671
|
trace: (...args) => captureTrace(args),
|
|
541
672
|
warn: (...args) => captureSync(args)
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
* The machine-mode `Console` layer. Building it clears the capture buffer so
|
|
545
|
-
* every run starts empty; the terminating bootstrap reads the buffer back
|
|
546
|
-
* through `readCapturedConsole` at teardown. A `Layer` is already lazy, so
|
|
547
|
-
* the layer is a value: the reset effect runs when the layer is built.
|
|
548
|
-
*
|
|
549
|
-
* The layer must replace the `Console` reference the module-level
|
|
550
|
-
* `Console.log`/`Console.error` wrappers read through their fiber context;
|
|
551
|
-
* v4 reads the override the same way it reads any provided service, so a
|
|
552
|
-
* plain provide is sufficient — no special `setConsole`-style primitive
|
|
553
|
-
* exists any more. The reference's identifier is `never` (it carries no
|
|
554
|
-
* requirement), so the layer's type is too.
|
|
555
|
-
*
|
|
556
|
-
* Human mode provides no Console binding at all: effect's own default
|
|
557
|
-
* console delegates every method to the global console, which is exactly the
|
|
558
|
-
* prose rendering a human-mode run uses. Mirroring it here would reimplement
|
|
559
|
-
* the library default (V.7).
|
|
560
|
-
*/
|
|
561
|
-
const machineConsoleLayer = Layer.effect(Console.Console, Effect.sync(() => {
|
|
673
|
+
});
|
|
674
|
+
const machineConsoleLayer = Layer.effect(Console.Console, Clock.clockWith((clock) => Effect.sync(() => {
|
|
562
675
|
resetCapturedConsole();
|
|
563
|
-
return
|
|
564
|
-
}));
|
|
565
|
-
/**
|
|
566
|
-
* The text captured so far by the machine console layer, joined into one
|
|
567
|
-
* document the way a terminal would have rendered it (one console call per
|
|
568
|
-
* line). Empty in human mode.
|
|
569
|
-
*/
|
|
676
|
+
return makeCapturingConsole(clock);
|
|
677
|
+
})));
|
|
570
678
|
function readCapturedConsole() {
|
|
571
679
|
return capturedConsoleChunks.join("\n");
|
|
572
680
|
}
|
|
573
|
-
/**
|
|
574
|
-
* Clears the capture buffer and the count/time tables. Called when the
|
|
575
|
-
* machine layer is constructed so every run starts empty.
|
|
576
|
-
*/
|
|
577
681
|
function resetCapturedConsole() {
|
|
578
682
|
capturedConsoleChunks.length = 0;
|
|
579
683
|
countByLabel.clear();
|
|
580
684
|
timeByLabel.clear();
|
|
581
685
|
}
|
|
582
686
|
//#endregion
|
|
583
|
-
//#region src/
|
|
584
|
-
|
|
585
|
-
* The decision's helper closure reaches three language built-ins the purity
|
|
586
|
-
* gate cannot resolve as globals, so each is bound at module scope and the
|
|
587
|
-
* helpers reference the bindings: `isArray` keeps `Array.isArray`'s narrow,
|
|
588
|
-
* `objectEntries`/`objectFromEntries`/`objectKeys` keep `Object`'s trio, and
|
|
589
|
-
* `stringify` keeps `JSON.stringify`'s exact text.
|
|
590
|
-
*/
|
|
591
|
-
const isArray = Array.isArray;
|
|
592
|
-
const { entries: objectEntries, fromEntries: objectFromEntries, keys: objectKeys } = Object;
|
|
593
|
-
const stringify = JSON.stringify;
|
|
594
|
-
/**
|
|
595
|
-
* U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
|
|
596
|
-
*
|
|
597
|
-
* The `--survivors` run re-tests exactly the mutants that survived a previous
|
|
598
|
-
* run. Its input is the previous run's mutation report, and the run is
|
|
599
|
-
* admitted only when a single structural hash of the resolved options, the
|
|
600
|
-
* recorded framework version, and the per-file source content all match the
|
|
601
|
-
* current run (KTD6). Because thresholds live inside the resolved options, a
|
|
602
|
-
* threshold-only change is caught for free. Every rejection exits 2 with a
|
|
603
|
-
* remediation naming the full run to do first; zero survivors exits 0 with a
|
|
604
|
-
* null score and writes no new report (AE3); and a report written by a
|
|
605
|
-
* survivors run is never admitted as the input of another survivors run
|
|
606
|
-
* (KTD7), so chaining two survivors runs fails loudly instead of re-testing a
|
|
607
|
-
* shrunken or stale set.
|
|
608
|
-
*
|
|
609
|
-
* All functions here are pure over their inputs — no file I/O — so the
|
|
610
|
-
* admission logic is fixture-testable in seconds.
|
|
611
|
-
*/
|
|
612
|
-
/** The path a `--survivors` run reads when no `survivorsPriorReport` is configured. */
|
|
613
|
-
const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
|
|
614
|
-
/**
|
|
615
|
-
* The remediation every rejection carries (R10): name the full run to do
|
|
616
|
-
* first, never the survivors run itself.
|
|
617
|
-
*/
|
|
618
|
-
const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
|
|
619
|
-
/**
|
|
620
|
-
* The survivors-run bookkeeping keys carried in the resolved options. They
|
|
621
|
-
* are run mechanics, not configuration: a survivors run adds them, so without
|
|
622
|
-
* stripping them the current run's hash would differ from the prior full
|
|
623
|
-
* run's hash for the very same configuration. Their presence in a report's
|
|
624
|
-
* embedded config is also the marker that the report was produced by a
|
|
625
|
-
* survivors run (KTD7).
|
|
626
|
-
*/
|
|
627
|
-
const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
|
|
628
|
-
function isRecord(value) {
|
|
629
|
-
return typeof value === "object" && value !== null && !isArray(value);
|
|
630
|
-
}
|
|
687
|
+
//#region src/Output.workflow.ts
|
|
688
|
+
const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
|
|
631
689
|
/**
|
|
632
|
-
* The
|
|
633
|
-
*
|
|
690
|
+
* The command of the output-mode workflow: a schema class, because `Workflow.make`
|
|
691
|
+
* derives the command type from it and pins the error channel at the construction site.
|
|
634
692
|
*/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
693
|
+
var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
|
|
694
|
+
stdoutIsTTY: S.Boolean,
|
|
695
|
+
text: S.optional(S.Boolean),
|
|
696
|
+
json: S.optional(S.Boolean),
|
|
697
|
+
envMode: S.optional(S.String),
|
|
698
|
+
agent: S.optional(S.String),
|
|
699
|
+
toolVars: S.optional(S.Record(S.String, S.String))
|
|
700
|
+
}) {};
|
|
641
701
|
/**
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* the
|
|
702
|
+
* The conflict error for mutually exclusive format flags. Defined locally so the
|
|
703
|
+
* workflow decision remains pure — the sealed effect surface does not include
|
|
704
|
+
* `effect/unstable/cli/CliError`, so the decision returns this local error and
|
|
705
|
+
* the shell maps it to `CliError.InvalidValue` at the boundary.
|
|
646
706
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
707
|
+
var ModeConflictError = class extends S.TaggedError()("ModeConflictError", {
|
|
708
|
+
option: S.String,
|
|
709
|
+
value: S.String,
|
|
710
|
+
expected: S.String
|
|
711
|
+
}) {};
|
|
712
|
+
const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
|
|
713
|
+
function r4(command) {
|
|
714
|
+
if (command.text === true && command.json === true) return Result.fail(ModeConflictError.make({
|
|
715
|
+
option: "json",
|
|
716
|
+
value: "text",
|
|
717
|
+
expected: CONFLICT_EXPECTED
|
|
718
|
+
}));
|
|
719
|
+
if (command.text === true) return Result.succeed({
|
|
720
|
+
mode: "human",
|
|
721
|
+
signal: "flag",
|
|
722
|
+
stdoutIsTTY: command.stdoutIsTTY
|
|
723
|
+
});
|
|
724
|
+
if (command.json === true) return Result.succeed({
|
|
725
|
+
mode: "machine",
|
|
726
|
+
signal: "flag",
|
|
727
|
+
stdoutIsTTY: command.stdoutIsTTY
|
|
728
|
+
});
|
|
729
|
+
if (command.envMode !== void 0 && command.envMode.length > 0) {
|
|
730
|
+
if (command.envMode === "machine") return Result.succeed({
|
|
731
|
+
mode: "machine",
|
|
732
|
+
signal: "env",
|
|
733
|
+
stdoutIsTTY: command.stdoutIsTTY
|
|
734
|
+
});
|
|
735
|
+
return Result.succeed({
|
|
736
|
+
mode: "human",
|
|
737
|
+
signal: "env",
|
|
738
|
+
stdoutIsTTY: command.stdoutIsTTY
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
if (!command.stdoutIsTTY) return Result.succeed({
|
|
742
|
+
mode: "machine",
|
|
743
|
+
signal: "tty",
|
|
744
|
+
stdoutIsTTY: false
|
|
745
|
+
});
|
|
746
|
+
if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed({
|
|
747
|
+
mode: "machine",
|
|
748
|
+
signal: "agent",
|
|
749
|
+
stdoutIsTTY: true
|
|
750
|
+
});
|
|
751
|
+
const toolVars = command.toolVars ?? {};
|
|
752
|
+
for (const variable of TOOL_VARIABLES$1) {
|
|
753
|
+
const value = toolVars[variable];
|
|
754
|
+
if (typeof value === "string" && value.length > 0) return Result.succeed({
|
|
755
|
+
mode: "machine",
|
|
756
|
+
signal: "tool",
|
|
757
|
+
stdoutIsTTY: true
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
return Result.succeed({
|
|
761
|
+
mode: "human",
|
|
762
|
+
signal: "tty",
|
|
763
|
+
stdoutIsTTY: true
|
|
764
|
+
});
|
|
650
765
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
* current run's source files and the sources the prior report embeds. The
|
|
654
|
-
* digest capability is supplied by the caller.
|
|
655
|
-
*/
|
|
656
|
-
function sourceContentHash(content, hash) {
|
|
657
|
-
return hash(content);
|
|
766
|
+
function modeDecision(command) {
|
|
767
|
+
return r4(command);
|
|
658
768
|
}
|
|
769
|
+
const resolveModeWorkflow = Workflow.make(ResolveModeCommand, modeDecision);
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/Output.ts
|
|
659
772
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
773
|
+
* Output — the machine/human output capability.
|
|
774
|
+
*
|
|
775
|
+
* The NDJSON run-event stream, wire protocol constants, mode resolution probes,
|
|
776
|
+
* and machine-mode terminal output. Pure mode resolution lives in
|
|
777
|
+
* Output.workflow.ts.
|
|
664
778
|
*/
|
|
665
|
-
function serializeSurvivorsHashInput(input) {
|
|
666
|
-
return stringify(sortKeys(input));
|
|
667
|
-
}
|
|
668
|
-
function sortKeys(value) {
|
|
669
|
-
if (isArray(value)) return value.map(sortKeys);
|
|
670
|
-
if (isRecord(value)) return objectFromEntries(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
671
|
-
return value;
|
|
672
|
-
}
|
|
673
779
|
/**
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* incremental report reader applies (`project-reader.ts`). Mutants without a
|
|
678
|
-
* replacement fall back to their mutator name, the same convention the
|
|
679
|
-
* incremental differ uses.
|
|
780
|
+
* The heartbeat interval (R19), matching Terraform's `apply_progress`
|
|
781
|
+
* cadence: long enough that a slow phase is not noisy, short enough that a
|
|
782
|
+
* consumer can tell "slow" from "hung" without waiting for a mutant event.
|
|
680
783
|
*/
|
|
681
|
-
|
|
784
|
+
const TICK_INTERVAL_MS = 1e4;
|
|
785
|
+
var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/RunEventStreamPortTag") {};
|
|
786
|
+
const RunEventStreamPort = RunEventStreamPortTag;
|
|
787
|
+
const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.tag("manifest", () => true), Match.orElse(() => false));
|
|
788
|
+
const wireKind = (event) => Match.value(event).pipe(Match.tag("stream", () => "stream"), Match.tag("phase", () => "phase"), Match.tag("plan", () => "plan"), Match.tag("mutant", () => "mutant"), Match.tag("tick", () => "tick"), Match.tag("verdict", () => "verdict"), Match.tag("error", () => "error"), Match.tag("help", () => "help"), Match.tag("manifest", () => "manifest"), Match.exhaustive);
|
|
789
|
+
const toWireLine = (event) => {
|
|
790
|
+
const fields = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_tag"));
|
|
791
|
+
return JSON.stringify({
|
|
792
|
+
kind: wireKind(event),
|
|
793
|
+
...fields
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
function numberText(value, fallback) {
|
|
797
|
+
if (typeof value === "number") return String(value);
|
|
798
|
+
return fallback;
|
|
799
|
+
}
|
|
800
|
+
function phaseLine(phase) {
|
|
801
|
+
if (typeof phase === "string") return `phase ${phase}`;
|
|
802
|
+
return "phase ";
|
|
803
|
+
}
|
|
804
|
+
const stderrProgressLine = (event, alreadyClosed) => {
|
|
805
|
+
if (alreadyClosed) return;
|
|
806
|
+
return Match.value(event).pipe(Match.tag("plan", (e) => `plan ${numberText(e.total, "0")} mutants`), Match.tag("phase", (e) => phaseLine(e.phase)), Match.tag("tick", (e) => `${numberText(e.completed, "0")}/${numberText(e.total, "?")} elapsed ${numberText(e.elapsedMs, "0")}ms`), Match.tag("verdict", (e) => `score ${numberText(e.score, "n/a")} killed ${numberText(e.counts.killed, "0")} survived ${numberText(e.counts.survived, "0")}`), Match.tag("error", (e) => {
|
|
807
|
+
if (typeof e.error === "string") return `error ${e.error}`;
|
|
808
|
+
return "error ";
|
|
809
|
+
}), Match.orElse(() => void 0));
|
|
810
|
+
};
|
|
811
|
+
const writeStderr = (stdio, line) => Stream.run(Stream.succeed(`${line}\n`), stdio.stderr({ endOnDone: false })).pipe(Effect.ignore);
|
|
812
|
+
const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
|
|
813
|
+
const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, stdio)) => Effect.gen(function* () {
|
|
814
|
+
const runId = generateRunId();
|
|
815
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
816
|
+
const queue = yield* Queue.unbounded();
|
|
817
|
+
const state = {
|
|
818
|
+
mode: resolved.mode,
|
|
819
|
+
signal: resolved.signal,
|
|
820
|
+
headerWritten: false,
|
|
821
|
+
terminalWritten: false,
|
|
822
|
+
progress: {
|
|
823
|
+
completed: 0,
|
|
824
|
+
total: null
|
|
825
|
+
},
|
|
826
|
+
findingsPrinted: 0
|
|
827
|
+
};
|
|
828
|
+
const queueStream = Stream.fromQueue(queue).pipe(Stream.tap((event) => Effect.sync(() => {
|
|
829
|
+
Match.value(event).pipe(Match.tag("plan", (e) => {
|
|
830
|
+
state.progress = {
|
|
831
|
+
...state.progress,
|
|
832
|
+
total: e.total
|
|
833
|
+
};
|
|
834
|
+
}), Match.tag("mutant", (e) => {
|
|
835
|
+
state.progress = {
|
|
836
|
+
completed: e.completed,
|
|
837
|
+
total: e.total
|
|
838
|
+
};
|
|
839
|
+
}), Match.orElse(() => {}));
|
|
840
|
+
})));
|
|
841
|
+
const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.drop(1), Stream.filter(() => state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
|
|
842
|
+
const now = yield* Clock.currentTimeMillis;
|
|
843
|
+
return Heartbeat.make({
|
|
844
|
+
elapsedMs: now - startedAt,
|
|
845
|
+
completed: state.progress.completed,
|
|
846
|
+
total: state.progress.total
|
|
847
|
+
});
|
|
848
|
+
})));
|
|
849
|
+
let terminalSeen = false;
|
|
850
|
+
const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap((event) => {
|
|
851
|
+
const line = stderrProgressLine(event, state.terminalWritten);
|
|
852
|
+
if (isTerminalEvent(event)) state.terminalWritten = true;
|
|
853
|
+
if (line === void 0) return Effect.void;
|
|
854
|
+
return writeStderr(stdio, line);
|
|
855
|
+
})).pipe(Stream.filter((event) => {
|
|
856
|
+
if (state.mode !== "machine") return false;
|
|
857
|
+
if (terminalSeen) return false;
|
|
858
|
+
if (isTerminalEvent(event)) terminalSeen = true;
|
|
859
|
+
return true;
|
|
860
|
+
}), Stream.map((event) => `${toWireLine(event)}\n`)));
|
|
861
|
+
let drainFiber = null;
|
|
682
862
|
return {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
863
|
+
queue,
|
|
864
|
+
runId,
|
|
865
|
+
startedAt,
|
|
866
|
+
isOpen: () => state.mode === "machine" && !state.terminalWritten && drainFiber !== null,
|
|
867
|
+
ensureOpen: (openResolved) => {
|
|
868
|
+
if (state.headerWritten) return;
|
|
869
|
+
state.mode = openResolved.mode;
|
|
870
|
+
state.signal = openResolved.signal;
|
|
871
|
+
},
|
|
872
|
+
open: Effect.gen(function* () {
|
|
873
|
+
if (drainFiber === null) {
|
|
874
|
+
if (!state.headerWritten) {
|
|
875
|
+
state.headerWritten = true;
|
|
876
|
+
if (state.mode === "machine") yield* Queue.offer(queue, RunStarted.make({
|
|
877
|
+
schemaVersion: "1.0",
|
|
878
|
+
runId,
|
|
879
|
+
mode: state.mode,
|
|
880
|
+
signal: state.signal
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
drainFiber = yield* Effect.forkDetach(drain);
|
|
695
884
|
}
|
|
696
|
-
}
|
|
885
|
+
}),
|
|
886
|
+
closeAndDrain: Effect.gen(function* () {
|
|
887
|
+
state.terminalWritten = true;
|
|
888
|
+
yield* Queue.end(queue);
|
|
889
|
+
if (drainFiber !== null) yield* Fiber.join(drainFiber);
|
|
890
|
+
})
|
|
697
891
|
};
|
|
698
|
-
}
|
|
892
|
+
});
|
|
893
|
+
Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
|
|
894
|
+
const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
|
|
699
895
|
/**
|
|
700
|
-
* The
|
|
701
|
-
*
|
|
702
|
-
*
|
|
896
|
+
* The log colouriser's gate (R8). Machine mode never emits colour, so a
|
|
897
|
+
* harness merging `2>&1` is not handed escape sequences it must strip, and
|
|
898
|
+
* `NO_COLOR` is honoured for the human path per the convention: any value
|
|
899
|
+
* other than an unset or empty variable disables colour.
|
|
703
900
|
*/
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
return
|
|
901
|
+
function isColorEnabled(resolved, noColor) {
|
|
902
|
+
if (resolved.mode !== "human") return false;
|
|
903
|
+
if (noColor === void 0) return true;
|
|
904
|
+
if (noColor.length === 0) return true;
|
|
905
|
+
return false;
|
|
708
906
|
}
|
|
907
|
+
var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/OutputModeProbeTag") {};
|
|
908
|
+
const OutputModeProbe = OutputModeProbeTag;
|
|
909
|
+
const outputModeProbeDescription = pipe(Cell.read((command) => Effect.succeed((() => {
|
|
910
|
+
const toolVarsRecord = {};
|
|
911
|
+
for (const variable of TOOL_VARIABLES) {
|
|
912
|
+
const value = process.env[variable];
|
|
913
|
+
if (value !== void 0) toolVarsRecord[variable] = value;
|
|
914
|
+
}
|
|
915
|
+
const envMode = process.env["STRYKER_MODE"];
|
|
916
|
+
const agent = process.env["AGENT"];
|
|
917
|
+
let result = { stdoutIsTTY: process.stdout.isTTY === true };
|
|
918
|
+
if (Object.keys(toolVarsRecord).length > 0) result = {
|
|
919
|
+
...result,
|
|
920
|
+
toolVars: toolVarsRecord
|
|
921
|
+
};
|
|
922
|
+
if (envMode !== void 0) result = {
|
|
923
|
+
...result,
|
|
924
|
+
envMode
|
|
925
|
+
};
|
|
926
|
+
if (agent !== void 0) result = {
|
|
927
|
+
...result,
|
|
928
|
+
agent
|
|
929
|
+
};
|
|
930
|
+
if (command.text !== void 0) result = {
|
|
931
|
+
...result,
|
|
932
|
+
text: command.text
|
|
933
|
+
};
|
|
934
|
+
if (command.json !== void 0) result = {
|
|
935
|
+
...result,
|
|
936
|
+
json: command.json
|
|
937
|
+
};
|
|
938
|
+
return result;
|
|
939
|
+
})())), Cell.decode((raw) => Result.succeed((() => {
|
|
940
|
+
const filteredToolVars = {};
|
|
941
|
+
if (raw.toolVars !== void 0) {
|
|
942
|
+
for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
|
|
943
|
+
}
|
|
944
|
+
let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
|
|
945
|
+
if (raw.text !== void 0) commandInput = {
|
|
946
|
+
...commandInput,
|
|
947
|
+
text: raw.text
|
|
948
|
+
};
|
|
949
|
+
if (raw.json !== void 0) commandInput = {
|
|
950
|
+
...commandInput,
|
|
951
|
+
json: raw.json
|
|
952
|
+
};
|
|
953
|
+
if (raw.envMode !== void 0) commandInput = {
|
|
954
|
+
...commandInput,
|
|
955
|
+
envMode: raw.envMode
|
|
956
|
+
};
|
|
957
|
+
if (raw.agent !== void 0) commandInput = {
|
|
958
|
+
...commandInput,
|
|
959
|
+
agent: raw.agent
|
|
960
|
+
};
|
|
961
|
+
if (Object.keys(filteredToolVars).length > 0) commandInput = {
|
|
962
|
+
...commandInput,
|
|
963
|
+
toolVars: filteredToolVars
|
|
964
|
+
};
|
|
965
|
+
return ResolveModeCommand.make(commandInput);
|
|
966
|
+
})())), Cell.decide(resolveModeWorkflow), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
|
|
967
|
+
onFailure: (error) => Effect.fail(error),
|
|
968
|
+
onSuccess: (mode) => Effect.succeed(mode)
|
|
969
|
+
})));
|
|
970
|
+
const detectModeWithProbe = (flags = {}) => Cell.apply(outputModeProbeDescription, flags).pipe(Effect.mapError((error) => CliError.InvalidValue.make({
|
|
971
|
+
option: error.option,
|
|
972
|
+
value: error.value,
|
|
973
|
+
expected: error.expected,
|
|
974
|
+
kind: "flag"
|
|
975
|
+
})));
|
|
976
|
+
const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: detectModeWithProbe({}) }));
|
|
709
977
|
/**
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
978
|
+
* Machine mode emits the U4 verdict envelope for a run that produced no
|
|
979
|
+
* mutants and no report file: a `--survivors` run with zero survivors (AE3)
|
|
980
|
+
* or a successful `--dryRunOnly` run that ended before the mutation
|
|
981
|
+
* pipeline. The envelope carries a null score and an empty mutant list and is
|
|
982
|
+
* written as the terminal `verdict` line of the stdout stream (U6), carrying
|
|
983
|
+
* the run id the stream header already opened with (KTD11 — never a fresh
|
|
984
|
+
* id). Human mode prints nothing (the sink drops in human mode).
|
|
713
985
|
*/
|
|
714
|
-
function
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
986
|
+
function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathService) {
|
|
987
|
+
const envelope = buildVerdictEnvelope({
|
|
988
|
+
schemaVersion: "1.0",
|
|
989
|
+
files: {},
|
|
990
|
+
thresholds,
|
|
991
|
+
projectRoot: basePath,
|
|
992
|
+
config,
|
|
993
|
+
framework: {
|
|
994
|
+
name: "StrykerJS",
|
|
995
|
+
version: strykerVersion
|
|
724
996
|
}
|
|
725
|
-
}
|
|
726
|
-
return
|
|
997
|
+
}, mode.mode, mode.signal, stream.runId, basePath, pathService);
|
|
998
|
+
return Queue.offer(stream.queue, VerdictReached.make({
|
|
999
|
+
schemaVersion: envelope.schemaVersion,
|
|
1000
|
+
runId: envelope.runId,
|
|
1001
|
+
mode: envelope.mode,
|
|
1002
|
+
signal: envelope.signal,
|
|
1003
|
+
score: envelope.score,
|
|
1004
|
+
thresholds: envelope.thresholds,
|
|
1005
|
+
reportFile: envelope.reportFile,
|
|
1006
|
+
counts: envelope.counts,
|
|
1007
|
+
mutants: envelope.mutants
|
|
1008
|
+
}));
|
|
727
1009
|
}
|
|
728
1010
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
*
|
|
755
|
-
|
|
1011
|
+
* Emits the machine-mode output from the run's finalizer — it runs on
|
|
1012
|
+
* success, failure and interruption alike (R30): a failed run writes the
|
|
1013
|
+
* `error` terminal event as the last line of the stdout stream; a successful
|
|
1014
|
+
* run whose only console output was the framework's help/version rendering
|
|
1015
|
+
* emits that captured document as the `help` terminal event, so `--help` in
|
|
1016
|
+
* machine mode never leaks an ANSI document. A successful run with an empty
|
|
1017
|
+
* buffer (the normal verdict path) emits nothing extra — the run already
|
|
1018
|
+
* wrote its terminal `verdict` line through the same module — unless the
|
|
1019
|
+
* stream is still open, which means the run never reached a verdict (the
|
|
1020
|
+
* `--dryRunOnly` early return): then a null-score `verdict` closes the
|
|
1021
|
+
* stream so the last stdout line is always a terminal event (R5).
|
|
1022
|
+
*/
|
|
1023
|
+
function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
|
|
1024
|
+
return Effect.gen(function* () {
|
|
1025
|
+
const captured = readCapturedConsole();
|
|
1026
|
+
if (Result.isSuccess(outcome)) {
|
|
1027
|
+
if (outcome.success.help) {
|
|
1028
|
+
yield* Queue.offer(stream.queue, HelpRendered.make({
|
|
1029
|
+
schemaVersion: "1.0",
|
|
1030
|
+
code: 0,
|
|
1031
|
+
help: captured
|
|
1032
|
+
}));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (captured.length > 0) {
|
|
1036
|
+
yield* Queue.offer(stream.queue, HelpRendered.make({
|
|
1037
|
+
schemaVersion: "1.0",
|
|
1038
|
+
code: 0,
|
|
1039
|
+
help: captured
|
|
1040
|
+
}));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const envelope = shapeEnvelope(outcome.failure, captured);
|
|
1047
|
+
yield* Queue.offer(stream.queue, RunFailed.make({
|
|
1048
|
+
schemaVersion: envelope.schemaVersion,
|
|
1049
|
+
code: envelope.code,
|
|
1050
|
+
error: envelope.error,
|
|
1051
|
+
remediation: envelope.remediation
|
|
1052
|
+
}));
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/Survivors.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Survivors — the survivors-admission capability.
|
|
1059
|
+
*
|
|
1060
|
+
* The prior-report decoding, source hashing, mutant conversion, and admission
|
|
1061
|
+
* pipeline for --survivors runs. Pure admission decision lives in
|
|
1062
|
+
* Survivors.workflow.ts.
|
|
1063
|
+
*/
|
|
1064
|
+
const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
|
|
1065
|
+
/**
|
|
1066
|
+
* The mutant shape the admission carries, named once because both the decision's
|
|
1067
|
+
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
1068
|
+
*/
|
|
1069
|
+
/**
|
|
1070
|
+
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
1071
|
+
* get the decode function, not the schema, so the report's wire shape is not a
|
|
1072
|
+
* surface commitment and the codec has exactly one caller.
|
|
1073
|
+
*
|
|
1074
|
+
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
1075
|
+
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
1076
|
+
* must not be refused for carrying one.
|
|
756
1077
|
*/
|
|
757
|
-
const PriorReportDocument = S.Struct({
|
|
758
|
-
config: S.optional(S.Record(S.String, S.Unknown)),
|
|
759
|
-
framework: S.optional(S.Struct({ version: S.optional(S.String) })),
|
|
760
|
-
files: S.Record(S.String, S.Struct({
|
|
761
|
-
source: S.String,
|
|
762
|
-
mutants: S.Array(S.Struct({
|
|
763
|
-
id: S.String,
|
|
764
|
-
mutatorName: S.String,
|
|
765
|
-
replacement: S.optional(S.String),
|
|
766
|
-
status: S.String,
|
|
767
|
-
location: S.Struct({
|
|
768
|
-
start: S.Struct({
|
|
769
|
-
line: S.Finite,
|
|
770
|
-
column: S.Finite
|
|
771
|
-
}),
|
|
772
|
-
end: S.Struct({
|
|
773
|
-
line: S.Finite,
|
|
774
|
-
column: S.Finite
|
|
775
|
-
})
|
|
776
|
-
})
|
|
777
|
-
}))
|
|
778
|
-
}))
|
|
779
|
-
});
|
|
780
1078
|
/**
|
|
781
1079
|
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
782
1080
|
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
@@ -784,165 +1082,80 @@ const PriorReportDocument = S.Struct({
|
|
|
784
1082
|
* a third-party report type.
|
|
785
1083
|
*/
|
|
786
1084
|
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
1085
|
+
const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
|
|
787
1086
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
* version it recorded. The report's files are not here — the survivors and the per-file
|
|
791
|
-
* source hashes derived from them need capabilities the command cannot hold, so they
|
|
792
|
-
* arrive already computed.
|
|
1087
|
+
* Thin by design: the digest is the caller's capability, and naming the call
|
|
1088
|
+
* keeps every hashing site in the admission path reading the same way.
|
|
793
1089
|
*/
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}) {};
|
|
1090
|
+
function sourceContentHash(content, hash) {
|
|
1091
|
+
return hash(content);
|
|
1092
|
+
}
|
|
798
1093
|
/**
|
|
799
|
-
* The
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* results arrive precomputed from the decode phase instead.
|
|
1094
|
+
* The per-file source hashes of the sources a prior report embeds.
|
|
1095
|
+
*
|
|
1096
|
+
* The current run's side of the comparison is gathered by the shell from disk;
|
|
1097
|
+
* this is the recorded side, read back out of the report.
|
|
804
1098
|
*/
|
|
805
|
-
var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
|
|
806
|
-
/**
|
|
807
|
-
* The prior run's report facts, `undefined` when no report exists — the run cannot be
|
|
808
|
-
* admitted without one ('no-report'). Explicitly nullable rather than key-optional: a
|
|
809
|
-
* missing report is a state the edge determined and states, not a key it forgot.
|
|
810
|
-
*/
|
|
811
|
-
priorReport: S.UndefinedOr(PriorReportFacts),
|
|
812
|
-
/** The current run's resolved options (defaults + config file + CLI). */
|
|
813
|
-
currentConfig: S.Record(S.String, S.Unknown),
|
|
814
|
-
/** The current CLI/framework version (`strykerVersion`). */
|
|
815
|
-
frameworkVersion: S.String,
|
|
816
|
-
/**
|
|
817
|
-
* Per-file content hashes of the current source, keyed by the prior report's relative
|
|
818
|
-
* file keys. The prior side is hashed from the sources the report embeds, so an editor
|
|
819
|
-
* save that shifts line ranges — which would silently re-test a different mutant than
|
|
820
|
-
* the one that survived — is caught here.
|
|
821
|
-
*/
|
|
822
|
-
sourceContentHashes: S.Record(S.String, S.String),
|
|
823
|
-
/** The same hashes for the sources the prior report embeds, computed at the edge. */
|
|
824
|
-
priorSourceHashes: S.Record(S.String, S.String),
|
|
825
|
-
/** The prior report's survivors, already converted to the internal mutant shape. */
|
|
826
|
-
priorSurvivors: S.Array(MutantShape)
|
|
827
|
-
}) {};
|
|
828
|
-
const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
|
|
829
|
-
const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
|
|
830
|
-
const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
|
|
831
|
-
/** The per-file source hashes of the sources the prior report embeds. */
|
|
832
1099
|
function priorSourceHashes(priorReport, hashContent) {
|
|
833
1100
|
return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
|
|
834
1101
|
}
|
|
835
1102
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
1103
|
+
* Converts a report mutant (1-based schema location) into the internal mutant
|
|
1104
|
+
* shape a run consumes (0-based positions, absolute file name) — the exact
|
|
1105
|
+
* inverse of `objectUtils.toSchemaLocation` and the same shift the
|
|
1106
|
+
* incremental report reader applies (`project-reader.ts`). Mutants without a
|
|
1107
|
+
* replacement fall back to their mutator name, the same convention the
|
|
1108
|
+
* incremental differ uses.
|
|
842
1109
|
*/
|
|
843
|
-
function
|
|
844
|
-
return
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1110
|
+
function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
1111
|
+
return Mutant.make({
|
|
1112
|
+
id: mutant.id,
|
|
1113
|
+
fileName: resolveAbsolutePath(file),
|
|
1114
|
+
mutatorName: mutant.mutatorName,
|
|
1115
|
+
replacement: mutant.replacement ?? mutant.mutatorName,
|
|
1116
|
+
location: {
|
|
1117
|
+
start: {
|
|
1118
|
+
line: mutant.location.start.line - 1,
|
|
1119
|
+
column: mutant.location.start.column - 1
|
|
1120
|
+
},
|
|
1121
|
+
end: {
|
|
1122
|
+
line: mutant.location.end.line - 1,
|
|
1123
|
+
column: mutant.location.end.column - 1
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
852
1126
|
});
|
|
853
1127
|
}
|
|
854
|
-
const rejection = (reason, detail) => ({
|
|
855
|
-
kind: "reject",
|
|
856
|
-
reason,
|
|
857
|
-
remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
|
|
858
|
-
});
|
|
859
|
-
function admissionVerdict(input) {
|
|
860
|
-
const priorReport = input.priorReport;
|
|
861
|
-
if (priorReport === void 0) return rejection("no-report", NO_REPORT_DETAIL);
|
|
862
|
-
if (wasProducedBySurvivorsRun(priorReport)) return rejection("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
|
|
863
|
-
if (input.priorSurvivors.length === 0) return { kind: "no-survivors" };
|
|
864
|
-
if (!hashesMatch(priorReport, input)) return rejection("mismatch", MISMATCH_DETAIL);
|
|
865
|
-
return {
|
|
866
|
-
kind: "admit",
|
|
867
|
-
survivors: input.priorSurvivors
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
|
|
871
|
-
var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
|
|
872
|
-
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
873
|
-
};
|
|
874
|
-
var NoSurvivors = class extends S.TaggedClass()("NoSurvivors", {}) {
|
|
875
|
-
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
876
|
-
};
|
|
877
|
-
S.Union([Admitted, NoSurvivors]);
|
|
878
|
-
var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
|
|
879
|
-
reason: S.Literals(["no-report", "mismatch"]),
|
|
880
|
-
remediation: S.String
|
|
881
|
-
}) {
|
|
882
|
-
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
883
|
-
};
|
|
884
|
-
/**
|
|
885
|
-
* The survivors admission decision: the classification `admissionVerdict`
|
|
886
|
-
* produces, assigned to the workflow channels — one arm per kind, no guard
|
|
887
|
-
* chain. A missing report, a survivors-sourced report and a hash mismatch are
|
|
888
|
-
* the same reject outcome with different reasons; only the rejection's
|
|
889
|
-
* remediation names the full run to do first (R10).
|
|
890
|
-
*/
|
|
891
|
-
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, (command) => Match.value(admissionVerdict(command)).pipe(Match.discriminator("kind")("reject", (verdict) => Result.fail(SurvivorsRejection.make({
|
|
892
|
-
reason: verdict.reason,
|
|
893
|
-
remediation: verdict.remediation
|
|
894
|
-
}))), Match.discriminator("kind")("no-survivors", () => Result.succeed(NoSurvivors.make())), Match.discriminator("kind")("admit", (verdict) => Result.succeed(Admitted.make({ survivors: verdict.survivors }))), Match.exhaustive));
|
|
895
|
-
//#endregion
|
|
896
|
-
//#region src/SurvivorsExit.ts
|
|
897
|
-
/** The exit class a rejected survivors run exits with (R6: exit 2). */
|
|
898
|
-
const SURVIVORS_REJECT_EXIT_CLASS = ExitClass.ConfigError;
|
|
899
|
-
//#endregion
|
|
900
|
-
//#region src/cli-request.schema.ts
|
|
901
|
-
const RunRequestSchema = S.TaggedStruct("run", {
|
|
902
|
-
options: S.Any,
|
|
903
|
-
survivors: S.Boolean
|
|
904
|
-
});
|
|
905
|
-
const LlmsRequestSchema = S.TaggedStruct("llms", { document: S.Any });
|
|
906
|
-
S.Union([RunRequestSchema, LlmsRequestSchema]);
|
|
907
|
-
//#endregion
|
|
908
|
-
//#region src/StrykerCliExecutor.ts
|
|
909
|
-
/**
|
|
910
|
-
* The default run: binds the host-resolved run options (the sink, the mode,
|
|
911
|
-
* the timing) to a fresh `Stryker` and runs mutation testing.
|
|
912
|
-
*/
|
|
913
|
-
const defaultRunMutationTest = (hostOptions) => (options) => new Stryker(options, hostOptions).runMutationTest();
|
|
914
1128
|
/**
|
|
915
|
-
* The
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
* (OutputModeConsoleState.ts).
|
|
1129
|
+
* The survivors of the prior report: exactly the mutants whose status is
|
|
1130
|
+
* `Survived`, converted to the internal mutant shape so a run can re-test
|
|
1131
|
+
* them.
|
|
919
1132
|
*/
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
const hashContent = (content) => createHash("sha256").update(content, "utf-8").digest("hex");
|
|
926
|
-
const resolveAbsolutePath = (file) => resolve(file);
|
|
1133
|
+
function extractSurvivors(priorReport, resolveAbsolutePath) {
|
|
1134
|
+
const survivors = [];
|
|
1135
|
+
for (const [file, fileResult] of objectEntries(priorReport.files)) for (const mutant of fileResult.mutants) if (mutant.status === "Survived") survivors.push(reportMutantToMutant(file, mutant, resolveAbsolutePath));
|
|
1136
|
+
return survivors;
|
|
1137
|
+
}
|
|
927
1138
|
/**
|
|
928
|
-
* The
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* mode keeps the stdout sink. The fix is the descriptor, never the log level.
|
|
1139
|
+
* The survivor spans as `file:startLine:startCol-endLine:endCol` mutate
|
|
1140
|
+
* ranges: the report's 1-based lines with the internal 0-based columns,
|
|
1141
|
+
* relative file names, deduplicated in first-seen order.
|
|
932
1142
|
*/
|
|
933
|
-
function
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
1143
|
+
function survivorMutateSpans(survivors, basePath) {
|
|
1144
|
+
const spans = [];
|
|
1145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const survivor of survivors) {
|
|
1147
|
+
const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
|
|
1148
|
+
const { start, end } = survivor.location;
|
|
1149
|
+
const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
|
|
1150
|
+
if (!seen.has(span)) {
|
|
1151
|
+
seen.add(span);
|
|
1152
|
+
spans.push(span);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return spans;
|
|
945
1156
|
}
|
|
1157
|
+
const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
|
|
1158
|
+
const resolveAbsolutePath = (file) => resolve(file);
|
|
946
1159
|
/**
|
|
947
1160
|
* The survivors admission, as a description whose phases chain by type and
|
|
948
1161
|
* read in the order they run. The read gathers the admission's whole input
|
|
@@ -954,68 +1167,56 @@ function hostOptionsOf(mode, stream) {
|
|
|
954
1167
|
* stashed context back and dispatches the decision to the verdict/run,
|
|
955
1168
|
* failing the run with a rejection.
|
|
956
1169
|
*/
|
|
957
|
-
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext) => pipe(
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
frameworkVersion:
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
...resolvedOptions,
|
|
1008
|
-
survivors: admitted.survivors,
|
|
1009
|
-
mutate: survivorMutateSpans(admitted.survivors),
|
|
1010
|
-
survivorsPriorReport: priorReportPath,
|
|
1011
|
-
incremental: false
|
|
1012
|
-
};
|
|
1013
|
-
return Effect.promise(() => runMutationTest(restricted));
|
|
1014
|
-
}), Match.orElse(() => Effect.die("unreachable admission decision variant"))),
|
|
1015
|
-
onFailure: (rejection) => Effect.fail(rejection)
|
|
1016
|
-
});
|
|
1017
|
-
}))
|
|
1018
|
-
);
|
|
1170
|
+
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath, services) => pipe(Cell.read((cliOptions) => Effect.provideContext(Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
|
|
1171
|
+
const priorReportPath = priorReportPathOf(resolvedOptions);
|
|
1172
|
+
return Effect.flatMap(readPriorReport(priorReportPath), (read) => Effect.flatMap(currentSourceHashesFor(priorReportFileKeys(read.raw)), (sourceContentHashes) => Ref.set(runContext, {
|
|
1173
|
+
resolvedOptions,
|
|
1174
|
+
priorReportPath,
|
|
1175
|
+
pathService
|
|
1176
|
+
}).pipe(Effect.as({
|
|
1177
|
+
resolvedOptions,
|
|
1178
|
+
priorReportRaw: read.raw,
|
|
1179
|
+
priorReportFound: read.found,
|
|
1180
|
+
priorReportPath,
|
|
1181
|
+
sourceContentHashes
|
|
1182
|
+
}))));
|
|
1183
|
+
}))), services)), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
1184
|
+
if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
|
|
1185
|
+
priorReport: void 0,
|
|
1186
|
+
currentConfig: resolvedOptions,
|
|
1187
|
+
frameworkVersion: strykerVersion,
|
|
1188
|
+
sourceContentHashes,
|
|
1189
|
+
priorSourceHashes: {},
|
|
1190
|
+
priorSurvivors: []
|
|
1191
|
+
}));
|
|
1192
|
+
return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
|
|
1193
|
+
priorReport: PriorReportFacts.make({
|
|
1194
|
+
config: document.config ?? {},
|
|
1195
|
+
frameworkVersion: document.framework?.version
|
|
1196
|
+
}),
|
|
1197
|
+
currentConfig: resolvedOptions,
|
|
1198
|
+
frameworkVersion: strykerVersion,
|
|
1199
|
+
sourceContentHashes,
|
|
1200
|
+
priorSourceHashes: priorSourceHashes(document, hashContent),
|
|
1201
|
+
priorSurvivors: extractSurvivors(document, resolveAbsolutePath)
|
|
1202
|
+
}));
|
|
1203
|
+
}), Cell.decide(admitSurvivorsRun), Cell.encode((outcome) => outcome), Cell.write((outcome) => Effect.flatMap(Ref.get(runContext), (context) => {
|
|
1204
|
+
if (context === void 0) return Effect.die("the survivors admission read must run before its write");
|
|
1205
|
+
const { resolvedOptions, priorReportPath, pathService } = context;
|
|
1206
|
+
return Result.match(outcome, {
|
|
1207
|
+
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
|
|
1208
|
+
const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
|
|
1209
|
+
return runMutationTest({
|
|
1210
|
+
...resolvedOptions,
|
|
1211
|
+
survivors: admittedMutants,
|
|
1212
|
+
mutate: survivorMutateSpans(admittedMutants, basePath),
|
|
1213
|
+
survivorsPriorReport: priorReportPath,
|
|
1214
|
+
incremental: false
|
|
1215
|
+
}).pipe(Effect.orDie);
|
|
1216
|
+
}), Match.orElse(() => Effect.die("unreachable admission decision variant"))),
|
|
1217
|
+
onFailure: (rejection) => Effect.fail(rejection)
|
|
1218
|
+
});
|
|
1219
|
+
})));
|
|
1019
1220
|
/**
|
|
1020
1221
|
* The `--survivors` request: re-test exactly the prior report's survivor set.
|
|
1021
1222
|
* The survivors flag was parsed as a boolean; the admission decides between
|
|
@@ -1029,65 +1230,38 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1029
1230
|
* is made; it is in this signature because the phase types put it there, not because the
|
|
1030
1231
|
* admission chose it.
|
|
1031
1232
|
*/
|
|
1032
|
-
function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions) {
|
|
1233
|
+
function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions, basePath) {
|
|
1033
1234
|
return Effect.gen(function* () {
|
|
1235
|
+
const services = yield* Effect.context();
|
|
1034
1236
|
const admissionContext = yield* Ref.make(void 0);
|
|
1035
|
-
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext), cliOptions);
|
|
1237
|
+
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath, services), cliOptions);
|
|
1036
1238
|
});
|
|
1037
1239
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
* config file + CLI, validated against the fork schema (which carries the
|
|
1041
|
-
* survivors-run properties). The admission hash compares these resolved
|
|
1042
|
-
* options against the prior report's embedded config.
|
|
1043
|
-
*/
|
|
1044
|
-
function resolveSurvivorsRunOptions(cliOptions) {
|
|
1045
|
-
return new ConfigReader(noopLogger, new OptionsValidator(forkCoreSchema, noopLogger)).readConfig(cliOptions);
|
|
1240
|
+
function resolveSurvivorsRunOptions(cliOptions, basePath) {
|
|
1241
|
+
return readConfig(cliOptions, basePath);
|
|
1046
1242
|
}
|
|
1047
|
-
/**
|
|
1048
|
-
* The prior report a `--survivors` run reads: the `survivorsPriorReport`
|
|
1049
|
-
* config option when set, else the default path. The report path is run
|
|
1050
|
-
* bookkeeping, never a CLI flag.
|
|
1051
|
-
*/
|
|
1052
1243
|
function priorReportPathOf(resolved) {
|
|
1053
1244
|
const configured = resolved["survivorsPriorReport"];
|
|
1054
|
-
|
|
1245
|
+
if (typeof configured === "string") return configured;
|
|
1246
|
+
return DEFAULT_SURVIVORS_PRIOR_REPORT;
|
|
1055
1247
|
}
|
|
1056
|
-
/**
|
|
1057
|
-
* Reads the prior report without validating it. Absence and malformation are different
|
|
1058
|
-
* outcomes and the caller must be able to tell them apart: an absent report is the
|
|
1059
|
-
* `no-report` rejection the decider states, while a present-but-malformed one is a decode
|
|
1060
|
-
* failure that stops the run. Text that is not JSON is reported as found, carrying the
|
|
1061
|
-
* text itself, so the codec refuses it and names what it got.
|
|
1062
|
-
*/
|
|
1063
1248
|
function readPriorReport(priorReportPath) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1249
|
+
return Effect.gen(function* () {
|
|
1250
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(priorReportPath).pipe(Effect.map((text) => ({
|
|
1251
|
+
found: true,
|
|
1252
|
+
raw: Result.match(S.decodeResult(S.fromJsonString(S.Unknown))(text), {
|
|
1253
|
+
onFailure: () => text,
|
|
1254
|
+
onSuccess: (value) => value
|
|
1255
|
+
})
|
|
1256
|
+
})), Effect.catchTag("PlatformError", (cause) => Match.value(cause.reason).pipe(Match.tag("NotFound", () => Effect.succeed({
|
|
1069
1257
|
found: false,
|
|
1070
1258
|
raw: void 0
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
raw: JSON.parse(text)
|
|
1077
|
-
};
|
|
1078
|
-
} catch {
|
|
1079
|
-
return {
|
|
1080
|
-
found: true,
|
|
1081
|
-
raw: text
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1259
|
+
})), Match.orElse(() => Effect.fail(ConfigFileUnreadableError.make({
|
|
1260
|
+
file: priorReportPath,
|
|
1261
|
+
cause
|
|
1262
|
+
}))))));
|
|
1263
|
+
});
|
|
1084
1264
|
}
|
|
1085
|
-
/**
|
|
1086
|
-
* The relative file names a report claims, read structurally rather than through the
|
|
1087
|
-
* codec because the current sources must be hashed before the report is decoded — the
|
|
1088
|
-
* read phase does the disk I/O, and the keys are what tell it which files to read. An
|
|
1089
|
-
* unrecognisable report yields no keys and is refused a phase later by the codec.
|
|
1090
|
-
*/
|
|
1091
1265
|
function priorReportFileKeys(raw) {
|
|
1092
1266
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
1093
1267
|
if (!("files" in raw)) return [];
|
|
@@ -1096,288 +1270,252 @@ function priorReportFileKeys(raw) {
|
|
|
1096
1270
|
return Object.keys(files);
|
|
1097
1271
|
}
|
|
1098
1272
|
function readSourceFile(file) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1273
|
+
return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
|
|
1274
|
+
file,
|
|
1275
|
+
cause
|
|
1276
|
+
}))));
|
|
1104
1277
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* The per-file content hashes of the current sources, keyed by the relative file names
|
|
1107
|
-
* the prior report uses — the current side of the admission comparison. The prior side is
|
|
1108
|
-
* hashed from the sources the report embeds, in the decode phase.
|
|
1109
|
-
*/
|
|
1110
1278
|
function currentSourceHashesFor(files) {
|
|
1111
|
-
|
|
1112
|
-
for (const file of files) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
|
|
1113
|
-
return hashes;
|
|
1279
|
+
return Effect.map(Effect.forEach(files, (file) => Effect.map(readSourceFile(file), (content) => [file, sourceContentHash(content, hashContent)]), { concurrency: 24 }), (pairs) => Object.fromEntries(pairs));
|
|
1114
1280
|
}
|
|
1281
|
+
//#endregion
|
|
1282
|
+
//#region src/Cli.ts
|
|
1283
|
+
const SIGNAL_NUMBERS = Object.freeze({
|
|
1284
|
+
SIGINT: 2,
|
|
1285
|
+
SIGTERM: 15
|
|
1286
|
+
});
|
|
1115
1287
|
/**
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
* id). Human mode prints nothing (the sink drops in human mode).
|
|
1288
|
+
* Installs the listeners and returns the reader.
|
|
1289
|
+
*
|
|
1290
|
+
* The listener records and returns: interrupting the run is the runtime's job,
|
|
1291
|
+
* and doing it from here would race the run's own finalizer for the stream.
|
|
1292
|
+
* `once` per signal, because a second delivery of the same signal cannot
|
|
1293
|
+
* change the answer.
|
|
1123
1294
|
*/
|
|
1124
|
-
function
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
thresholds,
|
|
1129
|
-
projectRoot: process.cwd(),
|
|
1130
|
-
config,
|
|
1131
|
-
framework: {
|
|
1132
|
-
name: "StrykerJS",
|
|
1133
|
-
version: strykerVersion
|
|
1134
|
-
}
|
|
1135
|
-
};
|
|
1136
|
-
const envelope = buildVerdictEnvelope(report, mode.mode, mode.signal, stream.runId);
|
|
1137
|
-
stream.sink({
|
|
1138
|
-
kind: "verdict",
|
|
1139
|
-
...envelope
|
|
1295
|
+
function observeTerminatingSignal() {
|
|
1296
|
+
let observed = null;
|
|
1297
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
1298
|
+
observed = SIGNAL_NUMBERS[signal] ?? null;
|
|
1140
1299
|
});
|
|
1300
|
+
return () => observed;
|
|
1141
1301
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
* so the run emits the null-score verdict without starting the pipeline. The
|
|
1145
|
-
* full resolved options ride along as the report's embedded config (KTD7).
|
|
1146
|
-
*/
|
|
1147
|
-
function emitEmptySurvivorsVerdict(stream, mode, resolved) {
|
|
1148
|
-
emitNullScoreVerdict(stream, mode, resolved.thresholds, resolved);
|
|
1302
|
+
function isObject(value) {
|
|
1303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1149
1304
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const value
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1305
|
+
function stringField(node, key) {
|
|
1306
|
+
const value = node[key];
|
|
1307
|
+
return (() => {
|
|
1308
|
+
if (typeof value === "string") return value;
|
|
1309
|
+
})();
|
|
1310
|
+
}
|
|
1311
|
+
function stringArrayField(node, key) {
|
|
1312
|
+
const value = node[key];
|
|
1313
|
+
if (!Array.isArray(value)) return [];
|
|
1314
|
+
const strings = [];
|
|
1315
|
+
for (const item of value) if (typeof item === "string") strings.push(item);
|
|
1316
|
+
return strings;
|
|
1317
|
+
}
|
|
1318
|
+
/** The compiled shapes discriminate on `_tag`; read it once, off the record. */
|
|
1319
|
+
function tagOf(node) {
|
|
1320
|
+
return node["_tag"];
|
|
1321
|
+
}
|
|
1322
|
+
function walkParam(param, isOptional, out) {
|
|
1323
|
+
if (!isObject(param)) return;
|
|
1324
|
+
switch (tagOf(param)) {
|
|
1325
|
+
case "Single":
|
|
1326
|
+
describeSingle(param, isOptional, out);
|
|
1327
|
+
return;
|
|
1328
|
+
case "Map":
|
|
1329
|
+
case "Transform":
|
|
1330
|
+
walkParam(param["param"], isOptional, out);
|
|
1331
|
+
return;
|
|
1332
|
+
case "Optional":
|
|
1333
|
+
walkParam(param["param"], true, out);
|
|
1334
|
+
return;
|
|
1335
|
+
case "Variadic":
|
|
1336
|
+
walkParam(param["param"], isOptional, out);
|
|
1337
|
+
return;
|
|
1338
|
+
default: return;
|
|
1165
1339
|
}
|
|
1166
|
-
return "see --reportFile or the verdict envelope on stdout";
|
|
1167
1340
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1341
|
+
const PRIMITIVE_KIND = {
|
|
1342
|
+
Boolean: "boolean",
|
|
1343
|
+
Choice: "choice",
|
|
1344
|
+
Date: "date",
|
|
1345
|
+
FileParse: "file",
|
|
1346
|
+
FileSchema: "file",
|
|
1347
|
+
FileText: "file",
|
|
1348
|
+
Float: "float",
|
|
1349
|
+
Integer: "integer",
|
|
1350
|
+
KeyValuePair: "key=value",
|
|
1351
|
+
None: "none",
|
|
1352
|
+
Path: "path",
|
|
1353
|
+
Redacted: "redacted",
|
|
1354
|
+
String: "text"
|
|
1355
|
+
};
|
|
1356
|
+
function kindOf(primitive) {
|
|
1357
|
+
const tag = stringField(primitive, "_tag");
|
|
1358
|
+
return (() => {
|
|
1359
|
+
if (tag === void 0) return "unknown";
|
|
1360
|
+
return PRIMITIVE_KIND[tag] ?? tag;
|
|
1361
|
+
})();
|
|
1362
|
+
}
|
|
1363
|
+
function choiceValues(primitive) {
|
|
1364
|
+
const keys = primitive["choiceKeys"];
|
|
1365
|
+
if (!Array.isArray(keys)) return;
|
|
1366
|
+
const values = [];
|
|
1367
|
+
for (const key of keys) if (typeof key === "string") values.push(key);
|
|
1368
|
+
return values;
|
|
1185
1369
|
}
|
|
1370
|
+
const REPORTER_NAMES = [
|
|
1371
|
+
"clear-text",
|
|
1372
|
+
"progress",
|
|
1373
|
+
"html",
|
|
1374
|
+
"json",
|
|
1375
|
+
"progress-stream"
|
|
1376
|
+
];
|
|
1186
1377
|
/**
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
* carry the offending flag or operand; when the unrecognized flag was given a
|
|
1190
|
-
* separate value (`--format text`), the value is the token the old parser
|
|
1191
|
-
* reported, so the token after the flag is named when one was given.
|
|
1378
|
+
* v4 option descriptions are stored as `Option.some(string)` on the compiled
|
|
1379
|
+
* `Single`; the walker unwraps the option.
|
|
1192
1380
|
*/
|
|
1193
|
-
function
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1381
|
+
function descriptionOf(single) {
|
|
1382
|
+
const description = single["description"];
|
|
1383
|
+
if (!isObject(description)) return "";
|
|
1384
|
+
switch (tagOf(description)) {
|
|
1385
|
+
case "Some": {
|
|
1386
|
+
const value = description["value"];
|
|
1387
|
+
return (() => {
|
|
1388
|
+
if (typeof value === "string") return value;
|
|
1389
|
+
return "";
|
|
1390
|
+
})();
|
|
1203
1391
|
}
|
|
1204
|
-
|
|
1205
|
-
if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
|
|
1392
|
+
default: return "";
|
|
1206
1393
|
}
|
|
1207
1394
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1395
|
+
function describeSingle(single, isOptional, out) {
|
|
1396
|
+
const name = stringField(single, "name") ?? "";
|
|
1397
|
+
const primitive = (() => {
|
|
1398
|
+
if (isObject(single["primitiveType"])) return single["primitiveType"];
|
|
1399
|
+
return {};
|
|
1400
|
+
})();
|
|
1401
|
+
const kind = kindOf(primitive);
|
|
1402
|
+
const choices = (() => {
|
|
1403
|
+
if (name === "reporters") return REPORTER_NAMES;
|
|
1404
|
+
if (kind === "choice") return choiceValues(primitive);
|
|
1405
|
+
})();
|
|
1406
|
+
const description = descriptionOf(single);
|
|
1407
|
+
const required = kind !== "boolean" && !isOptional;
|
|
1408
|
+
const described = {
|
|
1409
|
+
name,
|
|
1410
|
+
aliases: stringArrayField(single, "aliases"),
|
|
1411
|
+
kind,
|
|
1412
|
+
required,
|
|
1413
|
+
...(() => {
|
|
1414
|
+
if (choices !== void 0) return { choices };
|
|
1415
|
+
return {};
|
|
1416
|
+
})(),
|
|
1417
|
+
description
|
|
1229
1418
|
};
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
* emits that captured document as the `help` terminal event, so `--help` in
|
|
1237
|
-
* machine mode never leaks an ANSI document. A successful run with an empty
|
|
1238
|
-
* buffer (the normal verdict path) emits nothing extra — the run already
|
|
1239
|
-
* wrote its terminal `verdict` line through the same module — unless the
|
|
1240
|
-
* stream is still open, which means the run never reached a verdict (the
|
|
1241
|
-
* `--dryRunOnly` early return): then a null-score `verdict` closes the
|
|
1242
|
-
* stream so the last stdout line is always a terminal event (R5).
|
|
1243
|
-
*/
|
|
1244
|
-
function emitMachineModeOutput(stream, mode, exit, code, argv) {
|
|
1245
|
-
const captured = readCapturedConsole();
|
|
1246
|
-
const value = failureValue(exit);
|
|
1247
|
-
if (Exit.isFailure(exit) && S.is(CliError.ShowHelp)(value) && value.errors.length === 0) {
|
|
1248
|
-
const document = {
|
|
1249
|
-
kind: "help",
|
|
1250
|
-
schemaVersion: "1.0",
|
|
1251
|
-
code: 0,
|
|
1252
|
-
help: captured
|
|
1253
|
-
};
|
|
1254
|
-
stream.sink(document);
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
if (Exit.isFailure(exit)) {
|
|
1258
|
-
stream.sink({
|
|
1259
|
-
kind: "error",
|
|
1260
|
-
...buildErrorEnvelope(exit, code, captured, argv)
|
|
1419
|
+
if (single["kind"] === "argument") {
|
|
1420
|
+
out.args.push({
|
|
1421
|
+
name,
|
|
1422
|
+
kind,
|
|
1423
|
+
required,
|
|
1424
|
+
description
|
|
1261
1425
|
});
|
|
1262
1426
|
return;
|
|
1263
1427
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1428
|
+
out.flags.push(described);
|
|
1429
|
+
}
|
|
1430
|
+
function walkConfigNode(node, orderedParams, out) {
|
|
1431
|
+
if (!isObject(node)) return;
|
|
1432
|
+
switch (tagOf(node)) {
|
|
1433
|
+
case "Param": {
|
|
1434
|
+
const index = node["index"];
|
|
1435
|
+
const param = (() => {
|
|
1436
|
+
if (typeof index === "number") return orderedParams[index];
|
|
1437
|
+
})();
|
|
1438
|
+
if (param !== void 0) walkParam(param, false, out);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
case "Array":
|
|
1442
|
+
if (Array.isArray(node["children"])) for (const child of node["children"]) walkConfigNode(child, orderedParams, out);
|
|
1443
|
+
return;
|
|
1444
|
+
case "Nested":
|
|
1445
|
+
if (isObject(node["tree"])) walkConfigTree(node["tree"], orderedParams, out);
|
|
1446
|
+
return;
|
|
1447
|
+
default: return;
|
|
1273
1448
|
}
|
|
1274
|
-
if (stream.isOpen()) emitNullScoreVerdict(stream, mode, defaultOptions.thresholds, {});
|
|
1275
1449
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1450
|
+
function walkConfigTree(tree, orderedParams, out) {
|
|
1451
|
+
for (const key of Object.keys(tree)) walkConfigNode(tree[key], orderedParams, out);
|
|
1452
|
+
}
|
|
1453
|
+
function describeCommandNode(node) {
|
|
1454
|
+
if (!isObject(node)) return;
|
|
1455
|
+
const out = {
|
|
1456
|
+
flags: [],
|
|
1457
|
+
args: []
|
|
1458
|
+
};
|
|
1459
|
+
const config = node["config"];
|
|
1460
|
+
if (isObject(config) && isObject(config["tree"])) {
|
|
1461
|
+
let orderedParams = [];
|
|
1462
|
+
const maybeOrdered = config["orderedParams"];
|
|
1463
|
+
if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
|
|
1464
|
+
walkConfigTree(config["tree"], orderedParams, out);
|
|
1285
1465
|
}
|
|
1286
|
-
|
|
1466
|
+
const subcommands = [];
|
|
1467
|
+
const grouped = node["subcommands"];
|
|
1468
|
+
if (Array.isArray(grouped)) for (const group of grouped) {
|
|
1469
|
+
if (!isObject(group) || !Array.isArray(group["commands"])) continue;
|
|
1470
|
+
for (const child of group["commands"]) {
|
|
1471
|
+
const described = describeCommandNode(child);
|
|
1472
|
+
if (described !== void 0) subcommands.push(described);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
name: stringField(node, "name") ?? "",
|
|
1477
|
+
description: (() => {
|
|
1478
|
+
if (typeof node["description"] === "string") return node["description"];
|
|
1479
|
+
return "";
|
|
1480
|
+
})(),
|
|
1481
|
+
options: out.flags,
|
|
1482
|
+
args: out.args,
|
|
1483
|
+
subcommands
|
|
1484
|
+
};
|
|
1287
1485
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
* it fall through to 1 would make an unusable `--survivors` input indistinguishable
|
|
1300
|
-
* from a crash.
|
|
1301
|
-
*/
|
|
1302
|
-
function resolveCliExitCode(exit) {
|
|
1303
|
-
if (Exit.isSuccess(exit)) return 0;
|
|
1304
|
-
if (Cause.hasInterruptsOnly(exit.cause)) return 1;
|
|
1305
|
-
const failure = Cause.findErrorOption(exit.cause);
|
|
1306
|
-
if (Option.isSome(failure)) {
|
|
1307
|
-
const value = failure.value;
|
|
1308
|
-
if (S.is(CliError.ShowHelp)(value)) return value.errors.length > 0 ? 2 : 0;
|
|
1309
|
-
if (CliError.isCliError(value)) return 2;
|
|
1310
|
-
if (S.is(SurvivorsRejection)(value)) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
1311
|
-
if (value instanceof S.SchemaError) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
1486
|
+
function readCoreEntries() {
|
|
1487
|
+
try {
|
|
1488
|
+
const manifestPath = createRequire(import.meta.url).resolve("@systemfsoftware/stryker-js-platform-node/package.json");
|
|
1489
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
1490
|
+
const parsed = JSON.parse(raw);
|
|
1491
|
+
const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
|
|
1492
|
+
const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
|
|
1493
|
+
if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
|
|
1494
|
+
return [];
|
|
1495
|
+
} catch {
|
|
1496
|
+
return [];
|
|
1312
1497
|
}
|
|
1313
|
-
if (carriesConfigError(exit.cause)) return ExitClass.ConfigError;
|
|
1314
|
-
return 1;
|
|
1315
1498
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
* null verdict) and drains the stream, returning the classed exit code as its
|
|
1324
|
-
* value. SIGINT/SIGTERM interrupt the current fiber so the finalizer runs
|
|
1325
|
-
* before the process exits; the code is resolved exactly once (R6), in the
|
|
1326
|
-
* finalizer, where the terminal event's `code` is chosen from the same inputs
|
|
1327
|
-
* the teardown used before.
|
|
1328
|
-
*/
|
|
1329
|
-
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1330
|
-
const stream = yield* createRunEventStream(input.mode);
|
|
1331
|
-
const runMutationTest = input.runMutationTest ?? defaultRunMutationTest(hostOptionsOf(input.mode, stream));
|
|
1332
|
-
let currentFiber = null;
|
|
1333
|
-
let lastSignal = null;
|
|
1334
|
-
const resolveClassedExitCode = (exit) => {
|
|
1335
|
-
const signal = lastSignal;
|
|
1336
|
-
if (signal !== null) return 128 + signal;
|
|
1337
|
-
if (Exit.isFailure(exit)) return resolveCliExitCode(exit);
|
|
1338
|
-
return resolveExitCode(getPendingExitClasses(), null);
|
|
1499
|
+
function buildLLMSManifest(command, version) {
|
|
1500
|
+
const root = describeCommandNode(command) ?? {
|
|
1501
|
+
name: "",
|
|
1502
|
+
description: "",
|
|
1503
|
+
options: [],
|
|
1504
|
+
args: [],
|
|
1505
|
+
subcommands: []
|
|
1339
1506
|
};
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1507
|
+
return {
|
|
1508
|
+
schemaVersion: "1.0",
|
|
1509
|
+
tool: root.name,
|
|
1510
|
+
version,
|
|
1511
|
+
commands: [root],
|
|
1512
|
+
entries: readCoreEntries()
|
|
1345
1513
|
};
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
});
|
|
1352
|
-
stream.sink(llmsRequest.document);
|
|
1353
|
-
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1354
|
-
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1355
|
-
currentFiber = Fiber.getCurrent() ?? null;
|
|
1356
|
-
process.on("SIGINT", onSignal);
|
|
1357
|
-
process.on("SIGTERM", onSignal);
|
|
1358
|
-
}), () => Effect.gen(function* () {
|
|
1359
|
-
yield* stream.open;
|
|
1360
|
-
yield* input.program;
|
|
1361
|
-
const request = yield* Ref.get(input.requestRef);
|
|
1362
|
-
yield* Option.match(request, {
|
|
1363
|
-
onNone: () => Effect.void,
|
|
1364
|
-
onSome: (cliRequest) => dispatch(cliRequest)
|
|
1365
|
-
});
|
|
1366
|
-
}), () => Effect.sync(() => {
|
|
1367
|
-
process.removeListener("SIGINT", onSignal);
|
|
1368
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1369
|
-
}));
|
|
1370
|
-
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
1371
|
-
const exit = yield* Effect.exit(restore(program));
|
|
1372
|
-
const code = resolveClassedExitCode(exit);
|
|
1373
|
-
input.recordExitCode(code);
|
|
1374
|
-
if (input.mode.mode === "machine") emitMachineModeOutput(stream, input.mode, exit, code, input.argv);
|
|
1375
|
-
yield* stream.closeAndDrain;
|
|
1376
|
-
return code;
|
|
1377
|
-
}));
|
|
1378
|
-
});
|
|
1379
|
-
//#endregion
|
|
1380
|
-
//#region src/StrykerCliHandler.ts
|
|
1514
|
+
}
|
|
1515
|
+
/** The manifest as one JSON document, ready for stdout — the U4 convention. */
|
|
1516
|
+
function emitLLMSManifest(command, version) {
|
|
1517
|
+
return JSON.stringify(buildLLMSManifest(command, version));
|
|
1518
|
+
}
|
|
1381
1519
|
function createSplitter(separator) {
|
|
1382
1520
|
return (value) => value.split(separator).filter(Boolean);
|
|
1383
1521
|
}
|
|
@@ -1390,7 +1528,10 @@ const splitOnSpace = createSplitter(" ");
|
|
|
1390
1528
|
*/
|
|
1391
1529
|
function parseCleanDirOption(value) {
|
|
1392
1530
|
const v = value.toLocaleLowerCase();
|
|
1393
|
-
return
|
|
1531
|
+
return (() => {
|
|
1532
|
+
if (v === "always") return v;
|
|
1533
|
+
return v !== "false" && v !== "0";
|
|
1534
|
+
})();
|
|
1394
1535
|
}
|
|
1395
1536
|
/**
|
|
1396
1537
|
* Commander characterization: a pure integer is parsed as a number, anything
|
|
@@ -1408,31 +1549,12 @@ const optional = (option) => Flag.optional(option);
|
|
|
1408
1549
|
* `Option.some(false)` for an explicit `--no-x`, so both map back to
|
|
1409
1550
|
* `undefined` and leave the config-file default in force (KTD4).
|
|
1410
1551
|
*/
|
|
1411
|
-
const absentWhenFalse = (value) =>
|
|
1412
|
-
|
|
1413
|
-
"fatal",
|
|
1414
|
-
"error",
|
|
1415
|
-
"warn",
|
|
1416
|
-
"info",
|
|
1417
|
-
"debug",
|
|
1418
|
-
"trace",
|
|
1419
|
-
"off"
|
|
1420
|
-
];
|
|
1421
|
-
const LOG_LEVEL_LOOKUP = {
|
|
1422
|
-
fatal: true,
|
|
1423
|
-
error: true,
|
|
1424
|
-
warn: true,
|
|
1425
|
-
info: true,
|
|
1426
|
-
debug: true,
|
|
1427
|
-
trace: true,
|
|
1428
|
-
off: true
|
|
1552
|
+
const absentWhenFalse = (value) => {
|
|
1553
|
+
if (Option.isSome(value) && value.value) return true;
|
|
1429
1554
|
};
|
|
1430
|
-
function isLogLevel(value) {
|
|
1431
|
-
return LOG_LEVEL_LOOKUP[value] === true;
|
|
1432
|
-
}
|
|
1433
1555
|
function setLogLevel(target, key, value) {
|
|
1434
1556
|
const unwrapped = unwrap(value);
|
|
1435
|
-
if (unwrapped !== void 0
|
|
1557
|
+
if (unwrapped !== void 0) target[key] = unwrapped;
|
|
1436
1558
|
}
|
|
1437
1559
|
const runOptions = {
|
|
1438
1560
|
ignorePatterns: Flag.string("ignorePatterns").pipe(Flag.withDescription("A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have NO effect when using the `--inPlace` option. The directories `node_modules`, `.git` and some others are always ignored. Example: `--ignorePatterns dist`. These patterns are ALWAYS ignored: [`node_modules`, `.git`, `/reports`, `*.tsbuildinfo`, `/stryker.log`, `.stryker-tmp`]. Because Stryker always ignores these, you should rarely have to adjust the `ignorePatterns` setting at all. This is useful to speed up Stryker by reducing the size of the sandbox directory which has a positive effect on performance."), Flag.map(splitOnComma), optional),
|
|
@@ -1451,7 +1573,7 @@ const runOptions = {
|
|
|
1451
1573
|
"perTest",
|
|
1452
1574
|
"all",
|
|
1453
1575
|
"off"
|
|
1454
|
-
]).pipe(Flag.withDescription(`The coverage analysis strategy you want to use. Default value: "${
|
|
1576
|
+
]).pipe(Flag.withDescription(`The coverage analysis strategy you want to use. Default value: "${RENDERED_OPTION_DEFAULTS.coverageAnalysis}"`), optional),
|
|
1455
1577
|
testRunner: Flag.string("testRunner").pipe(Flag.withDescription("The name of the test runner you want to use"), optional),
|
|
1456
1578
|
testRunnerNodeArgs: Flag.string("testRunnerNodeArgs").pipe(Flag.withDescription("A list of node args to be passed to test runner child processes. Split on spaces (commander characterization): `--testRunnerNodeArgs \"--inspect-brk --trace-warnings\"`."), Flag.map(splitOnSpace), optional),
|
|
1457
1579
|
reporters: Flag.string("reporters").pipe(Flag.withDescription("A comma separated list of the names of the reporter(s) you want to use"), Flag.map(splitOnComma), optional),
|
|
@@ -1464,11 +1586,27 @@ const runOptions = {
|
|
|
1464
1586
|
concurrency: Flag.string("concurrency").pipe(Flag.withAlias("c"), Flag.withDescription("Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)"), Flag.map(parseConcurrency), optional),
|
|
1465
1587
|
disableBail: Flag.map(optional(Flag.boolean("disableBail")), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
|
|
1466
1588
|
maxTestRunnerReuse: Flag.integer("maxTestRunnerReuse").pipe(Flag.withDescription("Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse."), optional),
|
|
1467
|
-
logLevel: Flag.choice("logLevel",
|
|
1468
|
-
|
|
1589
|
+
logLevel: Flag.choice("logLevel", [
|
|
1590
|
+
"fatal",
|
|
1591
|
+
"error",
|
|
1592
|
+
"warn",
|
|
1593
|
+
"info",
|
|
1594
|
+
"debug",
|
|
1595
|
+
"trace",
|
|
1596
|
+
"off"
|
|
1597
|
+
]).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.logLevel}"`), optional),
|
|
1598
|
+
fileLogLevel: Flag.choice("fileLogLevel", [
|
|
1599
|
+
"fatal",
|
|
1600
|
+
"error",
|
|
1601
|
+
"warn",
|
|
1602
|
+
"info",
|
|
1603
|
+
"debug",
|
|
1604
|
+
"trace",
|
|
1605
|
+
"off"
|
|
1606
|
+
]).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.fileLogLevel}"`), optional),
|
|
1469
1607
|
inPlace: Flag.map(optional(Flag.boolean("inPlace")), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
|
|
1470
1608
|
tempDirName: Flag.string("tempDirName").pipe(Flag.withDescription("Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run"), optional),
|
|
1471
|
-
cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${
|
|
1609
|
+
cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${RENDERED_OPTION_DEFAULTS.tempDirName}" inside the current working directory by default) after a run.\n- false: Never delete the temp dir;\n- true: Delete the tmp dir after a successful run;\n- always: Always delete the temp dir, regardless of whether the run was successful.`), Flag.map(parseCleanDirOption), optional),
|
|
1472
1610
|
survivors: Flag.map(optional(Flag.boolean("survivors")), absentWhenFalse).pipe(Flag.withDescription("Re-run only the mutants that survived a previous run. Admits against the previous run's mutation report (the `survivorsPriorReport` config option, default `reports/mutation-report.json`) and re-tests exactly the survivor set. Exits 2 with a remediation naming a full run when the report is missing, drifted, or the configuration changed; exits 0 with a null score when the report has no survivors."))
|
|
1473
1611
|
};
|
|
1474
1612
|
const runArgs = { configFile: Argument.optional(Argument.string("configFile")) };
|
|
@@ -1550,7 +1688,7 @@ function makeStrykerCommand(requestRef) {
|
|
|
1550
1688
|
const strykerCommand = Command.make("stryker", rootConfig, (config) => {
|
|
1551
1689
|
if (config.llms === true) {
|
|
1552
1690
|
const document = {
|
|
1553
|
-
|
|
1691
|
+
_tag: "manifest",
|
|
1554
1692
|
schemaVersion: "1.0",
|
|
1555
1693
|
code: 0,
|
|
1556
1694
|
manifest: emitLLMSManifest(strykerCommand, strykerVersion)
|
|
@@ -1598,48 +1736,148 @@ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
|
|
|
1598
1736
|
GlobalFlag.Wizard,
|
|
1599
1737
|
GlobalFlag.Completions,
|
|
1600
1738
|
GlobalFlag.LogLevel
|
|
1601
|
-
] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, Layer.
|
|
1602
|
-
|
|
1603
|
-
* The transport entry: builds the command tree, resolves the mode once at
|
|
1604
|
-
* the edge (never a second probe), provides the CLI and Console layers the
|
|
1605
|
-
* framework renders through, delegates the whole run to the executor cell and
|
|
1606
|
-
* returns the classed exit code it computes. The executor is the I/O
|
|
1607
|
-
* sandwich; this function only frames it.
|
|
1608
|
-
*/
|
|
1609
|
-
function strykerCliEffect(argv, runMutationTest, recordExitCode, detectMode, createRunEventStream) {
|
|
1739
|
+
] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))));
|
|
1740
|
+
function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStream, lastSignal) {
|
|
1610
1741
|
return Effect.gen(function* () {
|
|
1611
|
-
const mode = detectMode
|
|
1742
|
+
const mode = yield* detectMode;
|
|
1612
1743
|
const requestRef = yield* Ref.make(Option.none());
|
|
1613
1744
|
const command = makeStrykerCommand(requestRef);
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1745
|
+
const consoleLayer = (() => {
|
|
1746
|
+
if (mode.mode === "machine") return machineConsoleLayer;
|
|
1747
|
+
return Layer.empty;
|
|
1748
|
+
})();
|
|
1749
|
+
const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(consoleLayer, cliLayer)));
|
|
1750
|
+
const result = yield* Effect.result(runStrykerCli({
|
|
1616
1751
|
program: cliEffect,
|
|
1617
1752
|
requestRef,
|
|
1618
1753
|
mode,
|
|
1619
1754
|
runMutationTest,
|
|
1620
|
-
|
|
1621
|
-
|
|
1755
|
+
argv,
|
|
1756
|
+
lastSignal
|
|
1622
1757
|
}, createRunEventStream));
|
|
1623
|
-
|
|
1624
|
-
|
|
1758
|
+
if (Result.isFailure(result)) return result.failure;
|
|
1759
|
+
return result.success;
|
|
1760
|
+
}).pipe(Effect.orElseSucceed(() => 2));
|
|
1625
1761
|
}
|
|
1762
|
+
const defaultRunMutationTest = (hostOptions, queue) => (options) => Effect.scoped(runMutationTest(options)).pipe(Effect.provideService(RunEvents, queue), Effect.provide(makeRunLayer(hostOptions)));
|
|
1763
|
+
function hostOptionsOf(mode, stream) {
|
|
1764
|
+
return {
|
|
1765
|
+
runId: stream.runId,
|
|
1766
|
+
resolvedMode: mode,
|
|
1767
|
+
runStartedAt: stream.startedAt,
|
|
1768
|
+
basePath: resolve(process.cwd()),
|
|
1769
|
+
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-platform-node/builtin-reporters")],
|
|
1770
|
+
allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1774
|
+
const stream = yield* createRunEventStream(input.mode);
|
|
1775
|
+
const hostOptions = hostOptionsOf(input.mode, stream);
|
|
1776
|
+
const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions, stream.queue);
|
|
1777
|
+
const basePath = hostOptions.basePath;
|
|
1778
|
+
const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
|
|
1779
|
+
let currentFiber = null;
|
|
1780
|
+
const onSignal = () => {
|
|
1781
|
+
process.removeListener("SIGINT", onSignal);
|
|
1782
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1783
|
+
if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
|
|
1784
|
+
};
|
|
1785
|
+
const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
|
|
1786
|
+
if (runRequest.survivors) return runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions)));
|
|
1787
|
+
return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
|
|
1788
|
+
})()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
|
|
1789
|
+
stream.ensureOpen({
|
|
1790
|
+
mode: "machine",
|
|
1791
|
+
signal: "flag",
|
|
1792
|
+
stdoutIsTTY: process.stdout.isTTY === true
|
|
1793
|
+
});
|
|
1794
|
+
yield* Queue.offer(stream.queue, ManifestRendered.make({
|
|
1795
|
+
schemaVersion: "1.0",
|
|
1796
|
+
code: 0,
|
|
1797
|
+
manifest: llmsRequest.document.manifest
|
|
1798
|
+
}));
|
|
1799
|
+
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1800
|
+
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1801
|
+
currentFiber = Fiber.getCurrent() ?? null;
|
|
1802
|
+
process.on("SIGINT", onSignal);
|
|
1803
|
+
process.on("SIGTERM", onSignal);
|
|
1804
|
+
}), () => Effect.gen(function* () {
|
|
1805
|
+
yield* stream.open;
|
|
1806
|
+
yield* input.program;
|
|
1807
|
+
const request = yield* Ref.get(input.requestRef);
|
|
1808
|
+
return yield* Option.match(request, {
|
|
1809
|
+
onNone: () => Effect.void,
|
|
1810
|
+
onSome: (cliRequest) => dispatch(cliRequest)
|
|
1811
|
+
});
|
|
1812
|
+
}), () => Effect.sync(() => {
|
|
1813
|
+
process.removeListener("SIGINT", onSignal);
|
|
1814
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1815
|
+
}));
|
|
1816
|
+
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
1817
|
+
const outcome = classifyRunOutcome(yield* Effect.exit(restore(program)), input.lastSignal(), input.argv);
|
|
1818
|
+
const code = runOutcomeCode(outcome);
|
|
1819
|
+
if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, outcome, basePath, pathService);
|
|
1820
|
+
yield* stream.closeAndDrain;
|
|
1821
|
+
return code;
|
|
1822
|
+
}));
|
|
1823
|
+
});
|
|
1824
|
+
//#endregion
|
|
1825
|
+
//#region src/StreamFile.ts
|
|
1826
|
+
const STREAM_FILE_DIR = "reports";
|
|
1827
|
+
const STREAM_FILE_NAME = "mutation-stream.jsonl";
|
|
1828
|
+
const encodeUtf8 = (line) => new TextEncoder().encode(line);
|
|
1829
|
+
const drainStreamFile = (framed) => Effect.gen(function* () {
|
|
1830
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1831
|
+
const file = (yield* Path.Path).join(STREAM_FILE_DIR, STREAM_FILE_NAME);
|
|
1832
|
+
yield* fs.makeDirectory(STREAM_FILE_DIR, { recursive: true }).pipe(Effect.orDie);
|
|
1833
|
+
yield* Stream.run(framed.pipe(Stream.map(encodeUtf8)), fs.sink(file, { flag: "w" })).pipe(Effect.orDie);
|
|
1834
|
+
});
|
|
1835
|
+
const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(function* () {
|
|
1836
|
+
const stdio = yield* Stdio.Stdio;
|
|
1837
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1838
|
+
const path = yield* Path.Path;
|
|
1839
|
+
const drainFramed = (framed) => drainStreamFile(framed).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
1840
|
+
return RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved, drainFramed) });
|
|
1841
|
+
}));
|
|
1626
1842
|
//#endregion
|
|
1627
1843
|
//#region src/main.ts
|
|
1628
1844
|
const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
|
|
1629
1845
|
process.title = "stryker";
|
|
1630
|
-
|
|
1631
|
-
|
|
1846
|
+
const lastSignal = observeTerminatingSignal();
|
|
1847
|
+
function isSupportedNodeVersion(version) {
|
|
1848
|
+
let withoutV = version;
|
|
1849
|
+
if (version.startsWith("v")) withoutV = version.slice(1);
|
|
1850
|
+
const dashBaseRaw = withoutV.split("-")[0];
|
|
1851
|
+
let dashBase = withoutV;
|
|
1852
|
+
if (dashBaseRaw !== void 0) dashBase = dashBaseRaw;
|
|
1853
|
+
const baseRaw = dashBase.split("+")[0];
|
|
1854
|
+
let base = dashBase;
|
|
1855
|
+
if (baseRaw !== void 0) base = baseRaw;
|
|
1856
|
+
const parts = base.split(".").map((p) => Number.parseInt(p, 10));
|
|
1857
|
+
const major = parts[0] ?? 0;
|
|
1858
|
+
const minor = parts[1] ?? 0;
|
|
1859
|
+
const patch = parts[2] ?? 0;
|
|
1860
|
+
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
|
|
1861
|
+
if (major !== 20) return major > 20;
|
|
1862
|
+
if (minor !== 0) return minor > 0;
|
|
1863
|
+
return patch >= 0;
|
|
1864
|
+
}
|
|
1865
|
+
if (!isSupportedNodeVersion(process.version)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${strykerEngines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
|
|
1632
1866
|
const program = Effect.gen(function* () {
|
|
1633
1867
|
const outputMode = yield* OutputModeProbe;
|
|
1634
1868
|
const runEvents = yield* RunEventStreamPort;
|
|
1635
|
-
return yield* strykerCliEffect(process.argv.slice(2), void 0,
|
|
1636
|
-
|
|
1637
|
-
}, outputMode.detectMode, runEvents.createRunEventStream);
|
|
1638
|
-
}).pipe(Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamLive).pipe(Layer.provide(NodeStdio.layer))));
|
|
1869
|
+
return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
|
|
1870
|
+
}).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
|
|
1639
1871
|
NodeRuntime.runMain(program, {
|
|
1640
1872
|
disableErrorReporting: true,
|
|
1641
|
-
teardown: (
|
|
1642
|
-
|
|
1873
|
+
teardown: (exit, onExit) => {
|
|
1874
|
+
if (Exit.isSuccess(exit) && typeof exit.value === "number") {
|
|
1875
|
+
onExit(exit.value);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const signal = lastSignal();
|
|
1879
|
+
if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
|
|
1880
|
+
else onExit(128 + signal);
|
|
1643
1881
|
}
|
|
1644
1882
|
});
|
|
1645
1883
|
//#endregion
|