@systemfsoftware/stryker-js-cli 3.2.0 → 4.0.1
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 +93 -0
- package/README.md +8 -7
- package/dist/main.mjs +928 -790
- package/package.json +17 -16
package/dist/main.mjs
CHANGED
|
@@ -1,303 +1,123 @@
|
|
|
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";
|
|
5
9
|
import * as Exit from "effect/Exit";
|
|
6
10
|
import * as Layer from "effect/Layer";
|
|
7
11
|
import * as Logger from "effect/Logger";
|
|
8
|
-
import { ExitClass, buildVerdictEnvelope, defaultOptions, defaultStages, forkCoreSchema, generateRunId, highestExitClass, makeRunLayer, readConfig, resolveExitCode, runMutationTest, strykerEngines, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-mutation-run";
|
|
9
|
-
import * as Context from "effect/Context";
|
|
10
|
-
import * as Result from "effect/Result";
|
|
11
|
-
import * as CliError from "effect/unstable/cli/CliError";
|
|
12
|
-
import * as Cause from "effect/Cause";
|
|
13
|
-
import * as Clock from "effect/Clock";
|
|
14
|
-
import * as Deferred from "effect/Deferred";
|
|
15
|
-
import * as Fiber from "effect/Fiber";
|
|
16
|
-
import * as Queue from "effect/Queue";
|
|
17
|
-
import * as Stdio from "effect/Stdio";
|
|
18
|
-
import * as Stream from "effect/Stream";
|
|
19
12
|
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
13
|
+
import { Heartbeat, HelpRendered, ManifestRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
|
|
14
|
+
import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
|
|
15
|
+
import * as Cause from "effect/Cause";
|
|
23
16
|
import * as Console from "effect/Console";
|
|
17
|
+
import * as Fiber from "effect/Fiber";
|
|
24
18
|
import * as FileSystem from "effect/FileSystem";
|
|
19
|
+
import * as Match from "effect/Match";
|
|
25
20
|
import * as Option from "effect/Option";
|
|
26
21
|
import * as Path from "effect/Path";
|
|
22
|
+
import * as Queue from "effect/Queue";
|
|
27
23
|
import * as Ref from "effect/Ref";
|
|
24
|
+
import * as Result from "effect/Result";
|
|
25
|
+
import * as S from "effect/Schema";
|
|
28
26
|
import * as Terminal from "effect/Terminal";
|
|
29
27
|
import * as Argument from "effect/unstable/cli/Argument";
|
|
30
28
|
import * as CliConfig from "effect/unstable/cli/CliConfig";
|
|
29
|
+
import * as CliError from "effect/unstable/cli/CliError";
|
|
31
30
|
import * as Command from "effect/unstable/cli/Command";
|
|
32
31
|
import * as Flag from "effect/unstable/cli/Flag";
|
|
33
32
|
import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
34
|
import { resolve } from "node:path";
|
|
35
|
-
import {
|
|
36
|
-
import
|
|
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";
|
|
37
39
|
import * as Predicate from "effect/Predicate";
|
|
38
|
-
import * as S from "effect/Schema";
|
|
39
40
|
import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
40
|
-
import
|
|
41
|
-
import { format, inspect } from "node:util";
|
|
42
|
-
import { createHash } from "node:crypto";
|
|
43
|
-
import { readFileSync } from "node:fs";
|
|
44
|
-
import { noopLogger } from "@systemfsoftware/stryker-js-plugin-api/logging";
|
|
41
|
+
import * as Context from "effect/Context";
|
|
45
42
|
import { pipe } from "effect/Function";
|
|
46
|
-
import
|
|
47
|
-
import
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
95
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
stdoutIsTTY: true
|
|
104
|
+
return RunFailed$1.make({
|
|
105
|
+
code: 1,
|
|
106
|
+
diagnostic: command.diagnostic
|
|
106
107
|
});
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* harness merging `2>&1` is not handed escape sequences it must strip, and
|
|
111
|
-
* `NO_COLOR` is honoured for the human path per the convention: any value
|
|
112
|
-
* other than an unset or empty variable disables colour.
|
|
113
|
-
*/
|
|
114
|
-
function isColorEnabled(resolved, noColor) {
|
|
115
|
-
return resolved.mode === "human" && (noColor === void 0 || noColor.length === 0);
|
|
109
|
+
function succeedRun(ok) {
|
|
110
|
+
return Result.succeed(ok);
|
|
116
111
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/output-mode-probe/OutputModeProbeTag") {};
|
|
120
|
-
const OutputModeProbe = OutputModeProbeTag;
|
|
121
|
-
const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: () => {
|
|
122
|
-
const envMode = process.env["STRYKER_MODE"];
|
|
123
|
-
const agent = process.env["AGENT"];
|
|
124
|
-
return Result.getOrThrow(resolveMode({
|
|
125
|
-
stdoutIsTTY: process.stdout.isTTY === true,
|
|
126
|
-
...envMode !== void 0 ? { envMode } : {},
|
|
127
|
-
...agent !== void 0 ? { agent } : {},
|
|
128
|
-
toolVars: Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]]))
|
|
129
|
-
}));
|
|
130
|
-
} }));
|
|
131
|
-
//#endregion
|
|
132
|
-
//#region src/stream-protocol.ts
|
|
133
|
-
/**
|
|
134
|
-
* The heartbeat interval (R19), matching Terraform's `apply_progress`
|
|
135
|
-
* cadence: long enough that a slow phase is not noisy, short enough that a
|
|
136
|
-
* consumer can tell "slow" from "hung" without waiting for a mutant event.
|
|
137
|
-
*/
|
|
138
|
-
const TICK_INTERVAL_MS = 1e4;
|
|
139
|
-
//#endregion
|
|
140
|
-
//#region src/run-event-stream.ts
|
|
141
|
-
var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/run-event-stream/RunEventStreamPortTag") {};
|
|
142
|
-
const RunEventStreamPort = RunEventStreamPortTag;
|
|
143
|
-
const isTerminalEvent = (event) => event.kind === "verdict" || event.kind === "error" || event.kind === "help" || event.kind === "manifest";
|
|
144
|
-
/**
|
|
145
|
-
* The push adapter from the run's synchronous sink to the callback mailbox.
|
|
146
|
-
* `Queue.offer`/`Queue.end` on an unbounded queue never block or fail, so the
|
|
147
|
-
* sync sink can drive them with `Effect.runSyncWith(ctx)` using the context
|
|
148
|
-
* captured where the sink is constructed. Takes the queue and the captured
|
|
149
|
-
* context, keeping the adapter pure with respect to its environment.
|
|
150
|
-
*/
|
|
151
|
-
function queueEmit(queue, ctx) {
|
|
152
|
-
return {
|
|
153
|
-
single: (event) => {
|
|
154
|
-
Effect.runSyncWith(ctx)(Queue.offer(queue, event));
|
|
155
|
-
},
|
|
156
|
-
end: () => {
|
|
157
|
-
Effect.runSyncWith(ctx)(Queue.end(queue));
|
|
158
|
-
}
|
|
159
|
-
};
|
|
112
|
+
function failRun(error) {
|
|
113
|
+
return Result.fail(error);
|
|
160
114
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* sink — `NodeStdio.layer` at the composition root supplies the service, and
|
|
164
|
-
* the sink owns the writable's backpressure, the scoped `'error'` listener a
|
|
165
|
-
* closed consumer raises, and the final `'finish'` wait (`endOnDone`). A
|
|
166
|
-
* write failure surfaces as a `PlatformError` which this catch swallows — a
|
|
167
|
-
* consumer closing the pipe must not replace the run's classed exit code
|
|
168
|
-
* (R31) — and the drain still completes only after every byte was handed to
|
|
169
|
-
* the OS (R30).
|
|
170
|
-
*/
|
|
171
|
-
const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
|
|
172
|
-
/**
|
|
173
|
-
* Creates a run's stream. The drain is an Effect the composition root forks
|
|
174
|
-
* before the run; until then the sink is unbound and every push is dropped,
|
|
175
|
-
* which is what makes human mode — or an absent drain — inert without
|
|
176
|
-
* per-call probing (R2). The run's clock zero is read from the runtime so
|
|
177
|
-
* the adapter never touches the wall clock directly.
|
|
178
|
-
*/
|
|
179
|
-
const makeRunEventStream = (stdio, resolved) => Effect.gen(function* () {
|
|
180
|
-
const runId = generateRunId();
|
|
181
|
-
const startedAt = yield* Clock.currentTimeMillis;
|
|
182
|
-
const ctx = yield* Effect.context();
|
|
183
|
-
const state = {
|
|
184
|
-
mode: resolved.mode,
|
|
185
|
-
signal: resolved.signal,
|
|
186
|
-
emit: null,
|
|
187
|
-
headerWritten: false,
|
|
188
|
-
terminalWritten: false,
|
|
189
|
-
progress: {
|
|
190
|
-
completed: 0,
|
|
191
|
-
total: null
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const registered = yield* Deferred.make();
|
|
195
|
-
const eventStream = Stream.callback((queue) => Effect.sync(() => {
|
|
196
|
-
state.emit = queueEmit(queue, ctx);
|
|
197
|
-
}).pipe(Effect.andThen(Deferred.succeed(registered, void 0))));
|
|
198
|
-
const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.filter(() => state.mode === "machine" && state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
|
|
199
|
-
return {
|
|
200
|
-
kind: "tick",
|
|
201
|
-
elapsedMs: (yield* Clock.currentTimeMillis) - startedAt,
|
|
202
|
-
completed: state.progress.completed,
|
|
203
|
-
total: state.progress.total
|
|
204
|
-
};
|
|
205
|
-
})));
|
|
206
|
-
let terminalSeen = false;
|
|
207
|
-
const framed = Stream.merge(eventStream, tickStream, { haltStrategy: "either" }).pipe(Stream.filter((event) => {
|
|
208
|
-
if (terminalSeen) return false;
|
|
209
|
-
if (isTerminalEvent(event)) terminalSeen = true;
|
|
210
|
-
return true;
|
|
211
|
-
}), Stream.map((event) => `${JSON.stringify(event)}\n`));
|
|
212
|
-
const drain = drainOf(stdio, framed);
|
|
213
|
-
let drainFiber = null;
|
|
214
|
-
const sink = (event) => {
|
|
215
|
-
const emit = state.emit;
|
|
216
|
-
if (emit === null || state.terminalWritten || state.mode !== "machine") return;
|
|
217
|
-
if (!state.headerWritten) {
|
|
218
|
-
state.headerWritten = true;
|
|
219
|
-
emit.single({
|
|
220
|
-
kind: "stream",
|
|
221
|
-
schemaVersion: "1.0",
|
|
222
|
-
runId,
|
|
223
|
-
mode: state.mode,
|
|
224
|
-
signal: state.signal
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
switch (event.kind) {
|
|
228
|
-
case "stream":
|
|
229
|
-
case "tick": return;
|
|
230
|
-
case "phase": break;
|
|
231
|
-
case "plan":
|
|
232
|
-
state.progress = {
|
|
233
|
-
...state.progress,
|
|
234
|
-
total: event.total
|
|
235
|
-
};
|
|
236
|
-
break;
|
|
237
|
-
case "mutant":
|
|
238
|
-
state.progress = {
|
|
239
|
-
completed: event.completed,
|
|
240
|
-
total: event.total
|
|
241
|
-
};
|
|
242
|
-
break;
|
|
243
|
-
case "verdict":
|
|
244
|
-
case "error":
|
|
245
|
-
case "help":
|
|
246
|
-
case "manifest":
|
|
247
|
-
emit.single(event);
|
|
248
|
-
state.terminalWritten = true;
|
|
249
|
-
emit.end();
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
emit.single(event);
|
|
253
|
-
};
|
|
254
|
-
return {
|
|
255
|
-
sink,
|
|
256
|
-
runId,
|
|
257
|
-
startedAt,
|
|
258
|
-
isOpen: () => state.emit !== null && state.mode === "machine" && !state.terminalWritten,
|
|
259
|
-
ensureOpen: (openResolved) => {
|
|
260
|
-
if (state.headerWritten) return;
|
|
261
|
-
state.mode = openResolved.mode;
|
|
262
|
-
state.signal = openResolved.signal;
|
|
263
|
-
},
|
|
264
|
-
open: Effect.gen(function* () {
|
|
265
|
-
if (drainFiber === null) {
|
|
266
|
-
drainFiber = yield* Effect.forkDetach(drain);
|
|
267
|
-
yield* Effect.race(Deferred.await(registered), Fiber.await(drainFiber));
|
|
268
|
-
}
|
|
269
|
-
}),
|
|
270
|
-
closeAndDrain: Effect.gen(function* () {
|
|
271
|
-
state.terminalWritten = true;
|
|
272
|
-
state.emit?.end();
|
|
273
|
-
if (drainFiber !== null) yield* Fiber.join(drainFiber);
|
|
274
|
-
})
|
|
275
|
-
};
|
|
276
|
-
});
|
|
277
|
-
const RunEventStreamLive = Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/signal-observer.ts
|
|
280
|
-
const SIGNAL_NUMBERS = Object.freeze({
|
|
281
|
-
SIGINT: 2,
|
|
282
|
-
SIGTERM: 15
|
|
283
|
-
});
|
|
284
|
-
/**
|
|
285
|
-
* Installs the listeners and returns the reader.
|
|
286
|
-
*
|
|
287
|
-
* The listener records and returns: interrupting the run is the runtime's job,
|
|
288
|
-
* and doing it from here would race the run's own finalizer for the stream.
|
|
289
|
-
* `once` per signal, because a second delivery of the same signal cannot
|
|
290
|
-
* change the answer.
|
|
291
|
-
*/
|
|
292
|
-
function observeTerminatingSignal() {
|
|
293
|
-
let observed = null;
|
|
294
|
-
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
295
|
-
observed = SIGNAL_NUMBERS[signal] ?? null;
|
|
296
|
-
});
|
|
297
|
-
return () => observed;
|
|
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);
|
|
298
117
|
}
|
|
118
|
+
const runOutcomeWorkflow = Workflow.make(RunOutcomeCommand, runOutcomeDecision);
|
|
299
119
|
//#endregion
|
|
300
|
-
//#region src/
|
|
120
|
+
//#region src/Survivors.workflow.ts
|
|
301
121
|
/**
|
|
302
122
|
* The mutant shape the admission carries, named once because both the decision's
|
|
303
123
|
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
@@ -318,15 +138,6 @@ const MutantShape = S.Struct({
|
|
|
318
138
|
})
|
|
319
139
|
})
|
|
320
140
|
});
|
|
321
|
-
/**
|
|
322
|
-
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
323
|
-
* get the decode function, not the schema, so the report's wire shape is not a
|
|
324
|
-
* surface commitment and the codec has exactly one caller.
|
|
325
|
-
*
|
|
326
|
-
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
327
|
-
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
328
|
-
* must not be refused for carrying one.
|
|
329
|
-
*/
|
|
330
141
|
const PriorReportDocument = S.Struct({
|
|
331
142
|
config: S.optional(S.Record(S.String, S.Unknown)),
|
|
332
143
|
framework: S.optional(S.Struct({ version: S.optional(S.String) })),
|
|
@@ -350,8 +161,6 @@ const PriorReportDocument = S.Struct({
|
|
|
350
161
|
}))
|
|
351
162
|
}))
|
|
352
163
|
});
|
|
353
|
-
//#endregion
|
|
354
|
-
//#region src/survivors-admission.workflow.ts
|
|
355
164
|
/**
|
|
356
165
|
* U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
|
|
357
166
|
*
|
|
@@ -475,22 +284,6 @@ function hashesMatch(priorReport, input) {
|
|
|
475
284
|
sourceContentHashes: input.sourceContentHashes
|
|
476
285
|
});
|
|
477
286
|
}
|
|
478
|
-
const rejection = (reason, detail) => ({
|
|
479
|
-
kind: "reject",
|
|
480
|
-
reason,
|
|
481
|
-
remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
|
|
482
|
-
});
|
|
483
|
-
function admissionVerdict(input) {
|
|
484
|
-
const priorReport = input.priorReport;
|
|
485
|
-
if (priorReport === void 0) return rejection("no-report", NO_REPORT_DETAIL);
|
|
486
|
-
if (wasProducedBySurvivorsRun(priorReport)) return rejection("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
|
|
487
|
-
if (input.priorSurvivors.length === 0) return { kind: "no-survivors" };
|
|
488
|
-
if (!hashesMatch(priorReport, input)) return rejection("mismatch", MISMATCH_DETAIL);
|
|
489
|
-
return {
|
|
490
|
-
kind: "admit",
|
|
491
|
-
survivors: input.priorSurvivors
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
287
|
const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
|
|
495
288
|
var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
|
|
496
289
|
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
@@ -505,48 +298,50 @@ var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
|
|
|
505
298
|
}) {
|
|
506
299
|
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
507
300
|
};
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
/** The exit class a rejected survivors run exits with (R6: exit 2). */
|
|
522
|
-
const SURVIVORS_REJECT_EXIT_CLASS = ExitClass.ConfigError;
|
|
523
|
-
//#endregion
|
|
524
|
-
//#region src/cli-exit-code.ts
|
|
525
|
-
function isExitClass(value) {
|
|
526
|
-
return value === ExitClass.VerdictFail || value === ExitClass.ConfigError || value === ExitClass.RuntimeError || value === ExitClass.InternalError;
|
|
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 }));
|
|
527
314
|
}
|
|
315
|
+
function admissionDecision(command) {
|
|
316
|
+
return decideAdmission(command);
|
|
317
|
+
}
|
|
318
|
+
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, admissionDecision);
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/Envelope.ts
|
|
528
321
|
/**
|
|
529
|
-
*
|
|
322
|
+
* Envelope — the failure envelope and console capture leaf.
|
|
530
323
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
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.
|
|
535
329
|
*/
|
|
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
|
+
}
|
|
536
338
|
function exitClassOf(value) {
|
|
537
339
|
if (typeof value !== "object" || value === null) return;
|
|
538
340
|
if (!("exitClass" in value)) return;
|
|
539
341
|
const raw = Reflect.get(value, "exitClass");
|
|
540
|
-
if (
|
|
342
|
+
if (!isExitClass(raw)) return;
|
|
541
343
|
return raw;
|
|
542
344
|
}
|
|
543
|
-
/**
|
|
544
|
-
* Collects every `exitClass` present in a value's nested `cause` chain.
|
|
545
|
-
*
|
|
546
|
-
* Depth-capped and cycle-safe: schema errors nest (`PrepareFailedError`
|
|
547
|
-
* wrapping `ConfigFileInvalidError` wrapping a validation detail), and a
|
|
548
|
-
* malformed chain must not recurse unboundedly.
|
|
549
|
-
*/
|
|
550
345
|
function collectExitClassesFromValue(value, out, seen, depth) {
|
|
551
346
|
if (depth > 10 || value === null || value === void 0) return;
|
|
552
347
|
if (typeof value !== "object") return;
|
|
@@ -560,105 +355,37 @@ function collectExitClassesFromValue(value, out, seen, depth) {
|
|
|
560
355
|
else collectExitClassesFromValue(causeVal, out, seen, depth + 1);
|
|
561
356
|
}
|
|
562
357
|
}
|
|
563
|
-
/**
|
|
564
|
-
* Collects every `exitClass` present in the failure's `Cause` and in each
|
|
565
|
-
* error's nested `cause` field.
|
|
566
|
-
*
|
|
567
|
-
* A `Cause` holds typed `Fail` and defect `Die` reasons
|
|
568
|
-
* (`repos/effect/packages/effect/src/Cause.ts:144-196`, `isFailReason` /
|
|
569
|
-
* `isDieReason`). `Effect.mapError` replaces the error value while keeping
|
|
570
|
-
* only the mapped error in `cause.reasons`
|
|
571
|
-
* (`repos/effect/packages/effect/src/internal/effect.ts:3253-3267`), so a
|
|
572
|
-
* wrapped config error is invisible at the top level and must be found by
|
|
573
|
-
* walking both the `Cause` reasons and each error's own `cause` field.
|
|
574
|
-
*/
|
|
575
358
|
function collectExitClasses(exit) {
|
|
576
359
|
const out = [];
|
|
577
360
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
578
361
|
if (Exit.isFailure(exit)) for (const reason of exit.cause.reasons) {
|
|
579
|
-
|
|
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;
|
|
580
366
|
if (candidate !== void 0) collectExitClassesFromValue(candidate, out, seen, 0);
|
|
581
367
|
}
|
|
582
368
|
return out;
|
|
583
369
|
}
|
|
584
|
-
/**
|
|
585
|
-
* Classifies a failed run for the finalizer: usage/parse failures
|
|
586
|
-
* (`CliError` — except a bare help request, which exits 0), rejected
|
|
587
|
-
* survivors runs (`SurvivorsRejection`), an unreadable prior report
|
|
588
|
-
* (`S.SchemaError`) all exit 2; otherwise the highest `exitClass` found by
|
|
589
|
-
* walking the failure's `Cause` reasons and each error's nested `cause` field
|
|
590
|
-
* wins by the precedence `4 > 3 > 2 > 1` (via `highestExitClass`); no class
|
|
591
|
-
* found is 1 (the framework's default). A successful run exits 0; the verdict
|
|
592
|
-
* gates (U5) then resolve the final classed code.
|
|
593
|
-
*
|
|
594
|
-
* The report parse failure shares the survivors class deliberately. It is not a
|
|
595
|
-
* verdict — the decider never sees the report — but the operator's answer is the
|
|
596
|
-
* same class of answer as a rejection: the input you named cannot be used. Letting
|
|
597
|
-
* it fall through to 1 would make an unusable `--survivors` input indistinguishable
|
|
598
|
-
* from a crash.
|
|
599
|
-
*
|
|
600
|
-
* Previously this matched only a top-level `ConfigError` via `carriesConfigError`,
|
|
601
|
-
* so a `PrepareFailedError` wrapping a `ConfigFileInvalidError` was invisible
|
|
602
|
-
* and classes 3 and 4 were unreachable. Walking the chain makes them reachable.
|
|
603
|
-
*/
|
|
604
|
-
function resolveCliExitCode(exit) {
|
|
605
|
-
if (Exit.isSuccess(exit)) return 0;
|
|
606
|
-
if (Cause.hasInterruptsOnly(exit.cause)) return 1;
|
|
607
|
-
const failure = Cause.findErrorOption(exit.cause);
|
|
608
|
-
if (Option.isSome(failure)) {
|
|
609
|
-
const value = failure.value;
|
|
610
|
-
if (S.is(CliError.ShowHelp)(value)) return value.errors.length > 0 ? 2 : 0;
|
|
611
|
-
if (CliError.isCliError(value)) return 2;
|
|
612
|
-
if (S.is(SurvivorsRejection)(value)) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
613
|
-
if (S.isSchemaError(value)) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
614
|
-
}
|
|
615
|
-
const classes = collectExitClasses(exit);
|
|
616
|
-
const highest = highestExitClass(classes);
|
|
617
|
-
if (highest !== null) return highest;
|
|
618
|
-
return 1;
|
|
619
|
-
}
|
|
620
|
-
//#endregion
|
|
621
|
-
//#region src/cli-failure-text.ts
|
|
622
|
-
/**
|
|
623
|
-
* The reason a domain error carries, when it carries one.
|
|
624
|
-
*
|
|
625
|
-
* Every stage error in this engine is an `S.TaggedError` whose payload field is
|
|
626
|
-
* `reason` — `DryRunNoTestsError`, `DryRunFailedError`, `PrepareFailedError`
|
|
627
|
-
* and friends. Those classes extend `Error`, but nothing assigns `.message`, so
|
|
628
|
-
* reading `.message` off one yields the empty string and the operator is told
|
|
629
|
-
* a run failed with no indication of why. Read the field the errors actually
|
|
630
|
-
* populate, and fall back only when it is absent.
|
|
631
|
-
*/
|
|
632
370
|
function reasonOf(value) {
|
|
633
371
|
if (!("reason" in value)) return;
|
|
634
372
|
const reason = Reflect.get(value, "reason");
|
|
635
373
|
if (typeof reason !== "string" || reason.length === 0) return;
|
|
636
374
|
const detail = causeTextOf(value);
|
|
637
|
-
|
|
375
|
+
if (detail === void 0) return reason;
|
|
376
|
+
return `${reason}: ${detail}`;
|
|
638
377
|
}
|
|
639
|
-
/**
|
|
640
|
-
* The human-readable text of a domain error's wrapped `cause`, if it has one.
|
|
641
|
-
*
|
|
642
|
-
* Recurses, because these errors nest: a stage error wraps a
|
|
643
|
-
* `TestRunnerFailed`, which wraps the spawn or import failure that actually
|
|
644
|
-
* happened. Stopping at the first layer reports a tag name — "TestRunnerFailed"
|
|
645
|
-
* — and leaves the operator to guess. Each layer contributes only what it
|
|
646
|
-
* knows, so the reader gets the chain down to the real fault.
|
|
647
|
-
*/
|
|
648
378
|
function causeTextOf(value, depth = 0) {
|
|
649
379
|
if (depth > 4 || !("cause" in value)) return;
|
|
650
380
|
const cause = Reflect.get(value, "cause");
|
|
651
381
|
return causeText(cause, depth + 1);
|
|
652
382
|
}
|
|
653
383
|
function configDetailOf(value) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
const msg = Reflect.get(value, "message");
|
|
660
|
-
if (typeof msg === "string" && msg.length > 0) return msg;
|
|
661
|
-
}
|
|
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;
|
|
662
389
|
}
|
|
663
390
|
function shouldVisitConfigValue(value, depth, seen) {
|
|
664
391
|
if (depth > 10) return false;
|
|
@@ -679,16 +406,15 @@ function pushConfigCauses(value, depth, stack) {
|
|
|
679
406
|
depth: depth + 1
|
|
680
407
|
});
|
|
681
408
|
}
|
|
682
|
-
/**
|
|
683
|
-
* The first config-class error's detail in cause-chain order, for the
|
|
684
|
-
* config remediation.
|
|
685
|
-
*/
|
|
686
409
|
function firstConfigErrorDetail(exit) {
|
|
687
410
|
if (!Exit.isFailure(exit)) return;
|
|
688
411
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
689
412
|
const stack = [];
|
|
690
413
|
for (const reason of exit.cause.reasons) {
|
|
691
|
-
|
|
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;
|
|
692
418
|
if (candidate !== void 0) stack.push({
|
|
693
419
|
value: candidate,
|
|
694
420
|
depth: 0
|
|
@@ -700,128 +426,195 @@ function firstConfigErrorDetail(exit) {
|
|
|
700
426
|
const { value, depth } = entry;
|
|
701
427
|
if (!shouldVisitConfigValue(value, depth, seen)) continue;
|
|
702
428
|
seen.add(value);
|
|
703
|
-
if (exitClassOf(value) ===
|
|
429
|
+
if (exitClassOf(value) === "ConfigError") {
|
|
704
430
|
const detail = configDetailOf(value);
|
|
705
431
|
if (detail !== void 0) return detail;
|
|
706
432
|
}
|
|
707
433
|
pushConfigCauses(value, depth, stack);
|
|
708
434
|
}
|
|
709
435
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
* failure whose cause chain contains an `exitClass` of `ConfigError` (2)
|
|
713
|
-
* names the config file, rejected survivors runs name the full run to do
|
|
714
|
-
* first. Everything else points at the report file and the verdict envelope,
|
|
715
|
-
* which is where a runtime failure's detail already is. The classification
|
|
716
|
-
* walks the `Cause` reasons and each error's nested `cause` field — see
|
|
717
|
-
* `collectExitClasses`.
|
|
718
|
-
*/
|
|
719
|
-
function remediationFor(exit, code) {
|
|
720
|
-
if (code > 128) return "the run was interrupted by a signal; re-run it to continue";
|
|
436
|
+
function describeFailure(exit) {
|
|
437
|
+
if (!Exit.isFailure(exit)) return "Unknown failure";
|
|
721
438
|
const value = failureValue(exit);
|
|
722
439
|
if (value !== void 0) {
|
|
723
|
-
if (CliError.isCliError(value)) return "re-run with --help to see the full usage";
|
|
724
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);
|
|
725
447
|
}
|
|
726
|
-
if (collectExitClasses(exit).includes(
|
|
448
|
+
if (collectExitClasses(exit).includes("ConfigError")) {
|
|
727
449
|
const detail = firstConfigErrorDetail(exit);
|
|
728
|
-
|
|
450
|
+
if (detail !== void 0) return detail;
|
|
729
451
|
}
|
|
730
|
-
|
|
452
|
+
const rendered = Cause.pretty(exit.cause);
|
|
453
|
+
if (rendered.length > 0) return rendered;
|
|
454
|
+
return "Unknown failure";
|
|
731
455
|
}
|
|
732
|
-
/**
|
|
733
|
-
* The failure's own text, used when the capture buffer is empty — a failure
|
|
734
|
-
* stryker reported through its own logger rather than the framework's
|
|
735
|
-
* `Console`. Falls back to a rendered cause.
|
|
736
|
-
*/
|
|
737
|
-
function describeFailure(exit) {
|
|
738
|
-
if (Exit.isFailure(exit)) {
|
|
739
|
-
const value = failureValue(exit);
|
|
740
|
-
if (value !== void 0) {
|
|
741
|
-
if (S.is(SurvivorsRejection)(value)) return value.remediation;
|
|
742
|
-
if (typeof value === "object" && value !== null) {
|
|
743
|
-
const reason = reasonOf(value);
|
|
744
|
-
if (reason !== void 0) return reason;
|
|
745
|
-
}
|
|
746
|
-
if (value instanceof Error && value.message.length > 0) return value.message;
|
|
747
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
|
|
748
|
-
return Cause.pretty(exit.cause);
|
|
749
|
-
}
|
|
750
|
-
return Cause.pretty(exit.cause);
|
|
751
|
-
}
|
|
752
|
-
return "";
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* The argument the framework reports it does not know, named the way the wire
|
|
756
|
-
* contract spells it. The v4 parser fails wrapped in a ShowHelp whose errors
|
|
757
|
-
* carry the offending flag or operand; when the unrecognized flag was given a
|
|
758
|
-
* separate value (`--format text`), the value is the token the old parser
|
|
759
|
-
* reported, so the token after the flag is named when one was given.
|
|
760
|
-
*/
|
|
761
456
|
function unrecognizedArgumentOf(exit, argv) {
|
|
762
457
|
if (!Exit.isFailure(exit)) return;
|
|
763
458
|
const value = failureValue(exit);
|
|
764
459
|
if (value === void 0 || !CliError.isCliError(value)) return;
|
|
765
|
-
const errors =
|
|
460
|
+
const errors = (() => {
|
|
461
|
+
if (S.is(CliError.ShowHelp)(value)) return value.errors;
|
|
462
|
+
return [value];
|
|
463
|
+
})();
|
|
766
464
|
for (const error of errors) {
|
|
767
465
|
if (S.is(CliError.UnrecognizedOption)(error)) {
|
|
768
466
|
const at = argv.indexOf(error.option);
|
|
769
|
-
const next =
|
|
770
|
-
|
|
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;
|
|
771
472
|
}
|
|
772
473
|
if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
|
|
773
474
|
if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
|
|
774
475
|
}
|
|
775
476
|
}
|
|
776
|
-
/**
|
|
777
|
-
* The first typed error in the exit's cause. The framework fails with
|
|
778
|
-
* `Cause.fail` (usage errors); the run handler is `Effect.promise`, whose
|
|
779
|
-
* rejected promises surface as *defects* (`Die` reasons) rather than
|
|
780
|
-
* failures — so stryker's own ConfigError/StrykerError values arrive there
|
|
781
|
-
* and must be read from the cause's `Die` reasons.
|
|
782
|
-
*/
|
|
783
477
|
function failureValue(exit) {
|
|
784
478
|
if (!Exit.isFailure(exit)) return;
|
|
785
479
|
const failure = Cause.findErrorOption(exit.cause);
|
|
786
480
|
if (Option.isSome(failure)) return failure.value;
|
|
787
|
-
const dieReason = exit.cause.reasons.find(Cause.isDieReason);
|
|
788
|
-
return dieReason === void 0 ? void 0 : dieReason.defect;
|
|
789
481
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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) {
|
|
794
572
|
return {
|
|
795
573
|
schemaVersion: "1.0",
|
|
796
|
-
code,
|
|
797
|
-
error:
|
|
798
|
-
remediation:
|
|
574
|
+
code: runOutcomeCode(Result.fail(error)),
|
|
575
|
+
error: errorText(error, captured),
|
|
576
|
+
remediation: remediationText(error)
|
|
799
577
|
};
|
|
800
578
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
* U6 — the machine-mode `Console` layer (KTD3, R7).
|
|
805
|
-
*
|
|
806
|
-
* The v4 CLI renders help and errors through an ANSI renderer and prints them
|
|
807
|
-
* with the `Console` reference — and there is no seam to intercept: the
|
|
808
|
-
* document is written *before* the failure propagates. Machine mode therefore
|
|
809
|
-
* replaces the `Console` reference itself with a capturing implementation:
|
|
810
|
-
* every write lands in an in-memory buffer instead of the real stdout/stderr,
|
|
811
|
-
* and the terminating bootstrap (StrykerCliHandler.ts) emits the buffer as
|
|
812
|
-
* one JSON envelope at teardown. Human mode keeps the default console so the
|
|
813
|
-
* framework's prose rendering is untouched.
|
|
814
|
-
*
|
|
815
|
-
* The v4 `Console.Console` interface is the sync `globalThis.console` shape
|
|
816
|
-
* (the v3 service's effect-returning surface became the module-level
|
|
817
|
-
* wrapper functions), so the capture implementation needs no `unsafe`
|
|
818
|
-
* mirror — every method stores into the buffer directly.
|
|
819
|
-
*/
|
|
579
|
+
function classifyRunOutcome(exit, signal, argv) {
|
|
580
|
+
return runOutcomeWorkflow(gatherRunOutcome(exit, signal, argv));
|
|
581
|
+
}
|
|
820
582
|
const capturedConsoleChunks = [];
|
|
821
583
|
const countByLabel = /* @__PURE__ */ new Map();
|
|
822
584
|
const timeByLabel = /* @__PURE__ */ new Map();
|
|
585
|
+
function inspectValue(value) {
|
|
586
|
+
if (typeof value === "string") return value;
|
|
587
|
+
return Formatter.format(value);
|
|
588
|
+
}
|
|
823
589
|
function formatArgs(args) {
|
|
824
|
-
|
|
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(" ");
|
|
825
618
|
}
|
|
826
619
|
function captureSync(args) {
|
|
827
620
|
capturedConsoleChunks.push(formatArgs(args));
|
|
@@ -835,86 +628,352 @@ function captureCount(label) {
|
|
|
835
628
|
countByLabel.set(key, next);
|
|
836
629
|
capturedConsoleChunks.push(`${key}: ${next}`);
|
|
837
630
|
}
|
|
838
|
-
function captureTimeEnd(label,
|
|
631
|
+
function captureTimeEnd(label, nowNanos) {
|
|
839
632
|
const key = label ?? "default";
|
|
840
633
|
const started = timeByLabel.get(key);
|
|
841
634
|
if (started !== void 0) {
|
|
842
635
|
timeByLabel.delete(key);
|
|
843
|
-
|
|
636
|
+
const diffMs = Number(nowNanos - started) / 1e6;
|
|
637
|
+
capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
|
|
844
638
|
}
|
|
845
639
|
}
|
|
846
640
|
function captureTrace(args) {
|
|
847
641
|
capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
|
|
848
642
|
}
|
|
849
|
-
const
|
|
643
|
+
const makeCapturingConsole = (clock) => ({
|
|
850
644
|
assert: (condition, ...args) => captureAssert(condition, args),
|
|
851
645
|
clear: () => {},
|
|
852
646
|
count: (label) => captureCount(label),
|
|
853
647
|
countReset: (label) => countByLabel.delete(label ?? "default"),
|
|
854
648
|
debug: (...args) => captureSync(args),
|
|
855
|
-
dir: (item,
|
|
856
|
-
dirxml: (item) => capturedConsoleChunks.push(
|
|
649
|
+
dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
650
|
+
dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
857
651
|
error: (...args) => captureSync(args),
|
|
858
652
|
group: () => {},
|
|
859
653
|
groupCollapsed: () => {},
|
|
860
654
|
groupEnd: () => {},
|
|
861
655
|
info: (...args) => captureSync(args),
|
|
862
656
|
log: (...args) => captureSync(args),
|
|
863
|
-
table: (tabularData) => capturedConsoleChunks.push(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
})),
|
|
867
|
-
time: (label) => timeByLabel.set(label ?? "default", performance.now()),
|
|
868
|
-
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()),
|
|
869
660
|
timeLog: (label, ...args) => {
|
|
870
661
|
const key = label ?? "default";
|
|
871
662
|
const started = timeByLabel.get(key);
|
|
872
|
-
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)}`);
|
|
873
670
|
},
|
|
874
671
|
trace: (...args) => captureTrace(args),
|
|
875
672
|
warn: (...args) => captureSync(args)
|
|
876
|
-
};
|
|
673
|
+
});
|
|
674
|
+
const machineConsoleLayer = Layer.effect(Console.Console, Clock.clockWith((clock) => Effect.sync(() => {
|
|
675
|
+
resetCapturedConsole();
|
|
676
|
+
return makeCapturingConsole(clock);
|
|
677
|
+
})));
|
|
678
|
+
function readCapturedConsole() {
|
|
679
|
+
return capturedConsoleChunks.join("\n");
|
|
680
|
+
}
|
|
681
|
+
function resetCapturedConsole() {
|
|
682
|
+
capturedConsoleChunks.length = 0;
|
|
683
|
+
countByLabel.clear();
|
|
684
|
+
timeByLabel.clear();
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/Output.workflow.ts
|
|
688
|
+
const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
|
|
877
689
|
/**
|
|
878
|
-
* The
|
|
879
|
-
*
|
|
880
|
-
* through `readCapturedConsole` at teardown. A `Layer` is already lazy, so
|
|
881
|
-
* the layer is a value: the reset effect runs when the layer is built.
|
|
882
|
-
*
|
|
883
|
-
* The layer must replace the `Console` reference the module-level
|
|
884
|
-
* `Console.log`/`Console.error` wrappers read through their fiber context;
|
|
885
|
-
* v4 reads the override the same way it reads any provided service, so a
|
|
886
|
-
* plain provide is sufficient — no special `setConsole`-style primitive
|
|
887
|
-
* exists any more. The reference's identifier is `never` (it carries no
|
|
888
|
-
* requirement), so the layer's type is too.
|
|
889
|
-
*
|
|
890
|
-
* Human mode provides no Console binding at all: effect's own default
|
|
891
|
-
* console delegates every method to the global console, which is exactly the
|
|
892
|
-
* prose rendering a human-mode run uses. Mirroring it here would reimplement
|
|
893
|
-
* the library default (V.7).
|
|
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.
|
|
894
692
|
*/
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
+
}) {};
|
|
899
701
|
/**
|
|
900
|
-
* The
|
|
901
|
-
*
|
|
902
|
-
*
|
|
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.
|
|
903
706
|
*/
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
});
|
|
906
765
|
}
|
|
766
|
+
function modeDecision(command) {
|
|
767
|
+
return r4(command);
|
|
768
|
+
}
|
|
769
|
+
const resolveModeWorkflow = Workflow.make(ResolveModeCommand, modeDecision);
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/Output.ts
|
|
772
|
+
/**
|
|
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.
|
|
778
|
+
*/
|
|
779
|
+
/**
|
|
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.
|
|
783
|
+
*/
|
|
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;
|
|
862
|
+
return {
|
|
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);
|
|
884
|
+
}
|
|
885
|
+
}),
|
|
886
|
+
closeAndDrain: Effect.gen(function* () {
|
|
887
|
+
state.terminalWritten = true;
|
|
888
|
+
yield* Queue.end(queue);
|
|
889
|
+
if (drainFiber !== null) yield* Fiber.join(drainFiber);
|
|
890
|
+
})
|
|
891
|
+
};
|
|
892
|
+
});
|
|
893
|
+
Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
|
|
894
|
+
const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
|
|
907
895
|
/**
|
|
908
|
-
*
|
|
909
|
-
*
|
|
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.
|
|
910
900
|
*/
|
|
911
|
-
function
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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;
|
|
915
906
|
}
|
|
916
|
-
|
|
917
|
-
|
|
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({}) }));
|
|
918
977
|
/**
|
|
919
978
|
* Machine mode emits the U4 verdict envelope for a run that produced no
|
|
920
979
|
* mutants and no report file: a `--survivors` run with zero survivors (AE3)
|
|
@@ -935,11 +994,18 @@ function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathSe
|
|
|
935
994
|
name: "StrykerJS",
|
|
936
995
|
version: strykerVersion
|
|
937
996
|
}
|
|
938
|
-
}, mode.mode, mode.signal, stream.runId, basePath,
|
|
939
|
-
stream.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
}));
|
|
943
1009
|
}
|
|
944
1010
|
/**
|
|
945
1011
|
* Emits the machine-mode output from the run's finalizer — it runs on
|
|
@@ -954,50 +1020,99 @@ function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathSe
|
|
|
954
1020
|
* `--dryRunOnly` early return): then a null-score `verdict` closes the
|
|
955
1021
|
* stream so the last stdout line is always a terminal event (R5).
|
|
956
1022
|
*/
|
|
957
|
-
function emitMachineModeOutput(stream, mode,
|
|
1023
|
+
function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
|
|
958
1024
|
return Effect.gen(function* () {
|
|
959
1025
|
const captured = readCapturedConsole();
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (captured.length > 0) {
|
|
979
|
-
const document = {
|
|
980
|
-
kind: "help",
|
|
981
|
-
schemaVersion: "1.0",
|
|
982
|
-
code: 0,
|
|
983
|
-
help: captured
|
|
984
|
-
};
|
|
985
|
-
stream.sink(document);
|
|
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);
|
|
986
1044
|
return;
|
|
987
1045
|
}
|
|
988
|
-
|
|
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
|
+
}));
|
|
989
1053
|
});
|
|
990
1054
|
}
|
|
991
1055
|
//#endregion
|
|
992
|
-
//#region src/
|
|
993
|
-
|
|
994
|
-
const
|
|
1056
|
+
//#region src/StreamFile.ts
|
|
1057
|
+
const DEFAULT_PROGRESS_STREAM_FILE = "reports/mutation-stream.jsonl";
|
|
1058
|
+
const encodeUtf8 = (line) => new TextEncoder().encode(line);
|
|
1059
|
+
const drainStreamFile = (fileName, framed) => Effect.gen(function* () {
|
|
1060
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1061
|
+
const path = yield* Path.Path;
|
|
1062
|
+
yield* fs.makeDirectory(path.dirname(fileName), { recursive: true }).pipe(Effect.orDie);
|
|
1063
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
1064
|
+
const handle = yield* fs.open(fileName, { flag: "w" });
|
|
1065
|
+
yield* Stream.runForEach(framed, (line) => handle.writeAll(encodeUtf8(line)).pipe(Effect.flatMap(() => handle.sync)));
|
|
1066
|
+
})).pipe(Effect.orDie);
|
|
1067
|
+
});
|
|
1068
|
+
const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(function* () {
|
|
1069
|
+
const stdio = yield* Stdio.Stdio;
|
|
1070
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1071
|
+
const path = yield* Path.Path;
|
|
1072
|
+
const fileNameRef = yield* Ref.make(DEFAULT_PROGRESS_STREAM_FILE);
|
|
1073
|
+
const drainFramed = (framed) => Effect.gen(function* () {
|
|
1074
|
+
const fileName = yield* Ref.get(fileNameRef);
|
|
1075
|
+
yield* drainStreamFile(fileName, framed).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
1076
|
+
});
|
|
1077
|
+
return RunEventStreamPort.of({ createRunEventStream: (resolved) => Effect.gen(function* () {
|
|
1078
|
+
return {
|
|
1079
|
+
...yield* makeRunEventStream(stdio, resolved, drainFramed),
|
|
1080
|
+
setProgressStreamFile: (fileName) => Ref.set(fileNameRef, fileName)
|
|
1081
|
+
};
|
|
1082
|
+
}) });
|
|
1083
|
+
}));
|
|
995
1084
|
//#endregion
|
|
996
|
-
//#region src/
|
|
997
|
-
|
|
1085
|
+
//#region src/Survivors.ts
|
|
1086
|
+
/**
|
|
1087
|
+
* Survivors — the survivors-admission capability.
|
|
1088
|
+
*
|
|
1089
|
+
* The prior-report decoding, source hashing, mutant conversion, and admission
|
|
1090
|
+
* pipeline for --survivors runs. Pure admission decision lives in
|
|
1091
|
+
* Survivors.workflow.ts.
|
|
1092
|
+
*/
|
|
1093
|
+
const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
|
|
998
1094
|
/**
|
|
999
|
-
* The
|
|
1095
|
+
* The mutant shape the admission carries, named once because both the decision's
|
|
1096
|
+
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
1097
|
+
*/
|
|
1098
|
+
/**
|
|
1099
|
+
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
1100
|
+
* get the decode function, not the schema, so the report's wire shape is not a
|
|
1101
|
+
* surface commitment and the codec has exactly one caller.
|
|
1000
1102
|
*
|
|
1103
|
+
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
1104
|
+
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
1105
|
+
* must not be refused for carrying one.
|
|
1106
|
+
*/
|
|
1107
|
+
/**
|
|
1108
|
+
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
1109
|
+
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
1110
|
+
* runs. A malformed report therefore never reaches the decider, and nothing here casts
|
|
1111
|
+
* a third-party report type.
|
|
1112
|
+
*/
|
|
1113
|
+
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
1114
|
+
const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
|
|
1115
|
+
/**
|
|
1001
1116
|
* Thin by design: the digest is the caller's capability, and naming the call
|
|
1002
1117
|
* keeps every hashing site in the admission path reading the same way.
|
|
1003
1118
|
*/
|
|
@@ -1011,11 +1126,8 @@ function sourceContentHash(content, hash) {
|
|
|
1011
1126
|
* this is the recorded side, read back out of the report.
|
|
1012
1127
|
*/
|
|
1013
1128
|
function priorSourceHashes(priorReport, hashContent) {
|
|
1014
|
-
return objectFromEntries(objectEntries
|
|
1129
|
+
return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
|
|
1015
1130
|
}
|
|
1016
|
-
//#endregion
|
|
1017
|
-
//#region src/survivors-mutants.ts
|
|
1018
|
-
const { entries: objectEntries } = Object;
|
|
1019
1131
|
/**
|
|
1020
1132
|
* Converts a report mutant (1-based schema location) into the internal mutant
|
|
1021
1133
|
* shape a run consumes (0-based positions, absolute file name) — the exact
|
|
@@ -1025,7 +1137,7 @@ const { entries: objectEntries } = Object;
|
|
|
1025
1137
|
* incremental differ uses.
|
|
1026
1138
|
*/
|
|
1027
1139
|
function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
1028
|
-
return {
|
|
1140
|
+
return Mutant.make({
|
|
1029
1141
|
id: mutant.id,
|
|
1030
1142
|
fileName: resolveAbsolutePath(file),
|
|
1031
1143
|
mutatorName: mutant.mutatorName,
|
|
@@ -1040,7 +1152,7 @@ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
|
1040
1152
|
column: mutant.location.end.column - 1
|
|
1041
1153
|
}
|
|
1042
1154
|
}
|
|
1043
|
-
};
|
|
1155
|
+
});
|
|
1044
1156
|
}
|
|
1045
1157
|
/**
|
|
1046
1158
|
* The survivors of the prior report: exactly the mutants whose status is
|
|
@@ -1057,11 +1169,11 @@ function extractSurvivors(priorReport, resolveAbsolutePath) {
|
|
|
1057
1169
|
* ranges: the report's 1-based lines with the internal 0-based columns,
|
|
1058
1170
|
* relative file names, deduplicated in first-seen order.
|
|
1059
1171
|
*/
|
|
1060
|
-
function survivorMutateSpans(survivors, basePath
|
|
1172
|
+
function survivorMutateSpans(survivors, basePath) {
|
|
1061
1173
|
const spans = [];
|
|
1062
1174
|
const seen = /* @__PURE__ */ new Set();
|
|
1063
1175
|
for (const survivor of survivors) {
|
|
1064
|
-
const file = toRelativeNormalizedFileName(survivor.fileName, basePath
|
|
1176
|
+
const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
|
|
1065
1177
|
const { start, end } = survivor.location;
|
|
1066
1178
|
const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
|
|
1067
1179
|
if (!seen.has(span)) {
|
|
@@ -1071,18 +1183,7 @@ function survivorMutateSpans(survivors, basePath, pathService) {
|
|
|
1071
1183
|
}
|
|
1072
1184
|
return spans;
|
|
1073
1185
|
}
|
|
1074
|
-
|
|
1075
|
-
//#region src/survivors-report.ts
|
|
1076
|
-
/**
|
|
1077
|
-
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
1078
|
-
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
1079
|
-
* runs. A malformed report therefore never reaches the decider, and nothing here casts
|
|
1080
|
-
* a third-party report type.
|
|
1081
|
-
*/
|
|
1082
|
-
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
1083
|
-
//#endregion
|
|
1084
|
-
//#region src/cli-survivors-admission.ts
|
|
1085
|
-
const hashContent = (content) => createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1186
|
+
const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
|
|
1086
1187
|
const resolveAbsolutePath = (file) => resolve(file);
|
|
1087
1188
|
/**
|
|
1088
1189
|
* The survivors admission, as a description whose phases chain by type and
|
|
@@ -1095,10 +1196,9 @@ const resolveAbsolutePath = (file) => resolve(file);
|
|
|
1095
1196
|
* stashed context back and dispatches the decision to the verdict/run,
|
|
1096
1197
|
* failing the run with a rejection.
|
|
1097
1198
|
*/
|
|
1098
|
-
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath) => pipe(Cell.read((cliOptions) => Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
|
|
1199
|
+
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) => {
|
|
1099
1200
|
const priorReportPath = priorReportPathOf(resolvedOptions);
|
|
1100
|
-
|
|
1101
|
-
return Ref.set(runContext, {
|
|
1201
|
+
return Effect.flatMap(readPriorReport(priorReportPath), (read) => Effect.flatMap(currentSourceHashesFor(priorReportFileKeys(read.raw)), (sourceContentHashes) => Ref.set(runContext, {
|
|
1102
1202
|
resolvedOptions,
|
|
1103
1203
|
priorReportPath,
|
|
1104
1204
|
pathService
|
|
@@ -1107,9 +1207,9 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1107
1207
|
priorReportRaw: read.raw,
|
|
1108
1208
|
priorReportFound: read.found,
|
|
1109
1209
|
priorReportPath,
|
|
1110
|
-
sourceContentHashes
|
|
1111
|
-
}));
|
|
1112
|
-
})))), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
1210
|
+
sourceContentHashes
|
|
1211
|
+
}))));
|
|
1212
|
+
}))), services)), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
1113
1213
|
if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
|
|
1114
1214
|
priorReport: void 0,
|
|
1115
1215
|
currentConfig: resolvedOptions,
|
|
@@ -1133,11 +1233,12 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1133
1233
|
if (context === void 0) return Effect.die("the survivors admission read must run before its write");
|
|
1134
1234
|
const { resolvedOptions, priorReportPath, pathService } = context;
|
|
1135
1235
|
return Result.match(outcome, {
|
|
1136
|
-
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () =>
|
|
1236
|
+
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
|
|
1237
|
+
const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
|
|
1137
1238
|
return runMutationTest({
|
|
1138
1239
|
...resolvedOptions,
|
|
1139
|
-
survivors:
|
|
1140
|
-
mutate: survivorMutateSpans(
|
|
1240
|
+
survivors: admittedMutants,
|
|
1241
|
+
mutate: survivorMutateSpans(admittedMutants, basePath),
|
|
1141
1242
|
survivorsPriorReport: priorReportPath,
|
|
1142
1243
|
incremental: false
|
|
1143
1244
|
}).pipe(Effect.orDie);
|
|
@@ -1160,38 +1261,35 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1160
1261
|
*/
|
|
1161
1262
|
function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions, basePath) {
|
|
1162
1263
|
return Effect.gen(function* () {
|
|
1264
|
+
const services = yield* Effect.context();
|
|
1163
1265
|
const admissionContext = yield* Ref.make(void 0);
|
|
1164
|
-
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath), cliOptions);
|
|
1266
|
+
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath, services), cliOptions);
|
|
1165
1267
|
});
|
|
1166
1268
|
}
|
|
1167
1269
|
function resolveSurvivorsRunOptions(cliOptions, basePath) {
|
|
1168
|
-
return readConfig(cliOptions,
|
|
1270
|
+
return readConfig(cliOptions, basePath);
|
|
1169
1271
|
}
|
|
1170
1272
|
function priorReportPathOf(resolved) {
|
|
1171
1273
|
const configured = resolved["survivorsPriorReport"];
|
|
1172
|
-
|
|
1274
|
+
if (typeof configured === "string") return configured;
|
|
1275
|
+
return DEFAULT_SURVIVORS_PRIOR_REPORT;
|
|
1173
1276
|
}
|
|
1174
1277
|
function readPriorReport(priorReportPath) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1278
|
+
return Effect.gen(function* () {
|
|
1279
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(priorReportPath).pipe(Effect.map((text) => ({
|
|
1280
|
+
found: true,
|
|
1281
|
+
raw: Result.match(S.decodeResult(S.fromJsonString(S.Unknown))(text), {
|
|
1282
|
+
onFailure: () => text,
|
|
1283
|
+
onSuccess: (value) => value
|
|
1284
|
+
})
|
|
1285
|
+
})), Effect.catchTag("PlatformError", (cause) => Match.value(cause.reason).pipe(Match.tag("NotFound", () => Effect.succeed({
|
|
1180
1286
|
found: false,
|
|
1181
1287
|
raw: void 0
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
raw: JSON.parse(text)
|
|
1188
|
-
};
|
|
1189
|
-
} catch {
|
|
1190
|
-
return {
|
|
1191
|
-
found: true,
|
|
1192
|
-
raw: text
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1288
|
+
})), Match.orElse(() => Effect.fail(ConfigFileUnreadableError.make({
|
|
1289
|
+
file: priorReportPath,
|
|
1290
|
+
cause
|
|
1291
|
+
}))))));
|
|
1292
|
+
});
|
|
1195
1293
|
}
|
|
1196
1294
|
function priorReportFileKeys(raw) {
|
|
1197
1295
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
@@ -1201,116 +1299,43 @@ function priorReportFileKeys(raw) {
|
|
|
1201
1299
|
return Object.keys(files);
|
|
1202
1300
|
}
|
|
1203
1301
|
function readSourceFile(file) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1302
|
+
return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
|
|
1303
|
+
file,
|
|
1304
|
+
cause
|
|
1305
|
+
}))));
|
|
1209
1306
|
}
|
|
1210
1307
|
function currentSourceHashesFor(files) {
|
|
1211
|
-
|
|
1212
|
-
for (const file of files) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
|
|
1213
|
-
return hashes;
|
|
1308
|
+
return Effect.map(Effect.forEach(files, (file) => Effect.map(readSourceFile(file), (content) => [file, sourceContentHash(content, hashContent)]), { concurrency: 24 }), (pairs) => Object.fromEntries(pairs));
|
|
1214
1309
|
}
|
|
1215
1310
|
//#endregion
|
|
1216
|
-
//#region src/
|
|
1217
|
-
const
|
|
1311
|
+
//#region src/Cli.ts
|
|
1312
|
+
const SIGNAL_NUMBERS = Object.freeze({
|
|
1313
|
+
SIGINT: 2,
|
|
1314
|
+
SIGTERM: 15
|
|
1315
|
+
});
|
|
1218
1316
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1317
|
+
* Installs the listeners and returns the reader.
|
|
1318
|
+
*
|
|
1319
|
+
* The listener records and returns: interrupting the run is the runtime's job,
|
|
1320
|
+
* and doing it from here would race the run's own finalizer for the stream.
|
|
1321
|
+
* `once` per signal, because a second delivery of the same signal cannot
|
|
1322
|
+
* change the answer.
|
|
1223
1323
|
*/
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
basePath: resolve(process.cwd()),
|
|
1231
|
-
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-mutation-report/stryker-plugins")],
|
|
1232
|
-
logSink: (line) => {
|
|
1233
|
-
if (mode.mode === "human") process.stdout.write(line);
|
|
1234
|
-
else process.stderr.write(line);
|
|
1235
|
-
},
|
|
1236
|
-
allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
|
|
1237
|
-
};
|
|
1324
|
+
function observeTerminatingSignal() {
|
|
1325
|
+
let observed = null;
|
|
1326
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
1327
|
+
observed = SIGNAL_NUMBERS[signal] ?? null;
|
|
1328
|
+
});
|
|
1329
|
+
return () => observed;
|
|
1238
1330
|
}
|
|
1239
|
-
/**
|
|
1240
|
-
* The single operation of the CLI's run cell: the impure shell that
|
|
1241
|
-
* wraps the transport's command effect with the run bootstrap. It creates the
|
|
1242
|
-
* run's stream from the resolved mode, binds the host options a run is
|
|
1243
|
-
* executed with, opens the stream, runs the command effect, dispatches the
|
|
1244
|
-
* request the handlers left, and on every outcome — success, failure and
|
|
1245
|
-
* interruption alike — emits the machine-mode terminal event (error/help/
|
|
1246
|
-
* null verdict) and drains the stream, returning the classed exit code as its
|
|
1247
|
-
* value. SIGINT/SIGTERM interrupt the current fiber so the finalizer runs
|
|
1248
|
-
* before the process exits; the code is resolved exactly once (R6), in the
|
|
1249
|
-
* finalizer, where the terminal event's `code` is chosen from the same inputs
|
|
1250
|
-
* the teardown used before.
|
|
1251
|
-
*/
|
|
1252
|
-
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1253
|
-
const stream = yield* createRunEventStream(input.mode);
|
|
1254
|
-
const hostOptions = hostOptionsOf(input.mode, stream);
|
|
1255
|
-
const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions);
|
|
1256
|
-
const basePath = hostOptions.basePath;
|
|
1257
|
-
const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
|
|
1258
|
-
let currentFiber = null;
|
|
1259
|
-
const verdictOf = (value) => {
|
|
1260
|
-
if (!Predicate.hasProperty(value, "verdict")) return [];
|
|
1261
|
-
const candidate = value.verdict;
|
|
1262
|
-
if (typeof candidate !== "number" || !isExitClass(candidate)) return [];
|
|
1263
|
-
return [candidate];
|
|
1264
|
-
};
|
|
1265
|
-
const resolveClassedExitCode = (exit) => {
|
|
1266
|
-
const signal = input.lastSignal();
|
|
1267
|
-
if (signal !== null) return 128 + signal;
|
|
1268
|
-
if (Exit.isFailure(exit)) return resolveCliExitCode(exit);
|
|
1269
|
-
return resolveExitCode(verdictOf(exit.value), null);
|
|
1270
|
-
};
|
|
1271
|
-
const onSignal = () => {
|
|
1272
|
-
process.removeListener("SIGINT", onSignal);
|
|
1273
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1274
|
-
if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
|
|
1275
|
-
};
|
|
1276
|
-
const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => runRequest.survivors ? runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions))) : runMutationTestImpl(runRequest.options).pipe(Effect.orDie)), Match.tag("llms", (llmsRequest) => Effect.sync(() => {
|
|
1277
|
-
stream.ensureOpen({
|
|
1278
|
-
mode: "machine",
|
|
1279
|
-
signal: "flag",
|
|
1280
|
-
stdoutIsTTY: process.stdout.isTTY === true
|
|
1281
|
-
});
|
|
1282
|
-
stream.sink(llmsRequest.document);
|
|
1283
|
-
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1284
|
-
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1285
|
-
currentFiber = Fiber.getCurrent() ?? null;
|
|
1286
|
-
process.on("SIGINT", onSignal);
|
|
1287
|
-
process.on("SIGTERM", onSignal);
|
|
1288
|
-
}), () => Effect.gen(function* () {
|
|
1289
|
-
yield* stream.open;
|
|
1290
|
-
yield* input.program;
|
|
1291
|
-
const request = yield* Ref.get(input.requestRef);
|
|
1292
|
-
return yield* Option.match(request, {
|
|
1293
|
-
onNone: () => Effect.void,
|
|
1294
|
-
onSome: (cliRequest) => dispatch(cliRequest)
|
|
1295
|
-
});
|
|
1296
|
-
}), () => Effect.sync(() => {
|
|
1297
|
-
process.removeListener("SIGINT", onSignal);
|
|
1298
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1299
|
-
}));
|
|
1300
|
-
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
1301
|
-
const exit = yield* Effect.exit(restore(program));
|
|
1302
|
-
const code = resolveClassedExitCode(exit);
|
|
1303
|
-
if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, exit, code, input.argv, basePath, pathService);
|
|
1304
|
-
yield* stream.closeAndDrain;
|
|
1305
|
-
return code;
|
|
1306
|
-
}));
|
|
1307
|
-
});
|
|
1308
1331
|
function isObject(value) {
|
|
1309
1332
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1310
1333
|
}
|
|
1311
1334
|
function stringField(node, key) {
|
|
1312
1335
|
const value = node[key];
|
|
1313
|
-
return
|
|
1336
|
+
return (() => {
|
|
1337
|
+
if (typeof value === "string") return value;
|
|
1338
|
+
})();
|
|
1314
1339
|
}
|
|
1315
1340
|
function stringArrayField(node, key) {
|
|
1316
1341
|
const value = node[key];
|
|
@@ -1359,7 +1384,10 @@ const PRIMITIVE_KIND = {
|
|
|
1359
1384
|
};
|
|
1360
1385
|
function kindOf(primitive) {
|
|
1361
1386
|
const tag = stringField(primitive, "_tag");
|
|
1362
|
-
return
|
|
1387
|
+
return (() => {
|
|
1388
|
+
if (tag === void 0) return "unknown";
|
|
1389
|
+
return PRIMITIVE_KIND[tag] ?? tag;
|
|
1390
|
+
})();
|
|
1363
1391
|
}
|
|
1364
1392
|
function choiceValues(primitive) {
|
|
1365
1393
|
const keys = primitive["choiceKeys"];
|
|
@@ -1368,8 +1396,13 @@ function choiceValues(primitive) {
|
|
|
1368
1396
|
for (const key of keys) if (typeof key === "string") values.push(key);
|
|
1369
1397
|
return values;
|
|
1370
1398
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1399
|
+
const REPORTER_NAMES = [
|
|
1400
|
+
"clear-text",
|
|
1401
|
+
"progress",
|
|
1402
|
+
"html",
|
|
1403
|
+
"json",
|
|
1404
|
+
"progress-stream"
|
|
1405
|
+
];
|
|
1373
1406
|
/**
|
|
1374
1407
|
* v4 option descriptions are stored as `Option.some(string)` on the compiled
|
|
1375
1408
|
* `Single`; the walker unwraps the option.
|
|
@@ -1380,16 +1413,25 @@ function descriptionOf(single) {
|
|
|
1380
1413
|
switch (tagOf(description)) {
|
|
1381
1414
|
case "Some": {
|
|
1382
1415
|
const value = description["value"];
|
|
1383
|
-
return
|
|
1416
|
+
return (() => {
|
|
1417
|
+
if (typeof value === "string") return value;
|
|
1418
|
+
return "";
|
|
1419
|
+
})();
|
|
1384
1420
|
}
|
|
1385
1421
|
default: return "";
|
|
1386
1422
|
}
|
|
1387
1423
|
}
|
|
1388
1424
|
function describeSingle(single, isOptional, out) {
|
|
1389
1425
|
const name = stringField(single, "name") ?? "";
|
|
1390
|
-
const primitive =
|
|
1426
|
+
const primitive = (() => {
|
|
1427
|
+
if (isObject(single["primitiveType"])) return single["primitiveType"];
|
|
1428
|
+
return {};
|
|
1429
|
+
})();
|
|
1391
1430
|
const kind = kindOf(primitive);
|
|
1392
|
-
const choices =
|
|
1431
|
+
const choices = (() => {
|
|
1432
|
+
if (name === "reporters") return REPORTER_NAMES;
|
|
1433
|
+
if (kind === "choice") return choiceValues(primitive);
|
|
1434
|
+
})();
|
|
1393
1435
|
const description = descriptionOf(single);
|
|
1394
1436
|
const required = kind !== "boolean" && !isOptional;
|
|
1395
1437
|
const described = {
|
|
@@ -1397,7 +1439,10 @@ function describeSingle(single, isOptional, out) {
|
|
|
1397
1439
|
aliases: stringArrayField(single, "aliases"),
|
|
1398
1440
|
kind,
|
|
1399
1441
|
required,
|
|
1400
|
-
...
|
|
1442
|
+
...(() => {
|
|
1443
|
+
if (choices !== void 0) return { choices };
|
|
1444
|
+
return {};
|
|
1445
|
+
})(),
|
|
1401
1446
|
description
|
|
1402
1447
|
};
|
|
1403
1448
|
if (single["kind"] === "argument") {
|
|
@@ -1416,7 +1461,9 @@ function walkConfigNode(node, orderedParams, out) {
|
|
|
1416
1461
|
switch (tagOf(node)) {
|
|
1417
1462
|
case "Param": {
|
|
1418
1463
|
const index = node["index"];
|
|
1419
|
-
const param =
|
|
1464
|
+
const param = (() => {
|
|
1465
|
+
if (typeof index === "number") return orderedParams[index];
|
|
1466
|
+
})();
|
|
1420
1467
|
if (param !== void 0) walkParam(param, false, out);
|
|
1421
1468
|
return;
|
|
1422
1469
|
}
|
|
@@ -1440,7 +1487,9 @@ function describeCommandNode(node) {
|
|
|
1440
1487
|
};
|
|
1441
1488
|
const config = node["config"];
|
|
1442
1489
|
if (isObject(config) && isObject(config["tree"])) {
|
|
1443
|
-
|
|
1490
|
+
let orderedParams = [];
|
|
1491
|
+
const maybeOrdered = config["orderedParams"];
|
|
1492
|
+
if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
|
|
1444
1493
|
walkConfigTree(config["tree"], orderedParams, out);
|
|
1445
1494
|
}
|
|
1446
1495
|
const subcommands = [];
|
|
@@ -1454,17 +1503,28 @@ function describeCommandNode(node) {
|
|
|
1454
1503
|
}
|
|
1455
1504
|
return {
|
|
1456
1505
|
name: stringField(node, "name") ?? "",
|
|
1457
|
-
description:
|
|
1506
|
+
description: (() => {
|
|
1507
|
+
if (typeof node["description"] === "string") return node["description"];
|
|
1508
|
+
return "";
|
|
1509
|
+
})(),
|
|
1458
1510
|
options: out.flags,
|
|
1459
1511
|
args: out.args,
|
|
1460
1512
|
subcommands
|
|
1461
1513
|
};
|
|
1462
1514
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1515
|
+
function readCoreEntries() {
|
|
1516
|
+
try {
|
|
1517
|
+
const manifestPath = createRequire(import.meta.url).resolve("@systemfsoftware/stryker-js-platform-node/package.json");
|
|
1518
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
1519
|
+
const parsed = JSON.parse(raw);
|
|
1520
|
+
const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
|
|
1521
|
+
const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
|
|
1522
|
+
if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
|
|
1523
|
+
return [];
|
|
1524
|
+
} catch {
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1468
1528
|
function buildLLMSManifest(command, version) {
|
|
1469
1529
|
const root = describeCommandNode(command) ?? {
|
|
1470
1530
|
name: "",
|
|
@@ -1477,15 +1537,14 @@ function buildLLMSManifest(command, version) {
|
|
|
1477
1537
|
schemaVersion: "1.0",
|
|
1478
1538
|
tool: root.name,
|
|
1479
1539
|
version,
|
|
1480
|
-
commands: [root]
|
|
1540
|
+
commands: [root],
|
|
1541
|
+
entries: readCoreEntries()
|
|
1481
1542
|
};
|
|
1482
1543
|
}
|
|
1483
1544
|
/** The manifest as one JSON document, ready for stdout — the U4 convention. */
|
|
1484
1545
|
function emitLLMSManifest(command, version) {
|
|
1485
1546
|
return JSON.stringify(buildLLMSManifest(command, version));
|
|
1486
1547
|
}
|
|
1487
|
-
//#endregion
|
|
1488
|
-
//#region src/stryker-cli.ts
|
|
1489
1548
|
function createSplitter(separator) {
|
|
1490
1549
|
return (value) => value.split(separator).filter(Boolean);
|
|
1491
1550
|
}
|
|
@@ -1498,7 +1557,10 @@ const splitOnSpace = createSplitter(" ");
|
|
|
1498
1557
|
*/
|
|
1499
1558
|
function parseCleanDirOption(value) {
|
|
1500
1559
|
const v = value.toLocaleLowerCase();
|
|
1501
|
-
return
|
|
1560
|
+
return (() => {
|
|
1561
|
+
if (v === "always") return v;
|
|
1562
|
+
return v !== "false" && v !== "0";
|
|
1563
|
+
})();
|
|
1502
1564
|
}
|
|
1503
1565
|
/**
|
|
1504
1566
|
* Commander characterization: a pure integer is parsed as a number, anything
|
|
@@ -1516,31 +1578,12 @@ const optional = (option) => Flag.optional(option);
|
|
|
1516
1578
|
* `Option.some(false)` for an explicit `--no-x`, so both map back to
|
|
1517
1579
|
* `undefined` and leave the config-file default in force (KTD4).
|
|
1518
1580
|
*/
|
|
1519
|
-
const absentWhenFalse = (value) =>
|
|
1520
|
-
|
|
1521
|
-
"fatal",
|
|
1522
|
-
"error",
|
|
1523
|
-
"warn",
|
|
1524
|
-
"info",
|
|
1525
|
-
"debug",
|
|
1526
|
-
"trace",
|
|
1527
|
-
"off"
|
|
1528
|
-
];
|
|
1529
|
-
const LOG_LEVEL_LOOKUP = {
|
|
1530
|
-
fatal: true,
|
|
1531
|
-
error: true,
|
|
1532
|
-
warn: true,
|
|
1533
|
-
info: true,
|
|
1534
|
-
debug: true,
|
|
1535
|
-
trace: true,
|
|
1536
|
-
off: true
|
|
1581
|
+
const absentWhenFalse = (value) => {
|
|
1582
|
+
if (Option.isSome(value) && value.value) return true;
|
|
1537
1583
|
};
|
|
1538
|
-
function isLogLevel(value) {
|
|
1539
|
-
return LOG_LEVEL_LOOKUP[value] === true;
|
|
1540
|
-
}
|
|
1541
1584
|
function setLogLevel(target, key, value) {
|
|
1542
1585
|
const unwrapped = unwrap(value);
|
|
1543
|
-
if (unwrapped !== void 0
|
|
1586
|
+
if (unwrapped !== void 0) target[key] = unwrapped;
|
|
1544
1587
|
}
|
|
1545
1588
|
const runOptions = {
|
|
1546
1589
|
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),
|
|
@@ -1548,6 +1591,7 @@ const runOptions = {
|
|
|
1548
1591
|
incremental: Flag.map(optional(Flag.boolean("incremental")), absentWhenFalse).pipe(Flag.withDescription("Enable 'incremental mode'. Stryker will store results in a file and use that file to speed up the next --incremental run")),
|
|
1549
1592
|
allowEmpty: Flag.map(optional(Flag.boolean("allowEmpty")), absentWhenFalse).pipe(Flag.withDescription("Allows stryker to exit without any errors in cases where no tests are found")),
|
|
1550
1593
|
incrementalFile: Flag.string("incrementalFile").pipe(Flag.withDescription("Specify the file to use for incremental mode."), optional),
|
|
1594
|
+
progressStreamFile: Flag.string("progressStreamFile").pipe(Flag.withDescription("Specify the file for the machine-mode progress stream."), optional),
|
|
1551
1595
|
force: Flag.map(optional(Flag.boolean("force")), absentWhenFalse).pipe(Flag.withDescription("Run all mutants, even if --incremental is provided and an incremental file exists. Can be used to force a rebuild of the incremental file.")),
|
|
1552
1596
|
mutate: Flag.string("mutate").pipe(Flag.withAlias("m"), Flag.withDescription("With `mutate` you configure the subset of files or just one specific file to be mutated. These should be your _production code files_, and definitely not your test files. (Whereas with `ignorePatterns` you prevent non-relevant files from being copied to the sandbox directory in the first place)\nThe default will try to guess your production code files based on sane defaults. It reads like this:\n- Include all js-like files inside the `src` or `lib` dir\n- Except files inside `__tests__` directories and file names ending with `test` or `spec`.\nIf the defaults are not sufficient for you, for example in a angular project you might want to **exclude** not only the `*.spec.ts` files but other files too, just like the default already does.\nIt is possible to override the defaults by: - supplying one or more [glob patterns](https://github.com/isaacs/minimatch) to include (e.g. `src/**/*.js`) - or one or more comma separated glob patterns preceded with `!` to exclude (e.g. `!src/**/*.spec.js`) - or both (e.g. `src/**/*.js,!src/**/*.spec.js`).\nNote: Stryker will use [minimatch](https://github.com/isaacs/minimatch) for parsing these patterns, see minimatch for the exact syntax."), Flag.map(splitOnComma), optional),
|
|
1553
1597
|
testFiles: Flag.string("testFiles").pipe(Flag.withAlias("t"), Flag.withDescription("With `testFiles` you can limit which test files are executed during mutation testing. When specified, only tests from these files will be run. This allows you to verify that a module's dedicated unit tests can kill all its mutants independently."), Flag.map(splitOnComma), optional),
|
|
@@ -1572,8 +1616,24 @@ const runOptions = {
|
|
|
1572
1616
|
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),
|
|
1573
1617
|
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.")),
|
|
1574
1618
|
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),
|
|
1575
|
-
logLevel: Flag.choice("logLevel",
|
|
1576
|
-
|
|
1619
|
+
logLevel: Flag.choice("logLevel", [
|
|
1620
|
+
"fatal",
|
|
1621
|
+
"error",
|
|
1622
|
+
"warn",
|
|
1623
|
+
"info",
|
|
1624
|
+
"debug",
|
|
1625
|
+
"trace",
|
|
1626
|
+
"off"
|
|
1627
|
+
]).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),
|
|
1628
|
+
fileLogLevel: Flag.choice("fileLogLevel", [
|
|
1629
|
+
"fatal",
|
|
1630
|
+
"error",
|
|
1631
|
+
"warn",
|
|
1632
|
+
"info",
|
|
1633
|
+
"debug",
|
|
1634
|
+
"trace",
|
|
1635
|
+
"off"
|
|
1636
|
+
]).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),
|
|
1577
1637
|
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.")),
|
|
1578
1638
|
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),
|
|
1579
1639
|
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),
|
|
@@ -1627,6 +1687,7 @@ function makeStrykerCommand(requestRef) {
|
|
|
1627
1687
|
setIfPresent(options, "incremental", config.incremental);
|
|
1628
1688
|
setIfPresent(options, "allowEmpty", config.allowEmpty);
|
|
1629
1689
|
setIfPresent(options, "incrementalFile", config.incrementalFile);
|
|
1690
|
+
setIfPresent(options, "progressStreamFile", config.progressStreamFile);
|
|
1630
1691
|
setIfPresent(options, "force", config.force);
|
|
1631
1692
|
setIfPresent(options, "mutate", config.mutate);
|
|
1632
1693
|
setIfPresent(options, "testFiles", config.testFiles);
|
|
@@ -1658,7 +1719,7 @@ function makeStrykerCommand(requestRef) {
|
|
|
1658
1719
|
const strykerCommand = Command.make("stryker", rootConfig, (config) => {
|
|
1659
1720
|
if (config.llms === true) {
|
|
1660
1721
|
const document = {
|
|
1661
|
-
|
|
1722
|
+
_tag: "manifest",
|
|
1662
1723
|
schemaVersion: "1.0",
|
|
1663
1724
|
code: 0,
|
|
1664
1725
|
manifest: emitLLMSManifest(strykerCommand, strykerVersion)
|
|
@@ -1706,21 +1767,18 @@ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
|
|
|
1706
1767
|
GlobalFlag.Wizard,
|
|
1707
1768
|
GlobalFlag.Completions,
|
|
1708
1769
|
GlobalFlag.LogLevel
|
|
1709
|
-
] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath
|
|
1710
|
-
/**
|
|
1711
|
-
* The transport entry: builds the command tree, resolves the mode once at
|
|
1712
|
-
* the edge (never a second probe), provides the CLI and Console layers the
|
|
1713
|
-
* framework renders through, delegates the whole run to the executor cell and
|
|
1714
|
-
* returns the classed exit code it computes. The executor is the I/O
|
|
1715
|
-
* sandwich; this function only frames it.
|
|
1716
|
-
*/
|
|
1770
|
+
] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))));
|
|
1717
1771
|
function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStream, lastSignal) {
|
|
1718
1772
|
return Effect.gen(function* () {
|
|
1719
|
-
const mode = detectMode
|
|
1773
|
+
const mode = yield* detectMode;
|
|
1720
1774
|
const requestRef = yield* Ref.make(Option.none());
|
|
1721
1775
|
const command = makeStrykerCommand(requestRef);
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1776
|
+
const consoleLayer = (() => {
|
|
1777
|
+
if (mode.mode === "machine") return machineConsoleLayer;
|
|
1778
|
+
return Layer.empty;
|
|
1779
|
+
})();
|
|
1780
|
+
const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(consoleLayer, cliLayer)));
|
|
1781
|
+
const result = yield* Effect.result(runStrykerCli({
|
|
1724
1782
|
program: cliEffect,
|
|
1725
1783
|
requestRef,
|
|
1726
1784
|
mode,
|
|
@@ -1728,18 +1786,97 @@ function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStrea
|
|
|
1728
1786
|
argv,
|
|
1729
1787
|
lastSignal
|
|
1730
1788
|
}, createRunEventStream));
|
|
1731
|
-
|
|
1732
|
-
|
|
1789
|
+
if (Result.isFailure(result)) return result.failure;
|
|
1790
|
+
return result.success;
|
|
1791
|
+
}).pipe(Effect.orElseSucceed(() => 2));
|
|
1733
1792
|
}
|
|
1793
|
+
const defaultRunMutationTest = (hostOptions, queue) => (options) => Effect.scoped(runMutationTest(options)).pipe(Effect.provideService(RunEvents, queue), Effect.provide(makeRunLayer(hostOptions)));
|
|
1794
|
+
function hostOptionsOf(mode, stream) {
|
|
1795
|
+
return {
|
|
1796
|
+
runId: stream.runId,
|
|
1797
|
+
resolvedMode: mode,
|
|
1798
|
+
runStartedAt: stream.startedAt,
|
|
1799
|
+
basePath: resolve(process.cwd()),
|
|
1800
|
+
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-platform-node/builtin-reporters")],
|
|
1801
|
+
allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1805
|
+
const stream = yield* createRunEventStream(input.mode);
|
|
1806
|
+
const hostOptions = hostOptionsOf(input.mode, stream);
|
|
1807
|
+
const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions, stream.queue);
|
|
1808
|
+
const basePath = hostOptions.basePath;
|
|
1809
|
+
const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
|
|
1810
|
+
let currentFiber = null;
|
|
1811
|
+
const onSignal = () => {
|
|
1812
|
+
process.removeListener("SIGINT", onSignal);
|
|
1813
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1814
|
+
if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
|
|
1815
|
+
};
|
|
1816
|
+
const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
|
|
1817
|
+
if (runRequest.survivors) return runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions)));
|
|
1818
|
+
return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
|
|
1819
|
+
})()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
|
|
1820
|
+
stream.ensureOpen({
|
|
1821
|
+
mode: "machine",
|
|
1822
|
+
signal: "flag",
|
|
1823
|
+
stdoutIsTTY: process.stdout.isTTY === true
|
|
1824
|
+
});
|
|
1825
|
+
yield* Queue.offer(stream.queue, ManifestRendered.make({
|
|
1826
|
+
schemaVersion: "1.0",
|
|
1827
|
+
code: 0,
|
|
1828
|
+
manifest: llmsRequest.document.manifest
|
|
1829
|
+
}));
|
|
1830
|
+
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1831
|
+
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1832
|
+
currentFiber = Fiber.getCurrent() ?? null;
|
|
1833
|
+
process.on("SIGINT", onSignal);
|
|
1834
|
+
process.on("SIGTERM", onSignal);
|
|
1835
|
+
}), () => Effect.gen(function* () {
|
|
1836
|
+
const parsed = yield* Effect.result(input.program);
|
|
1837
|
+
const request = yield* Ref.get(input.requestRef);
|
|
1838
|
+
const fileName = Option.match(request, {
|
|
1839
|
+
onNone: () => DEFAULT_PROGRESS_STREAM_FILE,
|
|
1840
|
+
onSome: (cliRequest) => Match.value(cliRequest).pipe(Match.tag("run", (runRequest) => {
|
|
1841
|
+
const fromCli = runRequest.options["progressStreamFile"];
|
|
1842
|
+
if (typeof fromCli === "string" && fromCli.length > 0) return fromCli;
|
|
1843
|
+
return DEFAULT_PROGRESS_STREAM_FILE;
|
|
1844
|
+
}), Match.orElse(() => DEFAULT_PROGRESS_STREAM_FILE))
|
|
1845
|
+
});
|
|
1846
|
+
if (stream.setProgressStreamFile !== void 0) yield* stream.setProgressStreamFile(fileName);
|
|
1847
|
+
yield* stream.open;
|
|
1848
|
+
if (Result.isFailure(parsed)) return yield* Effect.fail(parsed.failure);
|
|
1849
|
+
return yield* Option.match(request, {
|
|
1850
|
+
onNone: () => Effect.void,
|
|
1851
|
+
onSome: (cliRequest) => dispatch(cliRequest)
|
|
1852
|
+
});
|
|
1853
|
+
}), () => Effect.sync(() => {
|
|
1854
|
+
process.removeListener("SIGINT", onSignal);
|
|
1855
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1856
|
+
}));
|
|
1857
|
+
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
1858
|
+
const outcome = classifyRunOutcome(yield* Effect.exit(restore(program)), input.lastSignal(), input.argv);
|
|
1859
|
+
const code = runOutcomeCode(outcome);
|
|
1860
|
+
if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, outcome, basePath, pathService);
|
|
1861
|
+
yield* stream.closeAndDrain;
|
|
1862
|
+
return code;
|
|
1863
|
+
}));
|
|
1864
|
+
});
|
|
1734
1865
|
//#endregion
|
|
1735
1866
|
//#region src/main.ts
|
|
1736
1867
|
const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
|
|
1737
1868
|
process.title = "stryker";
|
|
1738
1869
|
const lastSignal = observeTerminatingSignal();
|
|
1739
1870
|
function isSupportedNodeVersion(version) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const
|
|
1871
|
+
let withoutV = version;
|
|
1872
|
+
if (version.startsWith("v")) withoutV = version.slice(1);
|
|
1873
|
+
const dashBaseRaw = withoutV.split("-")[0];
|
|
1874
|
+
let dashBase = withoutV;
|
|
1875
|
+
if (dashBaseRaw !== void 0) dashBase = dashBaseRaw;
|
|
1876
|
+
const baseRaw = dashBase.split("+")[0];
|
|
1877
|
+
let base = dashBase;
|
|
1878
|
+
if (baseRaw !== void 0) base = baseRaw;
|
|
1879
|
+
const parts = base.split(".").map((p) => Number.parseInt(p, 10));
|
|
1743
1880
|
const major = parts[0] ?? 0;
|
|
1744
1881
|
const minor = parts[1] ?? 0;
|
|
1745
1882
|
const patch = parts[2] ?? 0;
|
|
@@ -1753,7 +1890,7 @@ const program = Effect.gen(function* () {
|
|
|
1753
1890
|
const outputMode = yield* OutputModeProbe;
|
|
1754
1891
|
const runEvents = yield* RunEventStreamPort;
|
|
1755
1892
|
return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
|
|
1756
|
-
}).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive,
|
|
1893
|
+
}).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
|
|
1757
1894
|
NodeRuntime.runMain(program, {
|
|
1758
1895
|
disableErrorReporting: true,
|
|
1759
1896
|
teardown: (exit, onExit) => {
|
|
@@ -1762,7 +1899,8 @@ NodeRuntime.runMain(program, {
|
|
|
1762
1899
|
return;
|
|
1763
1900
|
}
|
|
1764
1901
|
const signal = lastSignal();
|
|
1765
|
-
|
|
1902
|
+
if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
|
|
1903
|
+
else onExit(128 + signal);
|
|
1766
1904
|
}
|
|
1767
1905
|
});
|
|
1768
1906
|
//#endregion
|