@systemfsoftware/stryker-js-cli 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +8 -7
- package/dist/main.mjs +905 -790
- package/package.json +14 -13
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 }));
|
|
314
|
+
}
|
|
315
|
+
function admissionDecision(command) {
|
|
316
|
+
return decideAdmission(command);
|
|
527
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
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function modeDecision(command) {
|
|
767
|
+
return r4(command);
|
|
906
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,70 @@ 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
|
-
/**
|
|
1056
|
+
//#region src/Survivors.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Survivors — the survivors-admission capability.
|
|
1059
|
+
*
|
|
1060
|
+
* The prior-report decoding, source hashing, mutant conversion, and admission
|
|
1061
|
+
* pipeline for --survivors runs. Pure admission decision lives in
|
|
1062
|
+
* Survivors.workflow.ts.
|
|
1063
|
+
*/
|
|
994
1064
|
const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
|
|
995
|
-
//#endregion
|
|
996
|
-
//#region src/survivors-hashing.ts
|
|
997
|
-
const { entries: objectEntries$1, fromEntries: objectFromEntries } = Object;
|
|
998
1065
|
/**
|
|
999
|
-
* The
|
|
1066
|
+
* The mutant shape the admission carries, named once because both the decision's
|
|
1067
|
+
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
1068
|
+
*/
|
|
1069
|
+
/**
|
|
1070
|
+
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
1071
|
+
* get the decode function, not the schema, so the report's wire shape is not a
|
|
1072
|
+
* surface commitment and the codec has exactly one caller.
|
|
1000
1073
|
*
|
|
1074
|
+
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
1075
|
+
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
1076
|
+
* must not be refused for carrying one.
|
|
1077
|
+
*/
|
|
1078
|
+
/**
|
|
1079
|
+
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
1080
|
+
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
1081
|
+
* runs. A malformed report therefore never reaches the decider, and nothing here casts
|
|
1082
|
+
* a third-party report type.
|
|
1083
|
+
*/
|
|
1084
|
+
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
1085
|
+
const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
|
|
1086
|
+
/**
|
|
1001
1087
|
* Thin by design: the digest is the caller's capability, and naming the call
|
|
1002
1088
|
* keeps every hashing site in the admission path reading the same way.
|
|
1003
1089
|
*/
|
|
@@ -1011,11 +1097,8 @@ function sourceContentHash(content, hash) {
|
|
|
1011
1097
|
* this is the recorded side, read back out of the report.
|
|
1012
1098
|
*/
|
|
1013
1099
|
function priorSourceHashes(priorReport, hashContent) {
|
|
1014
|
-
return objectFromEntries(objectEntries
|
|
1100
|
+
return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
|
|
1015
1101
|
}
|
|
1016
|
-
//#endregion
|
|
1017
|
-
//#region src/survivors-mutants.ts
|
|
1018
|
-
const { entries: objectEntries } = Object;
|
|
1019
1102
|
/**
|
|
1020
1103
|
* Converts a report mutant (1-based schema location) into the internal mutant
|
|
1021
1104
|
* shape a run consumes (0-based positions, absolute file name) — the exact
|
|
@@ -1025,7 +1108,7 @@ const { entries: objectEntries } = Object;
|
|
|
1025
1108
|
* incremental differ uses.
|
|
1026
1109
|
*/
|
|
1027
1110
|
function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
1028
|
-
return {
|
|
1111
|
+
return Mutant.make({
|
|
1029
1112
|
id: mutant.id,
|
|
1030
1113
|
fileName: resolveAbsolutePath(file),
|
|
1031
1114
|
mutatorName: mutant.mutatorName,
|
|
@@ -1040,7 +1123,7 @@ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
|
1040
1123
|
column: mutant.location.end.column - 1
|
|
1041
1124
|
}
|
|
1042
1125
|
}
|
|
1043
|
-
};
|
|
1126
|
+
});
|
|
1044
1127
|
}
|
|
1045
1128
|
/**
|
|
1046
1129
|
* The survivors of the prior report: exactly the mutants whose status is
|
|
@@ -1057,11 +1140,11 @@ function extractSurvivors(priorReport, resolveAbsolutePath) {
|
|
|
1057
1140
|
* ranges: the report's 1-based lines with the internal 0-based columns,
|
|
1058
1141
|
* relative file names, deduplicated in first-seen order.
|
|
1059
1142
|
*/
|
|
1060
|
-
function survivorMutateSpans(survivors, basePath
|
|
1143
|
+
function survivorMutateSpans(survivors, basePath) {
|
|
1061
1144
|
const spans = [];
|
|
1062
1145
|
const seen = /* @__PURE__ */ new Set();
|
|
1063
1146
|
for (const survivor of survivors) {
|
|
1064
|
-
const file = toRelativeNormalizedFileName(survivor.fileName, basePath
|
|
1147
|
+
const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
|
|
1065
1148
|
const { start, end } = survivor.location;
|
|
1066
1149
|
const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
|
|
1067
1150
|
if (!seen.has(span)) {
|
|
@@ -1071,18 +1154,7 @@ function survivorMutateSpans(survivors, basePath, pathService) {
|
|
|
1071
1154
|
}
|
|
1072
1155
|
return spans;
|
|
1073
1156
|
}
|
|
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");
|
|
1157
|
+
const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
|
|
1086
1158
|
const resolveAbsolutePath = (file) => resolve(file);
|
|
1087
1159
|
/**
|
|
1088
1160
|
* The survivors admission, as a description whose phases chain by type and
|
|
@@ -1095,10 +1167,9 @@ const resolveAbsolutePath = (file) => resolve(file);
|
|
|
1095
1167
|
* stashed context back and dispatches the decision to the verdict/run,
|
|
1096
1168
|
* failing the run with a rejection.
|
|
1097
1169
|
*/
|
|
1098
|
-
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath) => pipe(Cell.read((cliOptions) => Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
|
|
1170
|
+
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath, services) => pipe(Cell.read((cliOptions) => Effect.provideContext(Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
|
|
1099
1171
|
const priorReportPath = priorReportPathOf(resolvedOptions);
|
|
1100
|
-
|
|
1101
|
-
return Ref.set(runContext, {
|
|
1172
|
+
return Effect.flatMap(readPriorReport(priorReportPath), (read) => Effect.flatMap(currentSourceHashesFor(priorReportFileKeys(read.raw)), (sourceContentHashes) => Ref.set(runContext, {
|
|
1102
1173
|
resolvedOptions,
|
|
1103
1174
|
priorReportPath,
|
|
1104
1175
|
pathService
|
|
@@ -1107,9 +1178,9 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1107
1178
|
priorReportRaw: read.raw,
|
|
1108
1179
|
priorReportFound: read.found,
|
|
1109
1180
|
priorReportPath,
|
|
1110
|
-
sourceContentHashes
|
|
1111
|
-
}));
|
|
1112
|
-
})))), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
1181
|
+
sourceContentHashes
|
|
1182
|
+
}))));
|
|
1183
|
+
}))), services)), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
1113
1184
|
if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
|
|
1114
1185
|
priorReport: void 0,
|
|
1115
1186
|
currentConfig: resolvedOptions,
|
|
@@ -1133,11 +1204,12 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1133
1204
|
if (context === void 0) return Effect.die("the survivors admission read must run before its write");
|
|
1134
1205
|
const { resolvedOptions, priorReportPath, pathService } = context;
|
|
1135
1206
|
return Result.match(outcome, {
|
|
1136
|
-
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () =>
|
|
1207
|
+
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
|
|
1208
|
+
const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
|
|
1137
1209
|
return runMutationTest({
|
|
1138
1210
|
...resolvedOptions,
|
|
1139
|
-
survivors:
|
|
1140
|
-
mutate: survivorMutateSpans(
|
|
1211
|
+
survivors: admittedMutants,
|
|
1212
|
+
mutate: survivorMutateSpans(admittedMutants, basePath),
|
|
1141
1213
|
survivorsPriorReport: priorReportPath,
|
|
1142
1214
|
incremental: false
|
|
1143
1215
|
}).pipe(Effect.orDie);
|
|
@@ -1160,38 +1232,35 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
|
|
|
1160
1232
|
*/
|
|
1161
1233
|
function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions, basePath) {
|
|
1162
1234
|
return Effect.gen(function* () {
|
|
1235
|
+
const services = yield* Effect.context();
|
|
1163
1236
|
const admissionContext = yield* Ref.make(void 0);
|
|
1164
|
-
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath), cliOptions);
|
|
1237
|
+
return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath, services), cliOptions);
|
|
1165
1238
|
});
|
|
1166
1239
|
}
|
|
1167
1240
|
function resolveSurvivorsRunOptions(cliOptions, basePath) {
|
|
1168
|
-
return readConfig(cliOptions,
|
|
1241
|
+
return readConfig(cliOptions, basePath);
|
|
1169
1242
|
}
|
|
1170
1243
|
function priorReportPathOf(resolved) {
|
|
1171
1244
|
const configured = resolved["survivorsPriorReport"];
|
|
1172
|
-
|
|
1245
|
+
if (typeof configured === "string") return configured;
|
|
1246
|
+
return DEFAULT_SURVIVORS_PRIOR_REPORT;
|
|
1173
1247
|
}
|
|
1174
1248
|
function readPriorReport(priorReportPath) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1249
|
+
return Effect.gen(function* () {
|
|
1250
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(priorReportPath).pipe(Effect.map((text) => ({
|
|
1251
|
+
found: true,
|
|
1252
|
+
raw: Result.match(S.decodeResult(S.fromJsonString(S.Unknown))(text), {
|
|
1253
|
+
onFailure: () => text,
|
|
1254
|
+
onSuccess: (value) => value
|
|
1255
|
+
})
|
|
1256
|
+
})), Effect.catchTag("PlatformError", (cause) => Match.value(cause.reason).pipe(Match.tag("NotFound", () => Effect.succeed({
|
|
1180
1257
|
found: false,
|
|
1181
1258
|
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
|
-
}
|
|
1259
|
+
})), Match.orElse(() => Effect.fail(ConfigFileUnreadableError.make({
|
|
1260
|
+
file: priorReportPath,
|
|
1261
|
+
cause
|
|
1262
|
+
}))))));
|
|
1263
|
+
});
|
|
1195
1264
|
}
|
|
1196
1265
|
function priorReportFileKeys(raw) {
|
|
1197
1266
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
@@ -1201,116 +1270,43 @@ function priorReportFileKeys(raw) {
|
|
|
1201
1270
|
return Object.keys(files);
|
|
1202
1271
|
}
|
|
1203
1272
|
function readSourceFile(file) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1273
|
+
return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
|
|
1274
|
+
file,
|
|
1275
|
+
cause
|
|
1276
|
+
}))));
|
|
1209
1277
|
}
|
|
1210
1278
|
function currentSourceHashesFor(files) {
|
|
1211
|
-
|
|
1212
|
-
for (const file of files) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
|
|
1213
|
-
return hashes;
|
|
1279
|
+
return Effect.map(Effect.forEach(files, (file) => Effect.map(readSourceFile(file), (content) => [file, sourceContentHash(content, hashContent)]), { concurrency: 24 }), (pairs) => Object.fromEntries(pairs));
|
|
1214
1280
|
}
|
|
1215
1281
|
//#endregion
|
|
1216
|
-
//#region src/
|
|
1217
|
-
const
|
|
1282
|
+
//#region src/Cli.ts
|
|
1283
|
+
const SIGNAL_NUMBERS = Object.freeze({
|
|
1284
|
+
SIGINT: 2,
|
|
1285
|
+
SIGTERM: 15
|
|
1286
|
+
});
|
|
1218
1287
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1288
|
+
* Installs the listeners and returns the reader.
|
|
1289
|
+
*
|
|
1290
|
+
* The listener records and returns: interrupting the run is the runtime's job,
|
|
1291
|
+
* and doing it from here would race the run's own finalizer for the stream.
|
|
1292
|
+
* `once` per signal, because a second delivery of the same signal cannot
|
|
1293
|
+
* change the answer.
|
|
1223
1294
|
*/
|
|
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
|
-
};
|
|
1295
|
+
function observeTerminatingSignal() {
|
|
1296
|
+
let observed = null;
|
|
1297
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
1298
|
+
observed = SIGNAL_NUMBERS[signal] ?? null;
|
|
1299
|
+
});
|
|
1300
|
+
return () => observed;
|
|
1238
1301
|
}
|
|
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
1302
|
function isObject(value) {
|
|
1309
1303
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1310
1304
|
}
|
|
1311
1305
|
function stringField(node, key) {
|
|
1312
1306
|
const value = node[key];
|
|
1313
|
-
return
|
|
1307
|
+
return (() => {
|
|
1308
|
+
if (typeof value === "string") return value;
|
|
1309
|
+
})();
|
|
1314
1310
|
}
|
|
1315
1311
|
function stringArrayField(node, key) {
|
|
1316
1312
|
const value = node[key];
|
|
@@ -1359,7 +1355,10 @@ const PRIMITIVE_KIND = {
|
|
|
1359
1355
|
};
|
|
1360
1356
|
function kindOf(primitive) {
|
|
1361
1357
|
const tag = stringField(primitive, "_tag");
|
|
1362
|
-
return
|
|
1358
|
+
return (() => {
|
|
1359
|
+
if (tag === void 0) return "unknown";
|
|
1360
|
+
return PRIMITIVE_KIND[tag] ?? tag;
|
|
1361
|
+
})();
|
|
1363
1362
|
}
|
|
1364
1363
|
function choiceValues(primitive) {
|
|
1365
1364
|
const keys = primitive["choiceKeys"];
|
|
@@ -1368,8 +1367,13 @@ function choiceValues(primitive) {
|
|
|
1368
1367
|
for (const key of keys) if (typeof key === "string") values.push(key);
|
|
1369
1368
|
return values;
|
|
1370
1369
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1370
|
+
const REPORTER_NAMES = [
|
|
1371
|
+
"clear-text",
|
|
1372
|
+
"progress",
|
|
1373
|
+
"html",
|
|
1374
|
+
"json",
|
|
1375
|
+
"progress-stream"
|
|
1376
|
+
];
|
|
1373
1377
|
/**
|
|
1374
1378
|
* v4 option descriptions are stored as `Option.some(string)` on the compiled
|
|
1375
1379
|
* `Single`; the walker unwraps the option.
|
|
@@ -1380,16 +1384,25 @@ function descriptionOf(single) {
|
|
|
1380
1384
|
switch (tagOf(description)) {
|
|
1381
1385
|
case "Some": {
|
|
1382
1386
|
const value = description["value"];
|
|
1383
|
-
return
|
|
1387
|
+
return (() => {
|
|
1388
|
+
if (typeof value === "string") return value;
|
|
1389
|
+
return "";
|
|
1390
|
+
})();
|
|
1384
1391
|
}
|
|
1385
1392
|
default: return "";
|
|
1386
1393
|
}
|
|
1387
1394
|
}
|
|
1388
1395
|
function describeSingle(single, isOptional, out) {
|
|
1389
1396
|
const name = stringField(single, "name") ?? "";
|
|
1390
|
-
const primitive =
|
|
1397
|
+
const primitive = (() => {
|
|
1398
|
+
if (isObject(single["primitiveType"])) return single["primitiveType"];
|
|
1399
|
+
return {};
|
|
1400
|
+
})();
|
|
1391
1401
|
const kind = kindOf(primitive);
|
|
1392
|
-
const choices =
|
|
1402
|
+
const choices = (() => {
|
|
1403
|
+
if (name === "reporters") return REPORTER_NAMES;
|
|
1404
|
+
if (kind === "choice") return choiceValues(primitive);
|
|
1405
|
+
})();
|
|
1393
1406
|
const description = descriptionOf(single);
|
|
1394
1407
|
const required = kind !== "boolean" && !isOptional;
|
|
1395
1408
|
const described = {
|
|
@@ -1397,7 +1410,10 @@ function describeSingle(single, isOptional, out) {
|
|
|
1397
1410
|
aliases: stringArrayField(single, "aliases"),
|
|
1398
1411
|
kind,
|
|
1399
1412
|
required,
|
|
1400
|
-
...
|
|
1413
|
+
...(() => {
|
|
1414
|
+
if (choices !== void 0) return { choices };
|
|
1415
|
+
return {};
|
|
1416
|
+
})(),
|
|
1401
1417
|
description
|
|
1402
1418
|
};
|
|
1403
1419
|
if (single["kind"] === "argument") {
|
|
@@ -1416,7 +1432,9 @@ function walkConfigNode(node, orderedParams, out) {
|
|
|
1416
1432
|
switch (tagOf(node)) {
|
|
1417
1433
|
case "Param": {
|
|
1418
1434
|
const index = node["index"];
|
|
1419
|
-
const param =
|
|
1435
|
+
const param = (() => {
|
|
1436
|
+
if (typeof index === "number") return orderedParams[index];
|
|
1437
|
+
})();
|
|
1420
1438
|
if (param !== void 0) walkParam(param, false, out);
|
|
1421
1439
|
return;
|
|
1422
1440
|
}
|
|
@@ -1440,7 +1458,9 @@ function describeCommandNode(node) {
|
|
|
1440
1458
|
};
|
|
1441
1459
|
const config = node["config"];
|
|
1442
1460
|
if (isObject(config) && isObject(config["tree"])) {
|
|
1443
|
-
|
|
1461
|
+
let orderedParams = [];
|
|
1462
|
+
const maybeOrdered = config["orderedParams"];
|
|
1463
|
+
if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
|
|
1444
1464
|
walkConfigTree(config["tree"], orderedParams, out);
|
|
1445
1465
|
}
|
|
1446
1466
|
const subcommands = [];
|
|
@@ -1454,17 +1474,28 @@ function describeCommandNode(node) {
|
|
|
1454
1474
|
}
|
|
1455
1475
|
return {
|
|
1456
1476
|
name: stringField(node, "name") ?? "",
|
|
1457
|
-
description:
|
|
1477
|
+
description: (() => {
|
|
1478
|
+
if (typeof node["description"] === "string") return node["description"];
|
|
1479
|
+
return "";
|
|
1480
|
+
})(),
|
|
1458
1481
|
options: out.flags,
|
|
1459
1482
|
args: out.args,
|
|
1460
1483
|
subcommands
|
|
1461
1484
|
};
|
|
1462
1485
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1486
|
+
function readCoreEntries() {
|
|
1487
|
+
try {
|
|
1488
|
+
const manifestPath = createRequire(import.meta.url).resolve("@systemfsoftware/stryker-js-platform-node/package.json");
|
|
1489
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
1490
|
+
const parsed = JSON.parse(raw);
|
|
1491
|
+
const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
|
|
1492
|
+
const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
|
|
1493
|
+
if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
|
|
1494
|
+
return [];
|
|
1495
|
+
} catch {
|
|
1496
|
+
return [];
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1468
1499
|
function buildLLMSManifest(command, version) {
|
|
1469
1500
|
const root = describeCommandNode(command) ?? {
|
|
1470
1501
|
name: "",
|
|
@@ -1477,15 +1508,14 @@ function buildLLMSManifest(command, version) {
|
|
|
1477
1508
|
schemaVersion: "1.0",
|
|
1478
1509
|
tool: root.name,
|
|
1479
1510
|
version,
|
|
1480
|
-
commands: [root]
|
|
1511
|
+
commands: [root],
|
|
1512
|
+
entries: readCoreEntries()
|
|
1481
1513
|
};
|
|
1482
1514
|
}
|
|
1483
1515
|
/** The manifest as one JSON document, ready for stdout — the U4 convention. */
|
|
1484
1516
|
function emitLLMSManifest(command, version) {
|
|
1485
1517
|
return JSON.stringify(buildLLMSManifest(command, version));
|
|
1486
1518
|
}
|
|
1487
|
-
//#endregion
|
|
1488
|
-
//#region src/stryker-cli.ts
|
|
1489
1519
|
function createSplitter(separator) {
|
|
1490
1520
|
return (value) => value.split(separator).filter(Boolean);
|
|
1491
1521
|
}
|
|
@@ -1498,7 +1528,10 @@ const splitOnSpace = createSplitter(" ");
|
|
|
1498
1528
|
*/
|
|
1499
1529
|
function parseCleanDirOption(value) {
|
|
1500
1530
|
const v = value.toLocaleLowerCase();
|
|
1501
|
-
return
|
|
1531
|
+
return (() => {
|
|
1532
|
+
if (v === "always") return v;
|
|
1533
|
+
return v !== "false" && v !== "0";
|
|
1534
|
+
})();
|
|
1502
1535
|
}
|
|
1503
1536
|
/**
|
|
1504
1537
|
* Commander characterization: a pure integer is parsed as a number, anything
|
|
@@ -1516,31 +1549,12 @@ const optional = (option) => Flag.optional(option);
|
|
|
1516
1549
|
* `Option.some(false)` for an explicit `--no-x`, so both map back to
|
|
1517
1550
|
* `undefined` and leave the config-file default in force (KTD4).
|
|
1518
1551
|
*/
|
|
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
|
|
1552
|
+
const absentWhenFalse = (value) => {
|
|
1553
|
+
if (Option.isSome(value) && value.value) return true;
|
|
1537
1554
|
};
|
|
1538
|
-
function isLogLevel(value) {
|
|
1539
|
-
return LOG_LEVEL_LOOKUP[value] === true;
|
|
1540
|
-
}
|
|
1541
1555
|
function setLogLevel(target, key, value) {
|
|
1542
1556
|
const unwrapped = unwrap(value);
|
|
1543
|
-
if (unwrapped !== void 0
|
|
1557
|
+
if (unwrapped !== void 0) target[key] = unwrapped;
|
|
1544
1558
|
}
|
|
1545
1559
|
const runOptions = {
|
|
1546
1560
|
ignorePatterns: Flag.string("ignorePatterns").pipe(Flag.withDescription("A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have NO effect when using the `--inPlace` option. The directories `node_modules`, `.git` and some others are always ignored. Example: `--ignorePatterns dist`. These patterns are ALWAYS ignored: [`node_modules`, `.git`, `/reports`, `*.tsbuildinfo`, `/stryker.log`, `.stryker-tmp`]. Because Stryker always ignores these, you should rarely have to adjust the `ignorePatterns` setting at all. This is useful to speed up Stryker by reducing the size of the sandbox directory which has a positive effect on performance."), Flag.map(splitOnComma), optional),
|
|
@@ -1572,8 +1586,24 @@ const runOptions = {
|
|
|
1572
1586
|
concurrency: Flag.string("concurrency").pipe(Flag.withAlias("c"), Flag.withDescription("Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)"), Flag.map(parseConcurrency), optional),
|
|
1573
1587
|
disableBail: Flag.map(optional(Flag.boolean("disableBail")), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
|
|
1574
1588
|
maxTestRunnerReuse: Flag.integer("maxTestRunnerReuse").pipe(Flag.withDescription("Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse."), optional),
|
|
1575
|
-
logLevel: Flag.choice("logLevel",
|
|
1576
|
-
|
|
1589
|
+
logLevel: Flag.choice("logLevel", [
|
|
1590
|
+
"fatal",
|
|
1591
|
+
"error",
|
|
1592
|
+
"warn",
|
|
1593
|
+
"info",
|
|
1594
|
+
"debug",
|
|
1595
|
+
"trace",
|
|
1596
|
+
"off"
|
|
1597
|
+
]).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.logLevel}"`), optional),
|
|
1598
|
+
fileLogLevel: Flag.choice("fileLogLevel", [
|
|
1599
|
+
"fatal",
|
|
1600
|
+
"error",
|
|
1601
|
+
"warn",
|
|
1602
|
+
"info",
|
|
1603
|
+
"debug",
|
|
1604
|
+
"trace",
|
|
1605
|
+
"off"
|
|
1606
|
+
]).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.fileLogLevel}"`), optional),
|
|
1577
1607
|
inPlace: Flag.map(optional(Flag.boolean("inPlace")), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
|
|
1578
1608
|
tempDirName: Flag.string("tempDirName").pipe(Flag.withDescription("Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run"), optional),
|
|
1579
1609
|
cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${RENDERED_OPTION_DEFAULTS.tempDirName}" inside the current working directory by default) after a run.\n- false: Never delete the temp dir;\n- true: Delete the tmp dir after a successful run;\n- always: Always delete the temp dir, regardless of whether the run was successful.`), Flag.map(parseCleanDirOption), optional),
|
|
@@ -1658,7 +1688,7 @@ function makeStrykerCommand(requestRef) {
|
|
|
1658
1688
|
const strykerCommand = Command.make("stryker", rootConfig, (config) => {
|
|
1659
1689
|
if (config.llms === true) {
|
|
1660
1690
|
const document = {
|
|
1661
|
-
|
|
1691
|
+
_tag: "manifest",
|
|
1662
1692
|
schemaVersion: "1.0",
|
|
1663
1693
|
code: 0,
|
|
1664
1694
|
manifest: emitLLMSManifest(strykerCommand, strykerVersion)
|
|
@@ -1706,21 +1736,18 @@ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
|
|
|
1706
1736
|
GlobalFlag.Wizard,
|
|
1707
1737
|
GlobalFlag.Completions,
|
|
1708
1738
|
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
|
-
*/
|
|
1739
|
+
] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))));
|
|
1717
1740
|
function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStream, lastSignal) {
|
|
1718
1741
|
return Effect.gen(function* () {
|
|
1719
|
-
const mode = detectMode
|
|
1742
|
+
const mode = yield* detectMode;
|
|
1720
1743
|
const requestRef = yield* Ref.make(Option.none());
|
|
1721
1744
|
const command = makeStrykerCommand(requestRef);
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1745
|
+
const consoleLayer = (() => {
|
|
1746
|
+
if (mode.mode === "machine") return machineConsoleLayer;
|
|
1747
|
+
return Layer.empty;
|
|
1748
|
+
})();
|
|
1749
|
+
const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(consoleLayer, cliLayer)));
|
|
1750
|
+
const result = yield* Effect.result(runStrykerCli({
|
|
1724
1751
|
program: cliEffect,
|
|
1725
1752
|
requestRef,
|
|
1726
1753
|
mode,
|
|
@@ -1728,18 +1755,105 @@ function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStrea
|
|
|
1728
1755
|
argv,
|
|
1729
1756
|
lastSignal
|
|
1730
1757
|
}, createRunEventStream));
|
|
1731
|
-
|
|
1732
|
-
|
|
1758
|
+
if (Result.isFailure(result)) return result.failure;
|
|
1759
|
+
return result.success;
|
|
1760
|
+
}).pipe(Effect.orElseSucceed(() => 2));
|
|
1761
|
+
}
|
|
1762
|
+
const defaultRunMutationTest = (hostOptions, queue) => (options) => Effect.scoped(runMutationTest(options)).pipe(Effect.provideService(RunEvents, queue), Effect.provide(makeRunLayer(hostOptions)));
|
|
1763
|
+
function hostOptionsOf(mode, stream) {
|
|
1764
|
+
return {
|
|
1765
|
+
runId: stream.runId,
|
|
1766
|
+
resolvedMode: mode,
|
|
1767
|
+
runStartedAt: stream.startedAt,
|
|
1768
|
+
basePath: resolve(process.cwd()),
|
|
1769
|
+
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-platform-node/builtin-reporters")],
|
|
1770
|
+
allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
|
|
1771
|
+
};
|
|
1733
1772
|
}
|
|
1773
|
+
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1774
|
+
const stream = yield* createRunEventStream(input.mode);
|
|
1775
|
+
const hostOptions = hostOptionsOf(input.mode, stream);
|
|
1776
|
+
const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions, stream.queue);
|
|
1777
|
+
const basePath = hostOptions.basePath;
|
|
1778
|
+
const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
|
|
1779
|
+
let currentFiber = null;
|
|
1780
|
+
const onSignal = () => {
|
|
1781
|
+
process.removeListener("SIGINT", onSignal);
|
|
1782
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1783
|
+
if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
|
|
1784
|
+
};
|
|
1785
|
+
const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
|
|
1786
|
+
if (runRequest.survivors) return runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions)));
|
|
1787
|
+
return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
|
|
1788
|
+
})()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
|
|
1789
|
+
stream.ensureOpen({
|
|
1790
|
+
mode: "machine",
|
|
1791
|
+
signal: "flag",
|
|
1792
|
+
stdoutIsTTY: process.stdout.isTTY === true
|
|
1793
|
+
});
|
|
1794
|
+
yield* Queue.offer(stream.queue, ManifestRendered.make({
|
|
1795
|
+
schemaVersion: "1.0",
|
|
1796
|
+
code: 0,
|
|
1797
|
+
manifest: llmsRequest.document.manifest
|
|
1798
|
+
}));
|
|
1799
|
+
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1800
|
+
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1801
|
+
currentFiber = Fiber.getCurrent() ?? null;
|
|
1802
|
+
process.on("SIGINT", onSignal);
|
|
1803
|
+
process.on("SIGTERM", onSignal);
|
|
1804
|
+
}), () => Effect.gen(function* () {
|
|
1805
|
+
yield* stream.open;
|
|
1806
|
+
yield* input.program;
|
|
1807
|
+
const request = yield* Ref.get(input.requestRef);
|
|
1808
|
+
return yield* Option.match(request, {
|
|
1809
|
+
onNone: () => Effect.void,
|
|
1810
|
+
onSome: (cliRequest) => dispatch(cliRequest)
|
|
1811
|
+
});
|
|
1812
|
+
}), () => Effect.sync(() => {
|
|
1813
|
+
process.removeListener("SIGINT", onSignal);
|
|
1814
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1815
|
+
}));
|
|
1816
|
+
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
1817
|
+
const outcome = classifyRunOutcome(yield* Effect.exit(restore(program)), input.lastSignal(), input.argv);
|
|
1818
|
+
const code = runOutcomeCode(outcome);
|
|
1819
|
+
if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, outcome, basePath, pathService);
|
|
1820
|
+
yield* stream.closeAndDrain;
|
|
1821
|
+
return code;
|
|
1822
|
+
}));
|
|
1823
|
+
});
|
|
1824
|
+
//#endregion
|
|
1825
|
+
//#region src/StreamFile.ts
|
|
1826
|
+
const STREAM_FILE_DIR = "reports";
|
|
1827
|
+
const STREAM_FILE_NAME = "mutation-stream.jsonl";
|
|
1828
|
+
const encodeUtf8 = (line) => new TextEncoder().encode(line);
|
|
1829
|
+
const drainStreamFile = (framed) => Effect.gen(function* () {
|
|
1830
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1831
|
+
const file = (yield* Path.Path).join(STREAM_FILE_DIR, STREAM_FILE_NAME);
|
|
1832
|
+
yield* fs.makeDirectory(STREAM_FILE_DIR, { recursive: true }).pipe(Effect.orDie);
|
|
1833
|
+
yield* Stream.run(framed.pipe(Stream.map(encodeUtf8)), fs.sink(file, { flag: "w" })).pipe(Effect.orDie);
|
|
1834
|
+
});
|
|
1835
|
+
const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(function* () {
|
|
1836
|
+
const stdio = yield* Stdio.Stdio;
|
|
1837
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1838
|
+
const path = yield* Path.Path;
|
|
1839
|
+
const drainFramed = (framed) => drainStreamFile(framed).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
1840
|
+
return RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved, drainFramed) });
|
|
1841
|
+
}));
|
|
1734
1842
|
//#endregion
|
|
1735
1843
|
//#region src/main.ts
|
|
1736
1844
|
const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
|
|
1737
1845
|
process.title = "stryker";
|
|
1738
1846
|
const lastSignal = observeTerminatingSignal();
|
|
1739
1847
|
function isSupportedNodeVersion(version) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const
|
|
1848
|
+
let withoutV = version;
|
|
1849
|
+
if (version.startsWith("v")) withoutV = version.slice(1);
|
|
1850
|
+
const dashBaseRaw = withoutV.split("-")[0];
|
|
1851
|
+
let dashBase = withoutV;
|
|
1852
|
+
if (dashBaseRaw !== void 0) dashBase = dashBaseRaw;
|
|
1853
|
+
const baseRaw = dashBase.split("+")[0];
|
|
1854
|
+
let base = dashBase;
|
|
1855
|
+
if (baseRaw !== void 0) base = baseRaw;
|
|
1856
|
+
const parts = base.split(".").map((p) => Number.parseInt(p, 10));
|
|
1743
1857
|
const major = parts[0] ?? 0;
|
|
1744
1858
|
const minor = parts[1] ?? 0;
|
|
1745
1859
|
const patch = parts[2] ?? 0;
|
|
@@ -1753,7 +1867,7 @@ const program = Effect.gen(function* () {
|
|
|
1753
1867
|
const outputMode = yield* OutputModeProbe;
|
|
1754
1868
|
const runEvents = yield* RunEventStreamPort;
|
|
1755
1869
|
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,
|
|
1870
|
+
}).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
|
|
1757
1871
|
NodeRuntime.runMain(program, {
|
|
1758
1872
|
disableErrorReporting: true,
|
|
1759
1873
|
teardown: (exit, onExit) => {
|
|
@@ -1762,7 +1876,8 @@ NodeRuntime.runMain(program, {
|
|
|
1762
1876
|
return;
|
|
1763
1877
|
}
|
|
1764
1878
|
const signal = lastSignal();
|
|
1765
|
-
|
|
1879
|
+
if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
|
|
1880
|
+
else onExit(128 + signal);
|
|
1766
1881
|
}
|
|
1767
1882
|
});
|
|
1768
1883
|
//#endregion
|