@systemfsoftware/stryker-js-cli 6.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +1 -3
- package/dist/main.mjs +947 -891
- package/dist/{node-D-ynY_kk.mjs → node-B7U4hEuW.mjs} +7 -5
- package/dist/workers/checker-worker.mjs +2 -2
- package/dist/workers/child-process-test-runner-worker.mjs +23 -12
- package/package.json +15 -16
package/dist/main.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as nodePlatformLayer } from "./node-
|
|
2
|
+
import { n as nodePlatformLayer } from "./node-B7U4hEuW.mjs";
|
|
3
3
|
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
|
|
4
4
|
import * as NodePath from "@effect/platform-node-shared/NodePath";
|
|
5
5
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
@@ -8,22 +8,20 @@ import * as Effect from "effect/Effect";
|
|
|
8
8
|
import * as Exit from "effect/Exit";
|
|
9
9
|
import * as Layer from "effect/Layer";
|
|
10
10
|
import * as Logger from "effect/Logger";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
11
12
|
import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
|
|
12
13
|
import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, readConfig, runMutationTest, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-engine";
|
|
13
14
|
import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
|
|
14
|
-
import { Heartbeat, HelpRendered,
|
|
15
|
-
import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
|
|
15
|
+
import { Heartbeat, HelpRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
|
|
16
|
+
import { RENDERED_OPTION_DEFAULTS, StrykerOptionsSchema } from "@systemfsoftware/stryker-js/Schema";
|
|
16
17
|
import * as Cause from "effect/Cause";
|
|
17
18
|
import * as Console from "effect/Console";
|
|
18
19
|
import * as Fiber from "effect/Fiber";
|
|
19
|
-
import * as FileSystem from "effect/FileSystem";
|
|
20
20
|
import * as Match from "effect/Match";
|
|
21
|
-
import * as Option from "effect/Option";
|
|
22
21
|
import * as Path from "effect/Path";
|
|
23
22
|
import * as Queue from "effect/Queue";
|
|
24
23
|
import * as Ref from "effect/Ref";
|
|
25
24
|
import * as Result from "effect/Result";
|
|
26
|
-
import * as S from "effect/Schema";
|
|
27
25
|
import * as Terminal from "effect/Terminal";
|
|
28
26
|
import * as Argument from "effect/unstable/cli/Argument";
|
|
29
27
|
import * as CliConfig from "effect/unstable/cli/CliConfig";
|
|
@@ -32,10 +30,17 @@ import * as Command from "effect/unstable/cli/Command";
|
|
|
32
30
|
import * as Flag from "effect/unstable/cli/Flag";
|
|
33
31
|
import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
|
|
34
32
|
import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
33
|
+
import * as Arr from "effect/Array";
|
|
34
|
+
import * as S from "effect/Schema";
|
|
35
35
|
import { ExitClass, highestExitClass } from "@systemfsoftware/stryker-js/ExitClass";
|
|
36
36
|
import * as Clock from "effect/Clock";
|
|
37
37
|
import * as Formatter from "effect/Formatter";
|
|
38
38
|
import * as Predicate from "effect/Predicate";
|
|
39
|
+
import { LocationSchema, MutantStatusSchema, MutationTestResultSchema } from "@systemfsoftware/stryker-js/Report";
|
|
40
|
+
import { makeHtmlReporter } from "@systemfsoftware/stryker-js-html-reporter";
|
|
41
|
+
import { calculateMetrics } from "@systemfsoftware/stryker-js/Metrics";
|
|
42
|
+
import { MutationTestReportReady } from "@systemfsoftware/stryker-js/Reporter";
|
|
43
|
+
import * as FileSystem from "effect/FileSystem";
|
|
39
44
|
import * as Context from "effect/Context";
|
|
40
45
|
import * as Stdio from "effect/Stdio";
|
|
41
46
|
import * as Stream from "effect/Stream";
|
|
@@ -46,10 +51,6 @@ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
|
|
|
46
51
|
var engines = { "node": ">=20.0.0" };
|
|
47
52
|
//#endregion
|
|
48
53
|
//#region src/admit-survivors-run.workflow.ts
|
|
49
|
-
/**
|
|
50
|
-
* The mutant shape the admission carries, named once because both the decision's
|
|
51
|
-
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
52
|
-
*/
|
|
53
54
|
const MutantShape = S.Struct({
|
|
54
55
|
id: S.String,
|
|
55
56
|
fileName: S.String,
|
|
@@ -89,118 +90,38 @@ const PriorReportDocument = S.Struct({
|
|
|
89
90
|
}))
|
|
90
91
|
}))
|
|
91
92
|
});
|
|
92
|
-
/**
|
|
93
|
-
* U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
|
|
94
|
-
*
|
|
95
|
-
* The `--survivors` run re-tests exactly the mutants that survived a previous
|
|
96
|
-
* run. Its input is the previous run's mutation report, and the run is
|
|
97
|
-
* admitted only when a single structural hash of the resolved options, the
|
|
98
|
-
* recorded framework version, and the per-file source content all match the
|
|
99
|
-
* current run (KTD6).
|
|
100
|
-
*/
|
|
101
|
-
/**
|
|
102
|
-
* The decision's helpers reach three language built-ins the purity gate cannot
|
|
103
|
-
* resolve as globals, so each is bound at module scope.
|
|
104
|
-
*
|
|
105
|
-
* These live here, beside the decision, because `make-body-purity` follows the
|
|
106
|
-
* decision's reachable set: a helper `admissionVerdict` calls is checked as part
|
|
107
|
-
* of the body even though it is declared outside it. That is why the properties
|
|
108
|
-
* covering them are in this file's in-source block rather than beside a pure helper -
|
|
109
|
-
* testing a copy the decision does not run is worse than not testing it, because
|
|
110
|
-
* the suite goes green either way.
|
|
111
|
-
*/
|
|
112
93
|
const isArray = Array.isArray;
|
|
113
|
-
const { fromEntries: objectFromEntries$1, keys: objectKeys } = Object;
|
|
94
|
+
const { entries: objectEntries$1, fromEntries: objectFromEntries$1, keys: objectKeys } = Object;
|
|
114
95
|
const stringify = JSON.stringify;
|
|
115
|
-
function isRecord(value) {
|
|
116
|
-
return typeof value === "object" && value !== null && !isArray(value);
|
|
117
|
-
}
|
|
118
96
|
const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
|
|
119
97
|
const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
|
|
120
|
-
/**
|
|
121
|
-
* The resolved options without the survivors-run bookkeeping keys, so both
|
|
122
|
-
* sides of the admission comparison describe the same configuration.
|
|
123
|
-
*/
|
|
124
98
|
function stripSurvivorsKeys(config) {
|
|
125
|
-
|
|
126
|
-
const rest = { ...config };
|
|
127
|
-
for (const key of SURVIVORS_BOOKKEEPING_KEYS) delete rest[key];
|
|
128
|
-
return rest;
|
|
99
|
+
return objectFromEntries$1(objectEntries$1(config).filter(([key]) => !SURVIVORS_BOOKKEEPING_KEYS.includes(key)));
|
|
129
100
|
}
|
|
130
|
-
/**
|
|
131
|
-
* A report written by a survivors run embeds the bookkeeping key in its
|
|
132
|
-
* `config`. Such a report is never a valid input for another survivors run
|
|
133
|
-
* (KTD7): without this check the second run would either re-read a shrunken set
|
|
134
|
-
* or re-test a stale one.
|
|
135
|
-
*/
|
|
136
101
|
function wasProducedBySurvivorsRun(priorReport) {
|
|
137
|
-
|
|
138
|
-
return isRecord(config) && "survivorsPriorReport" in config;
|
|
102
|
+
return Option.exists(Option.liftPredicate(priorReport.config, Match.record), (config) => SURVIVORS_BOOKKEEPING_KEYS.some((bookkeeping) => bookkeeping in config));
|
|
139
103
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Serializes the comparison input with keys sorted at every level, so the result
|
|
142
|
-
* is a function of the data and not of key insertion order.
|
|
143
|
-
*/
|
|
144
104
|
function serializeSurvivorsHashInput(input) {
|
|
145
105
|
return stringify(sortKeys(input));
|
|
146
106
|
}
|
|
147
107
|
function sortKeys(value) {
|
|
148
|
-
|
|
149
|
-
if (isRecord(value)) return objectFromEntries$1(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
150
|
-
return value;
|
|
108
|
+
return Match.value(value).pipe(Match.when(isArray, (many) => many.map((member) => sortKeys(member))), Match.when(Match.record, (named) => objectFromEntries$1(objectKeys(named).sort().map((key) => [key, sortKeys(named[key])]))), Match.orElse((leaf) => leaf));
|
|
151
109
|
}
|
|
152
|
-
/**
|
|
153
|
-
* The prior report's facts the decision reads: its embedded configuration, which carries
|
|
154
|
-
* both the compared options and the survivors-run provenance marker, and the engine
|
|
155
|
-
* version it recorded. The report's files are not here — the survivors and the per-file
|
|
156
|
-
* source hashes derived from them need capabilities the command cannot hold, so they
|
|
157
|
-
* arrive already computed.
|
|
158
|
-
*/
|
|
159
110
|
var PriorReportFacts = class extends S.Class("PriorReportFacts")({
|
|
160
111
|
config: S.Record(S.String, S.Unknown),
|
|
161
112
|
frameworkVersion: S.UndefinedOr(S.String)
|
|
162
113
|
}) {};
|
|
163
|
-
/**
|
|
164
|
-
* The command of the admission workflow: a schema class, because `Workflow.make`
|
|
165
|
-
* constrains its first argument on the class value and a declared interface produces no
|
|
166
|
-
* value to pass. Every field is pure data — the two capabilities the previous shape
|
|
167
|
-
* carried, a digest function and a path resolver, can never be schema fields, so their
|
|
168
|
-
* results arrive precomputed from the decode phase instead.
|
|
169
|
-
*/
|
|
170
114
|
var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
|
|
171
|
-
/**
|
|
172
|
-
* The prior run's report facts, `undefined` when no report exists — the run cannot be
|
|
173
|
-
* admitted without one ('no-report'). Explicitly nullable rather than key-optional: a
|
|
174
|
-
* missing report is a state the edge determined and states, not a key it forgot.
|
|
175
|
-
*/
|
|
176
115
|
priorReport: S.UndefinedOr(PriorReportFacts),
|
|
177
|
-
/** The current run's resolved options (defaults + config file + CLI). */
|
|
178
116
|
currentConfig: S.Record(S.String, S.Unknown),
|
|
179
|
-
/** The current CLI/framework version (`strykerVersion`). */
|
|
180
117
|
frameworkVersion: S.String,
|
|
181
|
-
/**
|
|
182
|
-
* Per-file content hashes of the current source, keyed by the prior report's relative
|
|
183
|
-
* file keys. The prior side is hashed from the sources the report embeds, so an editor
|
|
184
|
-
* save that shifts line ranges — which would silently re-test a different mutant than
|
|
185
|
-
* the one that survived — is caught here.
|
|
186
|
-
*/
|
|
187
118
|
sourceContentHashes: S.Record(S.String, S.String),
|
|
188
|
-
/** The same hashes for the sources the prior report embeds, computed at the edge. */
|
|
189
119
|
priorSourceHashes: S.Record(S.String, S.String),
|
|
190
|
-
/** The prior report's survivors, already converted to the internal mutant shape. */
|
|
191
120
|
priorSurvivors: S.Array(MutantShape)
|
|
192
121
|
}) {};
|
|
193
122
|
const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
|
|
194
123
|
const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
|
|
195
124
|
const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
|
|
196
|
-
/**
|
|
197
|
-
* Whether the admission inputs agree: the prior report's embedded resolved options,
|
|
198
|
-
* framework version and source content against the current run's.
|
|
199
|
-
*
|
|
200
|
-
* The comparison is on the canonical serializations rather than digests of them. Equal
|
|
201
|
-
* serializations are equal runs, so the digest was a lossy restatement of the check that
|
|
202
|
-
* also demanded a capability no command can carry.
|
|
203
|
-
*/
|
|
204
125
|
function hashesMatch(priorReport, input) {
|
|
205
126
|
return serializeSurvivorsHashInput({
|
|
206
127
|
resolvedOptions: stripSurvivorsKeys(priorReport.config),
|
|
@@ -232,18 +153,46 @@ function reject(reason, detail) {
|
|
|
232
153
|
remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
|
|
233
154
|
}));
|
|
234
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The closed set of admissions a command can name, before any of them is turned
|
|
158
|
+
* into a decision. Deriving the variant first keeps the decision itself a total
|
|
159
|
+
* dispatch over a type rather than a fallback over predicates.
|
|
160
|
+
*/
|
|
161
|
+
const PriorReportAbsentOutcome = S.TaggedStruct("PriorReportAbsent", {});
|
|
162
|
+
const PriorReportIsSurvivorsRunOutcome = S.TaggedStruct("PriorReportIsSurvivorsRun", {});
|
|
163
|
+
const NoSurvivorsFoundOutcome = S.TaggedStruct("NoSurvivorsFound", {});
|
|
164
|
+
const PriorReportDriftedOutcome = S.TaggedStruct("PriorReportDrifted", {});
|
|
165
|
+
const SurvivorsMatchOutcome = S.TaggedStruct("SurvivorsMatch", { survivors: S.Array(MutantShape) });
|
|
166
|
+
S.Union([
|
|
167
|
+
PriorReportAbsentOutcome,
|
|
168
|
+
PriorReportIsSurvivorsRunOutcome,
|
|
169
|
+
NoSurvivorsFoundOutcome,
|
|
170
|
+
PriorReportDriftedOutcome,
|
|
171
|
+
SurvivorsMatchOutcome
|
|
172
|
+
]);
|
|
173
|
+
const ADMISSION_RULES = [
|
|
174
|
+
{
|
|
175
|
+
holds: (input) => input.priorReport === void 0,
|
|
176
|
+
outcome: PriorReportAbsentOutcome.make({})
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), wasProducedBySurvivorsRun),
|
|
180
|
+
outcome: PriorReportIsSurvivorsRunOutcome.make({})
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
holds: (input) => input.priorSurvivors.length === 0,
|
|
184
|
+
outcome: NoSurvivorsFoundOutcome.make({})
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), (facts) => !hashesMatch(facts, input)),
|
|
188
|
+
outcome: PriorReportDriftedOutcome.make({})
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
const admissionOutcomeOf = (input) => Option.getOrElse(Option.map(Arr.findFirst(ADMISSION_RULES, (rule) => rule.holds(input)), (rule) => rule.outcome), () => SurvivorsMatchOutcome.make({ survivors: input.priorSurvivors }));
|
|
235
192
|
function decideAdmission(input) {
|
|
236
|
-
|
|
237
|
-
if (priorReport === void 0) return reject("no-report", NO_REPORT_DETAIL);
|
|
238
|
-
if (wasProducedBySurvivorsRun(priorReport)) return reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
|
|
239
|
-
if (input.priorSurvivors.length === 0) return Result.succeed(NoSurvivors.make());
|
|
240
|
-
if (!hashesMatch(priorReport, input)) return reject("mismatch", MISMATCH_DETAIL);
|
|
241
|
-
return Result.succeed(Admitted.make({ survivors: input.priorSurvivors }));
|
|
242
|
-
}
|
|
243
|
-
function admissionDecision(command) {
|
|
244
|
-
return decideAdmission(command);
|
|
193
|
+
return Match.value(admissionOutcomeOf(input)).pipe(Match.tag("PriorReportAbsent", () => reject("no-report", NO_REPORT_DETAIL)), Match.tag("PriorReportIsSurvivorsRun", () => reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL)), Match.tag("NoSurvivorsFound", () => Result.succeed(NoSurvivors.make())), Match.tag("PriorReportDrifted", () => reject("mismatch", MISMATCH_DETAIL)), Match.tag("SurvivorsMatch", (matched) => Result.succeed(Admitted.make({ survivors: matched.survivors }))), Match.exhaustive);
|
|
245
194
|
}
|
|
246
|
-
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand,
|
|
195
|
+
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, decideAdmission);
|
|
247
196
|
//#endregion
|
|
248
197
|
//#region src/classify-run-outcome.workflow.ts
|
|
249
198
|
var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
|
|
@@ -288,42 +237,42 @@ var RunFailed$1 = class extends S.TaggedClass()("RunFailed", {
|
|
|
288
237
|
}) {
|
|
289
238
|
[RunOutcomeTypeId] = RunOutcomeTypeId;
|
|
290
239
|
};
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (command.helpErrorCount !== void 0) {
|
|
302
|
-
if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
|
|
303
|
-
return RunOk.make({ help: true });
|
|
304
|
-
}
|
|
305
|
-
if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
|
|
306
|
-
if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
|
|
307
|
-
reason: command.survivorsReason,
|
|
308
|
-
diagnostic: command.survivorsDiagnostic
|
|
309
|
-
});
|
|
310
|
-
if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
|
|
311
|
-
if (command.highestExitClass !== void 0) {
|
|
312
|
-
if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
|
|
313
|
-
return RunFailed$1.make({
|
|
314
|
-
code: classCode(command.highestExitClass),
|
|
315
|
-
diagnostic: command.diagnostic
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
return RunFailed$1.make({
|
|
319
|
-
code: 1,
|
|
240
|
+
const isSignaled = (command) => command.signal !== void 0;
|
|
241
|
+
const isSucceeded = (command) => command.succeeded;
|
|
242
|
+
const isHelpRun = (command) => command.helpErrorCount !== void 0;
|
|
243
|
+
const isSurvivorsRun = (command) => command.survivorsReason !== void 0;
|
|
244
|
+
const isHighestClassed = (command) => command.highestExitClass !== void 0;
|
|
245
|
+
const signaledOutcome = (command) => RunInterrupted.make({ code: 128 + command.signal });
|
|
246
|
+
const succeededOutcome = (command) => Option.match(Option.fromUndefinedOr(command.successExitClass), {
|
|
247
|
+
onNone: () => RunOk.make({ help: false }),
|
|
248
|
+
onSome: (exitClass) => RunFailed$1.make({
|
|
249
|
+
code: classCode(exitClass),
|
|
320
250
|
diagnostic: command.diagnostic
|
|
321
|
-
})
|
|
251
|
+
})
|
|
252
|
+
});
|
|
253
|
+
const helpOutcome = (command) => Match.value(command.helpErrorCount > 0).pipe(Match.when(true, () => RunParseFailed.make({ unrecognized: command.unrecognized })), Match.orElse(() => RunOk.make({ help: true })));
|
|
254
|
+
const parseFailedOutcome = (command) => RunParseFailed.make({ unrecognized: command.unrecognized });
|
|
255
|
+
const survivorsRejectedOutcome = (command) => RunSurvivorsRejected.make({
|
|
256
|
+
reason: command.survivorsReason,
|
|
257
|
+
diagnostic: command.survivorsDiagnostic
|
|
258
|
+
});
|
|
259
|
+
const configFailedOutcome = (command) => RunConfigFailed.make({ detail: command.configDetail });
|
|
260
|
+
const classedOutcome = (command) => Match.value(command.highestExitClass).pipe(Match.when("ConfigError", () => RunConfigFailed.make({ detail: command.configDetail })), Match.orElse((exitClass) => RunFailed$1.make({
|
|
261
|
+
code: classCode(exitClass),
|
|
262
|
+
diagnostic: command.diagnostic
|
|
263
|
+
})));
|
|
264
|
+
const genericFailureOutcome = (command) => RunFailed$1.make({
|
|
265
|
+
code: 1,
|
|
266
|
+
diagnostic: command.diagnostic
|
|
267
|
+
});
|
|
268
|
+
function classify(command) {
|
|
269
|
+
return Match.value(command).pipe(Match.when(isSignaled, signaledOutcome), Match.when(isSucceeded, succeededOutcome), Match.when((interrupted) => interrupted.interrupted, () => RunInterrupted.make({ code: 1 })), Match.when(isHelpRun, helpOutcome), Match.when((cliError) => cliError.cliError, parseFailedOutcome), Match.when(isSurvivorsRun, survivorsRejectedOutcome), Match.when((schemaError) => schemaError.schemaError, configFailedOutcome), Match.when(isHighestClassed, classedOutcome), Match.orElse(genericFailureOutcome));
|
|
322
270
|
}
|
|
323
271
|
const classifyRunOutcome$1 = Workflow.make(RunOutcomeCommand, (command) => Match.value(classify(command)).pipe(Match.tag("RunInterrupted", (error) => Result.fail(error)), Match.when((outcome) => !(outcome instanceof RunInterrupted), (decision) => Result.succeed(decision)), Match.exhaustive));
|
|
324
272
|
//#endregion
|
|
325
273
|
//#region src/Envelope.ts
|
|
326
274
|
const CONFIG_CODE = 2;
|
|
275
|
+
const UNKNOWN_FAILURE = "Unknown failure";
|
|
327
276
|
function runOutcomeCode(result) {
|
|
328
277
|
const outcome = Result.match(result, {
|
|
329
278
|
onSuccess: (success) => success,
|
|
@@ -334,170 +283,138 @@ function runOutcomeCode(result) {
|
|
|
334
283
|
function isExitClass(value) {
|
|
335
284
|
return S.is(ExitClass)(value);
|
|
336
285
|
}
|
|
286
|
+
const asExitClass = Option.liftPredicate(isExitClass);
|
|
337
287
|
function exitClassOf(value) {
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
288
|
+
if (!Predicate.isObjectOrArray(value)) return;
|
|
289
|
+
return Option.getOrUndefined(asExitClass(Reflect.get(value, "exitClass")));
|
|
290
|
+
}
|
|
291
|
+
const MAX_TRAVERSAL_DEPTH = 10;
|
|
292
|
+
function isUnseenObject(value, seen) {
|
|
293
|
+
return Predicate.isObjectOrArray(value) && !seen.has(value);
|
|
294
|
+
}
|
|
295
|
+
function isReachableValue(value, depth, seen) {
|
|
296
|
+
return depth <= MAX_TRAVERSAL_DEPTH && isUnseenObject(value, seen);
|
|
297
|
+
}
|
|
298
|
+
function isChildList(value) {
|
|
299
|
+
return Array.isArray(value);
|
|
300
|
+
}
|
|
301
|
+
function causeChildrenOf(value) {
|
|
302
|
+
const cause = Reflect.get(value, "cause");
|
|
303
|
+
return Match.value(cause).pipe(Match.when(isChildList, (children) => children), Match.orElse(() => [cause]));
|
|
304
|
+
}
|
|
305
|
+
function visitReachableValue(value, depth, seen, visit) {
|
|
306
|
+
if (isReachableValue(value, depth, seen)) {
|
|
307
|
+
seen.add(value);
|
|
308
|
+
visit(value);
|
|
309
|
+
causeChildrenOf(value).forEach((child) => visitReachableValue(child, depth + 1, seen, visit));
|
|
355
310
|
}
|
|
356
311
|
}
|
|
312
|
+
function findReachableValue(value, depth, seen, read) {
|
|
313
|
+
if (!isReachableValue(value, depth, seen)) return Option.none();
|
|
314
|
+
seen.add(value);
|
|
315
|
+
return Option.orElse(read(value), () => Arr.findFirst(causeChildrenOf(value), (child) => findReachableValue(child, depth + 1, seen, read)));
|
|
316
|
+
}
|
|
317
|
+
function causePayloadOf(reason) {
|
|
318
|
+
return Match.value(reason).pipe(Match.when(Cause.isFailReason, (failed) => failed.error), Match.when(Cause.isDieReason, (died) => died.defect), Match.orElse(() => void 0));
|
|
319
|
+
}
|
|
320
|
+
const failedExit = Option.liftPredicate(Exit.isFailure);
|
|
321
|
+
function failurePayloads(exit) {
|
|
322
|
+
return Option.getOrElse(Option.map(failedExit(exit), (failure) => failure.cause.reasons.map(causePayloadOf)), () => []);
|
|
323
|
+
}
|
|
324
|
+
function appendExitClass(value, out) {
|
|
325
|
+
const declared = exitClassOf(value);
|
|
326
|
+
if (declared !== void 0) out.push(declared);
|
|
327
|
+
}
|
|
357
328
|
function collectExitClasses(exit) {
|
|
358
329
|
const out = [];
|
|
359
330
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
360
|
-
|
|
361
|
-
let candidate;
|
|
362
|
-
if (Cause.isFailReason(reason)) candidate = reason.error;
|
|
363
|
-
else if (Cause.isDieReason(reason)) candidate = reason.defect;
|
|
364
|
-
else candidate = void 0;
|
|
365
|
-
if (candidate !== void 0) collectExitClassesFromValue(candidate, out, seen, 0);
|
|
366
|
-
}
|
|
331
|
+
failurePayloads(exit).forEach((payload) => visitReachableValue(payload, 0, seen, (value) => appendExitClass(value, out)));
|
|
367
332
|
return out;
|
|
368
333
|
}
|
|
334
|
+
function isNonEmptyString$1(value) {
|
|
335
|
+
return typeof value === "string" && value.length > 0;
|
|
336
|
+
}
|
|
337
|
+
const nonEmptyText = Option.liftPredicate(isNonEmptyString$1);
|
|
369
338
|
function reasonOf(value) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return `${reason}: ${detail}`;
|
|
339
|
+
const declared = Reflect.get(value, "reason");
|
|
340
|
+
return Option.getOrUndefined(Option.map(nonEmptyText(declared), (reason) => Option.match(Option.fromNullishOr(causeTextOf(value)), {
|
|
341
|
+
onNone: () => reason,
|
|
342
|
+
onSome: (detail) => `${reason}: ${detail}`
|
|
343
|
+
})));
|
|
376
344
|
}
|
|
377
|
-
function causeTextOf(value
|
|
378
|
-
if (depth > 4 || !("cause" in value)) return;
|
|
345
|
+
function causeTextOf(value) {
|
|
379
346
|
const cause = Reflect.get(value, "cause");
|
|
380
|
-
return causeText(cause,
|
|
347
|
+
return causeText(cause, 1);
|
|
381
348
|
}
|
|
382
|
-
function
|
|
383
|
-
if (!("reason" in value) && !("message" in value)) return;
|
|
349
|
+
function firstConfiguredText(value) {
|
|
384
350
|
const reason = Reflect.get(value, "reason");
|
|
385
|
-
if (typeof reason === "string" && reason.length > 0) return reason;
|
|
386
351
|
const message = Reflect.get(value, "message");
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
function
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
if (typeof value !== "object") return false;
|
|
393
|
-
if (seen.has(value)) return false;
|
|
394
|
-
return true;
|
|
395
|
-
}
|
|
396
|
-
function pushConfigCauses(value, depth, stack) {
|
|
397
|
-
if (!("cause" in value)) return;
|
|
398
|
-
const causeVal = Reflect.get(value, "cause");
|
|
399
|
-
if (Array.isArray(causeVal)) for (let index = causeVal.length - 1; index >= 0; index--) stack.push({
|
|
400
|
-
value: causeVal[index],
|
|
401
|
-
depth: depth + 1
|
|
402
|
-
});
|
|
403
|
-
else stack.push({
|
|
404
|
-
value: causeVal,
|
|
405
|
-
depth: depth + 1
|
|
406
|
-
});
|
|
352
|
+
return Option.orElse(nonEmptyText(reason), () => nonEmptyText(message));
|
|
353
|
+
}
|
|
354
|
+
function configDetailAt(value) {
|
|
355
|
+
if (exitClassOf(value) !== "ConfigError") return Option.none();
|
|
356
|
+
return firstConfiguredText(value);
|
|
407
357
|
}
|
|
408
358
|
function firstConfigErrorDetail(exit) {
|
|
409
|
-
if (!Exit.isFailure(exit)) return;
|
|
410
359
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
360
|
+
const roots = [...failurePayloads(exit)].reverse();
|
|
361
|
+
return Option.getOrUndefined(Arr.findFirst(roots, (root) => findReachableValue(root, 0, seen, configDetailAt)));
|
|
362
|
+
}
|
|
363
|
+
const PRIMITIVE_REFINEMENTS = [
|
|
364
|
+
Predicate.isString,
|
|
365
|
+
Predicate.isNumber,
|
|
366
|
+
Predicate.isBoolean,
|
|
367
|
+
Predicate.isBigInt,
|
|
368
|
+
Predicate.isSymbol
|
|
369
|
+
];
|
|
370
|
+
const isPrimitiveText = Predicate.some(PRIMITIVE_REFINEMENTS);
|
|
371
|
+
function isMessageError(value) {
|
|
372
|
+
return value instanceof Error && value.message.length > 0;
|
|
373
|
+
}
|
|
374
|
+
function declaresReasonText(value) {
|
|
375
|
+
return Predicate.isObjectOrArray(value) && Option.isSome(nonEmptyText(Reflect.get(value, "reason")));
|
|
376
|
+
}
|
|
377
|
+
function failureValueDescription(value) {
|
|
378
|
+
return Match.value(value).pipe(Match.when(S.is(SurvivorsRejection), (rejected) => Option.some(rejected.remediation)), Match.when(declaresReasonText, (carrier) => Option.fromNullishOr(reasonOf(carrier))), Match.when(isMessageError, (error) => Option.some(error.message)), Match.when(isPrimitiveText, (primitive) => Option.some(String(primitive))), Match.orElse(() => Option.none()));
|
|
379
|
+
}
|
|
380
|
+
function failureDescriptionOf(exit) {
|
|
381
|
+
return failureValueDescription(failureValue(exit)).pipe(Option.orElse(() => Option.fromNullishOr(firstConfigErrorDetail(exit))), Option.orElse(() => Option.flatMap(failedExit(exit), (failure) => nonEmptyText(Cause.pretty(failure.cause)))));
|
|
434
382
|
}
|
|
435
383
|
function describeFailure(exit) {
|
|
436
|
-
|
|
437
|
-
const value = failureValue(exit);
|
|
438
|
-
if (value !== void 0) {
|
|
439
|
-
if (S.is(SurvivorsRejection)(value)) return value.remediation;
|
|
440
|
-
if (typeof value === "object" && value !== null) {
|
|
441
|
-
const reason = reasonOf(value);
|
|
442
|
-
if (reason !== void 0) return reason;
|
|
443
|
-
}
|
|
444
|
-
if (value instanceof Error && value.message.length > 0) return value.message;
|
|
445
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
|
|
446
|
-
}
|
|
447
|
-
if (collectExitClasses(exit).includes("ConfigError")) {
|
|
448
|
-
const detail = firstConfigErrorDetail(exit);
|
|
449
|
-
if (detail !== void 0) return detail;
|
|
450
|
-
}
|
|
451
|
-
const rendered = Cause.pretty(exit.cause);
|
|
452
|
-
if (rendered.length > 0) return rendered;
|
|
453
|
-
return "Unknown failure";
|
|
384
|
+
return Option.getOrElse(failureDescriptionOf(exit), () => UNKNOWN_FAILURE);
|
|
454
385
|
}
|
|
455
386
|
function unrecognizedArgumentOf(exit, argv) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (next !== void 0 && !next.startsWith("-")) return next;
|
|
470
|
-
return error.option;
|
|
471
|
-
}
|
|
472
|
-
if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
|
|
473
|
-
if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
|
|
474
|
-
}
|
|
387
|
+
return Option.getOrUndefined(cliErrorList(exit).pipe(Option.flatMap((errors) => Arr.findFirst(errors, (error) => argumentHintOf(error, argv)))));
|
|
388
|
+
}
|
|
389
|
+
function cliErrorList(exit) {
|
|
390
|
+
return Match.value(failureValue(exit)).pipe(Match.when(S.is(CliError.ShowHelp), (help) => Option.some(help.errors)), Match.when(CliError.isCliError, (cliError) => Option.some([cliError])), Match.orElse(() => Option.none()));
|
|
391
|
+
}
|
|
392
|
+
function argumentHintOf(error, argv) {
|
|
393
|
+
return Match.value(error).pipe(Match.tag("UnrecognizedOption", (unrecognized) => Option.some(unrecognizedArgument(argv, unrecognized.option))), Match.tag("UnexpectedArgument", (unexpected) => Option.fromNullishOr(unexpected.arguments[0])), Match.tag("UnknownSubcommand", (unknown) => Option.some(unknown.subcommand)), Match.orElse(() => Option.none()));
|
|
394
|
+
}
|
|
395
|
+
function unrecognizedArgument(argv, option) {
|
|
396
|
+
return followingArgument(argv, option).pipe(Option.filter((argument) => !argument.startsWith("-")), Option.getOrElse(() => option));
|
|
397
|
+
}
|
|
398
|
+
function followingArgument(argv, option) {
|
|
399
|
+
return Match.value(argv.indexOf(option)).pipe(Match.when((at) => at >= 0, (at) => Option.fromNullishOr(argv[at + 1])), Match.orElse(() => Option.none()));
|
|
475
400
|
}
|
|
476
401
|
function failureValue(exit) {
|
|
477
|
-
|
|
478
|
-
const failure = Cause.findErrorOption(exit.cause);
|
|
479
|
-
if (Option.isSome(failure)) return failure.value;
|
|
402
|
+
return Option.getOrUndefined(Option.flatMap(failedExit(exit), (failure) => Cause.findErrorOption(failure.cause)));
|
|
480
403
|
}
|
|
481
404
|
const SIGNAL_REMEDIATION = "the run was interrupted by a signal; re-run it to continue";
|
|
482
405
|
const PARSE_REMEDIATION = "re-run with --help to see the full usage";
|
|
483
406
|
const DEFAULT_REMEDIATION = "see --reportFile or the verdict envelope on stdout";
|
|
407
|
+
function verdictExitClass(value) {
|
|
408
|
+
return Match.value(value).pipe(Match.when(Predicate.hasProperty("verdict"), (carrier) => Option.getOrUndefined(asExitClass(carrier.verdict))), Match.orElse(() => void 0));
|
|
409
|
+
}
|
|
484
410
|
function successExitClassOf(exit) {
|
|
485
|
-
|
|
486
|
-
const value = exit.value;
|
|
487
|
-
if (!Predicate.hasProperty(value, "verdict")) return;
|
|
488
|
-
const candidate = value.verdict;
|
|
489
|
-
if (!isExitClass(candidate)) return;
|
|
490
|
-
return candidate;
|
|
411
|
+
return Match.value(exit).pipe(Match.when(Exit.isSuccess, (success) => verdictExitClass(success.value)), Match.orElse(() => void 0));
|
|
491
412
|
}
|
|
492
413
|
function helpErrorCountOf(value) {
|
|
493
|
-
|
|
494
|
-
if (!S.is(CliError.ShowHelp)(value)) return;
|
|
495
|
-
return value.errors.length;
|
|
414
|
+
return Match.value(value).pipe(Match.when(S.is(CliError.ShowHelp), (help) => help.errors.length), Match.orElse(() => void 0));
|
|
496
415
|
}
|
|
497
416
|
function survivorsRejectionOf(value) {
|
|
498
|
-
|
|
499
|
-
if (!S.is(SurvivorsRejection)(value)) return;
|
|
500
|
-
return value;
|
|
417
|
+
return Match.value(value).pipe(Match.when(S.is(SurvivorsRejection), (survivors) => survivors), Match.orElse(() => void 0));
|
|
501
418
|
}
|
|
502
419
|
function present(value) {
|
|
503
420
|
if (value === null) return;
|
|
@@ -512,12 +429,20 @@ function survivorsDiagnosticOf(survivors) {
|
|
|
512
429
|
return survivors.remediation;
|
|
513
430
|
}
|
|
514
431
|
function omitUnknownFailure(diagnostic) {
|
|
515
|
-
if (diagnostic ===
|
|
432
|
+
if (diagnostic === UNKNOWN_FAILURE) return;
|
|
516
433
|
return diagnostic;
|
|
517
434
|
}
|
|
518
435
|
function capturedOrUnknown(captured) {
|
|
519
|
-
|
|
520
|
-
|
|
436
|
+
return Option.getOrElse(nonEmptyText(captured), () => UNKNOWN_FAILURE);
|
|
437
|
+
}
|
|
438
|
+
function hasOnlyInterrupts(exit) {
|
|
439
|
+
return Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
|
|
440
|
+
}
|
|
441
|
+
function carriesCliError(value) {
|
|
442
|
+
return value !== void 0 && CliError.isCliError(value);
|
|
443
|
+
}
|
|
444
|
+
function carriesSchemaError(value) {
|
|
445
|
+
return value !== void 0 && S.isSchemaError(value);
|
|
521
446
|
}
|
|
522
447
|
function gatherRunOutcome(exit, signal, argv) {
|
|
523
448
|
const value = failureValue(exit);
|
|
@@ -525,35 +450,24 @@ function gatherRunOutcome(exit, signal, argv) {
|
|
|
525
450
|
return RunOutcomeCommand.make({
|
|
526
451
|
succeeded: Exit.isSuccess(exit),
|
|
527
452
|
signal: present(signal),
|
|
528
|
-
interrupted:
|
|
453
|
+
interrupted: hasOnlyInterrupts(exit),
|
|
529
454
|
helpErrorCount: helpErrorCountOf(value),
|
|
530
|
-
cliError:
|
|
455
|
+
cliError: carriesCliError(value),
|
|
531
456
|
unrecognized: unrecognizedArgumentOf(exit, argv),
|
|
532
457
|
survivorsReason: survivorsReasonOf(survivors),
|
|
533
458
|
survivorsDiagnostic: survivorsDiagnosticOf(survivors),
|
|
534
|
-
schemaError:
|
|
459
|
+
schemaError: carriesSchemaError(value),
|
|
535
460
|
successExitClass: successExitClassOf(exit),
|
|
536
461
|
highestExitClass: present(highestExitClass(collectExitClasses(exit))),
|
|
537
462
|
configDetail: firstConfigErrorDetail(exit),
|
|
538
463
|
diagnostic: omitUnknownFailure(describeFailure(exit))
|
|
539
464
|
});
|
|
540
465
|
}
|
|
466
|
+
function capturedThenRecorded(captured, recorded) {
|
|
467
|
+
return Option.getOrElse(nonEmptyText(captured), () => Option.getOrElse(Option.fromNullishOr(recorded), () => UNKNOWN_FAILURE));
|
|
468
|
+
}
|
|
541
469
|
function errorText(error, captured) {
|
|
542
|
-
return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => {
|
|
543
|
-
if (failed.unrecognized !== void 0) return `Received unknown argument: '${failed.unrecognized}'`;
|
|
544
|
-
return capturedOrUnknown(captured);
|
|
545
|
-
}), Match.tag("RunSurvivorsRejected", (failed) => {
|
|
546
|
-
if (failed.diagnostic !== void 0) return failed.diagnostic;
|
|
547
|
-
return "Unknown failure";
|
|
548
|
-
}), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => {
|
|
549
|
-
if (captured.length > 0) return captured;
|
|
550
|
-
if (failed.detail !== void 0) return failed.detail;
|
|
551
|
-
return "Unknown failure";
|
|
552
|
-
}), Match.tag("RunFailed", (failed) => {
|
|
553
|
-
if (captured.length > 0) return captured;
|
|
554
|
-
if (failed.diagnostic !== void 0) return failed.diagnostic;
|
|
555
|
-
return "Unknown failure";
|
|
556
|
-
}), Match.exhaustive);
|
|
470
|
+
return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => Option.getOrElse(Option.map(Option.fromNullishOr(failed.unrecognized), (value) => `Received unknown argument: '${value}'`), () => capturedOrUnknown(captured))), Match.tag("RunSurvivorsRejected", (failed) => Option.getOrElse(Option.fromNullishOr(failed.diagnostic), () => UNKNOWN_FAILURE)), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => capturedThenRecorded(captured, failed.detail)), Match.tag("RunFailed", (failed) => capturedThenRecorded(captured, failed.diagnostic)), Match.exhaustive);
|
|
557
471
|
}
|
|
558
472
|
function remediationText(error) {
|
|
559
473
|
return Match.value(error).pipe(Match.tag("RunInterrupted", (failed) => {
|
|
@@ -585,35 +499,52 @@ function inspectValue(value) {
|
|
|
585
499
|
if (typeof value === "string") return value;
|
|
586
500
|
return Formatter.format(value);
|
|
587
501
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (index >= args.length) return match;
|
|
596
|
-
const arg = args[index++];
|
|
597
|
-
switch (match) {
|
|
598
|
-
case "%s": return String(arg);
|
|
599
|
-
case "%d":
|
|
600
|
-
case "%i":
|
|
601
|
-
case "%f": return Number(arg).toString();
|
|
602
|
-
case "%j": try {
|
|
603
|
-
return String(JSON.stringify(arg));
|
|
604
|
-
} catch {
|
|
605
|
-
return "[Circular]";
|
|
606
|
-
}
|
|
607
|
-
case "%o":
|
|
608
|
-
case "%O":
|
|
609
|
-
case "%p": return inspectValue(arg);
|
|
610
|
-
default: return match;
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
for (; index < args.length; index++) result += ` ${inspectValue(args[index])}`;
|
|
614
|
-
return result;
|
|
502
|
+
const PLACEHOLDER = /(%[sdijfopO%])/g;
|
|
503
|
+
const PLACEHOLDER_PART = /^%[sdijfopO%]$/;
|
|
504
|
+
function jsonArgumentText(argument) {
|
|
505
|
+
try {
|
|
506
|
+
return String(JSON.stringify(argument));
|
|
507
|
+
} catch {
|
|
508
|
+
return "[Circular]";
|
|
615
509
|
}
|
|
616
|
-
|
|
510
|
+
}
|
|
511
|
+
function placeholderText(placeholder, argument) {
|
|
512
|
+
return Match.value(placeholder).pipe(Match.when(Match.is("%s"), () => String(argument)), Match.when(Match.is("%d", "%i", "%f"), () => Number(argument).toString()), Match.when(Match.is("%j"), () => jsonArgumentText(argument)), Match.when(Match.is("%o", "%O", "%p"), () => inspectValue(argument)), Match.orElse((unmatched) => unmatched));
|
|
513
|
+
}
|
|
514
|
+
function appendLiteral(progress, literal) {
|
|
515
|
+
return {
|
|
516
|
+
args: progress.args,
|
|
517
|
+
cursor: progress.cursor,
|
|
518
|
+
text: progress.text + literal
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function substitutePlaceholder(progress, placeholder) {
|
|
522
|
+
return Match.value(progress.cursor).pipe(Match.when((cursor) => cursor < progress.args.length, (cursor) => ({
|
|
523
|
+
args: progress.args,
|
|
524
|
+
cursor: cursor + 1,
|
|
525
|
+
text: progress.text + placeholderText(placeholder, progress.args[cursor])
|
|
526
|
+
})), Match.orElse(() => appendLiteral(progress, placeholder)));
|
|
527
|
+
}
|
|
528
|
+
function consumePlaceholder(progress, placeholder) {
|
|
529
|
+
return Match.value(placeholder).pipe(Match.when(Match.is("%%"), () => appendLiteral(progress, "%")), Match.orElse(() => substitutePlaceholder(progress, placeholder)));
|
|
530
|
+
}
|
|
531
|
+
function formatStep(progress, part) {
|
|
532
|
+
return Match.value(part).pipe(Match.when((candidate) => PLACEHOLDER_PART.test(candidate), (placeholder) => consumePlaceholder(progress, placeholder)), Match.orElse((literal) => appendLiteral(progress, literal)));
|
|
533
|
+
}
|
|
534
|
+
function formatTemplate(template, args) {
|
|
535
|
+
const progress = template.split(PLACEHOLDER).reduce(formatStep, {
|
|
536
|
+
args,
|
|
537
|
+
cursor: 0,
|
|
538
|
+
text: ""
|
|
539
|
+
});
|
|
540
|
+
const trailing = progress.args.slice(progress.cursor).map((argument) => ` ${inspectValue(argument)}`);
|
|
541
|
+
return progress.text + trailing.join("");
|
|
542
|
+
}
|
|
543
|
+
function isTextTemplate(args) {
|
|
544
|
+
return typeof args[0] === "string";
|
|
545
|
+
}
|
|
546
|
+
function formatArgs(args) {
|
|
547
|
+
return Match.value(args).pipe(Match.when(isTextTemplate, (values) => formatTemplate(values[0], values.slice(1))), Match.orElse((values) => values.map(inspectValue).join(" ")));
|
|
617
548
|
}
|
|
618
549
|
function captureSync(args) {
|
|
619
550
|
capturedConsoleChunks.push(formatArgs(args));
|
|
@@ -621,21 +552,39 @@ function captureSync(args) {
|
|
|
621
552
|
function captureAssert(condition, args) {
|
|
622
553
|
if (!condition) capturedConsoleChunks.push(`Assertion failed: ${formatArgs(args)}`);
|
|
623
554
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
555
|
+
const DEFAULT_CONSOLE_LABEL = "default";
|
|
556
|
+
function consoleLabel(label) {
|
|
557
|
+
return Option.getOrElse(Option.fromNullishOr(label), () => DEFAULT_CONSOLE_LABEL);
|
|
558
|
+
}
|
|
559
|
+
function incrementCount(key) {
|
|
560
|
+
const next = Option.getOrElse(Option.fromNullishOr(countByLabel.get(key)), () => 0) + 1;
|
|
627
561
|
countByLabel.set(key, next);
|
|
562
|
+
return next;
|
|
563
|
+
}
|
|
564
|
+
function captureCount(label) {
|
|
565
|
+
const key = consoleLabel(label);
|
|
566
|
+
const next = incrementCount(key);
|
|
628
567
|
capturedConsoleChunks.push(`${key}: ${next}`);
|
|
629
568
|
}
|
|
569
|
+
function elapsedMs(started, nowNanos) {
|
|
570
|
+
return Number(nowNanos - started) / 1e6;
|
|
571
|
+
}
|
|
630
572
|
function captureTimeEnd(label, nowNanos) {
|
|
631
|
-
const key = label
|
|
573
|
+
const key = consoleLabel(label);
|
|
632
574
|
const started = timeByLabel.get(key);
|
|
633
575
|
if (started !== void 0) {
|
|
634
576
|
timeByLabel.delete(key);
|
|
635
|
-
|
|
636
|
-
capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
|
|
577
|
+
capturedConsoleChunks.push(`${key}: ${elapsedMs(started, nowNanos)}ms`);
|
|
637
578
|
}
|
|
638
579
|
}
|
|
580
|
+
function captureTimeLog(label, args, clock) {
|
|
581
|
+
const key = consoleLabel(label);
|
|
582
|
+
const started = timeByLabel.get(key);
|
|
583
|
+
if (started === void 0) return;
|
|
584
|
+
const elapsed = elapsedMs(started, clock.monotonicTimeNanosUnsafe());
|
|
585
|
+
const elapsedText = Match.value(args).pipe(Match.when((values) => values.length === 0, () => `${key}: ${elapsed}ms`), Match.orElse((values) => `${key}: ${elapsed}ms ${formatArgs(values)}`));
|
|
586
|
+
capturedConsoleChunks.push(elapsedText);
|
|
587
|
+
}
|
|
639
588
|
function captureTrace(args) {
|
|
640
589
|
capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
|
|
641
590
|
}
|
|
@@ -643,7 +592,7 @@ const makeCapturingConsole = (clock) => ({
|
|
|
643
592
|
assert: (condition, ...args) => captureAssert(condition, args),
|
|
644
593
|
clear: () => {},
|
|
645
594
|
count: (label) => captureCount(label),
|
|
646
|
-
countReset: (label) => countByLabel.delete(label
|
|
595
|
+
countReset: (label) => countByLabel.delete(consoleLabel(label)),
|
|
647
596
|
debug: (...args) => captureSync(args),
|
|
648
597
|
dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
649
598
|
dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
|
|
@@ -654,19 +603,9 @@ const makeCapturingConsole = (clock) => ({
|
|
|
654
603
|
info: (...args) => captureSync(args),
|
|
655
604
|
log: (...args) => captureSync(args),
|
|
656
605
|
table: (tabularData) => capturedConsoleChunks.push(Formatter.format(tabularData)),
|
|
657
|
-
time: (label) => timeByLabel.set(label
|
|
606
|
+
time: (label) => timeByLabel.set(consoleLabel(label), clock.monotonicTimeNanosUnsafe()),
|
|
658
607
|
timeEnd: (label) => captureTimeEnd(label, clock.monotonicTimeNanosUnsafe()),
|
|
659
|
-
timeLog: (label, ...args) =>
|
|
660
|
-
const key = label ?? "default";
|
|
661
|
-
const started = timeByLabel.get(key);
|
|
662
|
-
if (started === void 0) return;
|
|
663
|
-
const diffMs = Number(clock.monotonicTimeNanosUnsafe() - started) / 1e6;
|
|
664
|
-
if (args.length === 0) {
|
|
665
|
-
capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
capturedConsoleChunks.push(`${key}: ${diffMs}ms ${formatArgs(args)}`);
|
|
669
|
-
},
|
|
608
|
+
timeLog: (label, ...args) => captureTimeLog(label, args, clock),
|
|
670
609
|
trace: (...args) => captureTrace(args),
|
|
671
610
|
warn: (...args) => captureSync(args)
|
|
672
611
|
});
|
|
@@ -683,6 +622,506 @@ function resetCapturedConsole() {
|
|
|
683
622
|
timeByLabel.clear();
|
|
684
623
|
}
|
|
685
624
|
//#endregion
|
|
625
|
+
//#region src/merge-reports.schema.ts
|
|
626
|
+
var MergeReportsFailed = class extends S.TaggedError()("MergeReportsFailed", { reason: S.String }) {};
|
|
627
|
+
const PartMetaSchema = S.Struct({
|
|
628
|
+
package: S.String,
|
|
629
|
+
outcome: S.String
|
|
630
|
+
});
|
|
631
|
+
const StreamMutantLineSchema = S.Struct({
|
|
632
|
+
kind: S.Literal("mutant"),
|
|
633
|
+
id: S.String,
|
|
634
|
+
status: MutantStatusSchema,
|
|
635
|
+
file: S.String,
|
|
636
|
+
location: LocationSchema,
|
|
637
|
+
mutator: S.String,
|
|
638
|
+
replacement: S.NullOr(S.String)
|
|
639
|
+
});
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/merge-report-parts.workflow.ts
|
|
642
|
+
const MERGED_SCHEMA_VERSION = "1.7";
|
|
643
|
+
const DEFAULT_THRESHOLDS = {
|
|
644
|
+
high: 80,
|
|
645
|
+
low: 60
|
|
646
|
+
};
|
|
647
|
+
const FAILING_OUTCOME = "failure";
|
|
648
|
+
const PASSING_OUTCOME = "success";
|
|
649
|
+
const PERFECT_MUTATION_SCORE = 100;
|
|
650
|
+
const SURVIVOR_STATUSES = {
|
|
651
|
+
Survived: true,
|
|
652
|
+
NoCoverage: true
|
|
653
|
+
};
|
|
654
|
+
const SCORE_INCOMPLETE = "incomplete";
|
|
655
|
+
const SCORE_ABSENT = "no report";
|
|
656
|
+
const SCORE_UNDEFINED = "n/a";
|
|
657
|
+
const SCORE_PERFECT = "100.00";
|
|
658
|
+
const VERDICT_OK = "✅";
|
|
659
|
+
const VERDICT_FAIL = "❌";
|
|
660
|
+
const VERDICT_WARN = "⚠️";
|
|
661
|
+
const ABSENT_CELLS = [
|
|
662
|
+
"—",
|
|
663
|
+
"—",
|
|
664
|
+
"—",
|
|
665
|
+
"—",
|
|
666
|
+
"—"
|
|
667
|
+
];
|
|
668
|
+
const ALL_PACKAGES_LABEL$1 = "**all**";
|
|
669
|
+
const SEGMENT_SEPARATORS = /[/\\]/;
|
|
670
|
+
const EMPTY_SEGMENTS = [];
|
|
671
|
+
const EMPTY_PACKAGES = [];
|
|
672
|
+
const EMPTY_TEST_FILES = {};
|
|
673
|
+
const ReportPart = S.Struct({
|
|
674
|
+
label: S.String,
|
|
675
|
+
outcome: S.String,
|
|
676
|
+
incomplete: S.Boolean,
|
|
677
|
+
report: S.optional(MutationTestResultSchema)
|
|
678
|
+
});
|
|
679
|
+
const MergeVerdictRow = S.Struct({
|
|
680
|
+
label: S.String,
|
|
681
|
+
score: S.String,
|
|
682
|
+
cells: S.Array(S.String),
|
|
683
|
+
verdict: S.String
|
|
684
|
+
});
|
|
685
|
+
const MergeSurvivor = S.Struct({
|
|
686
|
+
file: S.String,
|
|
687
|
+
line: S.Finite,
|
|
688
|
+
column: S.Finite,
|
|
689
|
+
status: S.String,
|
|
690
|
+
mutatorName: S.String,
|
|
691
|
+
replacement: S.String
|
|
692
|
+
});
|
|
693
|
+
var MergeReportPartsCommand = class extends S.Class("MergeReportPartsCommand")({
|
|
694
|
+
parts: S.Array(ReportPart),
|
|
695
|
+
expectedPackages: S.optional(S.Array(S.String))
|
|
696
|
+
}) {};
|
|
697
|
+
const MergeReportPartsTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/MergeReportParts");
|
|
698
|
+
var MergedReports = class extends S.TaggedClass()("MergedReports", {
|
|
699
|
+
report: MutationTestResultSchema,
|
|
700
|
+
rows: S.Array(MergeVerdictRow),
|
|
701
|
+
survivors: S.Array(MergeSurvivor)
|
|
702
|
+
}) {
|
|
703
|
+
[MergeReportPartsTypeId] = MergeReportPartsTypeId;
|
|
704
|
+
};
|
|
705
|
+
var NoMergedReports = class extends S.TaggedClass()("NoMergedReports", { rows: S.Array(MergeVerdictRow) }) {
|
|
706
|
+
[MergeReportPartsTypeId] = MergeReportPartsTypeId;
|
|
707
|
+
};
|
|
708
|
+
var DuplicatePackageLabel = class extends S.TaggedError()("DuplicatePackageLabel", { label: S.String }) {};
|
|
709
|
+
var MissingPackages = class extends S.TaggedError()("MissingPackages", { packages: S.Array(S.String) }) {};
|
|
710
|
+
/** The value a present option carries, or the fallback a missing one takes. */
|
|
711
|
+
const orDefault = (present, fallback) => Option.getOrElse(present, () => fallback);
|
|
712
|
+
const thresholdsOf = (part) => orDefault(Option.map(Option.fromUndefinedOr(part), (present) => present.report.thresholds), DEFAULT_THRESHOLDS);
|
|
713
|
+
const hasReport = (part) => part.report !== void 0;
|
|
714
|
+
const scoreFor = (part) => Option.getOrUndefined(Option.map(Option.filter(Option.fromUndefinedOr(part), hasReport), (present) => scoreOf(present.report.files)));
|
|
715
|
+
const outcomeOf = (reports) => Match.value(reports.some((part) => part.outcome === FAILING_OUTCOME)).pipe(Match.when(true, () => FAILING_OUTCOME), Match.when(false, () => PASSING_OUTCOME), Match.exhaustive);
|
|
716
|
+
const normalizeName = (fileName) => fileName.split(SEGMENT_SEPARATORS).filter(Boolean).join("/");
|
|
717
|
+
const commonBasePath = (fileNames) => {
|
|
718
|
+
const directories = fileNames.map((fileName) => fileName.split(SEGMENT_SEPARATORS).slice(0, -1));
|
|
719
|
+
return Arr.reduce(directories, orDefault(Arr.head(directories), EMPTY_SEGMENTS), (left, right) => Arr.takeWhile(left, (segment, index) => right[index] === segment)).join("/");
|
|
720
|
+
};
|
|
721
|
+
const normalizedNames = (input) => {
|
|
722
|
+
const base = commonBasePath(Object.keys(input));
|
|
723
|
+
return Object.fromEntries(Object.entries(input).map(([fileName, value]) => [normalizeName(fileName.slice(base.length)), value]));
|
|
724
|
+
};
|
|
725
|
+
const uniqueId = (label, id) => `${label}_${id}`;
|
|
726
|
+
const uniqueIds = (label, ids) => Option.getOrUndefined(Option.map(Option.fromUndefinedOr(ids), (present) => present.map((id) => uniqueId(label, id))));
|
|
727
|
+
const rewrittenMutant = (label, mutant) => ({
|
|
728
|
+
...mutant,
|
|
729
|
+
id: uniqueId(label, mutant.id),
|
|
730
|
+
killedBy: uniqueIds(label, mutant.killedBy),
|
|
731
|
+
coveredBy: uniqueIds(label, mutant.coveredBy)
|
|
732
|
+
});
|
|
733
|
+
const withProjectRoot = (report, projectRoot) => Option.match(Option.fromUndefinedOr(projectRoot), {
|
|
734
|
+
onNone: () => report,
|
|
735
|
+
onSome: (root) => ({
|
|
736
|
+
...report,
|
|
737
|
+
projectRoot: root
|
|
738
|
+
})
|
|
739
|
+
});
|
|
740
|
+
const mergedFiles = (parts) => Object.fromEntries(parts.flatMap((part) => Object.entries(normalizedNames(part.report.files)).map(([fileName, file]) => [`${part.label}/${fileName}`, {
|
|
741
|
+
...file,
|
|
742
|
+
mutants: file.mutants.map((mutant) => rewrittenMutant(part.label, mutant))
|
|
743
|
+
}])));
|
|
744
|
+
const mergedTestFiles = (parts) => Object.fromEntries(parts.flatMap((part) => Object.entries(normalizedNames(orDefault(Option.fromUndefinedOr(part.report.testFiles), EMPTY_TEST_FILES))).map(([fileName, testFile]) => [`${part.label}/${fileName}`, {
|
|
745
|
+
...testFile,
|
|
746
|
+
tests: testFile.tests.map((test) => ({
|
|
747
|
+
...test,
|
|
748
|
+
id: uniqueId(part.label, test.id)
|
|
749
|
+
}))
|
|
750
|
+
}])));
|
|
751
|
+
const withTestFiles = (report, testFiles) => Match.value(Object.keys(testFiles).length > 0).pipe(Match.when(true, () => ({
|
|
752
|
+
...report,
|
|
753
|
+
testFiles
|
|
754
|
+
})), Match.when(false, () => report), Match.exhaustive);
|
|
755
|
+
const mergeParts = (parts) => {
|
|
756
|
+
const merged = {
|
|
757
|
+
files: mergedFiles(parts),
|
|
758
|
+
schemaVersion: MERGED_SCHEMA_VERSION,
|
|
759
|
+
thresholds: thresholdsOf(parts[0]),
|
|
760
|
+
config: {}
|
|
761
|
+
};
|
|
762
|
+
const projectRoots = parts.flatMap((part) => Option.toArray(Option.fromUndefinedOr(part.report.projectRoot)));
|
|
763
|
+
return withProjectRoot(withTestFiles(merged, mergedTestFiles(parts)), commonProjectRoot(projectRoots));
|
|
764
|
+
};
|
|
765
|
+
const commonProjectRoot = (roots) => Option.getOrUndefined(Option.map(Option.liftPredicate(roots, Arr.isReadonlyArrayNonEmpty), commonBasePath));
|
|
766
|
+
const mutationScoreOf = (totalDetected, totalValid) => Match.value(totalValid === 0).pipe(Match.when(true, () => NaN), Match.when(false, () => totalDetected / totalValid * 100), Match.exhaustive);
|
|
767
|
+
const scoreOf = (files) => {
|
|
768
|
+
const mutants = Object.values(files).flatMap((file) => file.mutants);
|
|
769
|
+
const count = (status) => mutants.filter((mutant) => mutant.status === status).length;
|
|
770
|
+
const killed = count("Killed");
|
|
771
|
+
const timeout = count("Timeout");
|
|
772
|
+
const survived = count("Survived");
|
|
773
|
+
const noCoverage = count("NoCoverage");
|
|
774
|
+
const compileErrors = count("CompileError");
|
|
775
|
+
const totalDetected = timeout + killed;
|
|
776
|
+
const cells = [
|
|
777
|
+
String(killed),
|
|
778
|
+
String(survived),
|
|
779
|
+
String(noCoverage),
|
|
780
|
+
String(timeout),
|
|
781
|
+
String(compileErrors)
|
|
782
|
+
];
|
|
783
|
+
return {
|
|
784
|
+
mutationScore: mutationScoreOf(totalDetected, survived + noCoverage + totalDetected),
|
|
785
|
+
cells
|
|
786
|
+
};
|
|
787
|
+
};
|
|
788
|
+
const cellsFor = (score) => orDefault(Option.map(Option.fromUndefinedOr(score), (present) => present.cells), ABSENT_CELLS);
|
|
789
|
+
const incompleteVerdict = (outcome) => Match.value(outcome === PASSING_OUTCOME).pipe(Match.when(true, () => VERDICT_WARN), Match.when(false, () => VERDICT_FAIL), Match.exhaustive);
|
|
790
|
+
const passingVerdict = (score, outcome) => Match.value({
|
|
791
|
+
perfect: score.mutationScore === PERFECT_MUTATION_SCORE,
|
|
792
|
+
passing: outcome === PASSING_OUTCOME
|
|
793
|
+
}).pipe(Match.when({
|
|
794
|
+
perfect: true,
|
|
795
|
+
passing: true
|
|
796
|
+
}, () => VERDICT_OK), Match.orElse(() => VERDICT_FAIL));
|
|
797
|
+
const renderedScore = (mutationScore) => Match.value(mutationScore).pipe(Match.when(PERFECT_MUTATION_SCORE, () => SCORE_PERFECT), Match.orElse(() => mutationScore.toFixed(2)));
|
|
798
|
+
const scoredRow = (label, score, outcome) => Match.value(Number.isNaN(score.mutationScore)).pipe(Match.when(true, () => ({
|
|
799
|
+
label,
|
|
800
|
+
score: SCORE_UNDEFINED,
|
|
801
|
+
cells: score.cells,
|
|
802
|
+
verdict: VERDICT_WARN
|
|
803
|
+
})), Match.when(false, () => ({
|
|
804
|
+
label,
|
|
805
|
+
score: renderedScore(score.mutationScore),
|
|
806
|
+
cells: score.cells,
|
|
807
|
+
verdict: passingVerdict(score, outcome)
|
|
808
|
+
})), Match.exhaustive);
|
|
809
|
+
const completeRow = (label, score, outcome) => Option.match(Option.fromUndefinedOr(score), {
|
|
810
|
+
onNone: () => ({
|
|
811
|
+
label,
|
|
812
|
+
score: SCORE_ABSENT,
|
|
813
|
+
cells: ABSENT_CELLS,
|
|
814
|
+
verdict: VERDICT_WARN
|
|
815
|
+
}),
|
|
816
|
+
onSome: (present) => scoredRow(label, present, outcome)
|
|
817
|
+
});
|
|
818
|
+
const verdictOf = (label, score, outcome, incomplete) => Match.value(incomplete).pipe(Match.when(true, () => ({
|
|
819
|
+
label,
|
|
820
|
+
score: SCORE_INCOMPLETE,
|
|
821
|
+
cells: cellsFor(score),
|
|
822
|
+
verdict: incompleteVerdict(outcome)
|
|
823
|
+
})), Match.when(false, () => completeRow(label, score, outcome)), Match.exhaustive);
|
|
824
|
+
/** The label a second part repeats, and `none` when it repeats none a report can name. */
|
|
825
|
+
const repeatedLabel = (parts) => Option.filter(Option.map(Option.fromUndefinedOr(parts.find((part, index) => parts.slice(0, index).some((earlier) => earlier.label === part.label))), (duplicate) => duplicate.label), (label) => label !== "");
|
|
826
|
+
const uniqueParts = (parts) => parts.filter((part, index) => parts.slice(0, index).every((earlier) => earlier.label !== part.label));
|
|
827
|
+
const chooseReports = (parts) => parts.flatMap((part) => Option.toArray(Option.map(Option.fromUndefinedOr(part.report), (report) => ({
|
|
828
|
+
...part,
|
|
829
|
+
report
|
|
830
|
+
}))));
|
|
831
|
+
const byFileThenLine = (left, right) => orDefault(Option.liftPredicate(left.file.localeCompare(right.file), (ordered) => ordered !== 0), left.line - right.line);
|
|
832
|
+
const survivorsOf = (report) => Object.entries(report.files).flatMap(([file, fileResult]) => fileResult.mutants.filter((mutant) => SURVIVOR_STATUSES[mutant.status] === true).map((mutant) => ({
|
|
833
|
+
file,
|
|
834
|
+
line: mutant.location.start.line,
|
|
835
|
+
column: mutant.location.start.column,
|
|
836
|
+
status: mutant.status,
|
|
837
|
+
mutatorName: mutant.mutatorName,
|
|
838
|
+
replacement: orDefault(Option.fromNullishOr(mutant.replacement), "")
|
|
839
|
+
}))).sort(byFileThenLine);
|
|
840
|
+
const rowOf = (label, part) => Option.match(Option.fromUndefinedOr(part), {
|
|
841
|
+
onNone: () => verdictOf(label, void 0, void 0, false),
|
|
842
|
+
onSome: (present) => verdictOf(label, scoreFor(present), present.outcome, present.incomplete)
|
|
843
|
+
});
|
|
844
|
+
const packageRows = (parts, expected) => {
|
|
845
|
+
const absent = expected.filter((name) => !parts.some((part) => part.label === name));
|
|
846
|
+
return [...parts.map((part) => part.label), ...absent].sort().map((label) => rowOf(label, parts.find((candidate) => candidate.label === label)));
|
|
847
|
+
};
|
|
848
|
+
const allPackagesRow = (reports, merged) => verdictOf(ALL_PACKAGES_LABEL$1, scoreOf(merged.files), outcomeOf(reports), reports.some((part) => part.incomplete));
|
|
849
|
+
const mergedReports = (reports, rows) => {
|
|
850
|
+
const report = mergeParts(reports);
|
|
851
|
+
return MergedReports.make({
|
|
852
|
+
report,
|
|
853
|
+
rows: [allPackagesRow(reports, report), ...rows],
|
|
854
|
+
survivors: survivorsOf(report)
|
|
855
|
+
});
|
|
856
|
+
};
|
|
857
|
+
const mergedOutcome = (reports, rows) => Match.value(reports.length === 0).pipe(Match.when(true, () => Result.succeed(NoMergedReports.make({ rows }))), Match.when(false, () => Result.succeed(mergedReports(reports, rows))), Match.exhaustive);
|
|
858
|
+
const decideMerge = (command) => {
|
|
859
|
+
const parts = uniqueParts(command.parts);
|
|
860
|
+
const expected = orDefault(Option.fromNullishOr(command.expectedPackages), EMPTY_PACKAGES);
|
|
861
|
+
const rows = packageRows(parts, expected);
|
|
862
|
+
return Match.value(parts.length === 0).pipe(Match.when(true, () => Result.fail(MissingPackages.make({ packages: [...expected] }))), Match.when(false, () => mergedOutcome(chooseReports(parts), rows)), Match.exhaustive);
|
|
863
|
+
};
|
|
864
|
+
const DuplicateLabelFound = S.TaggedStruct("DuplicateLabelFound", { label: S.String });
|
|
865
|
+
const LabelsDistinct = S.TaggedStruct("LabelsDistinct", {});
|
|
866
|
+
S.Union([DuplicateLabelFound, LabelsDistinct]);
|
|
867
|
+
const labelCheckOf = (parts) => Option.match(repeatedLabel(parts), {
|
|
868
|
+
onNone: () => LabelsDistinct.make({}),
|
|
869
|
+
onSome: (label) => DuplicateLabelFound.make({ label })
|
|
870
|
+
});
|
|
871
|
+
const decide = (command) => Match.value(labelCheckOf(command.parts)).pipe(Match.tag("DuplicateLabelFound", (found) => Result.fail(DuplicatePackageLabel.make({ label: found.label }))), Match.tag("LabelsDistinct", () => decideMerge(command)), Match.exhaustive);
|
|
872
|
+
const mergeReportParts = Workflow.make(MergeReportPartsCommand, decide);
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/MergeReports.ts
|
|
875
|
+
const PART_MARKER_FILE = "mutation-part.json";
|
|
876
|
+
const PART_REPORT_FILE = "mutation-report.json";
|
|
877
|
+
const PART_STREAM_FILE = "mutation-stream.jsonl";
|
|
878
|
+
const MERGED_REPORT_FILE = "mutation-report.json";
|
|
879
|
+
const MERGED_HTML_FILE = "mutation-report.html";
|
|
880
|
+
const MERGED_SUMMARY_FILE = "summary.md";
|
|
881
|
+
const STREAM_REPORT_THRESHOLDS = {
|
|
882
|
+
high: 100,
|
|
883
|
+
low: 80
|
|
884
|
+
};
|
|
885
|
+
const SURVIVOR_CAP = 100;
|
|
886
|
+
const ALL_PACKAGES_LABEL = "**all**";
|
|
887
|
+
const STEP_SUMMARY_VARIABLE = "GITHUB_STEP_SUMMARY";
|
|
888
|
+
/** The value when the condition holds, the fallback otherwise. */
|
|
889
|
+
const whenHolds = (condition, value, fallback) => Option.getOrElse(Option.filter(Option.some(value), () => condition), () => fallback);
|
|
890
|
+
const failMerge = (reason) => Effect.gen(function* () {
|
|
891
|
+
yield* Console.error(`stryker merge-reports: ${reason}`);
|
|
892
|
+
return yield* Effect.fail(MergeReportsFailed.make({ reason }));
|
|
893
|
+
});
|
|
894
|
+
const readOptional = (file) => Effect.gen(function* () {
|
|
895
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(file).pipe(Effect.orElseSucceed(() => void 0));
|
|
896
|
+
});
|
|
897
|
+
const streamMutant = (line) => {
|
|
898
|
+
const mutant = {
|
|
899
|
+
id: line.id,
|
|
900
|
+
mutatorName: line.mutator,
|
|
901
|
+
status: line.status,
|
|
902
|
+
location: line.location
|
|
903
|
+
};
|
|
904
|
+
if (line.replacement === null) return mutant;
|
|
905
|
+
return {
|
|
906
|
+
...mutant,
|
|
907
|
+
replacement: line.replacement
|
|
908
|
+
};
|
|
909
|
+
};
|
|
910
|
+
const decodeStreamLines = (text) => text.split("\n").map((raw) => raw.trim()).filter((line) => line.length > 0).flatMap((line) => Option.toArray(S.decodeUnknownOption(S.fromJsonString(StreamMutantLineSchema))(line)));
|
|
911
|
+
const groupStreamMutant = (groups, value) => {
|
|
912
|
+
const mutants = Option.getOrElse(Option.fromNullishOr(groups[value.file]), () => []);
|
|
913
|
+
mutants.push(streamMutant(value));
|
|
914
|
+
groups[value.file] = mutants;
|
|
915
|
+
return groups;
|
|
916
|
+
};
|
|
917
|
+
const groupedMutants = (text) => decodeStreamLines(text).reduce(groupStreamMutant, {});
|
|
918
|
+
const fileResults = (grouped) => Object.fromEntries(Object.entries(grouped).map(([file, mutants]) => [file, {
|
|
919
|
+
language: "javascript",
|
|
920
|
+
source: "",
|
|
921
|
+
mutants
|
|
922
|
+
}]));
|
|
923
|
+
const streamReport = (text) => Option.getOrUndefined(Option.map(Option.filter(Option.some(groupedMutants(text)), (grouped) => Object.keys(grouped).length > 0), (grouped) => ({
|
|
924
|
+
schemaVersion: "1.0",
|
|
925
|
+
thresholds: STREAM_REPORT_THRESHOLDS,
|
|
926
|
+
files: fileResults(grouped)
|
|
927
|
+
})));
|
|
928
|
+
const listDirectory = (dir) => Effect.gen(function* () {
|
|
929
|
+
return yield* (yield* FileSystem.FileSystem).readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
930
|
+
});
|
|
931
|
+
const isDirectory = (full) => Effect.gen(function* () {
|
|
932
|
+
const info = yield* (yield* FileSystem.FileSystem).stat(full).pipe(Effect.orElseSucceed(() => void 0));
|
|
933
|
+
return Option.match(Option.fromUndefinedOr(info), {
|
|
934
|
+
onNone: () => false,
|
|
935
|
+
onSome: (entry) => entry.type === "Directory"
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
const childPartDirs = (dir, name) => Effect.gen(function* () {
|
|
939
|
+
const full = (yield* Path.Path).join(dir, name);
|
|
940
|
+
if (yield* isDirectory(full)) return yield* findPartDirs(full);
|
|
941
|
+
return whenHolds(name === PART_MARKER_FILE, [dir], []);
|
|
942
|
+
});
|
|
943
|
+
const findPartDirs = (dir) => Effect.gen(function* () {
|
|
944
|
+
const names = [...yield* listDirectory(dir)].sort();
|
|
945
|
+
return (yield* Effect.forEach(names, (name) => childPartDirs(dir, name))).flat();
|
|
946
|
+
});
|
|
947
|
+
const partBase = (meta) => ({
|
|
948
|
+
label: meta.package,
|
|
949
|
+
outcome: meta.outcome,
|
|
950
|
+
incomplete: false
|
|
951
|
+
});
|
|
952
|
+
const partFromReportText = (dir, base, text) => {
|
|
953
|
+
const report = S.decodeUnknownOption(S.fromJsonString(MutationTestResultSchema))(text);
|
|
954
|
+
return Option.match(report, {
|
|
955
|
+
onNone: () => ({
|
|
956
|
+
dir,
|
|
957
|
+
part: Option.some(base),
|
|
958
|
+
unreadable: true
|
|
959
|
+
}),
|
|
960
|
+
onSome: (value) => ({
|
|
961
|
+
dir,
|
|
962
|
+
part: Option.some({
|
|
963
|
+
...base,
|
|
964
|
+
report: value
|
|
965
|
+
}),
|
|
966
|
+
unreadable: false
|
|
967
|
+
})
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
const readPartStream = (dir, base) => Effect.gen(function* () {
|
|
971
|
+
const path = yield* Path.Path;
|
|
972
|
+
const text = yield* readOptional(path.join(dir, PART_STREAM_FILE));
|
|
973
|
+
const report = Option.fromNullishOr(text).pipe(Option.flatMap((streamed) => Option.fromNullishOr(streamReport(streamed))));
|
|
974
|
+
return Option.match(report, {
|
|
975
|
+
onNone: () => ({
|
|
976
|
+
dir,
|
|
977
|
+
part: Option.some(base),
|
|
978
|
+
unreadable: false
|
|
979
|
+
}),
|
|
980
|
+
onSome: (value) => ({
|
|
981
|
+
dir,
|
|
982
|
+
part: Option.some({
|
|
983
|
+
...base,
|
|
984
|
+
incomplete: true,
|
|
985
|
+
report: value
|
|
986
|
+
}),
|
|
987
|
+
unreadable: false
|
|
988
|
+
})
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
const readPartReport = (dir, base) => Effect.gen(function* () {
|
|
992
|
+
const path = yield* Path.Path;
|
|
993
|
+
const text = yield* readOptional(path.join(dir, PART_REPORT_FILE));
|
|
994
|
+
return yield* Option.match(Option.fromUndefinedOr(text), {
|
|
995
|
+
onNone: () => readPartStream(dir, base),
|
|
996
|
+
onSome: (present) => Effect.succeed(partFromReportText(dir, base, present))
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
const readPart = (dir) => Effect.gen(function* () {
|
|
1000
|
+
const path = yield* Path.Path;
|
|
1001
|
+
const metaText = yield* readOptional(path.join(dir, PART_MARKER_FILE));
|
|
1002
|
+
const meta = Option.fromNullishOr(metaText).pipe(Option.flatMap((text) => S.decodeUnknownOption(S.fromJsonString(PartMetaSchema))(text)));
|
|
1003
|
+
return yield* Option.match(meta, {
|
|
1004
|
+
onNone: () => Effect.succeed({
|
|
1005
|
+
dir,
|
|
1006
|
+
part: Option.none(),
|
|
1007
|
+
unreadable: false
|
|
1008
|
+
}),
|
|
1009
|
+
onSome: (present) => readPartReport(dir, partBase(present))
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
const readParts = (partsDir) => Effect.gen(function* () {
|
|
1013
|
+
const dirs = yield* findPartDirs(partsDir);
|
|
1014
|
+
const reads = yield* Effect.forEach(dirs, readPart);
|
|
1015
|
+
return {
|
|
1016
|
+
parts: reads.flatMap((read) => Option.toArray(read.part)),
|
|
1017
|
+
skipped: reads.filter((read) => Option.isNone(read.part)).map((read) => read.dir),
|
|
1018
|
+
unreadable: reads.filter((read) => read.unreadable).map((read) => read.dir)
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
const decodePackageList = (raw) => Option.match(S.decodeUnknownOption(S.fromJsonString(S.Array(S.String)))(raw), {
|
|
1022
|
+
onNone: () => failMerge(`--packages is not a JSON array: ${raw}`),
|
|
1023
|
+
onSome: (packages) => Effect.succeed(packages)
|
|
1024
|
+
});
|
|
1025
|
+
const parsePackages = (raw) => Option.match(Option.filter(Option.fromNullishOr(raw), (text) => text.length > 0), {
|
|
1026
|
+
onNone: () => Effect.succeed(void 0),
|
|
1027
|
+
onSome: decodePackageList
|
|
1028
|
+
});
|
|
1029
|
+
const SUMMARY_TABLE_HEADER = ["| package | score | killed | survived | no cov | timeout | compile err | verdict |", "| --- | --: | --: | --: | --: | --: | --: | :-: |"];
|
|
1030
|
+
const verdictRow = (row) => `| ${row.label} | ${row.score} | ${row.cells.join(" | ")} | ${row.verdict} |`;
|
|
1031
|
+
const survivorLine = (survivor) => `- \`${survivor.file}:${survivor.line}:${survivor.column}\` ${survivor.status} \`${survivor.mutatorName}\` → \`${survivor.replacement}\``;
|
|
1032
|
+
const survivorSection = (survivors) => [
|
|
1033
|
+
"",
|
|
1034
|
+
"### Survivors",
|
|
1035
|
+
"",
|
|
1036
|
+
...survivors.slice(0, SURVIVOR_CAP).map(survivorLine),
|
|
1037
|
+
...whenHolds(survivors.length > SURVIVOR_CAP, [`- … and ${survivors.length - SURVIVOR_CAP} more; see mutation-report.html in the run artifact.`], [])
|
|
1038
|
+
];
|
|
1039
|
+
const warningSection = (skipped) => [
|
|
1040
|
+
"",
|
|
1041
|
+
"### Warnings",
|
|
1042
|
+
"",
|
|
1043
|
+
...skipped.map((name) => `- \`${name}\`: no readable mutation-part.json`)
|
|
1044
|
+
];
|
|
1045
|
+
const mergedCountLine = (packageRows) => `Merged ${packageRows.filter((row) => row.score !== "no report").length} of ${packageRows.length} package report(s).`;
|
|
1046
|
+
const summaryMarkdown = (rows, survivors, skipped, unreadableCount) => {
|
|
1047
|
+
const packageRows = rows.filter((row) => row.label !== ALL_PACKAGES_LABEL);
|
|
1048
|
+
return `${[
|
|
1049
|
+
"## Mutation",
|
|
1050
|
+
"",
|
|
1051
|
+
mergedCountLine(packageRows),
|
|
1052
|
+
"",
|
|
1053
|
+
...SUMMARY_TABLE_HEADER,
|
|
1054
|
+
...rows.map(verdictRow),
|
|
1055
|
+
...whenHolds(survivors.length > 0, survivorSection(survivors), []),
|
|
1056
|
+
...whenHolds(skipped.length > 0, warningSection(skipped), []),
|
|
1057
|
+
...whenHolds(unreadableCount > 0, ["", `Report exited non-zero: ${unreadableCount} unreadable part(s).`], [])
|
|
1058
|
+
].join("\n")}\n`;
|
|
1059
|
+
};
|
|
1060
|
+
const writeFile = (file, content, append) => Effect.gen(function* () {
|
|
1061
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1062
|
+
if (append) {
|
|
1063
|
+
yield* fs.writeFileString(file, content, { flag: "a" });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
yield* fs.writeFileString(file, content);
|
|
1067
|
+
}).pipe(Effect.catchCause(() => failMerge(`cannot write ${file}`)));
|
|
1068
|
+
async function* toStream(events) {
|
|
1069
|
+
yield* events;
|
|
1070
|
+
}
|
|
1071
|
+
const writeHtmlReport = (fileName, report) => Effect.gen(function* () {
|
|
1072
|
+
const decoded = S.decodeUnknownOption(StrykerOptionsSchema)({ htmlReporter: { fileName } });
|
|
1073
|
+
if (Option.isNone(decoded)) return yield* failMerge(`cannot configure the html report at ${fileName}`);
|
|
1074
|
+
const metrics = calculateMetrics(report.files);
|
|
1075
|
+
yield* Effect.promise(() => makeHtmlReporter(decoded.value, {})(toStream([new MutationTestReportReady({
|
|
1076
|
+
report,
|
|
1077
|
+
metrics
|
|
1078
|
+
})])));
|
|
1079
|
+
});
|
|
1080
|
+
const failureReason = (error, partsDir) => Match.value(error).pipe(Match.tag("DuplicatePackageLabel", (duplicate) => `duplicate package ${duplicate.label}`), Match.tag("MissingPackages", (missing) => {
|
|
1081
|
+
if (missing.packages.length === 0) return `no mutation report parts under ${partsDir}`;
|
|
1082
|
+
return `no mutation report parts under ${partsDir} for ${missing.packages.join(", ")}`;
|
|
1083
|
+
}), Match.exhaustive);
|
|
1084
|
+
const survivorList = (decision) => {
|
|
1085
|
+
if (S.is(MergedReports)(decision)) return decision.survivors;
|
|
1086
|
+
return [];
|
|
1087
|
+
};
|
|
1088
|
+
const mergedReport = (decision) => {
|
|
1089
|
+
if (S.is(MergedReports)(decision)) return decision.report;
|
|
1090
|
+
};
|
|
1091
|
+
const ensurePartsDir = (fs, partsDir) => Effect.gen(function* () {
|
|
1092
|
+
if (yield* fs.exists(partsDir).pipe(Effect.orElseSucceed(() => false))) return;
|
|
1093
|
+
return yield* failMerge(`no such parts directory ${partsDir}`);
|
|
1094
|
+
});
|
|
1095
|
+
const decideReports = (parts, expectedPackages, partsDir) => Effect.gen(function* () {
|
|
1096
|
+
const decision = mergeReportParts(MergeReportPartsCommand.make({
|
|
1097
|
+
parts,
|
|
1098
|
+
expectedPackages
|
|
1099
|
+
}));
|
|
1100
|
+
if (Result.isSuccess(decision)) return decision.success;
|
|
1101
|
+
return yield* failMerge(failureReason(decision.failure, partsDir));
|
|
1102
|
+
});
|
|
1103
|
+
const writeReportOutputs = (path, out, report) => Effect.forEach(Option.toArray(Option.fromUndefinedOr(report)), (present) => Effect.gen(function* () {
|
|
1104
|
+
yield* writeFile(path.join(out, MERGED_REPORT_FILE), JSON.stringify(present), false);
|
|
1105
|
+
yield* writeHtmlReport(path.join(out, MERGED_HTML_FILE), present);
|
|
1106
|
+
}), { discard: true });
|
|
1107
|
+
const appendStepSummary = (summary) => Effect.forEach(Option.toArray(Option.filter(Option.fromNullishOr(process.env[STEP_SUMMARY_VARIABLE]), (file) => file.length > 0)), (file) => writeFile(file, summary, true), { discard: true });
|
|
1108
|
+
const runMergeReports = (request) => Effect.gen(function* () {
|
|
1109
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1110
|
+
const path = yield* Path.Path;
|
|
1111
|
+
const out = request.out;
|
|
1112
|
+
const expectedPackages = yield* parsePackages(request.packages);
|
|
1113
|
+
yield* ensurePartsDir(fs, request.parts);
|
|
1114
|
+
const { parts, skipped, unreadable } = yield* readParts(request.parts);
|
|
1115
|
+
const decision = yield* decideReports(parts, expectedPackages, request.parts);
|
|
1116
|
+
yield* fs.makeDirectory(out, { recursive: true }).pipe(Effect.catchCause(() => failMerge(`cannot create ${out}`)));
|
|
1117
|
+
yield* writeReportOutputs(path, out, mergedReport(decision));
|
|
1118
|
+
const summary = summaryMarkdown(decision.rows, survivorList(decision), skipped, unreadable.length);
|
|
1119
|
+
yield* writeFile(path.join(out, MERGED_SUMMARY_FILE), summary, false);
|
|
1120
|
+
yield* appendStepSummary(summary);
|
|
1121
|
+
yield* Console.log(summary);
|
|
1122
|
+
yield* Effect.forEach(whenHolds(unreadable.length > 0, [`${unreadable.length} unreadable part(s): ${unreadable.join(", ")}`], []), failMerge, { discard: true });
|
|
1123
|
+
});
|
|
1124
|
+
//#endregion
|
|
686
1125
|
//#region src/resolve-output-mode.workflow.ts
|
|
687
1126
|
const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
|
|
688
1127
|
var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
|
|
@@ -721,62 +1160,62 @@ var MachineOutput = class extends S.TaggedClass()("MachineOutput", {
|
|
|
721
1160
|
[ResolveModeTypeId] = ResolveModeTypeId;
|
|
722
1161
|
};
|
|
723
1162
|
const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
signal: "flag",
|
|
736
|
-
stdoutIsTTY: command.stdoutIsTTY
|
|
737
|
-
}));
|
|
738
|
-
if (command.envMode !== void 0 && command.envMode.length > 0) {
|
|
739
|
-
if (command.envMode === "machine") return Result.succeed(MachineOutput.make({
|
|
740
|
-
signal: "env",
|
|
741
|
-
stdoutIsTTY: command.stdoutIsTTY
|
|
742
|
-
}));
|
|
743
|
-
return Result.succeed(HumanOutput.make({
|
|
744
|
-
signal: "env",
|
|
745
|
-
stdoutIsTTY: command.stdoutIsTTY
|
|
746
|
-
}));
|
|
747
|
-
}
|
|
748
|
-
if (!command.stdoutIsTTY) return Result.succeed(MachineOutput.make({
|
|
749
|
-
signal: "tty",
|
|
750
|
-
stdoutIsTTY: false
|
|
751
|
-
}));
|
|
752
|
-
if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed(MachineOutput.make({
|
|
1163
|
+
const configured = (value) => Option.filter(Option.fromNullishOr(value), (text) => text.length > 0);
|
|
1164
|
+
const modeFromEnv = (envMode, stdoutIsTTY) => Match.value(envMode).pipe(Match.when("machine", () => MachineOutput.make({
|
|
1165
|
+
signal: "env",
|
|
1166
|
+
stdoutIsTTY
|
|
1167
|
+
})), Match.orElse(() => HumanOutput.make({
|
|
1168
|
+
signal: "env",
|
|
1169
|
+
stdoutIsTTY
|
|
1170
|
+
})));
|
|
1171
|
+
const modeFromAgent = (command) => Option.match(configured(command.agent), {
|
|
1172
|
+
onNone: () => modeFromTools(command),
|
|
1173
|
+
onSome: () => MachineOutput.make({
|
|
753
1174
|
signal: "agent",
|
|
754
1175
|
stdoutIsTTY: true
|
|
755
|
-
})
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
const
|
|
1176
|
+
})
|
|
1177
|
+
});
|
|
1178
|
+
const modeFromTools = (command) => Match.value(anyToolVariableSet(command.toolVars)).pipe(Match.when(true, () => MachineOutput.make({
|
|
1179
|
+
signal: "tool",
|
|
1180
|
+
stdoutIsTTY: true
|
|
1181
|
+
})), Match.orElse(() => HumanOutput.make({
|
|
1182
|
+
signal: "tty",
|
|
1183
|
+
stdoutIsTTY: true
|
|
1184
|
+
})));
|
|
1185
|
+
const anyToolVariableSet = (toolVars) => Option.match(Option.fromNullishOr(toolVars), {
|
|
1186
|
+
onNone: () => false,
|
|
1187
|
+
onSome: (named) => TOOL_VARIABLES$1.some((variable) => Option.isSome(configured(named[variable])))
|
|
1188
|
+
});
|
|
1189
|
+
const modeBelowEnv = (command) => Match.value(command.stdoutIsTTY).pipe(Match.when(false, () => MachineOutput.make({
|
|
1190
|
+
signal: "tty",
|
|
1191
|
+
stdoutIsTTY: false
|
|
1192
|
+
})), Match.orElse(() => modeFromAgent(command)));
|
|
1193
|
+
const modeFromEnvironment = (command) => Option.match(configured(command.envMode), {
|
|
1194
|
+
onNone: () => modeBelowEnv(command),
|
|
1195
|
+
onSome: (envMode) => modeFromEnv(envMode, command.stdoutIsTTY)
|
|
1196
|
+
});
|
|
1197
|
+
const decideMode = (command) => Match.value(command).pipe(Match.when({
|
|
1198
|
+
text: true,
|
|
1199
|
+
json: true
|
|
1200
|
+
}, () => Result.fail(ModeConflictError.make({
|
|
1201
|
+
option: "json",
|
|
1202
|
+
value: "text",
|
|
1203
|
+
expected: CONFLICT_EXPECTED
|
|
1204
|
+
}))), Match.when({ text: true }, (texted) => Result.succeed(HumanOutput.make({
|
|
1205
|
+
signal: "flag",
|
|
1206
|
+
stdoutIsTTY: texted.stdoutIsTTY
|
|
1207
|
+
}))), Match.when({ json: true }, (jsoned) => Result.succeed(MachineOutput.make({
|
|
1208
|
+
signal: "flag",
|
|
1209
|
+
stdoutIsTTY: jsoned.stdoutIsTTY
|
|
1210
|
+
}))), Match.orElse((rest) => Result.succeed(modeFromEnvironment(rest))));
|
|
1211
|
+
const resolveOutputMode = Workflow.make(ResolveModeCommand, decideMode);
|
|
773
1212
|
//#endregion
|
|
774
1213
|
//#region src/Output.ts
|
|
775
1214
|
const TICK_INTERVAL_MS = 1e4;
|
|
776
1215
|
var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/RunEventStreamPortTag") {};
|
|
777
1216
|
const RunEventStreamPort = RunEventStreamPortTag;
|
|
778
|
-
const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.
|
|
779
|
-
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.
|
|
1217
|
+
const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.orElse(() => false));
|
|
1218
|
+
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.exhaustive);
|
|
780
1219
|
const toWireLine = (event) => {
|
|
781
1220
|
const fields = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_tag"));
|
|
782
1221
|
return JSON.stringify({
|
|
@@ -813,8 +1252,7 @@ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, st
|
|
|
813
1252
|
progress: {
|
|
814
1253
|
completed: 0,
|
|
815
1254
|
total: null
|
|
816
|
-
}
|
|
817
|
-
findingsPrinted: 0
|
|
1255
|
+
}
|
|
818
1256
|
};
|
|
819
1257
|
const queueStream = Stream.fromQueue(queue).pipe(Stream.tap((event) => Effect.sync(() => {
|
|
820
1258
|
Match.value(event).pipe(Match.tag("plan", (e) => {
|
|
@@ -838,41 +1276,57 @@ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, st
|
|
|
838
1276
|
});
|
|
839
1277
|
})));
|
|
840
1278
|
let terminalSeen = false;
|
|
841
|
-
const
|
|
1279
|
+
const noteTerminal = (event) => {
|
|
1280
|
+
state.terminalWritten = state.terminalWritten || isTerminalEvent(event);
|
|
1281
|
+
};
|
|
1282
|
+
const progressTap = (event) => {
|
|
842
1283
|
const line = stderrProgressLine(event, state.terminalWritten);
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1284
|
+
noteTerminal(event);
|
|
1285
|
+
return Option.match(Option.fromUndefinedOr(line), {
|
|
1286
|
+
onNone: () => Effect.void,
|
|
1287
|
+
onSome: (text) => writeStderr(stdio, text)
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
const framingOpen = () => state.mode === "machine" && !terminalSeen;
|
|
1291
|
+
const markTerminalFramed = (event) => {
|
|
1292
|
+
terminalSeen = terminalSeen || isTerminalEvent(event);
|
|
1293
|
+
};
|
|
1294
|
+
const frameEvent = (event) => {
|
|
1295
|
+
if (!framingOpen()) return false;
|
|
1296
|
+
markTerminalFramed(event);
|
|
850
1297
|
return true;
|
|
851
|
-
}
|
|
1298
|
+
};
|
|
1299
|
+
const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap(progressTap)).pipe(Stream.filter(frameEvent), Stream.map((event) => `${toWireLine(event)}\n`)));
|
|
852
1300
|
let drainFiber = null;
|
|
1301
|
+
const outputOpen = () => state.mode === "machine" && !state.terminalWritten;
|
|
1302
|
+
const offerStarted = () => Effect.gen(function* () {
|
|
1303
|
+
if (state.mode !== "machine") return;
|
|
1304
|
+
yield* Queue.offer(queue, RunStarted.make({
|
|
1305
|
+
schemaVersion: "1.0",
|
|
1306
|
+
runId,
|
|
1307
|
+
mode: state.mode,
|
|
1308
|
+
signal: state.signal
|
|
1309
|
+
}));
|
|
1310
|
+
});
|
|
1311
|
+
const openHeader = () => Effect.gen(function* () {
|
|
1312
|
+
if (state.headerWritten) return;
|
|
1313
|
+
state.headerWritten = true;
|
|
1314
|
+
yield* offerStarted();
|
|
1315
|
+
});
|
|
853
1316
|
return {
|
|
854
1317
|
queue,
|
|
855
1318
|
runId,
|
|
856
1319
|
startedAt,
|
|
857
|
-
isOpen: () =>
|
|
1320
|
+
isOpen: () => outputOpen() && drainFiber !== null,
|
|
858
1321
|
ensureOpen: (openResolved) => {
|
|
859
1322
|
if (state.headerWritten) return;
|
|
860
1323
|
state.mode = openResolved.mode;
|
|
861
1324
|
state.signal = openResolved.signal;
|
|
862
1325
|
},
|
|
863
1326
|
open: Effect.gen(function* () {
|
|
864
|
-
if (drainFiber
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (state.mode === "machine") yield* Queue.offer(queue, RunStarted.make({
|
|
868
|
-
schemaVersion: "1.0",
|
|
869
|
-
runId,
|
|
870
|
-
mode: state.mode,
|
|
871
|
-
signal: state.signal
|
|
872
|
-
}));
|
|
873
|
-
}
|
|
874
|
-
drainFiber = yield* Effect.forkDetach(drain);
|
|
875
|
-
}
|
|
1327
|
+
if (drainFiber !== null) return;
|
|
1328
|
+
yield* openHeader();
|
|
1329
|
+
drainFiber = yield* Effect.forkDetach(drain);
|
|
876
1330
|
}),
|
|
877
1331
|
closeAndDrain: Effect.gen(function* () {
|
|
878
1332
|
state.terminalWritten = true;
|
|
@@ -892,74 +1346,33 @@ const decisionToResolvedMode = (decision) => Match.value(decision).pipe(Match.ta
|
|
|
892
1346
|
signal: machine.signal,
|
|
893
1347
|
stdoutIsTTY: machine.stdoutIsTTY
|
|
894
1348
|
})), Match.exhaustive);
|
|
1349
|
+
const definedToolVars = (vars) => Object.fromEntries(Object.entries(vars ?? {}).filter((entry) => entry[1] !== void 0));
|
|
1350
|
+
const commandFor = (input) => ResolveModeCommand.make({
|
|
1351
|
+
stdoutIsTTY: input.stdoutIsTTY,
|
|
1352
|
+
text: input.text,
|
|
1353
|
+
json: input.json,
|
|
1354
|
+
envMode: input.envMode,
|
|
1355
|
+
agent: input.agent,
|
|
1356
|
+
toolVars: definedToolVars(input.toolVars)
|
|
1357
|
+
});
|
|
895
1358
|
function isColorEnabled(resolved, noColor) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (noColor.length === 0) return true;
|
|
899
|
-
return false;
|
|
1359
|
+
const requested = Option.exists(Option.fromUndefinedOr(noColor), (value) => value.length > 0);
|
|
1360
|
+
return resolved.mode === "human" && !requested;
|
|
900
1361
|
}
|
|
901
1362
|
var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/OutputModeProbeTag") {};
|
|
902
1363
|
const OutputModeProbe = OutputModeProbeTag;
|
|
1364
|
+
const envToolVars = () => definedToolVars(Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]])));
|
|
1365
|
+
const probeInput = (command) => ({
|
|
1366
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
1367
|
+
text: command.text,
|
|
1368
|
+
json: command.json,
|
|
1369
|
+
envMode: process.env["STRYKER_MODE"],
|
|
1370
|
+
agent: process.env["AGENT"],
|
|
1371
|
+
toolVars: envToolVars()
|
|
1372
|
+
});
|
|
903
1373
|
const outputModeProbeCell = Cell.layer({
|
|
904
|
-
read: (command) => Effect.succeed((
|
|
905
|
-
|
|
906
|
-
for (const variable of TOOL_VARIABLES) {
|
|
907
|
-
const value = process.env[variable];
|
|
908
|
-
if (value !== void 0) toolVarsRecord[variable] = value;
|
|
909
|
-
}
|
|
910
|
-
const envMode = process.env["STRYKER_MODE"];
|
|
911
|
-
const agent = process.env["AGENT"];
|
|
912
|
-
let result = { stdoutIsTTY: process.stdout.isTTY === true };
|
|
913
|
-
if (Object.keys(toolVarsRecord).length > 0) result = {
|
|
914
|
-
...result,
|
|
915
|
-
toolVars: toolVarsRecord
|
|
916
|
-
};
|
|
917
|
-
if (envMode !== void 0) result = {
|
|
918
|
-
...result,
|
|
919
|
-
envMode
|
|
920
|
-
};
|
|
921
|
-
if (agent !== void 0) result = {
|
|
922
|
-
...result,
|
|
923
|
-
agent
|
|
924
|
-
};
|
|
925
|
-
if (command.text !== void 0) result = {
|
|
926
|
-
...result,
|
|
927
|
-
text: command.text
|
|
928
|
-
};
|
|
929
|
-
if (command.json !== void 0) result = {
|
|
930
|
-
...result,
|
|
931
|
-
json: command.json
|
|
932
|
-
};
|
|
933
|
-
return result;
|
|
934
|
-
})()),
|
|
935
|
-
decode: (raw) => Result.succeed((() => {
|
|
936
|
-
const filteredToolVars = {};
|
|
937
|
-
if (raw.toolVars !== void 0) {
|
|
938
|
-
for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
|
|
939
|
-
}
|
|
940
|
-
let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
|
|
941
|
-
if (raw.text !== void 0) commandInput = {
|
|
942
|
-
...commandInput,
|
|
943
|
-
text: raw.text
|
|
944
|
-
};
|
|
945
|
-
if (raw.json !== void 0) commandInput = {
|
|
946
|
-
...commandInput,
|
|
947
|
-
json: raw.json
|
|
948
|
-
};
|
|
949
|
-
if (raw.envMode !== void 0) commandInput = {
|
|
950
|
-
...commandInput,
|
|
951
|
-
envMode: raw.envMode
|
|
952
|
-
};
|
|
953
|
-
if (raw.agent !== void 0) commandInput = {
|
|
954
|
-
...commandInput,
|
|
955
|
-
agent: raw.agent
|
|
956
|
-
};
|
|
957
|
-
if (Object.keys(filteredToolVars).length > 0) commandInput = {
|
|
958
|
-
...commandInput,
|
|
959
|
-
toolVars: filteredToolVars
|
|
960
|
-
};
|
|
961
|
-
return ResolveModeCommand.make(commandInput);
|
|
962
|
-
})()),
|
|
1374
|
+
read: (command) => Effect.succeed(probeInput(command)),
|
|
1375
|
+
decode: (raw) => Result.succeed(commandFor(raw)),
|
|
963
1376
|
decide: resolveOutputMode,
|
|
964
1377
|
encode: (outcome) => Result.map(outcome, decisionToResolvedMode),
|
|
965
1378
|
write: (outcome) => Result.match(outcome, {
|
|
@@ -1007,29 +1420,24 @@ function offerFailureEnvelope(stream, failed, captured) {
|
|
|
1007
1420
|
remediation: envelope.remediation
|
|
1008
1421
|
}));
|
|
1009
1422
|
}
|
|
1423
|
+
const emitHelpEnvelope = (stream, help) => Queue.offer(stream.queue, HelpRendered.make({
|
|
1424
|
+
schemaVersion: "1.0",
|
|
1425
|
+
code: 0,
|
|
1426
|
+
help
|
|
1427
|
+
}));
|
|
1428
|
+
const helpPayload = (ok, captured) => Option.filter(Option.some(captured), () => ok.help || captured.length > 0);
|
|
1429
|
+
const emitNullScoreVerdictWhenOpen = (stream, mode, basePath, pathService) => Effect.gen(function* () {
|
|
1430
|
+
if (!stream.isOpen()) return;
|
|
1431
|
+
yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
|
|
1432
|
+
});
|
|
1010
1433
|
function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
|
|
1011
1434
|
return Effect.gen(function* () {
|
|
1012
1435
|
const captured = readCapturedConsole();
|
|
1013
1436
|
if (Result.isSuccess(outcome)) {
|
|
1014
1437
|
const decision = outcome.success;
|
|
1015
|
-
return yield* Match.value(decision).pipe(Match.tag("RunOk", (ok) =>
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
schemaVersion: "1.0",
|
|
1019
|
-
code: 0,
|
|
1020
|
-
help: captured
|
|
1021
|
-
}));
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (captured.length > 0) {
|
|
1025
|
-
yield* Queue.offer(stream.queue, HelpRendered.make({
|
|
1026
|
-
schemaVersion: "1.0",
|
|
1027
|
-
code: 0,
|
|
1028
|
-
help: captured
|
|
1029
|
-
}));
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
|
|
1438
|
+
return yield* Match.value(decision).pipe(Match.tag("RunOk", (ok) => Option.match(helpPayload(ok, captured), {
|
|
1439
|
+
onSome: (help) => emitHelpEnvelope(stream, help),
|
|
1440
|
+
onNone: () => emitNullScoreVerdictWhenOpen(stream, mode, basePath, pathService)
|
|
1033
1441
|
})), Match.tag("RunParseFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunSurvivorsRejected", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunConfigFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.exhaustive);
|
|
1034
1442
|
}
|
|
1035
1443
|
return yield* offerFailureEnvelope(stream, outcome.failure, captured);
|
|
@@ -1066,59 +1474,16 @@ const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(funct
|
|
|
1066
1474
|
}));
|
|
1067
1475
|
//#endregion
|
|
1068
1476
|
//#region src/Survivors.ts
|
|
1069
|
-
/**
|
|
1070
|
-
* Survivors — the survivors-admission capability.
|
|
1071
|
-
*
|
|
1072
|
-
* The prior-report decoding, source hashing, mutant conversion, and admission
|
|
1073
|
-
* pipeline for --survivors runs. Pure admission decision lives in
|
|
1074
|
-
* admit-survivors-run.workflow.ts.
|
|
1075
|
-
*/
|
|
1076
1477
|
const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
|
|
1077
|
-
/**
|
|
1078
|
-
* The mutant shape the admission carries, named once because both the decision's
|
|
1079
|
-
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
1080
|
-
*/
|
|
1081
|
-
/**
|
|
1082
|
-
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
1083
|
-
* get the decode function, not the schema, so the report's wire shape is not a
|
|
1084
|
-
* surface commitment and the codec has exactly one caller.
|
|
1085
|
-
*
|
|
1086
|
-
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
1087
|
-
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
1088
|
-
* must not be refused for carrying one.
|
|
1089
|
-
*/
|
|
1090
|
-
/**
|
|
1091
|
-
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
1092
|
-
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
1093
|
-
* runs. A malformed report therefore never reaches the decider, and nothing here casts
|
|
1094
|
-
* a third-party report type.
|
|
1095
|
-
*/
|
|
1096
1478
|
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
1097
1479
|
const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
|
|
1098
|
-
|
|
1099
|
-
* Thin by design: the digest is the caller's capability, and naming the call
|
|
1100
|
-
* keeps every hashing site in the admission path reading the same way.
|
|
1101
|
-
*/
|
|
1480
|
+
const EMPTY_CONFIG = {};
|
|
1102
1481
|
function sourceContentHash(content, hash) {
|
|
1103
1482
|
return hash(content);
|
|
1104
1483
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* The per-file source hashes of the sources a prior report embeds.
|
|
1107
|
-
*
|
|
1108
|
-
* The current run's side of the comparison is gathered by the shell from disk;
|
|
1109
|
-
* this is the recorded side, read back out of the report.
|
|
1110
|
-
*/
|
|
1111
1484
|
function priorSourceHashes(priorReport, hashContent) {
|
|
1112
1485
|
return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
|
|
1113
1486
|
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Converts a report mutant (1-based schema location) into the internal mutant
|
|
1116
|
-
* shape a run consumes (0-based positions, absolute file name) — the exact
|
|
1117
|
-
* inverse of `objectUtils.toSchemaLocation` and the same shift the
|
|
1118
|
-
* incremental report reader applies (`project-reader.ts`). Mutants without a
|
|
1119
|
-
* replacement fall back to their mutator name, the same convention the
|
|
1120
|
-
* incremental differ uses.
|
|
1121
|
-
*/
|
|
1122
1487
|
function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
1123
1488
|
return Mutant.make({
|
|
1124
1489
|
id: mutant.id,
|
|
@@ -1137,34 +1502,11 @@ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
|
|
|
1137
1502
|
}
|
|
1138
1503
|
});
|
|
1139
1504
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
* The survivors of the prior report: exactly the mutants whose status is
|
|
1142
|
-
* `Survived`, converted to the internal mutant shape so a run can re-test
|
|
1143
|
-
* them.
|
|
1144
|
-
*/
|
|
1145
1505
|
function extractSurvivors(priorReport, resolveAbsolutePath) {
|
|
1146
|
-
|
|
1147
|
-
for (const [file, fileResult] of objectEntries(priorReport.files)) for (const mutant of fileResult.mutants) if (mutant.status === "Survived") survivors.push(reportMutantToMutant(file, mutant, resolveAbsolutePath));
|
|
1148
|
-
return survivors;
|
|
1506
|
+
return objectEntries(priorReport.files).flatMap(([file, fileResult]) => fileResult.mutants.filter((mutant) => mutant.status === "Survived").map((mutant) => reportMutantToMutant(file, mutant, resolveAbsolutePath)));
|
|
1149
1507
|
}
|
|
1150
|
-
/**
|
|
1151
|
-
* The survivor spans as `file:startLine:startCol-endLine:endCol` mutate
|
|
1152
|
-
* ranges: the report's 1-based lines with the internal 0-based columns,
|
|
1153
|
-
* relative file names, deduplicated in first-seen order.
|
|
1154
|
-
*/
|
|
1155
1508
|
function survivorMutateSpans(survivors, basePath) {
|
|
1156
|
-
|
|
1157
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1158
|
-
for (const survivor of survivors) {
|
|
1159
|
-
const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
|
|
1160
|
-
const { start, end } = survivor.location;
|
|
1161
|
-
const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
|
|
1162
|
-
if (!seen.has(span)) {
|
|
1163
|
-
seen.add(span);
|
|
1164
|
-
spans.push(span);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
return spans;
|
|
1509
|
+
return [...new Set(survivors.map((survivor) => `${toRelativeNormalizedFileName(survivor.fileName, basePath)}:${survivor.location.start.line + 1}:${survivor.location.start.column}-${survivor.location.end.line + 1}:${survivor.location.end.column}`))];
|
|
1168
1510
|
}
|
|
1169
1511
|
const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
|
|
1170
1512
|
const survivorsAdmissionCell = (basePath) => Cell.layer({
|
|
@@ -1195,8 +1537,8 @@ const survivorsAdmissionCell = (basePath) => Cell.layer({
|
|
|
1195
1537
|
}));
|
|
1196
1538
|
return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
|
|
1197
1539
|
priorReport: PriorReportFacts.make({
|
|
1198
|
-
config: document.config
|
|
1199
|
-
frameworkVersion: document.framework
|
|
1540
|
+
config: Option.getOrElse(Option.fromNullishOr(document.config), () => EMPTY_CONFIG),
|
|
1541
|
+
frameworkVersion: Option.getOrUndefined(Option.flatMap(Option.fromNullishOr(document.framework), (framework) => Option.fromNullishOr(framework.version)))
|
|
1200
1542
|
}),
|
|
1201
1543
|
currentConfig: resolvedOptions,
|
|
1202
1544
|
frameworkVersion: strykerVersion,
|
|
@@ -1245,11 +1587,7 @@ function readPriorReport(priorReportPath) {
|
|
|
1245
1587
|
});
|
|
1246
1588
|
}
|
|
1247
1589
|
function priorReportFileKeys(raw) {
|
|
1248
|
-
|
|
1249
|
-
if (!("files" in raw)) return [];
|
|
1250
|
-
const files = raw.files;
|
|
1251
|
-
if (typeof files !== "object" || files === null || Array.isArray(files)) return [];
|
|
1252
|
-
return Object.keys(files);
|
|
1590
|
+
return Option.getOrElse(Option.map(Option.flatMap(Option.liftPredicate(raw, Match.record), (document) => Option.liftPredicate(document["files"], Match.record)), (files) => Object.keys(files)), () => []);
|
|
1253
1591
|
}
|
|
1254
1592
|
function readSourceFile(file) {
|
|
1255
1593
|
return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
|
|
@@ -1266,14 +1604,6 @@ const SIGNAL_NUMBERS = Object.freeze({
|
|
|
1266
1604
|
SIGINT: 2,
|
|
1267
1605
|
SIGTERM: 15
|
|
1268
1606
|
});
|
|
1269
|
-
/**
|
|
1270
|
-
* Installs the listeners and returns the reader.
|
|
1271
|
-
*
|
|
1272
|
-
* The listener records and returns: interrupting the run is the runtime's job,
|
|
1273
|
-
* and doing it from here would race the run's own finalizer for the stream.
|
|
1274
|
-
* `once` per signal, because a second delivery of the same signal cannot
|
|
1275
|
-
* change the answer.
|
|
1276
|
-
*/
|
|
1277
1607
|
function observeTerminatingSignal() {
|
|
1278
1608
|
let observed = null;
|
|
1279
1609
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
@@ -1281,269 +1611,23 @@ function observeTerminatingSignal() {
|
|
|
1281
1611
|
});
|
|
1282
1612
|
return () => observed;
|
|
1283
1613
|
}
|
|
1284
|
-
function isObject(value) {
|
|
1285
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1286
|
-
}
|
|
1287
|
-
function stringField(node, key) {
|
|
1288
|
-
const value = node[key];
|
|
1289
|
-
return (() => {
|
|
1290
|
-
if (typeof value === "string") return value;
|
|
1291
|
-
})();
|
|
1292
|
-
}
|
|
1293
|
-
function stringArrayField(node, key) {
|
|
1294
|
-
const value = node[key];
|
|
1295
|
-
if (!Array.isArray(value)) return [];
|
|
1296
|
-
const strings = [];
|
|
1297
|
-
for (const item of value) if (typeof item === "string") strings.push(item);
|
|
1298
|
-
return strings;
|
|
1299
|
-
}
|
|
1300
|
-
/** The compiled shapes discriminate on `_tag`; read it once, off the record. */
|
|
1301
|
-
function tagOf(node) {
|
|
1302
|
-
return node["_tag"];
|
|
1303
|
-
}
|
|
1304
|
-
function walkParam(param, isOptional, out) {
|
|
1305
|
-
if (!isObject(param)) return;
|
|
1306
|
-
switch (tagOf(param)) {
|
|
1307
|
-
case "Single":
|
|
1308
|
-
describeSingle(param, isOptional, out);
|
|
1309
|
-
return;
|
|
1310
|
-
case "Map":
|
|
1311
|
-
case "Transform":
|
|
1312
|
-
walkParam(param["param"], isOptional, out);
|
|
1313
|
-
return;
|
|
1314
|
-
case "Optional":
|
|
1315
|
-
walkParam(param["param"], true, out);
|
|
1316
|
-
return;
|
|
1317
|
-
case "Variadic":
|
|
1318
|
-
walkParam(param["param"], isOptional, out);
|
|
1319
|
-
return;
|
|
1320
|
-
default: return;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
const PRIMITIVE_KIND = {
|
|
1324
|
-
Boolean: "boolean",
|
|
1325
|
-
Choice: "choice",
|
|
1326
|
-
Date: "date",
|
|
1327
|
-
FileParse: "file",
|
|
1328
|
-
FileSchema: "file",
|
|
1329
|
-
FileText: "file",
|
|
1330
|
-
Float: "float",
|
|
1331
|
-
Integer: "integer",
|
|
1332
|
-
KeyValuePair: "key=value",
|
|
1333
|
-
None: "none",
|
|
1334
|
-
Path: "path",
|
|
1335
|
-
Redacted: "redacted",
|
|
1336
|
-
String: "text"
|
|
1337
|
-
};
|
|
1338
|
-
function kindOf(primitive) {
|
|
1339
|
-
const tag = stringField(primitive, "_tag");
|
|
1340
|
-
return (() => {
|
|
1341
|
-
if (tag === void 0) return "unknown";
|
|
1342
|
-
return PRIMITIVE_KIND[tag] ?? tag;
|
|
1343
|
-
})();
|
|
1344
|
-
}
|
|
1345
|
-
function choiceValues(primitive) {
|
|
1346
|
-
const keys = primitive["choiceKeys"];
|
|
1347
|
-
if (!Array.isArray(keys)) return;
|
|
1348
|
-
const values = [];
|
|
1349
|
-
for (const key of keys) if (typeof key === "string") values.push(key);
|
|
1350
|
-
return values;
|
|
1351
|
-
}
|
|
1352
|
-
const REPORTER_NAMES = [
|
|
1353
|
-
"clear-text",
|
|
1354
|
-
"progress",
|
|
1355
|
-
"html",
|
|
1356
|
-
"json",
|
|
1357
|
-
"progress-stream"
|
|
1358
|
-
];
|
|
1359
|
-
/**
|
|
1360
|
-
* v4 option descriptions are stored as `Option.some(string)` on the compiled
|
|
1361
|
-
* `Single`; the walker unwraps the option.
|
|
1362
|
-
*/
|
|
1363
|
-
function descriptionOf(single) {
|
|
1364
|
-
const description = single["description"];
|
|
1365
|
-
if (!isObject(description)) return "";
|
|
1366
|
-
switch (tagOf(description)) {
|
|
1367
|
-
case "Some": {
|
|
1368
|
-
const value = description["value"];
|
|
1369
|
-
return (() => {
|
|
1370
|
-
if (typeof value === "string") return value;
|
|
1371
|
-
return "";
|
|
1372
|
-
})();
|
|
1373
|
-
}
|
|
1374
|
-
default: return "";
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
function describeSingle(single, isOptional, out) {
|
|
1378
|
-
const name = stringField(single, "name") ?? "";
|
|
1379
|
-
const primitive = (() => {
|
|
1380
|
-
if (isObject(single["primitiveType"])) return single["primitiveType"];
|
|
1381
|
-
return {};
|
|
1382
|
-
})();
|
|
1383
|
-
const kind = kindOf(primitive);
|
|
1384
|
-
const choices = (() => {
|
|
1385
|
-
if (name === "reporters") return REPORTER_NAMES;
|
|
1386
|
-
if (kind === "choice") return choiceValues(primitive);
|
|
1387
|
-
})();
|
|
1388
|
-
const description = descriptionOf(single);
|
|
1389
|
-
const required = kind !== "boolean" && !isOptional;
|
|
1390
|
-
const described = {
|
|
1391
|
-
name,
|
|
1392
|
-
aliases: stringArrayField(single, "aliases"),
|
|
1393
|
-
kind,
|
|
1394
|
-
required,
|
|
1395
|
-
...(() => {
|
|
1396
|
-
if (choices !== void 0) return { choices };
|
|
1397
|
-
return {};
|
|
1398
|
-
})(),
|
|
1399
|
-
description
|
|
1400
|
-
};
|
|
1401
|
-
if (single["kind"] === "argument") {
|
|
1402
|
-
out.args.push({
|
|
1403
|
-
name,
|
|
1404
|
-
kind,
|
|
1405
|
-
required,
|
|
1406
|
-
description
|
|
1407
|
-
});
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
out.flags.push(described);
|
|
1411
|
-
}
|
|
1412
|
-
function walkConfigNode(node, orderedParams, out) {
|
|
1413
|
-
if (!isObject(node)) return;
|
|
1414
|
-
switch (tagOf(node)) {
|
|
1415
|
-
case "Param": {
|
|
1416
|
-
const index = node["index"];
|
|
1417
|
-
const param = (() => {
|
|
1418
|
-
if (typeof index === "number") return orderedParams[index];
|
|
1419
|
-
})();
|
|
1420
|
-
if (param !== void 0) walkParam(param, false, out);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
case "Array":
|
|
1424
|
-
if (Array.isArray(node["children"])) for (const child of node["children"]) walkConfigNode(child, orderedParams, out);
|
|
1425
|
-
return;
|
|
1426
|
-
case "Nested":
|
|
1427
|
-
if (isObject(node["tree"])) walkConfigTree(node["tree"], orderedParams, out);
|
|
1428
|
-
return;
|
|
1429
|
-
default: return;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
function walkConfigTree(tree, orderedParams, out) {
|
|
1433
|
-
for (const key of Object.keys(tree)) walkConfigNode(tree[key], orderedParams, out);
|
|
1434
|
-
}
|
|
1435
|
-
function describeCommandNode(node) {
|
|
1436
|
-
if (!isObject(node)) return;
|
|
1437
|
-
const out = {
|
|
1438
|
-
flags: [],
|
|
1439
|
-
args: []
|
|
1440
|
-
};
|
|
1441
|
-
const config = node["config"];
|
|
1442
|
-
if (isObject(config) && isObject(config["tree"])) {
|
|
1443
|
-
let orderedParams = [];
|
|
1444
|
-
const maybeOrdered = config["orderedParams"];
|
|
1445
|
-
if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
|
|
1446
|
-
walkConfigTree(config["tree"], orderedParams, out);
|
|
1447
|
-
}
|
|
1448
|
-
const subcommands = [];
|
|
1449
|
-
const grouped = node["subcommands"];
|
|
1450
|
-
if (Array.isArray(grouped)) for (const group of grouped) {
|
|
1451
|
-
if (!isObject(group) || !Array.isArray(group["commands"])) continue;
|
|
1452
|
-
for (const child of group["commands"]) {
|
|
1453
|
-
const described = describeCommandNode(child);
|
|
1454
|
-
if (described !== void 0) subcommands.push(described);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
return {
|
|
1458
|
-
name: stringField(node, "name") ?? "",
|
|
1459
|
-
description: (() => {
|
|
1460
|
-
if (typeof node["description"] === "string") return node["description"];
|
|
1461
|
-
return "";
|
|
1462
|
-
})(),
|
|
1463
|
-
options: out.flags,
|
|
1464
|
-
args: out.args,
|
|
1465
|
-
subcommands
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
function readCoreEntries() {
|
|
1469
|
-
return Effect.gen(function* () {
|
|
1470
|
-
const path = yield* Path.Path;
|
|
1471
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1472
|
-
const resolved = import.meta.resolve("@systemfsoftware/stryker-js-engine/package.json");
|
|
1473
|
-
const manifestPath = yield* path.fromFileUrl(new URL(resolved));
|
|
1474
|
-
const raw = yield* fs.readFileString(manifestPath);
|
|
1475
|
-
let parsed;
|
|
1476
|
-
try {
|
|
1477
|
-
parsed = JSON.parse(raw);
|
|
1478
|
-
} catch {
|
|
1479
|
-
return [];
|
|
1480
|
-
}
|
|
1481
|
-
const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
|
|
1482
|
-
const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
|
|
1483
|
-
if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
|
|
1484
|
-
return [];
|
|
1485
|
-
}).pipe(Effect.catchCause(() => {
|
|
1486
|
-
return Effect.succeed([]);
|
|
1487
|
-
}));
|
|
1488
|
-
}
|
|
1489
|
-
function buildLLMSManifest(command, version) {
|
|
1490
|
-
return Effect.gen(function* () {
|
|
1491
|
-
const root = describeCommandNode(command) ?? {
|
|
1492
|
-
name: "",
|
|
1493
|
-
description: "",
|
|
1494
|
-
options: [],
|
|
1495
|
-
args: [],
|
|
1496
|
-
subcommands: []
|
|
1497
|
-
};
|
|
1498
|
-
const entries = yield* readCoreEntries();
|
|
1499
|
-
return {
|
|
1500
|
-
schemaVersion: "1.0",
|
|
1501
|
-
tool: root.name,
|
|
1502
|
-
version,
|
|
1503
|
-
commands: [root],
|
|
1504
|
-
entries
|
|
1505
|
-
};
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
function emitLLMSManifest(command, version) {
|
|
1509
|
-
return Effect.map(buildLLMSManifest(command, version), (manifest) => JSON.stringify(manifest));
|
|
1510
|
-
}
|
|
1511
1614
|
function createSplitter(separator) {
|
|
1512
1615
|
return (value) => value.split(separator).filter(Boolean);
|
|
1513
1616
|
}
|
|
1514
1617
|
const splitOnComma = createSplitter(",");
|
|
1515
1618
|
const splitOnSpace = createSplitter(" ");
|
|
1516
|
-
|
|
1517
|
-
* Commander characterization: `always` stays a string, everything else is a
|
|
1518
|
-
* boolean where `false`/`0` (case-insensitively) mean `false` — a tri-state a
|
|
1519
|
-
* plain boolean or choice would flatten.
|
|
1520
|
-
*/
|
|
1619
|
+
const CLEAN_TEMP_DIR_DISABLED = ["false", "0"];
|
|
1521
1620
|
function parseCleanDirOption(value) {
|
|
1522
|
-
const
|
|
1523
|
-
return (() =>
|
|
1524
|
-
if (v === "always") return v;
|
|
1525
|
-
return v !== "false" && v !== "0";
|
|
1526
|
-
})();
|
|
1621
|
+
const normalized = value.toLocaleLowerCase();
|
|
1622
|
+
return Match.value(normalized).pipe(Match.when("always", () => "always"), Match.orElse(() => !CLEAN_TEMP_DIR_DISABLED.some((disabled) => disabled === normalized)));
|
|
1527
1623
|
}
|
|
1528
|
-
/**
|
|
1529
|
-
* Commander characterization: a pure integer is parsed as a number, anything
|
|
1530
|
-
* else (e.g. `"50%"`) stays a string.
|
|
1531
|
-
*/
|
|
1532
1624
|
function parseConcurrency(value) {
|
|
1533
1625
|
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
1534
1626
|
return value;
|
|
1535
1627
|
}
|
|
1536
1628
|
const optional = (option) => Flag.optional(option);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
* `undefined` as absent but an explicit `false` would override a config-file
|
|
1540
|
-
* `true`. `Flag.optional` yields `Option.none` when the flag is absent and
|
|
1541
|
-
* `Option.some(false)` for an explicit `--no-x`, so both map back to
|
|
1542
|
-
* `undefined` and leave the config-file default in force (KTD4).
|
|
1543
|
-
*/
|
|
1544
|
-
const absentWhenFalse = (value) => {
|
|
1545
|
-
if (Option.isSome(value) && value.value) return true;
|
|
1546
|
-
};
|
|
1629
|
+
const absentWhenFalse = (value) => Option.getOrUndefined(Option.filter(value, (present) => present));
|
|
1630
|
+
const isUnknownArgument = (argument) => Option.exists(Option.fromUndefinedOr(argument), (text) => text.startsWith("-"));
|
|
1547
1631
|
function setLogLevel(target, key, value) {
|
|
1548
1632
|
const unwrapped = unwrap(value);
|
|
1549
1633
|
if (unwrapped !== void 0) target[key] = unwrapped;
|
|
@@ -1607,7 +1691,11 @@ const runConfig = {
|
|
|
1607
1691
|
...runOptions,
|
|
1608
1692
|
...runArgs
|
|
1609
1693
|
};
|
|
1610
|
-
const
|
|
1694
|
+
const mergeReportsOptions = {
|
|
1695
|
+
parts: Flag.string("parts").pipe(Flag.withDescription("The directory holding the downloaded mutation report parts (any download layout).")),
|
|
1696
|
+
out: Flag.string("out").pipe(Flag.withDescription("The directory to write the merged report, its html view, and the summary into.")),
|
|
1697
|
+
packages: Flag.string("packages").pipe(Flag.withDescription("A JSON array of the package names the run expected, falling back to the PACKAGES environment variable."), optional)
|
|
1698
|
+
};
|
|
1611
1699
|
function unwrap(value) {
|
|
1612
1700
|
if (Option.isOption(value)) return Option.match(value, {
|
|
1613
1701
|
onNone: () => void 0,
|
|
@@ -1619,30 +1707,16 @@ function setIfPresent(target, key, value) {
|
|
|
1619
1707
|
const unwrapped = unwrap(value);
|
|
1620
1708
|
if (unwrapped !== void 0) target[key] = unwrapped;
|
|
1621
1709
|
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Builds the full command tree — root plus the `run` subcommand — from the
|
|
1624
|
-
* same option/arg records the parser matches against. Each handler leaves the
|
|
1625
|
-
* request the executor runs: the run handler writes the parsed options (and
|
|
1626
|
-
* the survivors flag, which the admission consumes and the pipeline must not
|
|
1627
|
-
* see), the `--llms` handler writes the pre-rendered manifest document, and
|
|
1628
|
-
* the bare root writes nothing — `helpRequested` makes the framework render
|
|
1629
|
-
* help, which the executor's finalizer turns into the `help` terminal event.
|
|
1630
|
-
*/
|
|
1631
1710
|
function makeStrykerCommand(requestRef) {
|
|
1632
1711
|
const runCommand = Command.make("run", runConfig, (config) => {
|
|
1633
1712
|
const configFile = Option.getOrUndefined(config.configFile);
|
|
1634
|
-
if (configFile
|
|
1713
|
+
if (isUnknownArgument(configFile)) return Console.error(`Received unknown argument: '${configFile}'`).pipe(Effect.andThen(Effect.failSync(() => CliError.UnexpectedArgument.make({ arguments: [configFile] }))));
|
|
1635
1714
|
return Ref.set(requestRef, Option.some({
|
|
1636
1715
|
_tag: "run",
|
|
1637
1716
|
options: readStrykerOptions(config),
|
|
1638
1717
|
survivors: config.survivors === true
|
|
1639
1718
|
}));
|
|
1640
1719
|
}).pipe(Command.withDescription("Run mutation testing"));
|
|
1641
|
-
/**
|
|
1642
|
-
* Rebuilds the `PartialStrykerOptions` object commander produced: only options
|
|
1643
|
-
* actually given on the command line become keys (KTD4). `survivors` is
|
|
1644
|
-
* deliberately not forwarded — the survivor re-run logic (U8) consumes it.
|
|
1645
|
-
*/
|
|
1646
1720
|
function readStrykerOptions(config) {
|
|
1647
1721
|
const options = {};
|
|
1648
1722
|
setIfPresent(options, "ignorePatterns", config.ignorePatterns);
|
|
@@ -1679,25 +1753,18 @@ function makeStrykerCommand(requestRef) {
|
|
|
1679
1753
|
if (Option.isSome(config["configFile"])) options["configFile"] = config["configFile"].value;
|
|
1680
1754
|
return options;
|
|
1681
1755
|
}
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
};
|
|
1690
|
-
return yield* Ref.set(requestRef, Option.some({
|
|
1691
|
-
_tag: "llms",
|
|
1692
|
-
document
|
|
1693
|
-
}));
|
|
1694
|
-
}
|
|
1756
|
+
const mergeReportsCommand = Command.make("merge-reports", mergeReportsOptions, (config) => Ref.set(requestRef, Option.some({
|
|
1757
|
+
_tag: "merge-reports",
|
|
1758
|
+
parts: config.parts,
|
|
1759
|
+
out: config.out,
|
|
1760
|
+
packages: Option.getOrUndefined(config.packages) ?? process.env["PACKAGES"]
|
|
1761
|
+
}))).pipe(Command.withDescription("Merge per-package mutation reports into one report"));
|
|
1762
|
+
return Command.make("stryker", {}, (_config) => Effect.gen(function* () {
|
|
1695
1763
|
return yield* Effect.failSync(() => CliError.ShowHelp.make({
|
|
1696
1764
|
commandPath: ["stryker"],
|
|
1697
1765
|
errors: []
|
|
1698
1766
|
}));
|
|
1699
|
-
})).pipe(Command.withSubcommands([runCommand]));
|
|
1700
|
-
return strykerCommand;
|
|
1767
|
+
})).pipe(Command.withSubcommands([runCommand, mergeReportsCommand]));
|
|
1701
1768
|
}
|
|
1702
1769
|
const terminalLayer = Layer.succeed(Terminal.Terminal, Terminal.make({
|
|
1703
1770
|
columns: Effect.sync(() => process.stdout.columns),
|
|
@@ -1748,10 +1815,19 @@ function hostOptionsOf(mode, stream) {
|
|
|
1748
1815
|
resolvedMode: mode,
|
|
1749
1816
|
runStartedAt: stream.startedAt,
|
|
1750
1817
|
basePath: process.cwd(),
|
|
1751
|
-
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter")
|
|
1818
|
+
reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter")],
|
|
1752
1819
|
allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
|
|
1753
1820
|
};
|
|
1754
1821
|
}
|
|
1822
|
+
const isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
1823
|
+
const progressStreamFileName = (request) => Option.match(request, {
|
|
1824
|
+
onNone: () => DEFAULT_PROGRESS_STREAM_FILE,
|
|
1825
|
+
onSome: (cliRequest) => Match.value(cliRequest).pipe(Match.tag("merge-reports", () => DEFAULT_PROGRESS_STREAM_FILE), Match.tag("run", (runRequest) => Option.getOrElse(Option.filter(Option.fromNullishOr(runRequest.options["progressStreamFile"]), isNonEmptyString), () => DEFAULT_PROGRESS_STREAM_FILE)), Match.exhaustive)
|
|
1826
|
+
});
|
|
1827
|
+
const applyProgressStreamFile = (stream, fileName) => Option.match(Option.fromUndefinedOr(stream.setProgressStreamFile), {
|
|
1828
|
+
onNone: () => Effect.void,
|
|
1829
|
+
onSome: (setFileName) => setFileName(fileName)
|
|
1830
|
+
});
|
|
1755
1831
|
const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
1756
1832
|
const stream = yield* createRunEventStream(input.mode);
|
|
1757
1833
|
const hostOptions = hostOptionsOf(input.mode, stream);
|
|
@@ -1764,7 +1840,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
|
1764
1840
|
process.removeListener("SIGTERM", onSignal);
|
|
1765
1841
|
if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
|
|
1766
1842
|
};
|
|
1767
|
-
const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
|
|
1843
|
+
const dispatch = (request) => Match.value(request).pipe(Match.tag("merge-reports", (mergeRequest) => runMergeReports(mergeRequest).pipe(Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)))), Match.tag("run", (runRequest) => (() => {
|
|
1768
1844
|
if (runRequest.survivors) return Effect.gen(function* () {
|
|
1769
1845
|
const { admission, resolvedOptions, priorReportPath } = yield* runSurvivorsAdmission(runRequest.options, basePath).pipe(Effect.provide(hostRunLayer(hostOptions)));
|
|
1770
1846
|
return yield* Match.value(admission).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, input.mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
|
|
@@ -1780,18 +1856,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
|
1780
1856
|
}), Match.exhaustive);
|
|
1781
1857
|
});
|
|
1782
1858
|
return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
|
|
1783
|
-
})()), Match.
|
|
1784
|
-
stream.ensureOpen({
|
|
1785
|
-
mode: "machine",
|
|
1786
|
-
signal: "flag",
|
|
1787
|
-
stdoutIsTTY: process.stdout.isTTY === true
|
|
1788
|
-
});
|
|
1789
|
-
yield* Queue.offer(stream.queue, ManifestRendered.make({
|
|
1790
|
-
schemaVersion: "1.0",
|
|
1791
|
-
code: 0,
|
|
1792
|
-
manifest: llmsRequest.document.manifest
|
|
1793
|
-
}));
|
|
1794
|
-
})), Match.orElse(() => Effect.die("unreachable cli request variant")));
|
|
1859
|
+
})()), Match.exhaustive);
|
|
1795
1860
|
const program = Effect.acquireUseRelease(Effect.sync(() => {
|
|
1796
1861
|
currentFiber = Fiber.getCurrent() ?? null;
|
|
1797
1862
|
process.on("SIGINT", onSignal);
|
|
@@ -1799,15 +1864,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
|
1799
1864
|
}), () => Effect.gen(function* () {
|
|
1800
1865
|
const parsed = yield* Effect.result(input.program);
|
|
1801
1866
|
const request = yield* Ref.get(input.requestRef);
|
|
1802
|
-
|
|
1803
|
-
onNone: () => DEFAULT_PROGRESS_STREAM_FILE,
|
|
1804
|
-
onSome: (cliRequest) => Match.value(cliRequest).pipe(Match.tag("run", (runRequest) => {
|
|
1805
|
-
const fromCli = runRequest.options["progressStreamFile"];
|
|
1806
|
-
if (typeof fromCli === "string" && fromCli.length > 0) return fromCli;
|
|
1807
|
-
return DEFAULT_PROGRESS_STREAM_FILE;
|
|
1808
|
-
}), Match.orElse(() => DEFAULT_PROGRESS_STREAM_FILE))
|
|
1809
|
-
});
|
|
1810
|
-
if (stream.setProgressStreamFile !== void 0) yield* stream.setProgressStreamFile(fileName);
|
|
1867
|
+
yield* applyProgressStreamFile(stream, progressStreamFileName(request));
|
|
1811
1868
|
yield* stream.open;
|
|
1812
1869
|
if (Result.isFailure(parsed)) return yield* Effect.fail(parsed.failure);
|
|
1813
1870
|
return yield* Option.match(request, {
|
|
@@ -1829,14 +1886,18 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
|
|
|
1829
1886
|
//#endregion
|
|
1830
1887
|
//#region src/main.ts
|
|
1831
1888
|
const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
|
|
1889
|
+
/** The major version `engines.node` floors the CLI at. */
|
|
1890
|
+
const SUPPORTED_NODE_MAJOR = 20;
|
|
1832
1891
|
process.title = "stryker";
|
|
1833
1892
|
const lastSignal = observeTerminatingSignal();
|
|
1893
|
+
/** The numbers a `process.version` names; a component that does not parse stays `NaN`. */
|
|
1894
|
+
const versionNumbers = (version) => version.replace(/^v/, "").split(/[-+]/).slice(0, 1).flatMap((base) => base.split(".")).map((part) => Number.parseInt(part, 10));
|
|
1895
|
+
const componentAt = (numbers, index) => Option.getOrElse(Option.fromUndefinedOr(numbers[index]), () => 0);
|
|
1896
|
+
/** Every reason the CLI refuses the running Node.js, checked against the parsed version numbers. */
|
|
1897
|
+
const NODE_VERSION_REJECTIONS = [(numbers) => numbers.some(Number.isNaN), (numbers) => componentAt(numbers, 0) < SUPPORTED_NODE_MAJOR];
|
|
1834
1898
|
function isSupportedNodeVersion(version) {
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
if (major !== 20) return major > 20;
|
|
1838
|
-
if (minor !== 0) return minor > 0;
|
|
1839
|
-
return patch >= 0;
|
|
1899
|
+
const numbers = versionNumbers(version);
|
|
1900
|
+
return !NODE_VERSION_REJECTIONS.some((rejects) => rejects(numbers));
|
|
1840
1901
|
}
|
|
1841
1902
|
if (!isSupportedNodeVersion(process.version)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${engines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
|
|
1842
1903
|
const program = Effect.gen(function* () {
|
|
@@ -1844,17 +1905,12 @@ const program = Effect.gen(function* () {
|
|
|
1844
1905
|
const runEvents = yield* RunEventStreamPort;
|
|
1845
1906
|
return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
|
|
1846
1907
|
}).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
|
|
1908
|
+
const publishedExitCode = (exit) => Option.flatMap(Option.liftPredicate(exit, Exit.isSuccess), (succeeded) => Option.liftPredicate(succeeded.value, (value) => typeof value === "number"));
|
|
1909
|
+
const signalExitCode = (signal) => Option.getOrElse(Option.map(Option.fromNullishOr(signal), (stopped) => 128 + stopped), () => EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
|
|
1910
|
+
const teardownExitCode = (exit, signal) => Option.getOrElse(publishedExitCode(exit), () => signalExitCode(signal));
|
|
1847
1911
|
NodeRuntime.runMain(program, {
|
|
1848
1912
|
disableErrorReporting: true,
|
|
1849
|
-
teardown: (exit, onExit) =>
|
|
1850
|
-
if (Exit.isSuccess(exit) && typeof exit.value === "number") {
|
|
1851
|
-
onExit(exit.value);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
const signal = lastSignal();
|
|
1855
|
-
if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
|
|
1856
|
-
else onExit(128 + signal);
|
|
1857
|
-
}
|
|
1913
|
+
teardown: (exit, onExit) => onExit(teardownExitCode(exit, lastSignal()))
|
|
1858
1914
|
});
|
|
1859
1915
|
//#endregion
|
|
1860
1916
|
export {};
|