@systemfsoftware/stryker-js 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4724 +0,0 @@
1
- import { $ as PrepareError, D as layer, L as StrykerError, O as makeConcurrency, R as ALWAYS_IGNORE, T as IdGenerator, V as ConfigError, _ as validateOptions, a as loadPlugins, b as makeChildProcessTestRunner, et as StageError, h as readConfig, k as ChildProcessCrashedError, n as resolvePluginWorkerEntry, t as missingWorkerEntry, u as forkCoreSchema, v as buildTestRunner, w as makeWorkerClient, x as nodePlatformLayer, z as MUTATION_RANGE_REGEX } from "./plugin-worker-entry-DNu0SHO8.mjs";
2
- import { n as ansi, t as makeBuiltinReporterFactories } from "./builtin-reporters-wAuKFSsd.mjs";
3
- import * as Effect from "effect/Effect";
4
- import * as FileSystem from "effect/FileSystem";
5
- import * as Path from "effect/Path";
6
- import * as Stdio from "effect/Stdio";
7
- import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
8
- import { CheckerRpcs, DryRunCompleted, ExitClass, ExitClass as ExitClass$1, MutantTested, MutationTestReportReady, MutationTestingPlanReady, ReporterFailed, ReporterRpcs, StrykerOptionsSchema, TraceContextReference, formatTraceparent } from "@systemfsoftware/stryker-js-plugin-interface";
9
- import * as Clock from "effect/Clock";
10
- import * as Config from "effect/Config";
11
- import * as Context from "effect/Context";
12
- import * as DateTime from "effect/DateTime";
13
- import * as Fiber from "effect/Fiber";
14
- import * as Layer from "effect/Layer";
15
- import * as Match from "effect/Match";
16
- import * as Option from "effect/Option";
17
- import * as Queue from "effect/Queue";
18
- import * as Result from "effect/Result";
19
- import * as S from "effect/Schema";
20
- import * as Stream from "effect/Stream";
21
- import * as CliError from "effect/unstable/cli/CliError";
22
- import * as Runtime from "effect/Runtime";
23
- import { LocationSchema, Mutant, MutantStatusSchema, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js-instrumenter/mutants";
24
- import * as Arr from "effect/Array";
25
- import "effect/Cause";
26
- import * as Console from "effect/Console";
27
- import * as Exit from "effect/Exit";
28
- import * as Formatter from "effect/Formatter";
29
- import * as Predicate from "effect/Predicate";
30
- import * as Scope from "effect/Scope";
31
- import * as Duration from "effect/Duration";
32
- import * as MutableHashMap from "effect/MutableHashMap";
33
- import * as MutableHashSet from "effect/MutableHashSet";
34
- import { Schema } from "effect";
35
- import * as HashMap from "effect/HashMap";
36
- import * as HashSet from "effect/HashSet";
37
- import { Minimatch, minimatch } from "minimatch";
38
- import { encodeWorkerOptions, partsOfEffectSpan } from "@systemfsoftware/stryker-js-plugin-runtime";
39
- import * as Ref from "effect/Ref";
40
- import * as ChildProcess from "effect/unstable/process/ChildProcess";
41
- import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
42
- import * as Data from "effect/Data";
43
- import { disableTypeChecks, instrument } from "@systemfsoftware/stryker-js-instrumenter";
44
- import * as Equivalence from "effect/Equivalence";
45
- import * as Pool from "effect/Pool";
46
- import * as Semaphore from "effect/Semaphore";
47
- import { randomBytes } from "@noble/hashes/utils.js";
48
- import { makeHtmlReporter } from "@systemfsoftware/stryker-js-html-reporter";
49
- //#region src/classify-run-outcome.workflow.ts
50
- var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
51
- succeeded: S.Boolean,
52
- interrupted: S.Boolean,
53
- helpErrorCount: S.optional(S.Finite),
54
- cliError: S.Boolean,
55
- unrecognized: S.optional(S.String),
56
- survivorsReason: S.optional(S.Literals(["no-report", "mismatch"])),
57
- survivorsDiagnostic: S.optional(S.String),
58
- schemaError: S.Boolean,
59
- successExitClass: S.optional(ExitClass),
60
- highestExitClass: S.optional(ExitClass),
61
- configDetail: S.optional(S.String),
62
- diagnostic: S.optional(S.String)
63
- }) {};
64
- S.TaggedError()("RunExit", { code: S.Finite }), Runtime.errorExitCode;
65
- const CONFIG_CODE = 2;
66
- const classCode = (exitClass) => Match.value(exitClass).pipe(Match.when("VerdictFail", () => 1), Match.when("ConfigError", () => CONFIG_CODE), Match.when("RuntimeError", () => 3), Match.when("InternalError", () => 4), Match.exhaustive);
67
- const RunOutcomeTypeId = Symbol.for("@systemfsoftware/stryker-js/RunOutcome");
68
- var RunOk = class extends S.TaggedClass()("RunOk", { help: S.Boolean }) {
69
- [RunOutcomeTypeId] = RunOutcomeTypeId;
70
- };
71
- var RunInterrupted = class extends S.TaggedError()("RunInterrupted", { code: S.Finite }) {
72
- [RunOutcomeTypeId] = RunOutcomeTypeId;
73
- };
74
- var RunParseFailed = class extends S.TaggedClass()("RunParseFailed", { unrecognized: S.optional(S.String) }) {
75
- [RunOutcomeTypeId] = RunOutcomeTypeId;
76
- };
77
- var RunSurvivorsRejected = class extends S.TaggedClass()("RunSurvivorsRejected", {
78
- reason: S.Literals(["no-report", "mismatch"]),
79
- diagnostic: S.optional(S.String)
80
- }) {
81
- [RunOutcomeTypeId] = RunOutcomeTypeId;
82
- };
83
- var RunConfigFailed = class extends S.TaggedClass()("RunConfigFailed", { detail: S.optional(S.String) }) {
84
- [RunOutcomeTypeId] = RunOutcomeTypeId;
85
- };
86
- var RunFailed$1 = class extends S.TaggedClass()("RunFailed", {
87
- code: S.Finite,
88
- diagnostic: S.optional(S.String)
89
- }) {
90
- [RunOutcomeTypeId] = RunOutcomeTypeId;
91
- };
92
- const isSucceeded = (command) => command.succeeded;
93
- const isHelpRun = (command) => command.helpErrorCount !== void 0;
94
- const isSurvivorsRun = (command) => command.survivorsReason !== void 0;
95
- const isHighestClassed = (command) => command.highestExitClass !== void 0;
96
- const succeededOutcome = (command) => Option.match(Option.fromUndefinedOr(command.successExitClass), {
97
- onNone: () => RunOk.make({ help: false }),
98
- onSome: (exitClass) => RunFailed$1.make({
99
- code: classCode(exitClass),
100
- diagnostic: command.diagnostic
101
- })
102
- });
103
- const helpOutcome = (command) => Match.value(command.helpErrorCount > 0).pipe(Match.when(true, () => RunParseFailed.make({ unrecognized: command.unrecognized })), Match.orElse(() => RunOk.make({ help: true })));
104
- const parseFailedOutcome = (command) => RunParseFailed.make({ unrecognized: command.unrecognized });
105
- const survivorsRejectedOutcome = (command) => RunSurvivorsRejected.make({
106
- reason: command.survivorsReason,
107
- diagnostic: command.survivorsDiagnostic
108
- });
109
- const configFailedOutcome = (command) => RunConfigFailed.make({ detail: command.configDetail });
110
- const classedOutcome = (command) => Match.value(command.highestExitClass).pipe(Match.when("ConfigError", () => RunConfigFailed.make({ detail: command.configDetail })), Match.orElse((exitClass) => RunFailed$1.make({
111
- code: classCode(exitClass),
112
- diagnostic: command.diagnostic
113
- })));
114
- const genericFailureOutcome = (command) => RunFailed$1.make({
115
- code: 1,
116
- diagnostic: command.diagnostic
117
- });
118
- function classify(command) {
119
- return Match.value(command).pipe(Match.when(isSucceeded, succeededOutcome), Match.when((interrupted) => interrupted.interrupted, () => RunInterrupted.make({ code: 130 })), 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));
120
- }
121
- Workflow.make(RunOutcomeCommand, (command) => Match.value(classify(command)).pipe(Match.tag("RunInterrupted", (error) => Result.fail(error)), Match.when((outcome) => !S.is(RunInterrupted)(outcome), (decision) => Result.succeed(decision)), Match.exhaustive));
122
- //#endregion
123
- //#region src/config-defaults.ts
124
- const isNonNullObject = (value) => typeof value === "object" && value !== null;
125
- const isArrayValue = (value) => Array.isArray(value);
126
- const freezeArrayValue = (value) => Object.freeze(value.map(deepFreeze));
127
- const freezeMapEntry = ([entryKey, entryValue]) => [deepFreeze(entryKey), deepFreeze(entryValue)];
128
- const freezeMapValue = (value) => Object.freeze(new Map([...value.entries()].map(freezeMapEntry)));
129
- const freezeSetValue = (value) => Object.freeze(new Set([...value.values()].map(deepFreeze)));
130
- const freezeRegExpValue = (value) => Object.freeze(value);
131
- const freezeRecordValue = (value) => Object.freeze(Object.entries(value).reduce((frozen, [property, propertyValue]) => {
132
- frozen[property] = deepFreeze(propertyValue);
133
- return frozen;
134
- }, {}));
135
- function deepFreeze(target) {
136
- return Match.value(target).pipe(Match.when(isArrayValue, freezeArrayValue), Match.when((value) => value instanceof Map, freezeMapValue), Match.when((value) => value instanceof RegExp, freezeRegExpValue), Match.when((value) => value instanceof Set, freezeSetValue), Match.when(isNonNullObject, freezeRecordValue), Match.orElse(() => target));
137
- }
138
- const optionsPath = (...path) => path.join(".");
139
- const SUPPORTED_CONFIG_FILE_EXTENSIONS = [
140
- ".ts",
141
- ".mts",
142
- ".js",
143
- ".mjs"
144
- ];
145
- const configFileNames = (extensions) => extensions.map((extension) => `stryker.config${extension}`);
146
- const SUPPORTED_CONFIG_FILE_NAMES = Object.freeze(configFileNames(SUPPORTED_CONFIG_FILE_EXTENSIONS));
147
- const RELATIVE_SPECIFIER_PREFIXES = [
148
- "/",
149
- "./",
150
- "../",
151
- "file://"
152
- ];
153
- function isModuleSpecifier(value) {
154
- return RELATIVE_SPECIFIER_PREFIXES.every((prefix) => value.startsWith(prefix) === false);
155
- }
156
- const CONFIG_SYNTAX_HELP = `
157
- Example of how a config file should look:
158
- /**
159
- * @type {import('@systemfsoftware/stryker-js-plugin-interface').StrykerOptions}
160
- */
161
- export default {
162
- mutate: ['src/**/*.js'],
163
- testRunner: 'vitest',
164
- reporters: ['progress', 'clear-text', 'html'],
165
- concurrency: 4
166
- };
167
- See https://stryker-mutator.io/docs/stryker-js/config-file for more information.`.trim();
168
- const createDefaultOptions = S.decodeEffect(StrykerOptionsSchema)({}).pipe(Effect.orDie);
169
- const defaultOptions = Effect.map(createDefaultOptions, (opts) => deepFreeze(opts));
170
- const scopedUnserializable = (scope) => (description) => ({
171
- ...description,
172
- path: [scope, ...description.path]
173
- });
174
- const describeUnserializableChild = (child, scope) => Match.value(findUnserializables(child)).pipe(Match.when(void 0, () => []), Match.orElse((descriptions) => descriptions.map(scopedUnserializable(scope))));
175
- const collectUnserializables = (groups) => {
176
- const found = groups.flat();
177
- if (found.length > 0) return found;
178
- };
179
- const describeUnserializableArray = (value) => collectUnserializables(value.map((child, index) => describeUnserializableChild(child, index.toString())));
180
- const describeUnserializableRecord = (value) => collectUnserializables(Object.entries(value).map(([key, child]) => describeUnserializableChild(child, key)));
181
- const isPlainObjectValue = (value) => !Array.isArray(value) && value.constructor === Object;
182
- const classNameOf = (value) => Match.value(value.constructor).pipe(Match.when(Match.defined, (ctor) => ctor.name), Match.orElse(() => "Object"));
183
- const describeUnserializableInstance = (value) => [{
184
- path: [],
185
- reason: `Value is an instance of "${classNameOf(value)}", this detail will get lost in translation during serialization`
186
- }];
187
- const describeUnserializableObject = (value) => Match.value(value).pipe(Match.when(isArrayValue, describeUnserializableArray), Match.when(isPlainObjectValue, describeUnserializableRecord), Match.orElse(describeUnserializableInstance));
188
- const NON_JSON_PRIMITIVE_TYPES = [
189
- "bigint",
190
- "function",
191
- "symbol"
192
- ];
193
- const isNonJsonPrimitive = (value) => NON_JSON_PRIMITIVE_TYPES.includes(typeof value);
194
- const describeNonJsonPrimitive = (value) => [{
195
- path: [],
196
- reason: `Primitive type "${typeof value}" has no JSON representation`
197
- }];
198
- const describeNumber = (value) => {
199
- if (isFinite(value)) return void 0;
200
- return [{
201
- reason: `Number value \`${value}\` has no JSON representation`,
202
- path: []
203
- }];
204
- };
205
- function findUnserializables(thing) {
206
- return Match.value(thing).pipe(Match.when((value) => typeof value === "number", describeNumber), Match.when(isNonJsonPrimitive, describeNonJsonPrimitive), Match.when(isNonNullObject, describeUnserializableObject), Match.orElse(() => void 0));
207
- }
208
- function isWarningEnabled(warningType, warningOptions) {
209
- if (typeof warningOptions === "boolean") return warningOptions;
210
- else return warningOptions[warningType] === true;
211
- }
212
- //#endregion
213
- //#region src/admit-survivors-run.workflow.ts
214
- const MutantShape = S.Struct({
215
- id: S.String,
216
- fileName: S.String,
217
- mutatorName: S.String,
218
- replacement: S.String,
219
- location: S.Struct({
220
- start: S.Struct({
221
- line: S.Finite,
222
- column: S.Finite
223
- }),
224
- end: S.Struct({
225
- line: S.Finite,
226
- column: S.Finite
227
- })
228
- })
229
- });
230
- S.Struct({
231
- config: S.optional(S.Record(S.String, S.Unknown)),
232
- framework: S.optional(S.Struct({ version: S.optional(S.String) })),
233
- files: S.Record(S.String, S.Struct({
234
- source: S.String,
235
- mutants: S.Array(S.Struct({
236
- id: S.String,
237
- mutatorName: S.String,
238
- replacement: S.optional(S.String),
239
- status: S.String,
240
- location: S.Struct({
241
- start: S.Struct({
242
- line: S.Finite,
243
- column: S.Finite
244
- }),
245
- end: S.Struct({
246
- line: S.Finite,
247
- column: S.Finite
248
- })
249
- })
250
- }))
251
- }))
252
- });
253
- const isArray = Array.isArray;
254
- const { entries: objectEntries, fromEntries: objectFromEntries, keys: objectKeys } = Object;
255
- const stringify = JSON.stringify;
256
- const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
257
- const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
258
- function stripSurvivorsKeys(config) {
259
- return objectFromEntries(objectEntries(config).filter(([key]) => !SURVIVORS_BOOKKEEPING_KEYS.includes(key)));
260
- }
261
- function wasProducedBySurvivorsRun(priorReport) {
262
- return Option.exists(Option.liftPredicate(priorReport.config, Match.record), (config) => SURVIVORS_BOOKKEEPING_KEYS.some((bookkeeping) => bookkeeping in config));
263
- }
264
- function serializeSurvivorsHashInput(input) {
265
- return stringify(sortKeys(input));
266
- }
267
- function sortKeys(value) {
268
- return Match.value(value).pipe(Match.when(isArray, (many) => many.map((member) => sortKeys(member))), Match.when(Match.record, (named) => objectFromEntries(objectKeys(named).sort().map((key) => [key, sortKeys(named[key])]))), Match.orElse((leaf) => leaf));
269
- }
270
- var PriorReportFacts = class extends S.Class("PriorReportFacts")({
271
- config: S.Record(S.String, S.Unknown),
272
- frameworkVersion: S.UndefinedOr(S.String)
273
- }) {};
274
- var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
275
- priorReport: S.UndefinedOr(PriorReportFacts),
276
- currentConfig: S.Record(S.String, S.Unknown),
277
- frameworkVersion: S.String,
278
- sourceContentHashes: S.Record(S.String, S.String),
279
- priorSourceHashes: S.Record(S.String, S.String),
280
- priorSurvivors: S.Array(MutantShape)
281
- }) {};
282
- const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
283
- 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.";
284
- const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
285
- function hashesMatch(priorReport, input) {
286
- return serializeSurvivorsHashInput({
287
- resolvedOptions: stripSurvivorsKeys(priorReport.config),
288
- frameworkVersion: priorReport.frameworkVersion,
289
- sourceContentHashes: input.priorSourceHashes
290
- }) === serializeSurvivorsHashInput({
291
- resolvedOptions: stripSurvivorsKeys(input.currentConfig),
292
- frameworkVersion: input.frameworkVersion,
293
- sourceContentHashes: input.sourceContentHashes
294
- });
295
- }
296
- const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js/SurvivorsAdmission");
297
- var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
298
- [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
299
- };
300
- var NoSurvivors = class extends S.TaggedClass()("NoSurvivors", {}) {
301
- [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
302
- };
303
- S.Union([Admitted, NoSurvivors]);
304
- var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
305
- reason: S.Literals(["no-report", "mismatch"]),
306
- remediation: S.String
307
- }) {
308
- [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
309
- };
310
- function reject(reason, detail) {
311
- return Result.fail(SurvivorsRejection.make({
312
- reason,
313
- remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
314
- }));
315
- }
316
- /**
317
- * The closed set of admissions a command can name, before any of them is turned
318
- * into a decision. Deriving the variant first keeps the decision itself a total
319
- * dispatch over a type rather than a fallback over predicates.
320
- */
321
- const PriorReportAbsentOutcome = S.TaggedStruct("PriorReportAbsent", {});
322
- const PriorReportIsSurvivorsRunOutcome = S.TaggedStruct("PriorReportIsSurvivorsRun", {});
323
- const NoSurvivorsFoundOutcome = S.TaggedStruct("NoSurvivorsFound", {});
324
- const PriorReportDriftedOutcome = S.TaggedStruct("PriorReportDrifted", {});
325
- const SurvivorsMatchOutcome = S.TaggedStruct("SurvivorsMatch", { survivors: S.Array(MutantShape) });
326
- S.Union([
327
- PriorReportAbsentOutcome,
328
- PriorReportIsSurvivorsRunOutcome,
329
- NoSurvivorsFoundOutcome,
330
- PriorReportDriftedOutcome,
331
- SurvivorsMatchOutcome
332
- ]);
333
- const ADMISSION_RULES = [
334
- {
335
- holds: (input) => input.priorReport === void 0,
336
- outcome: PriorReportAbsentOutcome.make({})
337
- },
338
- {
339
- holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), wasProducedBySurvivorsRun),
340
- outcome: PriorReportIsSurvivorsRunOutcome.make({})
341
- },
342
- {
343
- holds: (input) => input.priorSurvivors.length === 0,
344
- outcome: NoSurvivorsFoundOutcome.make({})
345
- },
346
- {
347
- holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), (facts) => !hashesMatch(facts, input)),
348
- outcome: PriorReportDriftedOutcome.make({})
349
- }
350
- ];
351
- const admissionOutcomeOf = (input) => Option.getOrElse(Option.map(Arr.findFirst(ADMISSION_RULES, (rule) => rule.holds(input)), (rule) => rule.outcome), () => SurvivorsMatchOutcome.make({ survivors: input.priorSurvivors }));
352
- function decideAdmission(input) {
353
- 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);
354
- }
355
- Workflow.make(AdmitSurvivorsRunCommand, decideAdmission);
356
- //#endregion
357
- //#region src/exit-classification.ts
358
- const EXIT_CODE = {
359
- VerdictFail: 1,
360
- ConfigError: 2,
361
- RuntimeError: 3,
362
- InternalError: 4
363
- };
364
- function verdictExitClass(score, breakingThreshold) {
365
- return Option.match(Option.all([Option.fromNullishOr(score), Option.fromNullishOr(breakingThreshold)]), {
366
- onNone: () => null,
367
- onSome: ([actual, threshold]) => Match.value(actual < threshold).pipe(Match.when(true, () => "VerdictFail"), Match.when(false, () => null), Match.exhaustive)
368
- });
369
- }
370
- function resolveExitCode(pending, signal) {
371
- return Match.value(signal).pipe(Match.when(null, () => Option.match(Option.fromNullishOr(highestExitClass(pending)), {
372
- onNone: () => 0,
373
- onSome: (highest) => EXIT_CODE[highest]
374
- })), Match.orElse((present) => 128 + present));
375
- }
376
- function highestExitClass(pending) {
377
- return [...pending].reduce((highest, candidate) => Option.match(Option.fromNullishOr(highest), {
378
- onNone: () => candidate,
379
- onSome: (current) => Match.value(EXIT_CODE[candidate] > EXIT_CODE[current]).pipe(Match.when(true, () => candidate), Match.when(false, () => current), Match.exhaustive)
380
- }), null);
381
- }
382
- //#endregion
383
- //#region src/Envelope.ts
384
- function isExitClass(value) {
385
- return S.is(ExitClass)(value);
386
- }
387
- Option.liftPredicate(isExitClass);
388
- Option.liftPredicate(Exit.isFailure);
389
- Option.liftPredicate(S.is(S.NonEmptyString));
390
- const PRIMITIVE_REFINEMENTS = [
391
- Predicate.isString,
392
- Predicate.isNumber,
393
- Predicate.isBoolean,
394
- Predicate.isBigInt,
395
- Predicate.isSymbol
396
- ];
397
- Predicate.some(PRIMITIVE_REFINEMENTS);
398
- const capturedConsoleChunks = [];
399
- const countByLabel = /* @__PURE__ */ new Map();
400
- const timeByLabel = /* @__PURE__ */ new Map();
401
- function inspectValue(value) {
402
- if (typeof value === "string") return value;
403
- return Formatter.format(value);
404
- }
405
- const PLACEHOLDER = /(%[sdijfopO%])/g;
406
- const PLACEHOLDER_PART = /^%[sdijfopO%]$/;
407
- function jsonArgumentText(argument) {
408
- try {
409
- return String(JSON.stringify(argument));
410
- } catch {
411
- return "[Circular]";
412
- }
413
- }
414
- function placeholderText(placeholder, argument) {
415
- 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));
416
- }
417
- function appendLiteral(progress, literal) {
418
- return {
419
- args: progress.args,
420
- cursor: progress.cursor,
421
- text: progress.text + literal
422
- };
423
- }
424
- function substitutePlaceholder(progress, placeholder) {
425
- return Match.value(progress.cursor).pipe(Match.when((cursor) => cursor < progress.args.length, (cursor) => ({
426
- args: progress.args,
427
- cursor: cursor + 1,
428
- text: progress.text + placeholderText(placeholder, progress.args[cursor])
429
- })), Match.orElse(() => appendLiteral(progress, placeholder)));
430
- }
431
- function consumePlaceholder(progress, placeholder) {
432
- return Match.value(placeholder).pipe(Match.when(Match.is("%%"), () => appendLiteral(progress, "%")), Match.orElse(() => substitutePlaceholder(progress, placeholder)));
433
- }
434
- function formatStep(progress, part) {
435
- return Match.value(part).pipe(Match.when((candidate) => PLACEHOLDER_PART.test(candidate), (placeholder) => consumePlaceholder(progress, placeholder)), Match.orElse((literal) => appendLiteral(progress, literal)));
436
- }
437
- function formatTemplate(template, args) {
438
- const progress = template.split(PLACEHOLDER).reduce(formatStep, {
439
- args,
440
- cursor: 0,
441
- text: ""
442
- });
443
- const trailing = progress.args.slice(progress.cursor).map((argument) => ` ${inspectValue(argument)}`);
444
- return progress.text + trailing.join("");
445
- }
446
- function isTextTemplate(args) {
447
- return typeof args[0] === "string";
448
- }
449
- function formatArgs(args) {
450
- return Match.value(args).pipe(Match.when(isTextTemplate, (values) => formatTemplate(values[0], values.slice(1))), Match.orElse((values) => values.map(inspectValue).join(" ")));
451
- }
452
- function captureSync(args) {
453
- capturedConsoleChunks.push(formatArgs(args));
454
- }
455
- function captureAssert(condition, args) {
456
- if (!condition) capturedConsoleChunks.push(`Assertion failed: ${formatArgs(args)}`);
457
- }
458
- const DEFAULT_CONSOLE_LABEL = "default";
459
- function consoleLabel(label) {
460
- return Option.getOrElse(Option.fromNullishOr(label), () => DEFAULT_CONSOLE_LABEL);
461
- }
462
- function incrementCount(key) {
463
- const next = Option.getOrElse(Option.fromNullishOr(countByLabel.get(key)), () => 0) + 1;
464
- countByLabel.set(key, next);
465
- return next;
466
- }
467
- function captureCount(label) {
468
- const key = consoleLabel(label);
469
- const next = incrementCount(key);
470
- capturedConsoleChunks.push(`${key}: ${next}`);
471
- }
472
- function elapsedMs(started, nowNanos) {
473
- return Number(nowNanos - started) / 1e6;
474
- }
475
- function captureTimeEnd(label, nowNanos) {
476
- const key = consoleLabel(label);
477
- const started = timeByLabel.get(key);
478
- if (started !== void 0) {
479
- timeByLabel.delete(key);
480
- capturedConsoleChunks.push(`${key}: ${elapsedMs(started, nowNanos)}ms`);
481
- }
482
- }
483
- function captureTimeLog(label, args, clock) {
484
- const key = consoleLabel(label);
485
- const started = timeByLabel.get(key);
486
- if (started === void 0) return;
487
- const elapsed = elapsedMs(started, clock.monotonicTimeNanosUnsafe());
488
- const elapsedText = Match.value(args).pipe(Match.when((values) => values.length === 0, () => `${key}: ${elapsed}ms`), Match.orElse((values) => `${key}: ${elapsed}ms ${formatArgs(values)}`));
489
- capturedConsoleChunks.push(elapsedText);
490
- }
491
- function captureTrace(args) {
492
- capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
493
- }
494
- const makeCapturingConsole = (clock) => ({
495
- assert: (condition, ...args) => captureAssert(condition, args),
496
- clear: () => {},
497
- count: (label) => captureCount(label),
498
- countReset: (label) => countByLabel.delete(consoleLabel(label)),
499
- debug: (...args) => captureSync(args),
500
- dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
501
- dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
502
- error: (...args) => captureSync(args),
503
- group: () => {},
504
- groupCollapsed: () => {},
505
- groupEnd: () => {},
506
- info: (...args) => captureSync(args),
507
- log: (...args) => captureSync(args),
508
- table: (tabularData) => capturedConsoleChunks.push(Formatter.format(tabularData)),
509
- time: (label) => timeByLabel.set(consoleLabel(label), clock.monotonicTimeNanosUnsafe()),
510
- timeEnd: (label) => captureTimeEnd(label, clock.monotonicTimeNanosUnsafe()),
511
- timeLog: (label, ...args) => captureTimeLog(label, args, clock),
512
- trace: (...args) => captureTrace(args),
513
- warn: (...args) => captureSync(args)
514
- });
515
- Layer.effect(Console.Console, Clock.clockWith((clock) => Effect.sync(() => {
516
- resetCapturedConsole();
517
- return makeCapturingConsole(clock);
518
- })));
519
- function resetCapturedConsole() {
520
- capturedConsoleChunks.length = 0;
521
- countByLabel.clear();
522
- timeByLabel.clear();
523
- }
524
- //#endregion
525
- //#region src/resolve-output-mode.workflow.ts
526
- const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
527
- var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
528
- stdoutIsTTY: S.Boolean,
529
- text: S.optional(S.Boolean),
530
- json: S.optional(S.Boolean),
531
- envMode: S.optional(S.String),
532
- agent: S.optional(S.String),
533
- toolVars: S.optional(S.Record(S.String, S.String))
534
- }) {};
535
- const ResolveModeTypeId = Symbol.for("@systemfsoftware/stryker-js/ResolveMode");
536
- var ModeConflictError = class extends S.TaggedError()("ModeConflictError", {
537
- option: S.String,
538
- value: S.String,
539
- expected: S.String
540
- }) {
541
- [ResolveModeTypeId] = ResolveModeTypeId;
542
- };
543
- const ModeSignal$1 = S.Literals([
544
- "flag",
545
- "env",
546
- "tty",
547
- "agent",
548
- "tool"
549
- ]);
550
- var HumanOutput = class extends S.TaggedClass()("HumanOutput", {
551
- signal: ModeSignal$1,
552
- stdoutIsTTY: S.Boolean
553
- }) {
554
- [ResolveModeTypeId] = ResolveModeTypeId;
555
- };
556
- var MachineOutput = class extends S.TaggedClass()("MachineOutput", {
557
- signal: ModeSignal$1,
558
- stdoutIsTTY: S.Boolean
559
- }) {
560
- [ResolveModeTypeId] = ResolveModeTypeId;
561
- };
562
- const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
563
- const configured = (value) => Option.filter(Option.fromNullishOr(value), S.is(S.NonEmptyString));
564
- const modeFromEnv = (envMode, stdoutIsTTY) => Match.value(envMode).pipe(Match.when("machine", () => MachineOutput.make({
565
- signal: "env",
566
- stdoutIsTTY
567
- })), Match.orElse(() => HumanOutput.make({
568
- signal: "env",
569
- stdoutIsTTY
570
- })));
571
- const modeFromAgent = (command) => Option.match(configured(command.agent), {
572
- onNone: () => modeFromTools(command),
573
- onSome: () => MachineOutput.make({
574
- signal: "agent",
575
- stdoutIsTTY: true
576
- })
577
- });
578
- const modeFromTools = (command) => Match.value(anyToolVariableSet(command.toolVars)).pipe(Match.when(true, () => MachineOutput.make({
579
- signal: "tool",
580
- stdoutIsTTY: true
581
- })), Match.orElse(() => HumanOutput.make({
582
- signal: "tty",
583
- stdoutIsTTY: true
584
- })));
585
- const anyToolVariableSet = (toolVars) => Option.match(Option.fromNullishOr(toolVars), {
586
- onNone: () => false,
587
- onSome: (named) => TOOL_VARIABLES$1.some((variable) => Option.isSome(configured(named[variable])))
588
- });
589
- const modeBelowEnv = (command) => Match.value(command.stdoutIsTTY).pipe(Match.when(false, () => MachineOutput.make({
590
- signal: "tty",
591
- stdoutIsTTY: false
592
- })), Match.orElse(() => modeFromAgent(command)));
593
- const modeFromEnvironment = (command) => Option.match(configured(command.envMode), {
594
- onNone: () => modeBelowEnv(command),
595
- onSome: (envMode) => modeFromEnv(envMode, command.stdoutIsTTY)
596
- });
597
- const decideMode = (command) => Match.value(command).pipe(Match.when({
598
- text: true,
599
- json: true
600
- }, () => Result.fail(ModeConflictError.make({
601
- option: "json",
602
- value: "text",
603
- expected: CONFLICT_EXPECTED
604
- }))), Match.when({ text: true }, (texted) => Result.succeed(HumanOutput.make({
605
- signal: "flag",
606
- stdoutIsTTY: texted.stdoutIsTTY
607
- }))), Match.when({ json: true }, (jsoned) => Result.succeed(MachineOutput.make({
608
- signal: "flag",
609
- stdoutIsTTY: jsoned.stdoutIsTTY
610
- }))), Match.orElse((rest) => Result.succeed(modeFromEnvironment(rest))));
611
- const resolveOutputMode = Workflow.make(ResolveModeCommand, decideMode);
612
- //#endregion
613
- //#region src/RunEvent.schema.ts
614
- const RunPhase = S.Literals([
615
- "prepare",
616
- "instrument",
617
- "dry-run",
618
- "mutation-test"
619
- ]);
620
- const OutputMode = S.Literals(["human", "machine"]);
621
- const ModeSignal = S.Literals([
622
- "flag",
623
- "env",
624
- "tty",
625
- "agent",
626
- "tool"
627
- ]);
628
- var RunStarted = class extends S.TaggedClass()("stream", {
629
- schemaVersion: S.String,
630
- runId: S.String,
631
- mode: OutputMode,
632
- signal: ModeSignal
633
- }) {};
634
- var PhaseEntered = class extends S.TaggedClass()("phase", {
635
- phase: RunPhase,
636
- elapsedMs: S.Finite
637
- }) {};
638
- var PlanKnown = class extends S.TaggedClass()("plan", { total: S.Finite }) {};
639
- var RunMutantTested = class extends S.TaggedClass()("mutant", {
640
- id: S.String,
641
- status: MutantStatusSchema,
642
- file: S.String,
643
- location: LocationSchema,
644
- mutator: S.String,
645
- replacement: S.NullOr(S.String),
646
- completed: S.Finite,
647
- total: S.Finite
648
- }) {};
649
- var Heartbeat = class extends S.TaggedClass()("tick", {
650
- elapsedMs: S.Finite,
651
- completed: S.Finite,
652
- total: S.NullOr(S.Finite)
653
- }) {};
654
- const VerdictThresholds = S.Struct({
655
- high: S.Finite,
656
- low: S.Finite,
657
- break: S.NullOr(S.Finite)
658
- });
659
- const VerdictMutant = S.Struct({
660
- id: S.String,
661
- file: S.String,
662
- location: LocationSchema,
663
- mutator: S.String,
664
- replacement: S.NullOr(S.String),
665
- status: MutantStatusSchema
666
- });
667
- const VerdictCounts = S.Struct({
668
- killed: S.Finite,
669
- timeout: S.Finite,
670
- survived: S.Finite,
671
- noCoverage: S.Finite,
672
- runtimeErrors: S.Finite,
673
- compileErrors: S.Finite,
674
- ignored: S.Finite,
675
- pending: S.Finite
676
- });
677
- var VerdictReached = class extends S.TaggedClass()("verdict", {
678
- schemaVersion: S.String,
679
- runId: S.String,
680
- mode: OutputMode,
681
- signal: ModeSignal,
682
- score: S.NullOr(S.Finite),
683
- thresholds: VerdictThresholds,
684
- reportFile: S.NullOr(S.String),
685
- counts: VerdictCounts,
686
- mutants: S.Array(VerdictMutant)
687
- }) {};
688
- var RunFailed = class extends S.TaggedClass()("error", {
689
- schemaVersion: S.String,
690
- code: S.Finite,
691
- error: S.String,
692
- remediation: S.String
693
- }) {};
694
- var HelpRendered = class extends S.TaggedClass()("help", {
695
- schemaVersion: S.String,
696
- code: S.Literals([0]),
697
- help: S.String
698
- }) {};
699
- S.Union([
700
- RunStarted,
701
- PhaseEntered,
702
- PlanKnown,
703
- RunMutantTested,
704
- Heartbeat,
705
- VerdictReached,
706
- RunFailed,
707
- HelpRendered
708
- ]);
709
- S.TaggedClass()("RunCommand", {
710
- cliOptionsJson: S.String,
711
- targetMutatePatterns: S.Array(S.String)
712
- });
713
- S.TaggedClass()("RunOutput", {
714
- verdictJson: S.String,
715
- exitCode: S.Finite
716
- });
717
- S.TaggedError()("RunDecodeError", { message: S.String });
718
- S.TaggedError()("RunReadError", { message: S.String });
719
- S.TaggedError()("RunWriteError", { message: S.String });
720
- S.TaggedClass()("PlanMutationRunCommand", {
721
- configMutatePatterns: S.Array(S.String),
722
- configMutatorNames: S.Array(S.String),
723
- targetMutatePatterns: S.Array(S.String),
724
- availableMutators: S.Array(S.String)
725
- });
726
- S.TaggedClass()("MutationRunPlan", {
727
- mutatePatterns: S.Array(S.String),
728
- mutatorNames: S.Array(S.String)
729
- });
730
- //#endregion
731
- //#region src/RunEvents.ts
732
- var RunEvents = class extends Context.Service()("@systemfsoftware/stryker-js/RunEvents") {};
733
- var RunIdentity = class extends Context.Service()("@systemfsoftware/stryker-js/RunEvents/RunIdentity") {};
734
- //#endregion
735
- //#region src/dry-run.workflow.ts
736
- var DryRunError = class extends S.TaggedError()("DryRunError", {
737
- stage: S.Literals(["dryRun", "dryRunNoTests"]),
738
- reason: S.String
739
- }) {};
740
- var DryRunCommand = class extends S.TaggedClass()("DryRunCommand", {
741
- status: S.Literals([
742
- "Complete",
743
- "Error",
744
- "Timeout"
745
- ]),
746
- testCount: S.Finite,
747
- failedTestCount: S.Finite,
748
- allowEmpty: S.Boolean,
749
- errorMessage: S.optional(S.String),
750
- reason: S.optional(S.String)
751
- }) {};
752
- const DryRunDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/DryRunDecision");
753
- var DryRunPassed = class extends S.TaggedClass()("DryRunPassed", { testCount: S.Finite }) {
754
- [DryRunDecisionTypeId] = DryRunDecisionTypeId;
755
- };
756
- var DryRunFailed = class extends S.TaggedClass()("DryRunFailed", {
757
- testCount: S.Finite,
758
- failedTestCount: S.Finite
759
- }) {
760
- [DryRunDecisionTypeId] = DryRunDecisionTypeId;
761
- };
762
- const reasonOrDefault = (reason, fallback) => Option.getOrElse(() => fallback)(Option.fromUndefinedOr(reason));
763
- const hasFailedTests = (command) => command.failedTestCount > 0;
764
- const decideComplete = (command) => Match.value(command).pipe(Match.when({
765
- testCount: 0,
766
- allowEmpty: false
767
- }, () => Result.fail(DryRunError.make({
768
- stage: "dryRunNoTests",
769
- reason: "No tests were executed. Stryker will exit prematurely. Please check your configuration."
770
- }))), Match.when(hasFailedTests, () => Result.succeed(DryRunFailed.make({
771
- testCount: command.testCount,
772
- failedTestCount: command.failedTestCount
773
- }))), Match.orElse(() => Result.succeed(DryRunPassed.make({ testCount: command.testCount }))));
774
- const dryRun = Workflow.make(DryRunCommand, (command) => Match.value(command.status).pipe(Match.when("Error", () => Result.fail(DryRunError.make({
775
- stage: "dryRun",
776
- reason: reasonOrDefault(command.errorMessage, "Dry run error")
777
- }))), Match.when("Timeout", () => Result.fail(DryRunError.make({
778
- stage: "dryRun",
779
- reason: reasonOrDefault(command.reason, "Initial test run timed out")
780
- }))), Match.when("Complete", () => decideComplete(command)), Match.exhaustive));
781
- //#endregion
782
- //#region src/IncrementalDiff.paths.ts
783
- const REMEMBERED_REASON = "Remembered";
784
- const normalizeFileName$4 = (fileName) => fileName.replaceAll("\\", "/");
785
- const toRelativeNormalizedFileName = (fileName, basePath) => normalizeFileName$4(Match.value(fileName ?? "").pipe(Match.when((raw) => raw.startsWith(basePath), (raw) => raw.slice(basePath.length).replace(/^\/+/, "")), Match.orElse((raw) => raw)));
786
- //#endregion
787
- //#region src/IncrementalDiff.schema.ts
788
- const PositionSchema$1 = S.Struct({
789
- line: S.Finite,
790
- column: S.Finite
791
- });
792
- const PreviousLocationSchema = S.Struct({
793
- start: PositionSchema$1,
794
- end: PositionSchema$1
795
- });
796
- const PreviousMutantSchema = S.Struct({
797
- mutatorName: S.String,
798
- replacement: S.String,
799
- location: PreviousLocationSchema,
800
- status: S.String,
801
- testsCompleted: S.optional(S.Finite),
802
- coveredBy: S.optional(S.Array(S.String)),
803
- killedBy: S.optional(S.Array(S.String))
804
- });
805
- const PreviousFileSchema = S.Struct({
806
- source: S.optional(S.String),
807
- mutants: S.optional(S.Array(PreviousMutantSchema))
808
- });
809
- const PreviousTestFileSchema = S.Struct({ source: S.optional(S.String) });
810
- const PreviousFilesSchema = S.Record(S.String, PreviousFileSchema);
811
- const PreviousTestFilesSchema = S.Record(S.String, PreviousTestFileSchema);
812
- const ZERO = 0;
813
- const firstDefined = (first, second) => Option.getOrElse(Option.fromNullishOr(first), () => second);
814
- const staticField = (isStatic) => Option.match(Option.fromUndefinedOr(isStatic), {
815
- onNone: () => ({}),
816
- onSome: (present) => ({ static: present })
817
- });
818
- const coveredByField = (coveredBy) => Option.match(Option.fromUndefinedOr(coveredBy), {
819
- onNone: () => ({}),
820
- onSome: (present) => ({ coveredBy: [...present] })
821
- });
822
- const testFilterField = (testFilter) => Option.match(Option.fromUndefinedOr(testFilter), {
823
- onNone: () => ({}),
824
- onSome: (present) => ({ testFilter: [...present] })
825
- });
826
- const staticCoverageCountOf = (staticCoverage, mutantId) => Option.getOrElse(Option.flatMap(Option.fromNullishOr(staticCoverage), (countsByMutantId) => Option.fromUndefinedOr(countsByMutantId[mutantId])), () => ZERO);
827
- const testsByIdOf = (result) => {
828
- const testsById = MutableHashMap.empty();
829
- for (const test of result.tests) MutableHashMap.set(testsById, test.id, test);
830
- return testsById;
831
- };
832
- const addTestsForMutant = (testsByMutantId, mutantId, test) => {
833
- const existing = MutableHashMap.get(testsByMutantId, mutantId);
834
- const tests = Option.getOrElse(existing, () => MutableHashSet.empty());
835
- MutableHashSet.add(tests, test);
836
- if (Option.isNone(existing)) MutableHashMap.set(testsByMutantId, mutantId, tests);
837
- };
838
- const addTestCoverage = (testsByMutantId, test, coverage) => {
839
- const coveredMutantIds = Object.entries(coverage).filter(([, count]) => count > ZERO).map(([mutantId]) => mutantId);
840
- for (const mutantId of coveredMutantIds) addTestsForMutant(testsByMutantId, mutantId, test);
841
- };
842
- const testsByMutantIdOf = (mutantCoverage, testsById) => {
843
- const testsByMutantId = MutableHashMap.empty();
844
- for (const [testId, coverage] of Object.entries(mutantCoverage.perTest)) Option.match(MutableHashMap.get(testsById, testId), {
845
- onNone: () => void 0,
846
- onSome: (test) => addTestCoverage(testsByMutantId, test, coverage)
847
- });
848
- return testsByMutantId;
849
- };
850
- const addHits = (hitsByMutantId, coverage) => {
851
- for (const [mutantId, count] of Object.entries(coverage)) {
852
- const existing = Option.getOrElse(MutableHashMap.get(hitsByMutantId, mutantId), () => ZERO);
853
- MutableHashMap.set(hitsByMutantId, mutantId, existing + count);
854
- }
855
- };
856
- const hitsByMutantIdOf = (mutantCoverage) => {
857
- const hitsByMutantId = MutableHashMap.empty();
858
- const coverageByMutantId = [mutantCoverage.static, ...Object.values(mutantCoverage.perTest)];
859
- for (const coverage of coverageByMutantId) addHits(hitsByMutantId, coverage);
860
- return hitsByMutantId;
861
- };
862
- const testCoverageFrom = (result) => {
863
- const testsById = testsByIdOf(result);
864
- const mutantCoverage = Option.fromNullishOr(result.mutantCoverage);
865
- return {
866
- testsByMutantId: Option.match(mutantCoverage, {
867
- onNone: () => MutableHashMap.empty(),
868
- onSome: (coverage) => testsByMutantIdOf(coverage, testsById)
869
- }),
870
- testsById,
871
- staticCoverage: Option.match(mutantCoverage, {
872
- onNone: () => void 0,
873
- onSome: (coverage) => coverage.static
874
- }),
875
- hitsByMutantId: Option.match(mutantCoverage, {
876
- onNone: () => MutableHashMap.empty(),
877
- onSome: (coverage) => hitsByMutantIdOf(coverage)
878
- })
879
- };
880
- };
881
- const calculateTotalTime = (testResults) => [...testResults].reduce((acc, test) => acc + test.timeSpentMs, 0);
882
- const toTestIds = (testResults) => {
883
- const out = [];
884
- for (const test of testResults) out.push(test.id);
885
- return out;
886
- };
887
- const hasCoverageForPlan = (staticCoverage) => {
888
- if (staticCoverage === void 0) return false;
889
- return Object.keys(staticCoverage).length > 0;
890
- };
891
- const hasStaticCoverageForPlan = (staticCoverage, mutantId) => staticCoverageCountOf(staticCoverage, mutantId) > ZERO;
892
- const calculateTotalTimeForIds = (testIds, testTimeById) => testIds.reduce((acc, id) => {
893
- const t = testTimeById[id];
894
- if (t !== void 0) return acc + t;
895
- return acc;
896
- }, 0);
897
- const getHitLimit = (hitCount) => {
898
- if (hitCount === void 0) return;
899
- return hitCount * 100;
900
- };
901
- const getMutantActivation = (testFilter) => {
902
- if (testFilter !== void 0) return "runtime";
903
- return "static";
904
- };
905
- const getCoveredBy = (mutant) => {
906
- if (mutant.coveredBy === void 0) return;
907
- return [...mutant.coveredBy];
908
- };
909
- const getTestFilter = (globalFilter) => {
910
- if (globalFilter === void 0) return;
911
- return [...globalFilter];
912
- };
913
- const toRunPlan = (mutant, command, netTime, testFilter, isStatic, coveredBy) => {
914
- const timeout = command.options.timeoutFactor * netTime + command.options.timeoutMS + command.timeOverheadMS;
915
- const hitLimit = getHitLimit(command.hitsByMutantId[mutant.id]);
916
- const sandboxFileName = Option.getOrElse(Option.fromUndefinedOr(command.sandboxFileByName[mutant.fileName]), () => mutant.fileName);
917
- const reloadEnvironment = Option.match(Option.fromUndefinedOr(testFilter), {
918
- onNone: () => true,
919
- onSome: () => isStatic !== false
920
- });
921
- return {
922
- plan: "Run",
923
- mutantId: mutant.id,
924
- netTime,
925
- runOptions: {
926
- mutantActivation: getMutantActivation(testFilter),
927
- timeout,
928
- sandboxFileName,
929
- disableBail: command.options.disableBail,
930
- reloadEnvironment,
931
- ...testFilterField(testFilter),
932
- ...Option.match(Option.fromUndefinedOr(hitLimit), {
933
- onNone: () => ({}),
934
- onSome: (limit) => ({ hitLimit: limit })
935
- })
936
- },
937
- ...staticField(isStatic),
938
- ...coveredByField(coveredBy)
939
- };
940
- };
941
- const toEarlyResultPlan = (mutant, isStatic, status, statusReason, coveredBy) => ({
942
- plan: "EarlyResult",
943
- mutantId: mutant.id,
944
- status,
945
- ...Option.match(Option.fromUndefinedOr(firstDefined(statusReason, mutant.statusReason)), {
946
- onNone: () => ({}),
947
- onSome: (reason) => ({ statusReason: reason })
948
- }),
949
- ...staticField(isStatic),
950
- ...coveredByField(coveredBy)
951
- });
952
- const planForStaticallyCovered = (mutant, command, isStatic) => {
953
- const tests = Option.getOrElse(Option.fromUndefinedOr(command.testsByMutantId[mutant.id]), () => []);
954
- const coveredBy = [...tests];
955
- const ignoreStatic = command.options.ignoreStatic;
956
- const useCovered = Match.value(isStatic).pipe(Match.when(false, () => true), Match.orElse(() => ignoreStatic && tests.length > ZERO));
957
- return Match.value(useCovered).pipe(Match.when(true, () => toRunPlan(mutant, command, calculateTotalTimeForIds(tests, command.testTimeById), coveredBy, isStatic, coveredBy)), Match.orElse(() => Match.value(ignoreStatic).pipe(Match.when(true, () => toEarlyResultPlan(mutant, isStatic, "Ignored", "Static mutant (and \"ignoreStatic\" was enabled)", coveredBy)), Match.orElse(() => toRunPlan(mutant, command, command.timeSpentAllTests, getTestFilter(command.globalTestFilter), isStatic, coveredBy)))));
958
- };
959
- const decidePlanForMutant = (mutant, command) => {
960
- const isStatic = hasStaticCoverageForPlan(command.staticCoverage, mutant.id);
961
- return Option.match(Option.fromUndefinedOr(mutant.status), {
962
- onSome: (status) => toEarlyResultPlan(mutant, isStatic, status, mutant.statusReason, getCoveredBy(mutant)),
963
- onNone: () => Match.value(hasCoverageForPlan(command.staticCoverage)).pipe(Match.when(true, () => planForStaticallyCovered(mutant, command, isStatic)), Match.orElse(() => toRunPlan(mutant, command, command.timeSpentAllTests, getTestFilter(command.globalTestFilter), void 0, void 0)))
964
- });
965
- };
966
- const planMutantTests = (command) => {
967
- const plans = command.mutants.map((mutant) => decidePlanForMutant(mutant, command));
968
- return {
969
- plans,
970
- totalNetTime: plans.reduce((acc, plan) => {
971
- if (plan.plan === "Run") return acc + plan.netTime;
972
- return acc;
973
- }, 0)
974
- };
975
- };
976
- const hitsRecordOf = (testCoverage) => {
977
- const hitsByMutantId = {};
978
- for (const [mutantId, hits] of testCoverage.hitsByMutantId) hitsByMutantId[mutantId] = hits;
979
- return hitsByMutantId;
980
- };
981
- const testsByMutantIdRecordOf = (testCoverage) => {
982
- const testsByMutantId = {};
983
- for (const [mutantId, tests] of testCoverage.testsByMutantId) testsByMutantId[mutantId] = toTestIds(tests);
984
- return testsByMutantId;
985
- };
986
- const testTimeRecordOf = (testCoverage) => {
987
- const testTimeById = {};
988
- for (const [id, result] of testCoverage.testsById) testTimeById[id] = result.timeSpentMs;
989
- return testTimeById;
990
- };
991
- const coverageToCommand = (mutants, testCoverage, options, timeOverheadMS, globalTestFilter, sandboxFileByName) => ({
992
- mutants: [...mutants],
993
- timeOverheadMS,
994
- timeSpentAllTests: calculateTotalTime(MutableHashMap.values(testCoverage.testsById)),
995
- hitsByMutantId: hitsRecordOf(testCoverage),
996
- testsByMutantId: testsByMutantIdRecordOf(testCoverage),
997
- testTimeById: testTimeRecordOf(testCoverage),
998
- options,
999
- sandboxFileByName,
1000
- ...Option.match(Option.fromUndefinedOr(testCoverage.staticCoverage), {
1001
- onNone: () => ({}),
1002
- onSome: (staticCoverage) => ({ staticCoverage })
1003
- }),
1004
- ...testFilterField(globalTestFilter)
1005
- });
1006
- const materializeMutant = (original, decided) => {
1007
- const status = firstDefined(decided.status, original.status);
1008
- const statusReason = firstDefined(decided.statusReason, original.statusReason);
1009
- const isStatic = firstDefined(decided.static, original.static);
1010
- const coveredBy = firstDefined(decided.coveredBy, original.coveredBy);
1011
- return Mutant.make({
1012
- id: original.id,
1013
- fileName: original.fileName,
1014
- mutatorName: original.mutatorName,
1015
- replacement: original.replacement,
1016
- location: original.location,
1017
- ...Option.match(Option.fromUndefinedOr(status), {
1018
- onNone: () => ({}),
1019
- onSome: (present) => ({ status: present })
1020
- }),
1021
- ...Option.match(Option.fromUndefinedOr(statusReason), {
1022
- onNone: () => ({}),
1023
- onSome: (present) => ({ statusReason: present })
1024
- }),
1025
- ...staticField(isStatic),
1026
- ...coveredByField(coveredBy),
1027
- ...Option.match(Option.fromUndefinedOr(original.testsCompleted), {
1028
- onNone: () => ({}),
1029
- onSome: (present) => ({ testsCompleted: present })
1030
- }),
1031
- ...Option.match(Option.fromUndefinedOr(original.description), {
1032
- onNone: () => ({}),
1033
- onSome: (present) => ({ description: present })
1034
- })
1035
- });
1036
- };
1037
- const materializePlan = (plan, original) => {
1038
- if (plan.plan === "EarlyResult") return {
1039
- plan: "EarlyResult",
1040
- mutant: materializeMutant(original, plan)
1041
- };
1042
- const mutant = materializeMutant(original, plan);
1043
- return {
1044
- plan: "Run",
1045
- mutant,
1046
- netTime: plan.netTime,
1047
- runOptions: {
1048
- activeMutant: mutant,
1049
- mutantActivation: plan.runOptions.mutantActivation,
1050
- timeout: plan.runOptions.timeout,
1051
- sandboxFileName: plan.runOptions.sandboxFileName,
1052
- disableBail: plan.runOptions.disableBail,
1053
- reloadEnvironment: plan.runOptions.reloadEnvironment,
1054
- ...testFilterField(plan.runOptions.testFilter),
1055
- ...Option.match(Option.fromUndefinedOr(plan.runOptions.hitLimit), {
1056
- onNone: () => ({}),
1057
- onSome: (hitLimit) => ({ hitLimit })
1058
- })
1059
- }
1060
- };
1061
- };
1062
- const makeMutantTestPlanner = (command) => {
1063
- const { plans } = planMutantTests(command);
1064
- const byId = /* @__PURE__ */ new Map();
1065
- for (const mutant of command.mutants) byId.set(mutant.id, mutant);
1066
- return Effect.forEach(plans, (plan) => {
1067
- const original = byId.get(plan.mutantId);
1068
- if (original === void 0) return Effect.die(/* @__PURE__ */ new Error(`planner returned an unknown mutant id: ${plan.mutantId}`));
1069
- return Effect.succeed(materializePlan(plan, original));
1070
- });
1071
- };
1072
- const decidePlans = (mutants, testCoverage, options, timeOverheadMS, globalTestFilter, sandboxFileByName) => {
1073
- const command = coverageToCommand(mutants, testCoverage, options, timeOverheadMS, globalTestFilter, sandboxFileByName);
1074
- return makeMutantTestPlanner(command);
1075
- };
1076
- const REMEMBERED_STATUS = /* @__PURE__ */ new Set([
1077
- "Killed",
1078
- "Survived",
1079
- "Timeout",
1080
- "NoCoverage",
1081
- "Ignored"
1082
- ]);
1083
- const normalizeDiffFileName = (fileName) => fileName.replaceAll("\\", "/");
1084
- const toRelativeNormalized = (fileName, basePath) => {
1085
- const raw = Option.getOrElse(Option.fromUndefinedOr(fileName), () => "");
1086
- return Match.value(raw.startsWith(basePath)).pipe(Match.when(true, () => normalizeDiffFileName(raw.slice(basePath.length).replace(/^\/+/, ""))), Match.orElse(() => normalizeDiffFileName(raw)));
1087
- };
1088
- const mutantKeyOf = (mutatorName, replacement, start, end) => `${mutatorName}\u0000${replacement}\u0000${start.line}:${start.column}:${end.line}:${end.column}`;
1089
- const currentMutantKey = (mutant) => mutantKeyOf(mutant.mutatorName, mutant.replacement, mutant.location.start, mutant.location.end);
1090
- const previousMutantKey = (mutant) => mutantKeyOf(mutant.mutatorName, mutant.replacement, {
1091
- line: mutant.location.start.line - 1,
1092
- column: mutant.location.start.column - 1
1093
- }, {
1094
- line: mutant.location.end.line - 1,
1095
- column: mutant.location.end.column - 1
1096
- });
1097
- const changedSourceFiles = (previousFiles, currentRelativeFiles) => Object.entries(previousFiles).filter(([name, previous]) => previous.source !== currentRelativeFiles[name]).map(([name]) => name);
1098
- const changedTestFiles = (previousTestFiles, currentRelativeFiles, testIdsByRelativeFile) => Object.keys({
1099
- ...previousTestFiles,
1100
- ...testIdsByRelativeFile
1101
- }).filter((name) => previousTestFiles[name]?.source !== currentRelativeFiles[name]);
1102
- const NO_PREVIOUS_MUTANTS = [];
1103
- const findRemembered = (previousFiles, file, key) => {
1104
- return Option.getOrElse(Option.flatMap(Option.fromUndefinedOr(previousFiles[file]), (record) => Option.fromUndefinedOr(record.mutants)), () => NO_PREVIOUS_MUTANTS).find((candidate) => previousMutantKey(candidate) === key);
1105
- };
1106
- const hasChangedCoverage = (mutantId, coveringTestFilesByMutantId, changedTests) => (coveringTestFilesByMutantId[mutantId] ?? []).some((file) => changedTests.includes(file));
1107
- const isRememberable = (previous, mutant, input, file, changedFiles, changedTests) => Match.value(REMEMBERED_STATUS.has(previous.status)).pipe(Match.when(false, () => false), Match.orElse(() => Match.value(changedFiles.includes(file)).pipe(Match.when(true, () => false), Match.orElse(() => !hasChangedCoverage(mutant.id, input.coveringTestFilesByMutantId, changedTests)))));
1108
- const decideForMutant = (mutant, input, changedFiles, changedTests) => {
1109
- const file = toRelativeNormalized(mutant.fileName, input.basePath);
1110
- const previous = findRemembered(input.previousFiles, file, currentMutantKey(mutant));
1111
- return Option.match(Option.filter(Option.fromUndefinedOr(previous), (candidate) => isRememberable(candidate, mutant, input, file, changedFiles, changedTests)), {
1112
- onNone: () => ({ kind: "run" }),
1113
- onSome: (present) => ({
1114
- kind: "remembered",
1115
- previous: present
1116
- })
1117
- });
1118
- };
1119
- const countBy = (files) => files.reduce((acc, file) => {
1120
- acc[file] = (acc[file] ?? 0) + 1;
1121
- return acc;
1122
- }, {});
1123
- const uniqueFiles = (files) => files.filter((file, index, all) => all.indexOf(file) === index);
1124
- const countOf = (counts, file) => Option.getOrElse(Option.fromUndefinedOr(counts[file]), () => ZERO);
1125
- const statisticsOf = (addedFiles, removedFiles) => {
1126
- const addedByFile = countBy(addedFiles);
1127
- const removedByFile = countBy(removedFiles);
1128
- const files = uniqueFiles([...Object.keys(addedByFile), ...Object.keys(removedByFile)]);
1129
- const changesByFile = {};
1130
- for (const file of files) changesByFile[file] = {
1131
- added: countOf(addedByFile, file),
1132
- removed: countOf(removedByFile, file)
1133
- };
1134
- return {
1135
- changesByFile,
1136
- total: files.reduce((acc, file) => ({
1137
- added: acc.added + countOf(addedByFile, file),
1138
- removed: acc.removed + countOf(removedByFile, file)
1139
- }), {
1140
- added: ZERO,
1141
- removed: ZERO
1142
- })
1143
- };
1144
- };
1145
- const testStatisticsOf = (previousTestFiles, testIdsByRelativeFile) => {
1146
- const added = Object.keys(testIdsByRelativeFile).filter((name) => previousTestFiles[name] === void 0);
1147
- const removed = Object.keys(previousTestFiles).filter((name) => testIdsByRelativeFile[name] === void 0);
1148
- return statisticsOf(added, removed);
1149
- };
1150
- const removedMutantFiles = (input, currentKeysByFile) => Object.entries(input.previousFiles).flatMap(([file, previous]) => {
1151
- const keys = currentKeysByFile[file];
1152
- return (previous.mutants ?? []).filter((candidate) => {
1153
- if (keys === void 0) return true;
1154
- return !keys.includes(previousMutantKey(candidate));
1155
- }).map(() => file);
1156
- });
1157
- const rememberedEntryOf = (mutant, previous) => ({
1158
- mutantId: mutant.id,
1159
- status: previous.status,
1160
- ...Option.match(Option.fromUndefinedOr(previous.testsCompleted), {
1161
- onNone: () => ({}),
1162
- onSome: (testsCompleted) => ({ testsCompleted })
1163
- }),
1164
- ...Option.match(Option.fromUndefinedOr(previous.coveredBy), {
1165
- onNone: () => ({}),
1166
- onSome: (coveredBy) => ({ coveredBy })
1167
- }),
1168
- ...Option.match(Option.fromUndefinedOr(previous.killedBy), {
1169
- onNone: () => ({}),
1170
- onSome: (killedBy) => ({ killedBy })
1171
- })
1172
- });
1173
- const forcedDiff = (input) => {
1174
- const added = input.currentMutants.map((mutant) => toRelativeNormalizedFileName(mutant.fileName, input.basePath));
1175
- return {
1176
- mutants: [...input.currentMutants],
1177
- remembered: [],
1178
- mutantStatistics: statisticsOf(added, []),
1179
- testStatistics: testStatisticsOf(input.previousTestFiles, input.testIdsByRelativeFile)
1180
- };
1181
- };
1182
- const isRememberedEntry = (entry) => entry.decision.kind === "remembered";
1183
- const splitMutants = (input, changedFiles, changedTests) => {
1184
- const decisions = input.currentMutants.map((mutant) => ({
1185
- mutant,
1186
- decision: decideForMutant(mutant, input, changedFiles, changedTests)
1187
- }));
1188
- const toRun = decisions.filter(({ decision }) => decision.kind === "run").map(({ mutant }) => mutant);
1189
- return {
1190
- toRun,
1191
- addedFiles: toRun.map((mutant) => toRelativeNormalizedFileName(mutant.fileName, input.basePath)),
1192
- remembered: decisions.filter(isRememberedEntry).map(({ mutant, decision }) => rememberedEntryOf(mutant, decision.previous))
1193
- };
1194
- };
1195
- const incrementalDiffOfChanges = (input) => {
1196
- const changedFiles = changedSourceFiles(input.previousFiles, input.currentRelativeFiles);
1197
- const changedTests = changedTestFiles(input.previousTestFiles, input.currentRelativeFiles, input.testIdsByRelativeFile);
1198
- const { toRun, addedFiles, remembered } = splitMutants(input, changedFiles, changedTests);
1199
- const currentKeysByFile = input.currentMutants.reduce((acc, mutant) => {
1200
- const file = toRelativeNormalizedFileName(mutant.fileName, input.basePath);
1201
- acc[file] = [...acc[file] ?? [], currentMutantKey(mutant)];
1202
- return acc;
1203
- }, {});
1204
- const removedFiles = removedMutantFiles(input, currentKeysByFile);
1205
- return {
1206
- mutants: toRun,
1207
- remembered,
1208
- mutantStatistics: statisticsOf(addedFiles, removedFiles),
1209
- testStatistics: testStatisticsOf(input.previousTestFiles, input.testIdsByRelativeFile)
1210
- };
1211
- };
1212
- const computeIncrementalDiff = (input) => Match.value(input.force).pipe(Match.when(true, () => forcedDiff(input)), Match.orElse(() => incrementalDiffOfChanges(input)));
1213
- const previousFilesOf = (rawReport) => Match.value(rawReport).pipe(Match.when(Predicate.isObject, (report) => Option.getOrElse(Option.filter(Option.fromUndefinedOr(report["files"]), S.is(PreviousFilesSchema)), () => ({}))), Match.orElse(() => ({})));
1214
- const previousTestFilesOf = (rawReport) => Match.value(rawReport).pipe(Match.when(Predicate.isObject, (report) => Option.getOrElse(Option.filter(Option.fromUndefinedOr(report["testFiles"]), S.is(PreviousTestFilesSchema)), () => ({}))), Match.orElse(() => ({})));
1215
- const hasTestFileName = (result) => result.fileName !== void 0;
1216
- const testIdsByRelativeFile = (testCoverage, basePath) => {
1217
- const byFile = {};
1218
- const located = [...MutableHashMap.values(testCoverage.testsById)].filter(hasTestFileName);
1219
- for (const result of located) {
1220
- const file = toRelativeNormalizedFileName(result.fileName, basePath);
1221
- const ids = Option.getOrElse(Option.fromUndefinedOr(byFile[file]), () => []);
1222
- ids.push(result.id);
1223
- byFile[file] = ids;
1224
- }
1225
- return byFile;
1226
- };
1227
- const coveredTestFiles = (tests, basePath) => {
1228
- const byFile = {};
1229
- const located = [...tests].filter(hasTestFileName);
1230
- for (const test of located) byFile[toRelativeNormalizedFileName(test.fileName, basePath)] = true;
1231
- return Object.keys(byFile);
1232
- };
1233
- const coveringTestFilesByMutantId = (testCoverage, basePath) => {
1234
- const byMutant = {};
1235
- for (const [mutantId, tests] of testCoverage.testsByMutantId) byMutant[mutantId] = coveredTestFiles(tests, basePath);
1236
- return byMutant;
1237
- };
1238
- const incrementalDiff = (input) => {
1239
- const output = computeIncrementalDiff({
1240
- basePath: input.basePath,
1241
- currentMutants: [...input.currentMutants],
1242
- previousFiles: previousFilesOf(input.incrementalReport),
1243
- previousTestFiles: previousTestFilesOf(input.incrementalReport),
1244
- currentRelativeFiles: input.currentRelativeFiles,
1245
- testIdsByRelativeFile: testIdsByRelativeFile(input.testCoverage, input.basePath),
1246
- coveringTestFilesByMutantId: coveringTestFilesByMutantId(input.testCoverage, input.basePath),
1247
- force: input.force ?? false
1248
- });
1249
- return {
1250
- mutants: output.mutants,
1251
- remembered: output.remembered,
1252
- mutantStatistics: {
1253
- changesByFile: MutableHashMap.fromIterable(Object.entries(output.mutantStatistics.changesByFile)),
1254
- total: output.mutantStatistics.total
1255
- },
1256
- testStatistics: {
1257
- changesByFile: MutableHashMap.fromIterable(Object.entries(output.testStatistics.changesByFile)),
1258
- total: output.testStatistics.total
1259
- }
1260
- };
1261
- };
1262
- const ReporterStageTypeId = Symbol.for("@systemfsoftware/stryker-js/ReporterStage");
1263
- const carriesAttachments = (stage) => "attachments" in stage;
1264
- const makeReporterStage = (attachments) => {
1265
- return {
1266
- [ReporterStageTypeId]: ReporterStageTypeId,
1267
- attachments
1268
- };
1269
- };
1270
- const stageAttachments = (stage) => {
1271
- if (!carriesAttachments(stage)) throw new Error("not a reporter stage constructed by attachReporterFactories");
1272
- return stage.attachments;
1273
- };
1274
- const isTerminalReportEvent = (event) => S.is(MutationTestReportReady)(event);
1275
- const availableReporters = (available) => {
1276
- if (available.length === 0) return "(none)";
1277
- return available.join(", ");
1278
- };
1279
- const unknownReporterMessage = (unknown, available) => {
1280
- const quoted = unknown.map((name) => `"${name}"`).join(", ");
1281
- const candidates = availableReporters(available);
1282
- if (unknown.length > 1) return `Unknown reporters ${quoted}. Available reporters: ${candidates}.`;
1283
- return `Unknown reporter ${quoted}. Available reporters: ${candidates}.`;
1284
- };
1285
- const validateReporterNames = (configured, available) => {
1286
- const known = HashSet.fromIterable(available.map((name) => name.toLowerCase()));
1287
- const unknown = configured.filter((name) => !HashSet.has(known, name.toLowerCase()));
1288
- if (unknown.length === 0) return Effect.void;
1289
- return Effect.fail(ConfigError.make({ message: unknownReporterMessage(unknown, available) }));
1290
- };
1291
- const acquireReporterIterator = (events) => Effect.acquireRelease(Effect.sync(() => events[Symbol.asyncIterator]()), (iterator) => Effect.promise(() => Promise.resolve(iterator.return?.()).then(() => void 0)).pipe(Effect.ignore));
1292
- const markDetachedEffect = (ports) => Effect.gen(function* () {
1293
- if (ports.latch.state === "terminal") return;
1294
- ports.latch.state = "detached";
1295
- yield* Queue.shutdown(ports.queue);
1296
- yield* Queue.shutdown(ports.inbox);
1297
- });
1298
- const pumpEmitter = (ports) => Stream.fromQueue(ports.inbox).pipe(Stream.runForEach((event) => Effect.asVoid(Queue.offer(ports.queue, event))), Effect.ensuring(Effect.asVoid(Queue.end(ports.queue))));
1299
- const closedIteratorResult = {
1300
- done: true,
1301
- value: void 0
1302
- };
1303
- const declaresTerminalReport = (result) => {
1304
- if (result.done !== true) return isTerminalReportEvent(result.value);
1305
- return false;
1306
- };
1307
- const closeIterator = (iterator, value) => {
1308
- const finish = iterator.return?.bind(iterator);
1309
- return Match.value(finish).pipe(Match.when(void 0, () => Promise.resolve(closedIteratorResult)), Match.orElse((close) => close(value)));
1310
- };
1311
- const attachReporterFactories = (inputs, options, init) => Effect.forEach(inputs, (input) => Effect.gen(function* () {
1312
- const inbox = yield* Queue.bounded(256);
1313
- const queue = yield* Queue.bounded(256);
1314
- const latch = { state: "streaming" };
1315
- const events = Stream.toAsyncIterable(Stream.fromQueue(queue));
1316
- const iterator = yield* acquireReporterIterator(events);
1317
- const ports = {
1318
- inbox,
1319
- queue,
1320
- latch
1321
- };
1322
- const singleUse = { [Symbol.asyncIterator]: () => {
1323
- const observe = (result) => {
1324
- if (declaresTerminalReport(result)) latch.state = "terminal";
1325
- return result;
1326
- };
1327
- return {
1328
- next: () => iterator.next().then(observe),
1329
- return: (value) => closeIterator(iterator, value)
1330
- };
1331
- } };
1332
- const consumer = Effect.flatten(Effect.try({
1333
- try: () => input.factory(options, init)(singleUse),
1334
- catch: (reason) => new Data.Error(`Reporter "${input.name}" factory threw: ${String(reason)}`)
1335
- })).pipe(Effect.ensuring(markDetachedEffect(ports)));
1336
- const consumerFiber = yield* Effect.forkScoped(consumer);
1337
- const emitter = yield* Effect.forkScoped(pumpEmitter(ports));
1338
- return {
1339
- name: input.name,
1340
- inbox,
1341
- queue,
1342
- latch,
1343
- emitter,
1344
- consumer: consumerFiber
1345
- };
1346
- })).pipe(Effect.map(makeReporterStage));
1347
- const REPORTER_EVENT_BATCH_BOUND = 128;
1348
- const workerStreamErrorOf = (cause) => ReporterFailed.make({
1349
- reporterName: "worker",
1350
- event: "mutationTestReportReady",
1351
- cause: errorToString(cause)
1352
- });
1353
- const reporterInitPayload = (init) => ({
1354
- ...traceparentInit(init.traceparent),
1355
- ...tracestateInit(init.tracestate)
1356
- });
1357
- const reporterWorkerFactory = (client) => (_options, init) => (events) => client.init(reporterInitPayload(init)).pipe(Effect.andThen(Stream.runForEach(Stream.grouped(Stream.fromAsyncIterable(events, workerStreamErrorOf), 128), (batch) => client.onEventBatch([...batch]))), Effect.andThen(client.flush()), Effect.mapError((cause) => ReporterFailed.make({
1358
- reporterName: "worker",
1359
- event: "mutationTestReportReady",
1360
- cause: errorToString(cause)
1361
- })));
1362
- const spawnReporterWorker = (params) => Effect.gen(function* () {
1363
- const optionsJson = yield* encodeWorkerOptions(params.options);
1364
- return yield* makeWorkerClient({
1365
- rpcs: ReporterRpcs,
1366
- entrypoint: params.entrypoint,
1367
- workingDirectory: params.projectBasePath,
1368
- execArgv: [...params.execArgv],
1369
- optionsJson,
1370
- tempDirPrefix: params.tempDirPrefix
1371
- });
1372
- });
1373
- const warnEventDropped = (attachment) => Effect.gen(function* () {
1374
- if (attachment.latch.state === "detached") return;
1375
- yield* Effect.logWarning(`Reporter "${attachment.name}" stream closed before an event could be delivered.`);
1376
- });
1377
- const REPORTER_STALL_TIMEOUT = Duration.seconds(30);
1378
- const detachStalledReporter = (attachment) => Effect.logWarning(`Reporter "${attachment.name}" did not drain its events for ${Duration.toSeconds(REPORTER_STALL_TIMEOUT)} seconds and was detached; exit code unchanged.`).pipe(Effect.andThen(() => markDetachedEffect({
1379
- inbox: attachment.inbox,
1380
- queue: attachment.queue,
1381
- latch: attachment.latch
1382
- })));
1383
- const offerToInbox = (attachment, event) => Effect.gen(function* () {
1384
- const offered = yield* Queue.offer(attachment.inbox, event).pipe(Effect.timeoutOption(REPORTER_STALL_TIMEOUT));
1385
- return yield* Option.match(offered, {
1386
- onNone: () => detachStalledReporter(attachment),
1387
- onSome: (accepted) => Match.value(accepted).pipe(Match.when(true, () => Effect.void), Match.orElse(() => warnEventDropped(attachment)))
1388
- });
1389
- });
1390
- const deliverReporterEvent = (attachment, event) => Effect.gen(function* () {
1391
- if (attachment.latch.state === "detached") return;
1392
- yield* offerToInbox(attachment, event);
1393
- });
1394
- const offerReporterEvent = (stage, event) => Effect.forEach(stageAttachments(stage), (attachment) => deliverReporterEvent(attachment, event), { discard: true });
1395
- const offerTerminalReport = (stage, report, metrics) => offerReporterEvent(stage, MutationTestReportReady.make({
1396
- report,
1397
- metrics
1398
- }));
1399
- const terminalDrainClass = (summary) => {
1400
- if (summary.terminalFailed.length > 0) return "RuntimeError";
1401
- return null;
1402
- };
1403
- const settleFailedAttachment = (attachment, cause) => Effect.gen(function* () {
1404
- if (attachment.latch.state === "terminal") {
1405
- yield* Effect.logError(`Reporter "${attachment.name}" failed while draining the terminal report.`).pipe(Effect.annotateLogs("cause", cause));
1406
- return {
1407
- kind: "terminal-failed",
1408
- name: attachment.name
1409
- };
1410
- }
1411
- yield* Effect.logWarning(`Reporter "${attachment.name}" failed before the terminal report and was detached; exit code unchanged.`).pipe(Effect.annotateLogs("cause", cause));
1412
- return {
1413
- kind: "detached",
1414
- name: attachment.name
1415
- };
1416
- });
1417
- const settleAttachment = (attachment) => Effect.gen(function* () {
1418
- const settled = yield* Effect.exit(Fiber.join(attachment.consumer)).pipe(Effect.timeoutOption(REPORTER_STALL_TIMEOUT));
1419
- return yield* Option.match(settled, {
1420
- onNone: () => Effect.logWarning(`Reporter "${attachment.name}" did not finish draining for ${Duration.toSeconds(REPORTER_STALL_TIMEOUT)} seconds and was detached; exit code unchanged.`).pipe(Effect.as({
1421
- kind: "detached",
1422
- name: attachment.name
1423
- })),
1424
- onSome: (exit) => Exit.match(exit, {
1425
- onSuccess: () => Effect.succeed({
1426
- kind: "completed",
1427
- name: attachment.name
1428
- }),
1429
- onFailure: (cause) => settleFailedAttachment(attachment, cause)
1430
- })
1431
- });
1432
- });
1433
- const failedReporterNames = (outcome) => {
1434
- if (outcome.kind === "terminal-failed") return [outcome.name];
1435
- return [];
1436
- };
1437
- const closeReporterStage = (stage) => Effect.gen(function* () {
1438
- const attachments = stageAttachments(stage);
1439
- yield* Effect.forEach(attachments, (attachment) => Queue.end(attachment.inbox), { discard: true });
1440
- yield* Effect.forEach(attachments, (attachment) => Effect.exit(Fiber.join(attachment.emitter)), { discard: true });
1441
- return { terminalFailed: (yield* Effect.forEach(attachments, settleAttachment, { concurrency: "unbounded" })).flatMap((outcome) => failedReporterNames(outcome)) };
1442
- });
1443
- const traceparentInit = (traceparent) => {
1444
- if (traceparent === void 0) return {};
1445
- return { traceparent };
1446
- };
1447
- const tracestateInit = (tracestate) => {
1448
- if (tracestate === void 0) return {};
1449
- return { tracestate };
1450
- };
1451
- const hasTraceFields = (init) => {
1452
- if (init.traceparent !== void 0) return true;
1453
- return init.tracestate !== void 0;
1454
- };
1455
- const initFromPhaseSpan = (span) => {
1456
- if (span === void 0) return void 0;
1457
- const parts = partsOfEffectSpan(span);
1458
- return {
1459
- traceparent: formatTraceparent(parts),
1460
- ...tracestateInit(parts.traceState)
1461
- };
1462
- };
1463
- const environmentTraceInit = () => Effect.gen(function* () {
1464
- const traceparent = yield* Config.string("TRACEPARENT").pipe(Effect.option);
1465
- const tracestate = yield* Config.string("TRACESTATE").pipe(Effect.option);
1466
- return {
1467
- ...traceparentInit(Option.getOrUndefined(traceparent)),
1468
- ...tracestateInit(Option.getOrUndefined(tracestate))
1469
- };
1470
- });
1471
- const initFromEnvironment = () => Effect.map(environmentTraceInit(), (init) => {
1472
- if (!hasTraceFields(init)) return void 0;
1473
- return init;
1474
- });
1475
- const currentReporterInit = (span) => Effect.gen(function* () {
1476
- const fromEnvironment = yield* initFromEnvironment();
1477
- const current = Option.getOrUndefined(yield* Effect.currentSpan.pipe(Effect.option));
1478
- return [
1479
- initFromPhaseSpan(span),
1480
- initFromPhaseSpan(current),
1481
- fromEnvironment
1482
- ].find(Predicate.isNotUndefined) ?? {};
1483
- });
1484
- const withPhaseSpan = (spanName, attributes, effect) => Effect.useSpan(spanName, { attributes }, (span) => effect(span).pipe(Effect.provideService(TraceContextReference, Option.some(partsOfEffectSpan(span)))));
1485
- //#endregion
1486
- //#region src/run/RunEnvironment.ts
1487
- var RunEnvironment = class extends Context.Service()("@systemfsoftware/stryker-js/run/RunEnvironment") {};
1488
- //#endregion
1489
- //#region src/run/dry-run.cell.ts
1490
- function buildDryRunFiles(prev) {
1491
- const files = [...MutableHashMap.keys(prev.project.filesToMutate)].map((name) => prev.sandbox.sandboxFileFor(name));
1492
- let testFiles;
1493
- if (prev.project.testFiles.length > 0) testFiles = prev.project.testFiles.map((file) => prev.sandbox.sandboxFileFor(file));
1494
- return {
1495
- files,
1496
- testFiles
1497
- };
1498
- }
1499
- const isCompleteDryRun = (result) => result.status === "complete";
1500
- const isFailedDryRun = (result) => result.status === "error";
1501
- const decodeCompleteDryRun = (result, allowEmpty) => Result.succeed(DryRunCommand.make({
1502
- status: "Complete",
1503
- testCount: result.tests.length,
1504
- failedTestCount: result.tests.filter((test) => test.status === "failed").length,
1505
- allowEmpty
1506
- }));
1507
- const decodeFailedDryRun = (result, allowEmpty) => Result.succeed(DryRunCommand.make({
1508
- status: "Error",
1509
- testCount: 0,
1510
- failedTestCount: 0,
1511
- allowEmpty,
1512
- errorMessage: result.errorMessage
1513
- }));
1514
- const decodeTimedOutDryRun = (result, allowEmpty) => Result.succeed(DryRunCommand.make({
1515
- status: "Timeout",
1516
- testCount: 0,
1517
- failedTestCount: 0,
1518
- allowEmpty,
1519
- ...result.reason !== void 0 && { reason: result.reason }
1520
- }));
1521
- const totalTestTime = (tests) => tests.reduce((total, test) => total + test.timeSpentMs, 0);
1522
- const overheadMillisOf = (grossMillis, tests) => Math.max(0, grossMillis - totalTestTime(tests));
1523
- const withOriginalFileName = (test, prev) => Match.value(test.fileName).pipe(Match.when(Predicate.isString, (fileName) => ({
1524
- ...test,
1525
- fileName: prev.sandbox.originalFileFor(fileName)
1526
- })), Match.orElse(() => test));
1527
- const withOriginalFileNames = (tests, prev) => tests.map((test) => withOriginalFileName(test, prev));
1528
- const announceDryRunOutcome = (tests, prev, gross, overheadMillis) => Match.value(tests.length).pipe(Match.when(0, () => Effect.logInfo("No tests were found")), Match.orElse(() => Effect.logInfo(`Initial test run succeeded. Ran ${tests.length} tests in ${Duration.format(gross)} (net ${totalTestTime(tests)} ms, overhead ${overheadMillis} ms).`).pipe(Effect.andThen(Effect.when(Effect.logInfo("Note: running the dry-run only. No mutations will be tested."), Effect.succeed(prev.options.dryRunOnly))))));
1529
- const completeDryRunPassed = (raw) => Effect.gen(function* () {
1530
- const prevDone = raw.prev;
1531
- const rawResult = raw.rawResult;
1532
- if (rawResult.status !== "complete") return yield* StageError.make({
1533
- stage: "dryRun",
1534
- reason: "Unexpected dry-run status after decision"
1535
- });
1536
- const tests = withOriginalFileNames(rawResult.tests, prevDone);
1537
- const dryRunResult = {
1538
- ...rawResult,
1539
- tests,
1540
- status: "complete"
1541
- };
1542
- const overheadMillis = overheadMillisOf(Duration.toMillis(raw.gross), tests);
1543
- yield* offerReporterEvent(prevDone.reporterStage, DryRunCompleted.make({
1544
- timing: {
1545
- net: totalTestTime(tests),
1546
- overhead: overheadMillis
1547
- },
1548
- capabilities: { reloadEnvironment: raw.capabilities.reloadEnvironment },
1549
- testCount: tests.length,
1550
- tests: [...dryRunResult.tests]
1551
- })).pipe(Effect.ignoreCause);
1552
- yield* announceDryRunOutcome(tests, prevDone, raw.gross, overheadMillis);
1553
- return {
1554
- ...prevDone,
1555
- dryRunResult,
1556
- testCoverage: testCoverageFrom(dryRunResult),
1557
- timeOverhead: Duration.millis(overheadMillis)
1558
- };
1559
- });
1560
- const dryRunCell = Cell.layer({
1561
- read: (command) => Effect.gen(function* () {
1562
- yield* Scope.Scope;
1563
- const idGenerator = yield* IdGenerator;
1564
- const { files, testFiles } = buildDryRunFiles(command);
1565
- const dryRunTimeout = command.options.dryRunTimeoutMinutes * 60 * 1e3;
1566
- yield* Effect.logInfo("Starting dry run");
1567
- const { rawResult, capabilities, gross } = yield* Effect.scoped(Effect.gen(function* () {
1568
- const childRunnerEffect = Effect.suspend(() => resolvePluginWorkerEntry({
1569
- loaded: command.loadedPlugins,
1570
- kind: "TestRunner",
1571
- name: command.options.testRunner
1572
- }).pipe(Effect.mapError(missingWorkerEntry("dryRun", "test runner", command.options.testRunner)), Effect.flatMap((runnerEntry) => makeChildProcessTestRunner({
1573
- options: command.options,
1574
- fileDescriptions: command.project.fileDescriptions,
1575
- sandboxWorkingDirectory: command.sandbox.workingDirectory,
1576
- workerEntrypoint: runnerEntry.entrypoint,
1577
- idGenerator
1578
- }))));
1579
- const runner = yield* buildTestRunner({
1580
- options: command.options,
1581
- fileDescriptions: command.project.fileDescriptions,
1582
- sandboxWorkingDirectory: command.sandbox.workingDirectory,
1583
- idGenerator,
1584
- retire: Effect.void,
1585
- testFiles: testFiles ?? []
1586
- }, childRunnerEffect);
1587
- const extra = Option.match(Option.fromUndefinedOr(testFiles), {
1588
- onNone: () => ({}),
1589
- onSome: (files) => ({ testFiles: files })
1590
- });
1591
- const timed = yield* Effect.timed(runner.dryRun({
1592
- timeout: dryRunTimeout,
1593
- coverageAnalysis: command.options.coverageAnalysis,
1594
- disableBail: command.options.disableBail,
1595
- files,
1596
- ...extra
1597
- }).pipe(Effect.mapError((cause) => StageError.make({
1598
- stage: "dryRun",
1599
- reason: "Dry run failed",
1600
- cause
1601
- }))));
1602
- const gross = timed[0];
1603
- return {
1604
- rawResult: timed[1],
1605
- capabilities: yield* runner.capabilities.pipe(Effect.mapError((cause) => StageError.make({
1606
- stage: "dryRun",
1607
- reason: "Failed to get test runner capabilities",
1608
- cause
1609
- }))),
1610
- gross
1611
- };
1612
- })).pipe(Effect.mapError((cause) => {
1613
- if (S.is(StageError)(cause)) return cause;
1614
- return StageError.make({
1615
- stage: "dryRun",
1616
- reason: "Dry run failed to start test runner",
1617
- cause
1618
- });
1619
- }));
1620
- return {
1621
- prev: command,
1622
- rawResult,
1623
- capabilities,
1624
- gross
1625
- };
1626
- }),
1627
- decode: (raw) => Match.value(raw.rawResult).pipe(Match.when(isCompleteDryRun, (complete) => decodeCompleteDryRun(complete, raw.prev.options.allowEmpty)), Match.when(isFailedDryRun, (failed) => decodeFailedDryRun(failed, raw.prev.options.allowEmpty)), Match.orElse((timedOut) => decodeTimedOutDryRun(timedOut, raw.prev.options.allowEmpty))),
1628
- decide: dryRun,
1629
- encode: (outcome) => outcome,
1630
- write: (outcome, raw) => withPhaseSpan("dryRun", {}, () => Effect.gen(function* () {
1631
- const env = yield* RunEnvironment;
1632
- const now = yield* Clock.currentTimeMillis;
1633
- const queue = yield* RunEvents;
1634
- yield* Queue.offer(queue, PhaseEntered.make({
1635
- phase: "dry-run",
1636
- elapsedMs: now - env.runStartedAt
1637
- }));
1638
- const out = outcome;
1639
- if (Result.isFailure(out)) {
1640
- const err = out.failure;
1641
- return yield* StageError.make({
1642
- stage: err.stage,
1643
- reason: err.reason,
1644
- cause: err
1645
- });
1646
- }
1647
- return yield* Match.value(out.success).pipe(Match.tag("DryRunFailed", (decision) => Effect.fail(StageError.make({
1648
- stage: "dryRun",
1649
- reason: "There were failed tests in the initial test run.",
1650
- cause: decision
1651
- }))), Match.tag("DryRunPassed", () => completeDryRunPassed(raw)), Match.exhaustive);
1652
- }))
1653
- });
1654
- //#endregion
1655
- //#region src/plan-instrumentation.workflow.ts
1656
- var InstrumentError = class extends S.TaggedError()("InstrumentError", {
1657
- stage: S.Literal("instrument"),
1658
- reason: S.String
1659
- }) {};
1660
- var InstrumentCommand = class extends S.TaggedClass()("InstrumentCommand", {
1661
- fileCount: S.Finite,
1662
- inPlace: S.Boolean,
1663
- pluginCount: S.Finite
1664
- }) {};
1665
- const InstrumentDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/InstrumentDecision");
1666
- var InPlaceInstrument = class extends S.TaggedClass()("InPlaceInstrument", {
1667
- workingDirectoryHint: S.String,
1668
- backupDirectoryHint: S.String,
1669
- fileCount: S.Finite
1670
- }) {
1671
- [InstrumentDecisionTypeId] = InstrumentDecisionTypeId;
1672
- };
1673
- var EphemeralInstrument = class extends S.TaggedClass()("EphemeralInstrument", {
1674
- workingDirectoryHint: S.String,
1675
- fileCount: S.Finite
1676
- }) {
1677
- [InstrumentDecisionTypeId] = InstrumentDecisionTypeId;
1678
- };
1679
- const planInstrumentation = Workflow.make(InstrumentCommand, (command) => Match.value(command).pipe(Match.when({ fileCount: 0 }, () => Result.fail(InstrumentError.make({
1680
- stage: "instrument",
1681
- reason: "No files to instrument."
1682
- }))), Match.when({ inPlace: true }, () => Result.succeed(InPlaceInstrument.make({
1683
- workingDirectoryHint: "inPlace",
1684
- backupDirectoryHint: "backup",
1685
- fileCount: command.fileCount
1686
- }))), Match.orElse(() => Result.succeed(EphemeralInstrument.make({
1687
- workingDirectoryHint: "temp",
1688
- fileCount: command.fileCount
1689
- })))));
1690
- //#endregion
1691
- //#region src/IncrementalReport.schema.ts
1692
- const PositionSchema = S.Struct({
1693
- line: S.Finite,
1694
- column: S.Finite
1695
- });
1696
- const LocationSchema$1 = S.Struct({
1697
- start: PositionSchema,
1698
- end: PositionSchema
1699
- });
1700
- const OpenEndLocationSchema = S.Struct({
1701
- start: PositionSchema,
1702
- end: S.optional(PositionSchema)
1703
- });
1704
- const MutantResultLikeSchema = S.Struct({
1705
- id: S.String,
1706
- mutatorName: S.String,
1707
- replacement: S.String,
1708
- location: LocationSchema$1,
1709
- status: S.Literals([
1710
- "Killed",
1711
- "Survived",
1712
- "NoCoverage",
1713
- "Timeout",
1714
- "CompileError",
1715
- "RuntimeError",
1716
- "Ignored",
1717
- "Pending"
1718
- ]),
1719
- killedBy: S.optional(S.Array(S.String)),
1720
- coveredBy: S.optional(S.Array(S.String)),
1721
- static: S.optional(S.Boolean),
1722
- statusReason: S.optional(S.String),
1723
- testsCompleted: S.optional(S.Finite),
1724
- description: S.optional(S.String),
1725
- duration: S.optional(S.Finite)
1726
- });
1727
- const FileResultLikeSchema = S.Struct({
1728
- language: S.String,
1729
- source: S.String,
1730
- mutants: S.Array(MutantResultLikeSchema)
1731
- });
1732
- const TestDefinitionLikeSchema = S.Struct({
1733
- id: S.String,
1734
- name: S.String,
1735
- location: S.optional(OpenEndLocationSchema)
1736
- });
1737
- const TestFileLikeSchema = S.Struct({
1738
- source: S.optional(S.String),
1739
- tests: S.Array(TestDefinitionLikeSchema)
1740
- });
1741
- const ThresholdsLikeSchema = S.Struct({
1742
- high: S.Finite,
1743
- low: S.Finite
1744
- });
1745
- const IncrementalReportSchema = S.StructWithRest(S.Struct({
1746
- schemaVersion: S.String,
1747
- thresholds: ThresholdsLikeSchema,
1748
- files: S.Record(S.String, FileResultLikeSchema),
1749
- testFiles: S.optional(S.Record(S.String, TestFileLikeSchema))
1750
- }), [S.Record(S.String, S.Unknown)]);
1751
- var IncrementalReportError = class extends S.TaggedError()("IncrementalReportError", { message: S.String }) {};
1752
- //#endregion
1753
- //#region src/Project.ts
1754
- const DEFAULT_GLOB$1 = "**/*.{js,ts,jsx,tsx,html,vue,mjs,mts,cts,cjs}";
1755
- const normalizeFileName$3 = (fileName) => fileName.replace(/\\/g, "/");
1756
- function escapeRegExp(value) {
1757
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1758
- }
1759
- function consumeDoubleStar(pattern, index) {
1760
- if (pattern[index + 2] === "/") return {
1761
- consumed: 3,
1762
- output: "(?:.*\\/)?"
1763
- };
1764
- return {
1765
- consumed: 2,
1766
- output: ".*"
1767
- };
1768
- }
1769
- function consumeStar(pattern, index) {
1770
- if (pattern[index + 1] === "*") return consumeDoubleStar(pattern, index);
1771
- return {
1772
- consumed: 1,
1773
- output: "[^/]*"
1774
- };
1775
- }
1776
- function consumeBrace(pattern, index) {
1777
- const close = pattern.indexOf("}", index);
1778
- if (close === -1) return {
1779
- consumed: 1,
1780
- output: "\\{"
1781
- };
1782
- const escaped = pattern.slice(index + 1, close).split(",").map((part) => escapeRegExp(part));
1783
- return {
1784
- consumed: close - index + 1,
1785
- output: `(${escaped.join("|")})`
1786
- };
1787
- }
1788
- function consumeCharClass(pattern, index) {
1789
- const close = pattern.indexOf("]", index);
1790
- if (close === -1) return {
1791
- consumed: 1,
1792
- output: "\\["
1793
- };
1794
- return {
1795
- consumed: close - index + 1,
1796
- output: pattern.slice(index, close + 1)
1797
- };
1798
- }
1799
- function consumeGlobChar(pattern, index) {
1800
- const char = pattern[index];
1801
- return Match.value(char).pipe(Match.when("*", () => consumeStar(pattern, index)), Match.when("?", () => ({
1802
- consumed: 1,
1803
- output: "[^/]"
1804
- })), Match.when("{", () => consumeBrace(pattern, index)), Match.when("[", () => consumeCharClass(pattern, index)), Match.orElse(() => ({
1805
- consumed: 1,
1806
- output: escapeRegExp(Option.getOrElse(Option.fromUndefinedOr(char), () => ""))
1807
- })));
1808
- }
1809
- function globToRegExp(pattern) {
1810
- let index = 0;
1811
- let acc = "";
1812
- while (index < pattern.length) {
1813
- const step = consumeGlobChar(pattern, index);
1814
- acc += step.output;
1815
- index += step.consumed;
1816
- }
1817
- return new RegExp(`^${acc}$`);
1818
- }
1819
- function resolveAgainstBase(basePath, pattern) {
1820
- const normalized = normalizeFileName$3(pattern);
1821
- const base = trimTrailingSlashes(normalizeFileName$3(basePath));
1822
- return Match.value(normalized.startsWith("/")).pipe(Match.when(true, () => normalized), Match.orElse(() => Match.value(normalized.startsWith("./")).pipe(Match.when(true, () => `${base}/${normalized.slice(2)}`), Match.orElse(() => `${base}/${normalized}`))));
1823
- }
1824
- function trimTrailingSlashes(value) {
1825
- const trimmed = value.replace(/\/+$/, "");
1826
- return Match.value(trimmed.length === 0).pipe(Match.when(true, () => value.slice(0, 1)), Match.orElse(() => trimmed));
1827
- }
1828
- function globPatternOf(pattern) {
1829
- if (typeof pattern === "boolean") return Match.value(pattern).pipe(Match.when(true, () => DEFAULT_GLOB$1), Match.orElse(() => void 0));
1830
- return normalizeFileName$3(pattern);
1831
- }
1832
- function hasHiddenSegment(fileName, base) {
1833
- return Match.value(fileName.startsWith(base)).pipe(Match.when(true, () => fileName.slice(base.length)), Match.orElse(() => fileName)).split("/").some((segment) => segment.startsWith("."));
1834
- }
1835
- const isExcludedHiddenFile = (normalizedFile, base, allowHiddenFiles, patternHasDot) => Match.value(allowHiddenFiles).pipe(Match.when(true, () => false), Match.orElse(() => Match.value(hasHiddenSegment(normalizedFile, base)).pipe(Match.when(false, () => false), Match.orElse(() => !patternHasDot))));
1836
- function createPureMatcher(pattern, allowHiddenFiles, basePath) {
1837
- const relative = globPatternOf(pattern);
1838
- if (relative === void 0) return () => false;
1839
- const regex = globToRegExp(resolveAgainstBase(basePath, relative));
1840
- const patternHasDot = relative.includes(".");
1841
- const base = `${trimTrailingSlashes(normalizeFileName$3(basePath))}/`;
1842
- return (fileName) => {
1843
- const normalizedFile = normalizeFileName$3(fileName);
1844
- return Match.value(isExcludedHiddenFile(normalizedFile, base, allowHiddenFiles, patternHasDot)).pipe(Match.when(true, () => false), Match.orElse(() => regex.test(normalizedFile)));
1845
- };
1846
- }
1847
- const rangeListOf = (mutate) => Option.filter(Option.fromUndefinedOr(mutate), Array.isArray);
1848
- function unionPair(first, second) {
1849
- const ranges = Option.all([rangeListOf(first.mutate), rangeListOf(second.mutate)]);
1850
- return Option.match(ranges, {
1851
- onSome: ([firstRanges, secondRanges]) => ({ mutate: [...secondRanges, ...firstRanges] }),
1852
- onNone: () => Match.value(second.mutate).pipe(Match.when(true, () => ({ mutate: true })), Match.orElse(() => Match.value(first.mutate).pipe(Match.when(false, () => ({ mutate: second.mutate })), Match.orElse(() => ({ mutate: first.mutate })))))
1853
- });
1854
- }
1855
- function unionFileDescriptions(first, second) {
1856
- return Option.match(Option.fromUndefinedOr(second), {
1857
- onNone: () => first,
1858
- onSome: (defined) => unionPair(first, defined)
1859
- });
1860
- }
1861
- function overlapOf(firstRange, secondRange) {
1862
- const startLine = Math.max(firstRange.start.line, secondRange.start.line);
1863
- const endLine = Math.min(firstRange.end.line, secondRange.end.line);
1864
- const startColumn = Match.value(firstRange.start.line === startLine).pipe(Match.when(true, () => firstRange.start.column), Match.orElse(() => secondRange.start.column));
1865
- const endColumn = Match.value(firstRange.end.line === endLine).pipe(Match.when(true, () => firstRange.end.column), Match.orElse(() => secondRange.end.column));
1866
- return Match.value(startLine > endLine).pipe(Match.when(true, () => void 0), Match.orElse(() => ({
1867
- start: {
1868
- line: startLine,
1869
- column: startColumn
1870
- },
1871
- end: {
1872
- line: endLine,
1873
- column: endColumn
1874
- }
1875
- })));
1876
- }
1877
- const isLocation = (value) => value !== void 0;
1878
- function overlapRanges(firstRanges, secondRanges) {
1879
- return firstRanges.flatMap((firstRange) => secondRanges.map((secondRange) => overlapOf(firstRange, secondRange))).filter(isLocation);
1880
- }
1881
- function intersectFileDescriptions(first, second) {
1882
- const ranges = Option.all([rangeListOf(first.mutate), rangeListOf(second.mutate)]);
1883
- return Option.match(ranges, {
1884
- onSome: ([firstRanges, secondRanges]) => ({ mutate: overlapRanges(firstRanges, secondRanges) }),
1885
- onNone: () => Match.value(first.mutate).pipe(Match.when(true, () => second), Match.orElse(() => Match.value(second.mutate).pipe(Match.when(true, () => first), Match.orElse(() => ({ mutate: false })))))
1886
- });
1887
- }
1888
- const rangeGroup = (match, index, fallback) => Option.getOrElse(Option.fromUndefinedOr(match[index]), () => fallback);
1889
- function mutationRangeOf(mutatePattern) {
1890
- const match = MUTATION_RANGE_REGEX.exec(mutatePattern);
1891
- if (match === null) return;
1892
- const startLine = Number(rangeGroup(match, 3, "1"));
1893
- const startColumn = Number(rangeGroup(match, 4, "0"));
1894
- const endLine = Number(rangeGroup(match, 5, "1"));
1895
- const endColumn = Number(rangeGroup(match, 6, String(Number.MAX_SAFE_INTEGER)));
1896
- return {
1897
- pattern: rangeGroup(match, 1, mutatePattern),
1898
- mutate: [{
1899
- start: {
1900
- line: startLine - 1,
1901
- column: startColumn
1902
- },
1903
- end: {
1904
- line: endLine - 1,
1905
- column: endColumn
1906
- }
1907
- }]
1908
- };
1909
- }
1910
- function describeMatchingFiles(fileNames, pattern, mutate, basePath) {
1911
- const matches = createPureMatcher(pattern, false, basePath);
1912
- const entries = Array.from(fileNames).filter((fileName) => matches(fileName)).map((fileName) => [fileName, { mutate }]);
1913
- return HashMap.fromIterable(entries);
1914
- }
1915
- function filterMutatePatternPure(fileNames, mutatePattern, basePath) {
1916
- const range = Option.fromUndefinedOr(mutationRangeOf(mutatePattern));
1917
- return describeMatchingFiles(fileNames, Option.getOrElse(Option.map(range, (found) => found.pattern), () => mutatePattern), Option.getOrElse(Option.map(range, (found) => found.mutate), () => true), basePath);
1918
- }
1919
- function resolveFileDescriptionsPure(inputFileNames, mutatePatterns, targetMutatePatterns, basePath) {
1920
- const initial = HashMap.fromIterable(inputFileNames.map((name) => [name, { mutate: false }]));
1921
- const afterMutate = mutatePatterns.reduce((acc, pattern) => {
1922
- if (pattern.startsWith("!")) {
1923
- const withoutBang = pattern.substring(1);
1924
- const files = filterMutatePatternPure(HashMap.keys(acc), withoutBang, basePath);
1925
- return Array.from(HashMap.keys(files)).reduce((inner, fileName) => HashMap.set(inner, fileName, { mutate: false }), acc);
1926
- }
1927
- const files = filterMutatePatternPure(inputFileNames, pattern, basePath);
1928
- return HashMap.reduce(files, acc, (inner, file, fileName) => {
1929
- const existingOpt = HashMap.get(inner, fileName);
1930
- if (Option.isSome(existingOpt)) {
1931
- const existing = existingOpt.value;
1932
- return HashMap.set(inner, fileName, unionFileDescriptions(file, existing));
1933
- }
1934
- return HashMap.set(inner, fileName, unionFileDescriptions(file, void 0));
1935
- });
1936
- }, initial);
1937
- if (targetMutatePatterns !== void 0) {
1938
- const seen = targetMutatePatterns.reduce((acc, pattern) => {
1939
- const files = filterMutatePatternPure(HashMap.keys(afterMutate), pattern, basePath);
1940
- return HashMap.reduce(files, acc, (inner, description, fileName) => Option.match(HashMap.get(afterMutate, fileName), {
1941
- onNone: () => inner,
1942
- onSome: (current) => {
1943
- const intersected = intersectFileDescriptions(current, description);
1944
- const prevSeen = Option.getOrElse(HashMap.get(inner, fileName), () => void 0);
1945
- return HashMap.set(inner, fileName, unionFileDescriptions(intersected, prevSeen));
1946
- }
1947
- }));
1948
- }, HashMap.empty());
1949
- const final = HashMap.reduce(afterMutate, HashMap.empty(), (acc, _description, fileName) => {
1950
- const seenOpt = HashMap.get(seen, fileName);
1951
- if (Option.isSome(seenOpt)) {
1952
- const seenValue = seenOpt.value;
1953
- return HashMap.set(acc, fileName, seenValue);
1954
- }
1955
- return HashMap.set(acc, fileName, { mutate: false });
1956
- });
1957
- return Object.fromEntries(final);
1958
- }
1959
- return Object.fromEntries(afterMutate);
1960
- }
1961
- function resolveTestFilesPure(inputFileNames, testFilePatterns, basePath) {
1962
- if (testFilePatterns.length === 0) return [];
1963
- const allMatched = testFilePatterns.flatMap((pattern) => {
1964
- const matches = createPureMatcher(pattern, false, basePath);
1965
- return inputFileNames.filter((fileName) => matches(fileName));
1966
- });
1967
- return Array.from(HashSet.fromIterable(allMatched));
1968
- }
1969
- const selectFiles = (input) => ({
1970
- fileDescriptions: resolveFileDescriptionsPure(input.inputFileNames, input.mutatePatterns, input.targetMutatePatterns, input.basePath),
1971
- testFiles: resolveTestFilesPure(input.inputFileNames, input.testFilePatterns, input.basePath)
1972
- });
1973
- const toPosition = (position) => ({
1974
- line: position.line,
1975
- column: position.column
1976
- });
1977
- const toLocation = (location) => ({
1978
- start: toPosition(location.start),
1979
- end: toPosition(location.end)
1980
- });
1981
- const toOpenEndLocation = (location) => Option.match(Option.fromUndefinedOr(location.end), {
1982
- onNone: () => ({ start: toPosition(location.start) }),
1983
- onSome: (end) => ({
1984
- start: toPosition(location.start),
1985
- end: toPosition(end)
1986
- })
1987
- });
1988
- const withMappedMutantLocations = (report) => Object.fromEntries(Object.entries(report.files).map(([fileName, file]) => [fileName, {
1989
- ...file,
1990
- mutants: file.mutants.map((mutant) => ({
1991
- ...mutant,
1992
- location: toLocation(mutant.location)
1993
- }))
1994
- }]));
1995
- const withMappedTestLocations = (testFiles) => Object.fromEntries(Object.entries(testFiles).map(([fileName, file]) => [fileName, {
1996
- ...file,
1997
- tests: file.tests.map((test) => Option.match(Option.fromUndefinedOr(test.location), {
1998
- onNone: () => ({ ...test }),
1999
- onSome: (location) => ({
2000
- ...test,
2001
- location: toOpenEndLocation(location)
2002
- })
2003
- }))
2004
- }]));
2005
- const reshape = (decoded) => Option.match(Option.fromUndefinedOr(decoded.testFiles), {
2006
- onNone: () => ({
2007
- ...decoded,
2008
- files: withMappedMutantLocations(decoded)
2009
- }),
2010
- onSome: (testFiles) => ({
2011
- ...decoded,
2012
- files: withMappedMutantLocations(decoded),
2013
- testFiles: withMappedTestLocations(testFiles)
2014
- })
2015
- });
2016
- const decodeIncrementalReport = (raw) => Result.match(S.decodeUnknownResult(IncrementalReportSchema)(raw), {
2017
- onFailure: () => Result.fail(IncrementalReportError.make({ message: "The incremental report is not a mutation testing report; delete it or re-run without --incremental." })),
2018
- onSuccess: (decoded) => Result.succeed(reshape(decoded))
2019
- });
2020
- const parseJson = (text) => JSON.parse(text);
2021
- const stringArrayEquivalence = Equivalence.Array(Equivalence.String);
2022
- function makeProjectFile(name, mutate, content, originalContent) {
2023
- return {
2024
- name,
2025
- mutate,
2026
- content,
2027
- originalContent
2028
- };
2029
- }
2030
- function withContent(file, content) {
2031
- return {
2032
- ...file,
2033
- content
2034
- };
2035
- }
2036
- function hasChanges(file) {
2037
- return file.content !== void 0 && file.content !== file.originalContent;
2038
- }
2039
- function toInstrumenterFile(file) {
2040
- return Effect.gen(function* () {
2041
- return {
2042
- content: yield* readContent(file),
2043
- mutate: file.mutate,
2044
- name: file.name
2045
- };
2046
- });
2047
- }
2048
- function readContent(file) {
2049
- const inMemory = Option.orElse(Option.fromUndefinedOr(file.content), () => Option.fromUndefinedOr(file.originalContent));
2050
- return Option.match(inMemory, {
2051
- onSome: (content) => Effect.succeed(content),
2052
- onNone: () => Effect.gen(function* () {
2053
- return yield* (yield* FileSystem.FileSystem).readFileString(file.name);
2054
- })
2055
- });
2056
- }
2057
- function readOriginal(file) {
2058
- return Effect.gen(function* () {
2059
- return yield* (yield* FileSystem.FileSystem).readFileString(file.name);
2060
- });
2061
- }
2062
- function writeInPlace(file) {
2063
- const content = Option.filter(Option.fromUndefinedOr(file.content), () => hasChanges(file));
2064
- return Option.match(content, {
2065
- onNone: () => Effect.void,
2066
- onSome: (text) => Effect.gen(function* () {
2067
- yield* (yield* FileSystem.FileSystem).writeFileString(file.name, text);
2068
- })
2069
- });
2070
- }
2071
- function writeToSandbox(file, sandboxDir, basePath) {
2072
- return Effect.gen(function* () {
2073
- const fs = yield* FileSystem.FileSystem;
2074
- const path = yield* Path.Path;
2075
- const relative = path.relative(basePath, file.name);
2076
- const targetFileName = path.join(sandboxDir, relative);
2077
- yield* fs.makeDirectory(path.dirname(targetFileName), { recursive: true });
2078
- const content = yield* readContent(file);
2079
- yield* fs.writeFileString(targetFileName, content);
2080
- return targetFileName;
2081
- });
2082
- }
2083
- function backupTo(file, backupDir, basePath) {
2084
- return Effect.gen(function* () {
2085
- const fs = yield* FileSystem.FileSystem;
2086
- const path = yield* Path.Path;
2087
- const relative = path.relative(basePath, file.name);
2088
- const backupFileName = path.join(backupDir, relative);
2089
- yield* fs.makeDirectory(path.dirname(backupFileName), { recursive: true });
2090
- const content = yield* readContent(file);
2091
- yield* fs.writeFileString(backupFileName, content);
2092
- return backupFileName;
2093
- });
2094
- }
2095
- function addProjectFile(files, filesToMutate, name, desc) {
2096
- const file = makeProjectFile(name, desc.mutate);
2097
- MutableHashMap.set(files, name, file);
2098
- Match.value(desc.mutate === false).pipe(Match.when(true, () => void 0), Match.orElse(() => MutableHashMap.set(filesToMutate, name, file)));
2099
- }
2100
- function makeProject(fileDescriptions, incrementalReport, testFiles = []) {
2101
- const files = MutableHashMap.empty();
2102
- const filesToMutate = MutableHashMap.empty();
2103
- Object.entries(fileDescriptions).forEach(([name, desc]) => addProjectFile(files, filesToMutate, name, desc));
2104
- return {
2105
- fileDescriptions,
2106
- incrementalReport,
2107
- testFiles,
2108
- files,
2109
- filesToMutate
2110
- };
2111
- }
2112
- function withFile(project, file) {
2113
- const files = MutableHashMap.fromIterable(project.files);
2114
- MutableHashMap.set(files, file.name, file);
2115
- const filesToMutate = MutableHashMap.fromIterable(project.filesToMutate);
2116
- if (file.mutate !== false) MutableHashMap.set(filesToMutate, file.name, file);
2117
- else MutableHashMap.remove(filesToMutate, file.name);
2118
- return {
2119
- ...project,
2120
- files,
2121
- filesToMutate
2122
- };
2123
- }
2124
- function withInstrumentedFiles(project, instrumented) {
2125
- let next = project;
2126
- for (const { name, content } of instrumented) Option.match(MutableHashMap.get(next.files, name), {
2127
- onNone: () => void 0,
2128
- onSome: (existing) => {
2129
- next = withFile(next, withContent(existing, content));
2130
- }
2131
- });
2132
- return next;
2133
- }
2134
- const applyIgnoreRule = (included, negate, matches) => Match.value(negate).pipe(Match.when(included, () => included), Match.orElse(() => Match.value(matches()).pipe(Match.when(true, () => negate), Match.orElse(() => included))));
2135
- function resolveInputFileNames(ignoreRules, basePath) {
2136
- return Effect.gen(function* () {
2137
- const fs = yield* FileSystem.FileSystem;
2138
- const pathService = yield* Path.Path;
2139
- const ignoreMatchers = ignoreRules.map((pattern) => new Minimatch(pattern, {
2140
- dot: true,
2141
- flipNegate: true,
2142
- nocase: true
2143
- }));
2144
- const matchesDirectoryPartially = (entryPath, rule) => rule.match(`/${entryPath}`, true) || rule.match(entryPath, true);
2145
- const matchesFile = (entryName, entryPath, rule) => [
2146
- entryName,
2147
- entryPath,
2148
- `/${entryPath}`
2149
- ].some((candidate) => rule.match(candidate));
2150
- const matchesDirectoryTail = (entryPath, rule) => [rule.match(`/${entryPath}/`), rule.match(`${entryPath}/`)].some((matched) => matched);
2151
- const matchesNegatedDirectory = (entryPath, rule) => rule.negate && matchesDirectoryPartially(entryPath, rule);
2152
- const matchesDirectory = (entryName, entryPath, rule) => [
2153
- matchesFile(entryName, entryPath, rule),
2154
- matchesDirectoryTail(entryPath, rule),
2155
- matchesNegatedDirectory(entryPath, rule)
2156
- ].some((matched) => matched);
2157
- const isIncluded = (name, entryPath, isDirectory) => ignoreMatchers.reduce((included, rule) => applyIgnoreRule(included, rule.negate, () => Match.value(isDirectory).pipe(Match.when(true, () => matchesDirectory(name, entryPath, rule)), Match.orElse(() => matchesFile(name, entryPath, rule)))), true);
2158
- const crawlDir = (dir, rootDir) => Effect.gen(function* () {
2159
- const entries = yield* fs.readDirectory(dir);
2160
- const relativeName = pathService.relative(rootDir, dir);
2161
- const filtered = (yield* Effect.forEach(entries, (name) => Effect.gen(function* () {
2162
- const full = pathService.join(dir, name);
2163
- return {
2164
- name,
2165
- full,
2166
- isDirectory: yield* fs.stat(full).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false)),
2167
- entryPath: `${(() => {
2168
- if (relativeName.length > 0) return `${relativeName}/`;
2169
- return "";
2170
- })()}${name}`
2171
- };
2172
- }), { concurrency: 256 })).filter(({ name, entryPath, isDirectory }) => isIncluded(name, entryPath, isDirectory));
2173
- return (yield* Effect.forEach(filtered, (entry) => {
2174
- if (entry.isDirectory) return crawlDir(entry.full, rootDir);
2175
- return Effect.succeed([entry.full]);
2176
- }, { concurrency: 256 })).flat();
2177
- });
2178
- return yield* crawlDir(basePath, basePath);
2179
- });
2180
- }
2181
- function parseIncrementalReport(contents) {
2182
- return Option.match(Option.fromUndefinedOr(contents), {
2183
- onNone: () => Effect.succeedNone,
2184
- onSome: (text) => Effect.gen(function* () {
2185
- const parsed = yield* Effect.try(() => parseJson(text));
2186
- const rawReport = yield* Effect.fromResult(decodeIncrementalReport(parsed));
2187
- const isMutationTestResult = (_value) => true;
2188
- if (!isMutationTestResult(rawReport)) throw new Error("Invalid incremental report shape");
2189
- return Option.fromUndefinedOr(rawReport);
2190
- })
2191
- });
2192
- }
2193
- function readIncrementalReport(incremental, incrementalFile) {
2194
- return Match.value(incremental).pipe(Match.when(false, () => Effect.succeedNone), Match.orElse(() => Effect.gen(function* () {
2195
- return yield* parseIncrementalReport(yield* (yield* FileSystem.FileSystem).readFileString(incrementalFile).pipe(Effect.catchTag("PlatformError", (error) => Match.value(error.reason).pipe(Match.tag("NotFound", () => Effect.logInfo(`No incremental result file found at ${incrementalFile}, a full mutation testing run will be performed.`).pipe(Effect.as(void 0))), Match.orElse(() => Effect.fail(error))))));
2196
- })));
2197
- }
2198
- function readProject(options, targetMutatePatterns, basePath) {
2199
- const { mutate, tempDirName, ignorePatterns, incremental, incrementalFile, progressStreamFile, htmlReporter, jsonReporter, testFiles } = options;
2200
- const mutatePatterns = mutate;
2201
- const testFilePatterns = testFiles;
2202
- const ignoreRules = [
2203
- ...ALWAYS_IGNORE,
2204
- tempDirName,
2205
- incrementalFile,
2206
- progressStreamFile,
2207
- htmlReporter.fileName,
2208
- jsonReporter.fileName,
2209
- ...ignorePatterns
2210
- ];
2211
- return Effect.gen(function* () {
2212
- const inputFileNames = yield* resolveInputFileNames(ignoreRules, basePath);
2213
- const defaults = yield* defaultOptions;
2214
- const decision = selectFiles((() => {
2215
- if (targetMutatePatterns === void 0) return {
2216
- inputFileNames,
2217
- mutatePatterns,
2218
- testFilePatterns,
2219
- basePath
2220
- };
2221
- return {
2222
- inputFileNames,
2223
- mutatePatterns,
2224
- testFilePatterns,
2225
- basePath,
2226
- targetMutatePatterns
2227
- };
2228
- })());
2229
- if (!stringArrayEquivalence(mutatePatterns, defaults.mutate)) yield* Effect.forEach(mutatePatterns, (pattern) => Effect.gen(function* () {
2230
- const excluding = pattern.startsWith("!");
2231
- const inner = Match.value(excluding).pipe(Match.when(true, () => pattern.substring(1)), Match.orElse(() => pattern));
2232
- const probed = selectFiles({
2233
- inputFileNames,
2234
- mutatePatterns: [inner],
2235
- testFilePatterns: [],
2236
- basePath
2237
- });
2238
- if (Object.keys(probed.fileDescriptions).length > 0) return;
2239
- yield* Effect.logWarning(Match.value(excluding).pipe(Match.when(true, () => `Glob pattern "${pattern}" did not exclude any files.`), Match.orElse(() => `Glob pattern "${pattern}" did not result in any files.`)));
2240
- }));
2241
- yield* Effect.forEach(testFilePatterns, (pattern) => Effect.gen(function* () {
2242
- if (selectFiles({
2243
- inputFileNames,
2244
- mutatePatterns: [],
2245
- testFilePatterns: [pattern],
2246
- basePath
2247
- }).testFiles.length === 0) yield* Effect.logWarning(`Glob pattern "${pattern}" did not match any test files.`);
2248
- }));
2249
- const incrementalReport = yield* readIncrementalReport(incremental, incrementalFile);
2250
- return makeProject(decision.fileDescriptions, Option.getOrUndefined(incrementalReport), [...decision.testFiles]);
2251
- });
2252
- }
2253
- //#endregion
2254
- //#region ../../node_modules/.pnpm/@jsr+std__jsonc@1.0.2/node_modules/@jsr/std__jsonc/parse.js
2255
- /**
2256
- * Converts a JSON with Comments (JSONC) string into an object.
2257
- *
2258
- * @example Usage
2259
- * ```ts
2260
- * import { parse } from "@std/jsonc";
2261
- * import { assertEquals } from "@std/assert";
2262
- *
2263
- * assertEquals(parse('{"foo": "bar"}'), { foo: "bar" });
2264
- * assertEquals(parse('{"foo": "bar", }'), { foo: "bar" });
2265
- * assertEquals(parse('{"foo": "bar", } /* comment *\/'), { foo: "bar" });
2266
- * ```
2267
- *
2268
- * @throws {SyntaxError} If the JSONC string is invalid.
2269
- * @param text A valid JSONC string.
2270
- * @returns The parsed JsonValue from the JSONC string.
2271
- */ function parse(text) {
2272
- if (new.target) throw new TypeError("Cannot create an instance: parse is not a constructor");
2273
- return new JsoncParser(text).parse();
2274
- }
2275
- var JsoncParser = class {
2276
- #whitespace = /* @__PURE__ */ new Set(" \r\n");
2277
- #numberEndToken = /* @__PURE__ */ new Set([..."[]{}:,/", ...this.#whitespace]);
2278
- #text;
2279
- #length;
2280
- #tokenized;
2281
- constructor(text) {
2282
- this.#text = `${text}`;
2283
- this.#length = this.#text.length;
2284
- this.#tokenized = this.#tokenize();
2285
- }
2286
- parse() {
2287
- const token = this.#getNext();
2288
- const res = this.#parseJsonValue(token);
2289
- const { done, value } = this.#tokenized.next();
2290
- if (!done) throw new SyntaxError(buildErrorMessage(value));
2291
- return res;
2292
- }
2293
- /** Read the next token. If the token is read to the end, it throws a SyntaxError. */ #getNext() {
2294
- const { done, value } = this.#tokenized.next();
2295
- if (done) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
2296
- return value;
2297
- }
2298
- /** Split the JSONC string into token units. Whitespace and comments are skipped. */ *#tokenize() {
2299
- for (let i = 0; i < this.#length; i++) {
2300
- if (this.#whitespace.has(this.#text[i])) continue;
2301
- if (this.#text[i] === "/" && this.#text[i + 1] === "*") {
2302
- i += 2;
2303
- let hasEndOfComment = false;
2304
- for (; i < this.#length; i++) if (this.#text[i] === "*" && this.#text[i + 1] === "/") {
2305
- hasEndOfComment = true;
2306
- break;
2307
- }
2308
- if (!hasEndOfComment) throw new SyntaxError("Cannot parse JSONC: unexpected end of JSONC input");
2309
- i++;
2310
- continue;
2311
- }
2312
- if (this.#text[i] === "/" && this.#text[i + 1] === "/") {
2313
- i += 2;
2314
- for (; i < this.#length; i++) if (this.#text[i] === "\n" || this.#text[i] === "\r") break;
2315
- continue;
2316
- }
2317
- switch (this.#text[i]) {
2318
- case "{":
2319
- yield {
2320
- type: "BeginObject",
2321
- position: i
2322
- };
2323
- break;
2324
- case "}":
2325
- yield {
2326
- type: "EndObject",
2327
- position: i
2328
- };
2329
- break;
2330
- case "[":
2331
- yield {
2332
- type: "BeginArray",
2333
- position: i
2334
- };
2335
- break;
2336
- case "]":
2337
- yield {
2338
- type: "EndArray",
2339
- position: i
2340
- };
2341
- break;
2342
- case ":":
2343
- yield {
2344
- type: "NameSeparator",
2345
- position: i
2346
- };
2347
- break;
2348
- case ",":
2349
- yield {
2350
- type: "ValueSeparator",
2351
- position: i
2352
- };
2353
- break;
2354
- case "\"": {
2355
- const startIndex = i;
2356
- let shouldEscapeNext = false;
2357
- i++;
2358
- for (; i < this.#length; i++) {
2359
- if (this.#text[i] === "\"" && !shouldEscapeNext) break;
2360
- shouldEscapeNext = this.#text[i] === "\\" && !shouldEscapeNext;
2361
- }
2362
- yield {
2363
- type: "String",
2364
- sourceText: this.#text.substring(startIndex, i + 1),
2365
- position: startIndex
2366
- };
2367
- break;
2368
- }
2369
- default: {
2370
- const startIndex = i;
2371
- for (; i < this.#length; i++) if (this.#numberEndToken.has(this.#text[i])) break;
2372
- i--;
2373
- yield {
2374
- type: "NullOrTrueOrFalseOrNumber",
2375
- sourceText: this.#text.substring(startIndex, i + 1),
2376
- position: startIndex
2377
- };
2378
- }
2379
- }
2380
- }
2381
- }
2382
- #parseJsonValue(value) {
2383
- switch (value.type) {
2384
- case "BeginObject": return this.#parseObject();
2385
- case "BeginArray": return this.#parseArray();
2386
- case "NullOrTrueOrFalseOrNumber": return this.#parseNullOrTrueOrFalseOrNumber(value);
2387
- case "String": return this.#parseString(value);
2388
- default: throw new SyntaxError(buildErrorMessage(value));
2389
- }
2390
- }
2391
- #parseObject() {
2392
- const target = {};
2393
- while (true) {
2394
- const token1 = this.#getNext();
2395
- if (token1.type === "EndObject") return target;
2396
- if (token1.type !== "String") throw new SyntaxError(buildErrorMessage(token1));
2397
- const key = this.#parseString(token1);
2398
- const token2 = this.#getNext();
2399
- if (token2.type !== "NameSeparator") throw new SyntaxError(buildErrorMessage(token2));
2400
- const token3 = this.#getNext();
2401
- Object.defineProperty(target, key, {
2402
- value: this.#parseJsonValue(token3),
2403
- writable: true,
2404
- enumerable: true,
2405
- configurable: true
2406
- });
2407
- const token4 = this.#getNext();
2408
- if (token4.type === "EndObject") return target;
2409
- if (token4.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token4));
2410
- }
2411
- }
2412
- #parseArray() {
2413
- const target = [];
2414
- while (true) {
2415
- const token1 = this.#getNext();
2416
- if (token1.type === "EndArray") return target;
2417
- target.push(this.#parseJsonValue(token1));
2418
- const token2 = this.#getNext();
2419
- if (token2.type === "EndArray") return target;
2420
- if (token2.type !== "ValueSeparator") throw new SyntaxError(buildErrorMessage(token2));
2421
- }
2422
- }
2423
- #parseString(value) {
2424
- let parsed;
2425
- try {
2426
- parsed = JSON.parse(value.sourceText);
2427
- } catch {
2428
- throw new SyntaxError(buildErrorMessage(value));
2429
- }
2430
- if (typeof parsed !== "string") throw new TypeError(`Parsed value is not a string: ${parsed}`);
2431
- return parsed;
2432
- }
2433
- #parseNullOrTrueOrFalseOrNumber(value) {
2434
- if (value.sourceText === "null") return null;
2435
- if (value.sourceText === "true") return true;
2436
- if (value.sourceText === "false") return false;
2437
- let parsed;
2438
- try {
2439
- parsed = JSON.parse(value.sourceText);
2440
- } catch {
2441
- throw new SyntaxError(buildErrorMessage(value));
2442
- }
2443
- if (typeof parsed !== "number") throw new TypeError(`Parsed value is not a number: ${parsed}`);
2444
- return parsed;
2445
- }
2446
- };
2447
- function buildErrorMessage({ type, sourceText, position }) {
2448
- let token = "";
2449
- switch (type) {
2450
- case "BeginObject":
2451
- token = "{";
2452
- break;
2453
- case "EndObject":
2454
- token = "}";
2455
- break;
2456
- case "BeginArray":
2457
- token = "[";
2458
- break;
2459
- case "EndArray":
2460
- token = "]";
2461
- break;
2462
- case "NameSeparator":
2463
- token = ":";
2464
- break;
2465
- case "ValueSeparator":
2466
- token = ",";
2467
- break;
2468
- case "NullOrTrueOrFalseOrNumber":
2469
- case "String": token = 30 < sourceText.length ? `${sourceText.slice(0, 30)}...` : sourceText;
2470
- }
2471
- return `Cannot parse JSONC: unexpected token "${token}" in JSONC at position ${position}`;
2472
- }
2473
- //#endregion
2474
- //#region src/file-matching.ts
2475
- const DEFAULT_GLOB = "**/*.{js,ts,jsx,tsx,html,vue,mjs,mts,cts,cjs}";
2476
- const normalizeFileName$2 = (fileName) => fileName.replace(/\\/g, "/");
2477
- const normalizePattern = (pattern, pathService) => Match.value(pattern).pipe(Match.when(Match.string, (value) => normalizeFileName$2(pathService.resolve(value))), Match.when(true, () => DEFAULT_GLOB), Match.orElse(() => false));
2478
- function createFileMatcher(pattern, pathService, allowHiddenFiles = true) {
2479
- return Match.value(normalizePattern(pattern, pathService)).pipe(Match.when(Match.string, (normalized) => (fileName) => minimatch(normalizeFileName$2(pathService.resolve(fileName)), normalized, { dot: allowHiddenFiles })), Match.orElse((normalized) => () => normalized));
2480
- }
2481
- function matchesFile(pattern, fileName, pathService, allowHiddenFiles = true) {
2482
- return createFileMatcher(pattern, pathService, allowHiddenFiles)(fileName);
2483
- }
2484
- Schema.TaggedError()("BuildCommandFailedError", {
2485
- command: Schema.String,
2486
- description: Schema.String,
2487
- cause: Schema.optional(Schema.Unknown)
2488
- });
2489
- Schema.TaggedError()("SpawnFailedError", {
2490
- command: Schema.String,
2491
- cause: Schema.Unknown
2492
- });
2493
- /**
2494
- * Error returned when a tsconfig file fails to parse, or parses to a value that
2495
- * does not match the shape this package consumes.
2496
- */
2497
- var TsConfigParseError = class extends Schema.TaggedError()("TsConfigParseError", {
2498
- file: Schema.String,
2499
- reason: Schema.String,
2500
- exitClass: Schema.Literal("ConfigError")
2501
- }) {};
2502
- const JsonRecord = Schema.Record(Schema.String, Schema.Unknown);
2503
- /**
2504
- * The tsconfig shape this package reads and rewrites in the sandbox.
2505
- *
2506
- * The index signature keeps every key the schema does not declare — including
2507
- * `compilerOptions` and `$schema` — so unknown keys survive the write-back untouched.
2508
- * `S.mutableKey` keeps the fields the preprocessor rewrites writable (v4's `mutable`
2509
- * only covers arrays), and `S.mutable` keeps the parsed arrays themselves mutable so
2510
- * `TSConfigPreprocessor` can assign rewritten values back onto the config.
2511
- */
2512
- const TsConfigSchema = Schema.StructWithRest(Schema.Struct({
2513
- extends: Schema.mutableKey(Schema.optional(Schema.Union([Schema.String, Schema.Array(Schema.String)]))),
2514
- references: Schema.mutableKey(Schema.optional(Schema.mutable(Schema.Array(Schema.StructWithRest(Schema.Struct({ path: Schema.mutableKey(Schema.String) }), [JsonRecord]))))),
2515
- files: Schema.mutableKey(Schema.optional(Schema.mutable(Schema.Array(Schema.String)))),
2516
- include: Schema.mutableKey(Schema.optional(Schema.mutable(Schema.Array(Schema.String)))),
2517
- exclude: Schema.mutableKey(Schema.optional(Schema.mutable(Schema.Array(Schema.String))))
2518
- }), [JsonRecord]);
2519
- /** A tsconfig `extends` entry list: the array form of `extends`. */
2520
- const ExtendsArraySchema = Schema.Array(Schema.String);
2521
- Schema.TaggedClass()("SandboxCommand", {
2522
- fileEntries: Schema.Array(Schema.Struct({
2523
- name: Schema.String,
2524
- hasChanges: Schema.Boolean
2525
- })),
2526
- basePath: Schema.String,
2527
- workingDirectory: Schema.String,
2528
- backupDirectory: Schema.String,
2529
- inPlace: Schema.Boolean
2530
- });
2531
- Schema.TaggedClass()("SandboxDecision", { entries: Schema.Array(Schema.Struct({
2532
- original: Schema.String,
2533
- target: Schema.String,
2534
- needsBackup: Schema.Boolean
2535
- })) });
2536
- //#endregion
2537
- //#region src/Sandbox.ts
2538
- const combinePreprocessors = (preprocessors) => (project) => Effect.forEach(preprocessors, (pre) => pre(project), { discard: true });
2539
- const makeDisableTypeChecksPreprocessor = (options, impl) => (project) => Effect.gen(function* () {
2540
- const pathService = yield* Path.Path;
2541
- const matches = createFileMatcher(options.disableTypeChecks, pathService);
2542
- (yield* Effect.forEach([...project.files], ([name, file]) => Match.value(matches(pathService.resolve(name))).pipe(Match.when(true, () => Effect.gen(function* () {
2543
- const instrumenterFile = yield* toInstrumenterFile(file);
2544
- const content = yield* Effect.map(impl(instrumenterFile), (instrumented) => instrumented.content).pipe(Effect.mapError((cause) => StrykerError.make({
2545
- message: "disableTypeChecks failed",
2546
- cause
2547
- })), Effect.catch((_error) => Effect.gen(function* () {
2548
- if (isWarningEnabled("preprocessorErrors", options.warnings)) yield* Effect.logWarning(`Unable to disable type checking for file "${name}". Shouldn't type checking be disabled for this file? Consider configuring a more restrictive "${optionsPath("disableTypeChecks")}" settings (or turn it completely off with \`false\`)`);
2549
- })));
2550
- if (content !== void 0) return withContent(file, content);
2551
- })), Match.orElse(() => Effect.succeedNone)), { concurrency: 24 })).forEach(mergeUpdatedInto(project));
2552
- });
2553
- const mergeUpdatedFile = (project, updated) => {
2554
- const key = updated.name;
2555
- MutableHashMap.set(project.files, key, updated);
2556
- if (Option.isSome(MutableHashMap.get(project.filesToMutate, key))) MutableHashMap.set(project.filesToMutate, key, updated);
2557
- };
2558
- const updateOf = (updated) => Match.value(updated).pipe(Match.when(void 0, () => void 0), Match.when(Option.isOption, (option) => Option.getOrUndefined(option)), Match.orElse((file) => file));
2559
- const mergeUpdatedInto = (project) => (updated) => {
2560
- const file = updateOf(updated);
2561
- if (file === void 0) return;
2562
- mergeUpdatedFile(project, file);
2563
- };
2564
- const tsConfigParseError = (file, reason) => TsConfigParseError.make({
2565
- file,
2566
- reason,
2567
- exitClass: "ConfigError"
2568
- });
2569
- const parseJsonText = (jsonText) => {
2570
- try {
2571
- return Result.succeed(parse(jsonText.replace(/^\uFEFF/, "")));
2572
- } catch (error) {
2573
- return Result.fail(errorToString(error));
2574
- }
2575
- };
2576
- const parseTsConfigShape = (fileName, parsed) => Match.value(parsed).pipe(Match.when(Schema.is(TsConfigSchema), (config) => Result.succeed(config)), Match.orElse(() => Result.fail(tsConfigParseError(fileName, `parsed to ${JSON.stringify(parsed)}, which does not match the tsconfig shape this package consumes`))));
2577
- function parseTsConfig(fileName, jsonText) {
2578
- return Result.match(parseJsonText(jsonText), {
2579
- onFailure: (reason) => Result.fail(tsConfigParseError(fileName, reason)),
2580
- onSuccess: (parsed) => parseTsConfigShape(fileName, parsed)
2581
- });
2582
- }
2583
- const makeTSConfigPreprocessor = (options, basePath) => (project) => {
2584
- if (options.inPlace) return Effect.void;
2585
- const tryRewriteReference = (reference, originTSConfigFileName, pathService) => {
2586
- const fileName = pathService.resolve(pathService.dirname(originTSConfigFileName), reference);
2587
- if (pathService.relative(basePath, fileName).startsWith("..")) return [
2588
- "..",
2589
- "..",
2590
- normalizeFileName(reference)
2591
- ].join("/");
2592
- return false;
2593
- };
2594
- const rewriteReferenceOrKeep = (reference, tsconfigFileName, pathService) => Match.value(tryRewriteReference(reference, tsconfigFileName, pathService)).pipe(Match.when(Predicate.isString, (rewritten) => rewritten), Match.orElse(() => reference));
2595
- const rewriteEntryAtIndex = (value, index, tsconfigFileName, pathService) => {
2596
- const entry = value[index];
2597
- if (typeof entry !== "string") return;
2598
- value[index] = rewriteReferenceOrKeep(entry, tsconfigFileName, pathService);
2599
- };
2600
- const rewriteFileArrayProperty = (config, tsconfigFileName, prop, pathService) => {
2601
- const value = config[prop];
2602
- if (Array.isArray(value)) value.forEach((_entry, index) => rewriteEntryAtIndex(value, index, tsconfigFileName, pathService));
2603
- };
2604
- const rewriteTSConfigFile = (tsconfigFileName, pathService) => {
2605
- const tsconfigFileOpt = MutableHashMap.get(project.files, tsconfigFileName);
2606
- if (Option.isNone(tsconfigFileOpt)) return Effect.void;
2607
- const tsconfigFile = tsconfigFileOpt.value;
2608
- return Effect.flatMap(readContent(tsconfigFile), (content) => Effect.flatMap(Effect.sync(() => parseTsConfig(tsconfigFileName, content)), (parsed) => {
2609
- if (Result.isSuccess(parsed)) {
2610
- const config = parsed.success;
2611
- return Effect.all([rewriteExtends(config, tsconfigFileName, pathService), rewriteProjectReferences(config, tsconfigFileName, pathService)], { discard: true }).pipe(Effect.flatMap(() => Effect.gen(function* () {
2612
- rewriteFileArrayProperty(config, tsconfigFileName, "include", pathService);
2613
- rewriteFileArrayProperty(config, tsconfigFileName, "exclude", pathService);
2614
- rewriteFileArrayProperty(config, tsconfigFileName, "files", pathService);
2615
- const content = yield* Schema.encodeEffect(Schema.fromJsonString(TsConfigSchema, { space: 2 }))(config).pipe(Effect.orDie);
2616
- Object.assign(tsconfigFile, { content });
2617
- })));
2618
- }
2619
- const reason = parsed.failure.reason;
2620
- return Effect.logWarning(`Could not rewrite tsconfig file "${tsconfigFileName}": ${reason}. Its extends, project references, and file array properties were not rewritten for the sandbox, so this file still points at paths outside it.`);
2621
- }));
2622
- };
2623
- const rewriteExtendsEntry = (config, extend, tsconfigFileName, pathService) => {
2624
- const rewritten = tryRewriteReference(extend, tsconfigFileName, pathService);
2625
- if (rewritten !== false) return Effect.succeed(rewritten);
2626
- return rewriteTSConfigFile(pathService.resolve(pathService.dirname(tsconfigFileName), extend), pathService).pipe(Effect.as(extend));
2627
- };
2628
- const rewriteSingleExtends = (config, extend, tsconfigFileName, pathService) => Effect.flatMap(rewriteExtendsEntry(config, extend, tsconfigFileName, pathService), (rewritten) => {
2629
- config.extends = rewritten;
2630
- return Effect.void;
2631
- });
2632
- const rewriteExtendsArray = (config, extendEntries, tsconfigFileName, pathService) => Effect.forEach(extendEntries, (entry) => rewriteExtendsEntry(config, entry, tsconfigFileName, pathService)).pipe(Effect.flatMap((rewritten) => {
2633
- config.extends = rewritten;
2634
- return Effect.void;
2635
- }));
2636
- const rewriteExtends = (config, tsconfigFileName, pathService) => Match.value(config.extends).pipe(Match.when(Predicate.isString, (extend) => rewriteSingleExtends(config, extend, tsconfigFileName, pathService)), Match.when(Schema.is(ExtendsArraySchema), (extendEntries) => rewriteExtendsArray(config, extendEntries, tsconfigFileName, pathService)), Match.orElse(() => Effect.void));
2637
- const referencedTsConfigPath = (referencePath) => Match.value(referencePath.endsWith(".json")).pipe(Match.when(true, () => referencePath), Match.orElse(() => `${referencePath}/tsconfig.json`));
2638
- const rewriteReference = (ref, originTSConfigFileName, pathService) => Match.value(tryRewriteReference(ref.path, originTSConfigFileName, pathService)).pipe(Match.when(Predicate.isString, (rewritten) => {
2639
- ref.path = rewritten;
2640
- return Effect.void;
2641
- }), Match.orElse(() => rewriteTSConfigFile(pathService.resolve(pathService.dirname(originTSConfigFileName), referencedTsConfigPath(ref.path)), pathService)));
2642
- const rewriteProjectReferences = (config, originTSConfigFileName, pathService) => {
2643
- const references = config.references;
2644
- if (!references) return Effect.void;
2645
- return Effect.forEach(references, (ref) => rewriteReference(ref, originTSConfigFileName, pathService)).pipe(Effect.asVoid);
2646
- };
2647
- return Effect.gen(function* () {
2648
- const pathService = yield* Path.Path;
2649
- return yield* rewriteTSConfigFile(pathService.resolve(options.tsconfigFile), pathService);
2650
- });
2651
- };
2652
- const createPreprocessor = (options, basePath) => combinePreprocessors([makeDisableTypeChecksPreprocessor(options, disableTypeChecks), makeTSConfigPreprocessor(options, basePath)]);
2653
- var TemporaryDirectory = class extends Context.Service()("@systemfsoftware/stryker-js/Sandbox/TemporaryDirectory") {};
2654
- const removesTempDir = (exit, cleanTempDir) => Exit.isSuccess(exit) || cleanTempDir === "always";
2655
- const removeEmptyParentDirectory = (parent, fs) => Effect.gen(function* () {
2656
- if ((yield* fs.readDirectory(parent)).length === 0) yield* fs.remove(parent, {
2657
- recursive: true,
2658
- force: true
2659
- });
2660
- });
2661
- const removeTempDirectory = (tmp, parent, fs) => Effect.gen(function* () {
2662
- yield* Effect.logDebug(`Deleting stryker temp directory ${tmp}`);
2663
- yield* fs.remove(tmp, {
2664
- recursive: true,
2665
- force: true
2666
- });
2667
- if (yield* fs.exists(parent)) yield* removeEmptyParentDirectory(parent, fs);
2668
- });
2669
- const TemporaryDirectoryLive = (options) => Layer.effect(TemporaryDirectory, Effect.gen(function* () {
2670
- const fs = yield* FileSystem.FileSystem;
2671
- const parent = (yield* Path.Path).resolve(options.tempDirName);
2672
- yield* fs.makeDirectory(parent, { recursive: true });
2673
- let prefix = "sandbox-";
2674
- if (options.inPlace) prefix = "backup-";
2675
- const tmp = yield* fs.makeTempDirectory({
2676
- directory: parent,
2677
- prefix
2678
- });
2679
- yield* Effect.logDebug(`Using temp directory "${tmp}"`);
2680
- yield* Effect.addFinalizer((exit) => Effect.gen(function* () {
2681
- if (removesTempDir(exit, options.cleanTempDir)) {
2682
- yield* removeTempDirectory(tmp, parent, fs);
2683
- return;
2684
- }
2685
- yield* Effect.logDebug("Not removing the temp dir because an error occurred");
2686
- }).pipe(Effect.orDie));
2687
- return { path: tmp };
2688
- }));
2689
- const toFileMap = (entries) => new Map(entries);
2690
- const directoryChain = (from, pathService) => {
2691
- const parent = pathService.dirname(from);
2692
- if (parent === from) return [from];
2693
- return [from, ...directoryChain(parent, pathService)];
2694
- };
2695
- const binDirectoriesFrom = (from, pathService) => directoryChain(pathService.resolve(from), pathService).map((directory) => pathService.join(directory, "node_modules", ".bin"));
2696
- const inheritedPath = () => Config.string("PATH").pipe(Effect.option, Effect.map((value) => Option.getOrUndefined(value) ?? ""));
2697
- const failOnBuildFailure = (command, result) => Match.value(result.exitCode).pipe(Match.when((exitCode) => exitCode !== 0, (exitCode) => Effect.fail(StrykerError.make({ message: `Build command "${command}" failed with exit code ${String(exitCode)}.\n${result.stderr}` }))), Match.orElse(() => Effect.void));
2698
- const runBuildCommandIn = (command, workingDirectory) => Effect.gen(function* () {
2699
- const pathService = yield* Path.Path;
2700
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
2701
- const separator = Match.value(pathService.sep).pipe(Match.when("\\", () => ";"), Match.orElse(() => ":"));
2702
- const inherited = yield* inheritedPath();
2703
- const newPath = [...binDirectoriesFrom(workingDirectory, pathService), inherited].join(separator);
2704
- const childCommand = ChildProcess.make(command, {
2705
- shell: true,
2706
- cwd: workingDirectory,
2707
- env: { PATH: newPath },
2708
- extendEnv: true
2709
- });
2710
- const result = yield* Effect.scoped(Effect.gen(function* () {
2711
- const handle = yield* spawner.spawn(childCommand);
2712
- const stderrChunks = yield* Stream.runCollect(Stream.decodeText(handle.stderr)).pipe(Effect.map((chunks) => [...chunks].join("")), Effect.orElseSucceed(() => ""));
2713
- const exitCode = yield* handle.exitCode;
2714
- return {
2715
- exitCode: Number(exitCode),
2716
- stderr: stderrChunks
2717
- };
2718
- })).pipe(Effect.mapError((cause) => StrykerError.make({
2719
- message: `Failed to spawn build command "${command}"`,
2720
- cause
2721
- })));
2722
- yield* failOnBuildFailure(command, result);
2723
- });
2724
- const directoryRole = (dir, walk) => Match.value(walk.path.basename(dir)).pipe(Match.when((name) => name === walk.tempDirName, () => "skipped"), Match.when((name) => name === "node_modules", () => "nodeModules"), Match.orElse(() => "searchable"));
2725
- const enqueueChildDirectory = (dir, entry, walk) => Effect.gen(function* () {
2726
- const child = walk.path.join(dir, entry);
2727
- if ((yield* walk.fs.stat(walk.path.join(walk.basePath, child)).pipe(Effect.map((info) => info.type), Effect.orElseSucceed(() => "Unknown"))) === "Directory") walk.queue.push(child);
2728
- });
2729
- const enqueueChildDirectories = (dir, walk) => Effect.gen(function* () {
2730
- const entries = yield* walk.fs.readDirectory(walk.path.join(walk.basePath, dir)).pipe(Effect.orElseSucceed(() => []));
2731
- yield* Effect.forEach(entries, (entry) => enqueueChildDirectory(dir, entry, walk), {
2732
- concurrency: 1,
2733
- discard: true
2734
- });
2735
- });
2736
- const visitDirectory = (dir, walk) => Match.value(directoryRole(dir, walk)).pipe(Match.when("nodeModules", () => Effect.sync(() => {
2737
- walk.found.push(dir);
2738
- })), Match.when("skipped", () => Effect.void), Match.orElse(() => enqueueChildDirectories(dir, walk)));
2739
- const findNodeModulesList = (basePath, tempDirName) => Effect.gen(function* () {
2740
- const walk = {
2741
- basePath,
2742
- tempDirName,
2743
- fs: yield* FileSystem.FileSystem,
2744
- path: yield* Path.Path,
2745
- queue: ["."],
2746
- found: []
2747
- };
2748
- let dir = walk.queue.pop();
2749
- while (dir !== void 0) {
2750
- yield* visitDirectory(dir, walk);
2751
- dir = walk.queue.pop();
2752
- }
2753
- return walk.found;
2754
- });
2755
- const symlinkJunction = (to, from) => Effect.gen(function* () {
2756
- const fsService = yield* FileSystem.FileSystem;
2757
- const pathService = yield* Path.Path;
2758
- yield* fsService.makeDirectory(pathService.dirname(from), { recursive: true });
2759
- yield* fsService.symlink(to, from);
2760
- });
2761
- const moveEntry = (from, to) => Effect.gen(function* () {
2762
- const fs = yield* FileSystem.FileSystem;
2763
- if ((yield* fs.stat(from)).type === "Directory") yield* moveDirectoryRecursive(from, to);
2764
- else yield* fs.rename(from, to).pipe(Effect.catch(() => fs.copyFile(from, to).pipe(Effect.andThen(fs.remove(from)))));
2765
- });
2766
- const moveDirectoryRecursive = (from, to) => Effect.gen(function* () {
2767
- const fs = yield* FileSystem.FileSystem;
2768
- const pathService = yield* Path.Path;
2769
- if (!(yield* fs.exists(from))) return;
2770
- yield* fs.makeDirectory(to, { recursive: true });
2771
- const entries = yield* fs.readDirectory(from);
2772
- yield* Effect.forEach(entries, (entry) => moveEntry(pathService.join(from, entry), pathService.join(to, entry)), {
2773
- concurrency: 1,
2774
- discard: true
2775
- });
2776
- yield* fs.remove(from, {
2777
- recursive: true,
2778
- force: true
2779
- });
2780
- });
2781
- const inPlaceSandboxFile = (name, file, backupDirectory, basePath) => Effect.gen(function* () {
2782
- if (!hasChanges(file)) return name;
2783
- yield* backupTo(file, backupDirectory, basePath);
2784
- yield* Effect.logDebug("Stored backup file");
2785
- yield* writeInPlace(file);
2786
- return name;
2787
- });
2788
- const sandboxFile = (name, file, workingDirectory, backupDirectory, basePath, options) => Match.value(options.inPlace).pipe(Match.when(true, () => inPlaceSandboxFile(name, file, backupDirectory, basePath)), Match.orElse(() => writeToSandbox(file, workingDirectory, basePath)));
2789
- const sandboxFileNameOf = (fileMap, fileName) => Match.value(fileMap.get(fileName)).pipe(Match.when(Predicate.isString, (sandboxFileName) => sandboxFileName), Match.orElse(() => {
2790
- throw StrykerError.make({ message: `Cannot find sandbox file for ${fileName}` });
2791
- }));
2792
- const withoutLeadingSeparator = (suffix) => Match.value(suffix.startsWith("/")).pipe(Match.when(true, () => suffix.slice(1)), Match.orElse(() => suffix));
2793
- const relativeToBase = (resolvedSandbox, resolvedWorking, base, pathService) => {
2794
- const trimmed = withoutLeadingSeparator(resolvedSandbox.slice(resolvedWorking.length));
2795
- return Match.value(trimmed.length === 0).pipe(Match.when(true, () => base), Match.orElse(() => pathService.join(base, trimmed)));
2796
- };
2797
- const originalFileNameOf = (sandboxFileName, workingDirectory, base, pathService) => {
2798
- const resolvedSandbox = pathService.resolve(sandboxFileName);
2799
- const resolvedWorking = pathService.resolve(workingDirectory);
2800
- return Match.value(resolvedSandbox.startsWith(resolvedWorking)).pipe(Match.when(true, () => relativeToBase(resolvedSandbox, resolvedWorking, base, pathService)), Match.orElse(() => resolvedSandbox.replace(resolvedWorking, base)));
2801
- };
2802
- const buildSandboxHandle = (fileMap, workingDirectory, base, pathService) => ({
2803
- workingDirectory,
2804
- sandboxFileFor: (fileName) => sandboxFileNameOf(fileMap, fileName),
2805
- originalFileFor: (sandboxFileName) => originalFileNameOf(sandboxFileName, workingDirectory, base, pathService)
2806
- });
2807
- const announceSandbox = (options, workingDirectory, backupDirectory, basePath, pathService) => Match.value(options.inPlace).pipe(Match.when(true, () => Effect.logInfo(`In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: ${pathService.relative(basePath, backupDirectory)}`)), Match.orElse(() => Effect.logDebug(`Creating a sandbox for files in ${workingDirectory}`)));
2808
- const hasBackupToRestore = (options, backupDirectory) => Match.value(options.inPlace).pipe(Match.when(true, () => backupDirectory !== ""), Match.orElse(() => false));
2809
- const restoreOriginalFiles = (workingDirectory, backupDirectory, basePath) => Effect.addFinalizer(() => Effect.gen(function* () {
2810
- const fs = yield* FileSystem.FileSystem;
2811
- const p = yield* Path.Path;
2812
- if (!(yield* fs.exists(backupDirectory))) return;
2813
- yield* Effect.logInfo(`Resetting your original files from ${p.relative(basePath, backupDirectory)}.`);
2814
- yield* moveDirectoryRecursive(backupDirectory, workingDirectory).pipe(Effect.orDie);
2815
- }).pipe(Effect.orDie));
2816
- const isNonEmptyString = (value) => Match.value(value).pipe(Match.when(Predicate.isString, (text) => text !== ""), Match.orElse(() => false));
2817
- const runConfiguredBuild = (options, workingDirectory) => Match.value(options.buildCommand).pipe(Match.when(isNonEmptyString, (command) => Effect.logInfo(`Running build command "${command}" in "${workingDirectory}".`).pipe(Effect.andThen(() => runBuildCommandIn(command, workingDirectory)))), Match.orElse(() => Effect.void));
2818
- const linksNodeModules = (options) => Match.value(options.symlinkNodeModules).pipe(Match.when(true, () => !options.inPlace), Match.orElse(() => false));
2819
- const linkNodeModules = (nodeModules, workingDirectory, basePath, pathService) => Effect.gen(function* () {
2820
- const resolvedTo = pathService.resolve(pathService.join(basePath, nodeModules));
2821
- const resolvedFrom = pathService.join(workingDirectory, nodeModules);
2822
- yield* Effect.logDebug(`Create symlink from ${resolvedTo} to ${resolvedFrom}`);
2823
- yield* symlinkJunction(resolvedTo, resolvedFrom).pipe(Effect.catch((_error) => Effect.logWarning(`Unexpected error while trying to symlink "${nodeModules}" in sandbox directory.`)));
2824
- });
2825
- const linkFoundNodeModules = (options, workingDirectory, basePath, pathService) => Effect.gen(function* () {
2826
- const nodeModulesList = yield* findNodeModulesList(basePath, options.tempDirName);
2827
- if (nodeModulesList.length === 0) {
2828
- yield* Effect.logDebug(`Could not find a node_modules folder to symlink into the sandbox directory. Search "${basePath}" and its parent directories`);
2829
- return;
2830
- }
2831
- yield* Effect.forEach(nodeModulesList, (nodeModules) => linkNodeModules(nodeModules, workingDirectory, basePath, pathService), {
2832
- concurrency: 1,
2833
- discard: true
2834
- });
2835
- });
2836
- const symlinkNodeModules = (options, workingDirectory, basePath, pathService) => Effect.gen(function* () {
2837
- yield* Effect.logDebug("Start symlink node_modules");
2838
- if (linksNodeModules(options)) yield* linkFoundNodeModules(options, workingDirectory, basePath, pathService);
2839
- });
2840
- const makeSandbox = (input) => Effect.gen(function* () {
2841
- const { options, project, workingDirectory, backupDirectory, basePath } = input;
2842
- yield* Scope.Scope;
2843
- const pathService = yield* Path.Path;
2844
- yield* announceSandbox(options, workingDirectory, backupDirectory, basePath, pathService);
2845
- yield* Effect.when(restoreOriginalFiles(workingDirectory, backupDirectory, basePath), Effect.succeed(hasBackupToRestore(options, backupDirectory)));
2846
- yield* createPreprocessor(options, basePath)(project).pipe(Effect.mapError((cause) => StrykerError.make({
2847
- message: "Sandbox preprocessor failed",
2848
- cause
2849
- })));
2850
- const entries = yield* Effect.forEach([...project.files], ([original, file]) => Effect.map(sandboxFile(original, file, workingDirectory, backupDirectory, basePath, options), (target) => [original, target]), {
2851
- concurrency: 24,
2852
- discard: false
2853
- });
2854
- const fileMap = toFileMap(entries);
2855
- yield* runConfiguredBuild(options, workingDirectory);
2856
- yield* symlinkNodeModules(options, workingDirectory, basePath, pathService);
2857
- return buildSandboxHandle(fileMap, workingDirectory, basePath, pathService);
2858
- });
2859
- //#endregion
2860
- //#region src/run/instrument.cell.ts
2861
- const instrumentCell = Cell.layer({
2862
- read: (command) => Effect.gen(function* () {
2863
- yield* Scope.Scope;
2864
- const env = yield* RunEnvironment;
2865
- const filesToMutate = yield* Effect.forEach([...MutableHashMap.values(command.project.filesToMutate)], (file) => toInstrumenterFile(file), { concurrency: 24 }).pipe(Effect.mapError((cause) => StageError.make({
2866
- stage: "instrument",
2867
- reason: "Failed to read files to mutate",
2868
- cause
2869
- })));
2870
- const instrumentResult = yield* instrument(filesToMutate, {
2871
- ignorers: [...command.ignorers],
2872
- excludedMutations: [...command.options.mutator.excludedMutations]
2873
- }, env.basePath).pipe(Effect.mapError((cause) => StageError.make({
2874
- stage: "instrument",
2875
- reason: "Instrumenter failed",
2876
- cause
2877
- })));
2878
- const instrumentedProject = withInstrumentedFiles(command.project, instrumentResult.files);
2879
- const basePath = env.basePath;
2880
- let workingDirectory = command.temporaryDirectoryPath;
2881
- let backupDirectory = "";
2882
- if (command.options.inPlace) {
2883
- workingDirectory = basePath;
2884
- backupDirectory = command.temporaryDirectoryPath;
2885
- }
2886
- return {
2887
- prev: command,
2888
- filesToMutate,
2889
- instrumentResult,
2890
- instrumentedProject,
2891
- sandbox: yield* makeSandbox({
2892
- options: command.options,
2893
- project: instrumentedProject,
2894
- workingDirectory,
2895
- backupDirectory,
2896
- basePath
2897
- }).pipe(Effect.mapError((cause) => StageError.make({
2898
- stage: "instrument",
2899
- reason: "Sandbox initialization failed",
2900
- cause
2901
- }))),
2902
- concurrency: yield* makeConcurrency(command.options).pipe(Effect.mapError((cause) => StageError.make({
2903
- stage: "instrument",
2904
- reason: "Failed to compute concurrency",
2905
- cause
2906
- })))
2907
- };
2908
- }),
2909
- decode: (raw) => Result.succeed(InstrumentCommand.make({
2910
- fileCount: raw.filesToMutate.length,
2911
- inPlace: raw.prev.options.inPlace,
2912
- pluginCount: raw.prev.loadedPlugins.pluginModulePaths.length
2913
- })),
2914
- decide: planInstrumentation,
2915
- encode: (outcome) => outcome,
2916
- write: (output, raw) => withPhaseSpan("instrument", { fileCount: raw.filesToMutate.length }, () => Effect.gen(function* () {
2917
- const env = yield* RunEnvironment;
2918
- const now = yield* Clock.currentTimeMillis;
2919
- const queue = yield* RunEvents;
2920
- yield* Queue.offer(queue, PhaseEntered.make({
2921
- phase: "instrument",
2922
- elapsedMs: now - env.runStartedAt
2923
- }));
2924
- const out = output;
2925
- if (Result.isFailure(out)) {
2926
- const err = out.failure;
2927
- return yield* StageError.make({
2928
- stage: err.stage,
2929
- reason: err.reason,
2930
- cause: err
2931
- });
2932
- }
2933
- return {
2934
- ...raw.prev,
2935
- project: raw.instrumentedProject,
2936
- mutants: raw.instrumentResult.mutants,
2937
- sandbox: raw.sandbox,
2938
- concurrency: {
2939
- testRunners: raw.concurrency.testRunners,
2940
- checkers: raw.concurrency.checkers
2941
- }
2942
- };
2943
- }))
2944
- });
2945
- //#endregion
2946
- //#region src/MutationTest.schema.ts
2947
- var MutationTestCommand = class extends S.TaggedClass()("MutationTestCommand", {
2948
- dryRunOnly: S.Boolean,
2949
- allowEmpty: S.Boolean,
2950
- testCount: S.Finite,
2951
- isZero: S.Boolean
2952
- }) {};
2953
- //#endregion
2954
- //#region src/admit-mutation-test.workflow.ts
2955
- var MutationTestError = class extends S.TaggedError()("MutationTestError", {
2956
- stage: S.Literal("mutationTest"),
2957
- reason: S.String
2958
- }) {};
2959
- const MutationTestDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/MutationTestDecision");
2960
- var MutationTestProceed = class extends S.TaggedClass()("MutationTestProceed", {}) {
2961
- [MutationTestDecisionTypeId] = MutationTestDecisionTypeId;
2962
- };
2963
- var MutationTestDryRunOnly = class extends S.TaggedClass()("MutationTestDryRunOnly", {}) {
2964
- [MutationTestDecisionTypeId] = MutationTestDecisionTypeId;
2965
- };
2966
- var MutationTestNoTests = class extends S.TaggedClass()("MutationTestNoTests", {}) {
2967
- [MutationTestDecisionTypeId] = MutationTestDecisionTypeId;
2968
- };
2969
- const isInvalidTestCount = (command) => command.testCount < 0;
2970
- const admitMutationTest = Workflow.make(MutationTestCommand, (command) => Match.value(command).pipe(Match.when(isInvalidTestCount, () => Result.fail(MutationTestError.make({
2971
- stage: "mutationTest",
2972
- reason: "Invalid test count"
2973
- }))), Match.when({ dryRunOnly: true }, () => Result.succeed(MutationTestDryRunOnly.make({}))), Match.when({
2974
- isZero: true,
2975
- allowEmpty: true
2976
- }, () => Result.succeed(MutationTestNoTests.make({}))), Match.orElse(() => Result.succeed(MutationTestProceed.make({})))));
2977
- //#endregion
2978
- //#region src/admit-checker-answer.workflow.ts
2979
- var CheckerAnsweredUnrequested = class extends S.TaggedError()("CheckerAnsweredUnrequested", {
2980
- checkerName: S.String,
2981
- phase: S.Literals(["check", "group"]),
2982
- unrequestedIds: S.Array(S.String),
2983
- requestedIds: S.Array(S.String)
2984
- }) {};
2985
- var CheckerSkippedRequested = class extends S.TaggedError()("CheckerSkippedRequested", {
2986
- checkerName: S.String,
2987
- phase: S.Literals(["check", "group"]),
2988
- missingIds: S.Array(S.String)
2989
- }) {};
2990
- const isCheckResult = (_value) => true;
2991
- const CheckResultSchema = S.Unknown.pipe(S.refine(isCheckResult));
2992
- var CheckerCommand = class extends S.TaggedClass()("CheckerCommand", {
2993
- checkerName: S.String,
2994
- requestedIds: S.Array(S.String),
2995
- phase: S.Literals(["check", "group"]),
2996
- idGroups: S.optional(S.Array(S.Array(S.String))),
2997
- answers: S.optional(S.Record(S.String, CheckResultSchema))
2998
- }) {};
2999
- const CheckerDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/CheckerDecision");
3000
- var CheckGroupDecision = class extends S.TaggedClass()("CheckGroupDecision", { groups: S.Array(S.Array(S.String)) }) {
3001
- [CheckerDecisionTypeId] = CheckerDecisionTypeId;
3002
- };
3003
- var CheckResultDecision = class extends S.TaggedClass()("CheckResultDecision", { pairs: S.Array(S.Struct({
3004
- id: S.String,
3005
- result: CheckResultSchema
3006
- })) }) {
3007
- [CheckerDecisionTypeId] = CheckerDecisionTypeId;
3008
- };
3009
- const idRecord = (ids) => Object.fromEntries(ids.map((id) => [id, true]));
3010
- const acknowledgedIds = (submitted, requested) => idRecord(submitted.filter((id) => requested[id] === true));
3011
- const answeredUnrequested = (command, submitted, requested) => Option.liftPredicate(Arr.isReadonlyArrayNonEmpty)(submitted.filter((id) => !(id in requested))).pipe(Option.map((unrequestedIds) => CheckerAnsweredUnrequested.make({
3012
- checkerName: command.checkerName,
3013
- phase: command.phase,
3014
- unrequestedIds,
3015
- requestedIds: [...command.requestedIds]
3016
- })));
3017
- const skippedRequested = (command, acknowledged) => Option.liftPredicate(Arr.isReadonlyArrayNonEmpty)(command.requestedIds.filter((id) => !(id in acknowledged))).pipe(Option.map((missingIds) => CheckerSkippedRequested.make({
3018
- checkerName: command.checkerName,
3019
- phase: command.phase,
3020
- missingIds
3021
- })));
3022
- /** The first contract breach the answer commits: an unrequested id outranks an unanswered one. */
3023
- const contractBreach = (command, requested, submitted) => Option.firstSomeOf([answeredUnrequested(command, submitted, requested), skippedRequested(command, acknowledgedIds(submitted, requested))]);
3024
- const admit = (command, requested, submitted, decision) => Option.match(contractBreach(command, requested, submitted), {
3025
- onNone: () => Result.succeed(decision),
3026
- onSome: (breach) => Result.fail(breach)
3027
- });
3028
- const idGroupsOf = (command) => Option.getOrElse(() => [])(Option.fromUndefinedOr(command.idGroups));
3029
- const answersOf = (command) => Option.getOrElse(() => ({}))(Option.fromUndefinedOr(command.answers));
3030
- const evaluateGroup = (command) => {
3031
- const idGroups = idGroupsOf(command);
3032
- return admit(command, idRecord(command.requestedIds), idGroups.flat(), CheckGroupDecision.make({ groups: idGroups.map((group) => [...group]) }));
3033
- };
3034
- const evaluateCheckResult = (command) => {
3035
- const requested = idRecord(command.requestedIds);
3036
- const entries = Object.entries(answersOf(command));
3037
- const pairs = entries.filter(([id]) => requested[id] === true).map(([id, result]) => ({
3038
- id,
3039
- result
3040
- }));
3041
- return admit(command, requested, entries.map(([id]) => id), CheckResultDecision.make({ pairs }));
3042
- };
3043
- const admitCheckerAnswer = Workflow.make(CheckerCommand, (command) => Match.value(command.phase).pipe(Match.when("group", () => evaluateGroup(command)), Match.when("check", () => evaluateCheckResult(command)), Match.exhaustive));
3044
- //#endregion
3045
- //#region src/Checker.ts
3046
- /**
3047
- * Checker — the Checker capability.
3048
- *
3049
- * Owns the checker port, its contract, the child-process edge, and the
3050
- * Cell pipelines that drive a checker through a pure workflow. A checker
3051
- * speaks `Mutant`; the engine schedules `MutantRunPlan` — this module bridges
3052
- * the two and verifies the join.
3053
- */
3054
- /**
3055
- * Pair a checker's answers back to the run plans they were asked about.
3056
- *
3057
- * A checker's port speaks `Mutant`; the engine schedules `MutantRunPlan`. Going
3058
- * one way is a projection, but coming back is a join that can fail two ways —
3059
- * the checker answered about something it was not asked about, or it did not
3060
- * answer about something it was. Both are the plugin breaking its contract, and
3061
- * each carries its own tag, so a caller matches on the failure rather than
3062
- * parsing ids out of a message.
3063
- *
3064
- * Pure: the pairing is a decision over two lists, so it runs without a checker,
3065
- * a process or a clock — which is the point, because this is the part worth
3066
- * testing.
3067
- */
3068
- const pairCheckResults = (checkerName, plans, answers) => Match.value(partitionAnswers(plansById(plans), answers)).pipe(Match.when((partition) => partition.unrequested.length > 0, (partition) => Result.fail(CheckerAnsweredUnrequested.make({
3069
- checkerName,
3070
- phase: "check",
3071
- unrequestedIds: [...partition.unrequested],
3072
- requestedIds: plans.map((plan) => plan.mutant.id)
3073
- }))), Match.orElse((partition) => admitAnsweredPlans(checkerName, plans, partition.paired)));
3074
- /**
3075
- * Resolve a checker's id groups back to run plans.
3076
- *
3077
- * Same join as `pairCheckResults` and the same two failures, over groups rather
3078
- * than single answers. A mutant absent from every group is as much a dropped
3079
- * mutant as one absent from the check results — it would go on to be scheduled
3080
- * as though the checker had approved it.
3081
- */
3082
- const pairGroups = (checkerName, plans, idGroups) => Match.value(partitionGroups(plansById(plans), idGroups)).pipe(Match.when((partition) => partition.unrequested.length > 0, (partition) => Result.fail(CheckerAnsweredUnrequested.make({
3083
- checkerName,
3084
- phase: "group",
3085
- unrequestedIds: [...partition.unrequested],
3086
- requestedIds: plans.map((plan) => plan.mutant.id)
3087
- }))), Match.orElse((partition) => admitGroupedPlans(checkerName, plans, partition)));
3088
- const plansById = (plans) => new Map(plans.map((plan) => [plan.mutant.id, plan]));
3089
- const missingPlanIds = (plans, present) => plans.map((plan) => plan.mutant.id).filter((id) => !present.has(id));
3090
- const answeredPlanIds = (paired) => new Set(paired.map(([plan]) => plan.mutant.id));
3091
- const partitionAnswers = (byId, answers) => {
3092
- const entries = Object.entries(answers).map(([id, answer]) => ({
3093
- id,
3094
- answer,
3095
- plan: Option.fromUndefinedOr(byId.get(id))
3096
- }));
3097
- return {
3098
- paired: entries.flatMap((entry) => Option.match(entry.plan, {
3099
- onNone: () => [],
3100
- onSome: (plan) => [[plan, entry.answer]]
3101
- })),
3102
- unrequested: entries.flatMap((entry) => Option.match(entry.plan, {
3103
- onNone: () => [entry.id],
3104
- onSome: () => []
3105
- }))
3106
- };
3107
- };
3108
- const admitAnsweredPlans = (checkerName, plans, paired) => Match.value(missingPlanIds(plans, answeredPlanIds(paired))).pipe(Match.when((missing) => missing.length > 0, (missing) => Result.fail(CheckerSkippedRequested.make({
3109
- checkerName,
3110
- phase: "check",
3111
- missingIds: [...missing]
3112
- }))), Match.orElse(() => Result.succeed(paired)));
3113
- const partitionGroupIds = (byId, idGroup) => {
3114
- const entries = idGroup.map((id) => ({
3115
- id,
3116
- plan: Option.fromUndefinedOr(byId.get(id))
3117
- }));
3118
- return {
3119
- plans: entries.flatMap((entry) => Option.match(entry.plan, {
3120
- onNone: () => [],
3121
- onSome: (plan) => [plan]
3122
- })),
3123
- grouped: new Set(idGroup),
3124
- unrequested: entries.flatMap((entry) => Option.match(entry.plan, {
3125
- onNone: () => [entry.id],
3126
- onSome: () => []
3127
- }))
3128
- };
3129
- };
3130
- const partitionGroups = (byId, idGroups) => {
3131
- const parts = idGroups.map((idGroup) => partitionGroupIds(byId, idGroup));
3132
- return {
3133
- groups: parts.map((part) => part.plans),
3134
- grouped: new Set(parts.flatMap((part) => [...part.grouped])),
3135
- unrequested: parts.flatMap((part) => part.unrequested)
3136
- };
3137
- };
3138
- const admitGroupedPlans = (checkerName, plans, partition) => Match.value(missingPlanIds(plans, partition.grouped)).pipe(Match.when((missing) => missing.length > 0, (missing) => Result.fail(CheckerSkippedRequested.make({
3139
- checkerName,
3140
- phase: "group",
3141
- missingIds: [...missing]
3142
- }))), Match.orElse(() => Result.succeed(partition.groups)));
3143
- const makeCheckerChildProcess = (params) => Effect.gen(function* () {
3144
- const crashed = (cause) => ChildProcessCrashedError.make({
3145
- pid: 0,
3146
- exit: {
3147
- _tag: "Code",
3148
- code: 1
3149
- },
3150
- cause
3151
- });
3152
- const optionsJson = yield* encodeWorkerOptions(params.options);
3153
- const client = yield* makeWorkerClient({
3154
- rpcs: CheckerRpcs,
3155
- entrypoint: params.workerEntrypoint,
3156
- workingDirectory: params.workingDirectory,
3157
- execArgv: [...params.execArgv],
3158
- optionsJson,
3159
- tempDirPrefix: "stryker-checker-"
3160
- }).pipe(Effect.mapError((error) => Match.value(error).pipe(Match.tag("ChildProcessCrashedError", "OutOfMemoryError", (crash) => crash), Match.tag("WorkerBootTimeoutError", () => crashed(`Checker worker failed to start: its boot window closed before it accepted the RPC connection`)), Match.exhaustive)));
3161
- return {
3162
- check: (checkerName, mutants) => client.check({
3163
- checkerName,
3164
- mutants: [...mutants]
3165
- }).pipe(Effect.mapError((error) => crashed(error.message))),
3166
- group: (checkerName, mutants) => client.group({
3167
- checkerName,
3168
- mutants: [...mutants]
3169
- }).pipe(Effect.mapError((error) => crashed(error.message)))
3170
- };
3171
- });
3172
- const createCheckerFactory = (options, fileDescriptions, workerEntrypoint, idGenerator, workingDirectory) => makeCheckerChildProcess({
3173
- options,
3174
- fileDescriptions,
3175
- workerEntrypoint,
3176
- workingDirectory,
3177
- execArgv: [...options.checkerNodeArgs],
3178
- idGenerator
3179
- });
3180
- const writeDecidedAnswers = (plans, checkerName, answers) => Effect.fromResult(pairCheckResults(checkerName, plans, Object.fromEntries(answers.map((answer) => [answer.id, answer.result]))));
3181
- const writeDecidedGroups = (plans, checkerName, idGroups) => Effect.fromResult(pairGroups(checkerName, plans, idGroups));
3182
- const writeCheckerDecision = (plans, checkerName, decision) => Match.value(decision).pipe(Match.tag("CheckResultDecision", (d) => writeDecidedAnswers(plans, checkerName, d.pairs)), Match.tag("CheckGroupDecision", () => Effect.fail(CheckerSkippedRequested.make({
3183
- checkerName,
3184
- phase: "check",
3185
- missingIds: []
3186
- }))), Match.exhaustive);
3187
- const writeCheckerOutcome = (plans, checkerName, outcome) => Result.match(outcome, {
3188
- onFailure: (error) => Effect.fail(error),
3189
- onSuccess: (decision) => writeCheckerDecision(plans, checkerName, decision)
3190
- });
3191
- const writeGroupDecision = (plans, checkerName, decision) => Match.value(decision).pipe(Match.tag("CheckGroupDecision", (d) => writeDecidedGroups(plans, checkerName, d.groups)), Match.tag("CheckResultDecision", () => Effect.fail(CheckerSkippedRequested.make({
3192
- checkerName,
3193
- phase: "group",
3194
- missingIds: []
3195
- }))), Match.exhaustive);
3196
- const writeGroupOutcome = (plans, checkerName, outcome) => Result.match(outcome, {
3197
- onFailure: (error) => Effect.fail(error),
3198
- onSuccess: (decision) => writeGroupDecision(plans, checkerName, decision)
3199
- });
3200
- /**
3201
- * Ask a checker about run plans and get run plans back.
3202
- *
3203
- * The port speaks `Mutant` because that is all a checker needs; the engine
3204
- * schedules `MutantRunPlan`. This is the two-line shell around that translation:
3205
- * project the plans down, call the checker, and hand the answers to the pure decision
3206
- * that joins them back. The join is where the work is, and it is pure.
3207
- */
3208
- const checkPlans = (checker, checkerName, plans) => {
3209
- return Cell.layer({
3210
- read: (command) => command.checker.check(command.checkerName, command.plans.map((plan) => plan.mutant)).pipe(Effect.map((answers) => ({
3211
- checkerName: command.checkerName,
3212
- requestedIds: command.plans.map((plan) => plan.mutant.id),
3213
- answers
3214
- }))),
3215
- decode: (raw) => Result.succeed(CheckerCommand.make({
3216
- checkerName: raw.checkerName,
3217
- requestedIds: [...raw.requestedIds],
3218
- phase: "check",
3219
- answers: { ...raw.answers }
3220
- })),
3221
- decide: admitCheckerAnswer,
3222
- encode: (outcome) => outcome,
3223
- write: (outcome, raw) => writeCheckerOutcome(plans, raw.checkerName, outcome)
3224
- }).run({
3225
- checker,
3226
- checkerName,
3227
- plans
3228
- });
3229
- };
3230
- /**
3231
- * Ask a checker how to group run plans, and get groups of run plans back.
3232
- */
3233
- const groupPlans = (checker, checkerName, plans) => {
3234
- return Cell.layer({
3235
- read: (command) => command.checker.group(command.checkerName, command.plans.map((plan) => plan.mutant)).pipe(Effect.map((idGroups) => ({
3236
- checkerName: command.checkerName,
3237
- requestedIds: command.plans.map((plan) => plan.mutant.id),
3238
- idGroups
3239
- }))),
3240
- decode: (raw) => Result.succeed(CheckerCommand.make({
3241
- checkerName: raw.checkerName,
3242
- requestedIds: [...raw.requestedIds],
3243
- phase: "group",
3244
- idGroups: raw.idGroups.map((group) => [...group])
3245
- })),
3246
- decide: admitCheckerAnswer,
3247
- encode: (outcome) => outcome,
3248
- write: (outcome, raw) => writeGroupOutcome(plans, raw.checkerName, outcome)
3249
- }).run({
3250
- checker,
3251
- checkerName,
3252
- plans
3253
- });
3254
- };
3255
- const checkGroupedPlans = (checker, checkerName, plans) => Effect.gen(function* () {
3256
- const groups = yield* groupPlans(checker, checkerName, plans);
3257
- return (yield* Effect.forEach(groups, (group) => checkPlans(checker, checkerName, group), { concurrency: 1 })).flat();
3258
- });
3259
- //#endregion
3260
- //#region src/mutant-result-mapping.ts
3261
- const toSchemaPosition = (pos) => ({
3262
- column: pos.column + 1,
3263
- line: pos.line + 1
3264
- });
3265
- const toSchemaLocation = (location) => ({
3266
- start: toSchemaPosition(location.start),
3267
- end: toSchemaPosition(location.end)
3268
- });
3269
- const checkStatusToMutantStatus = (_status) => "CompileError";
3270
- const mutantResult = (mutant, status, outcome = {}) => ({
3271
- _tag: "Mutant",
3272
- id: mutant.id,
3273
- fileName: mutant.fileName,
3274
- mutatorName: mutant.mutatorName,
3275
- replacement: mutant.replacement,
3276
- location: toSchemaLocation(mutant.location),
3277
- status,
3278
- coveredBy: mutant.coveredBy,
3279
- static: mutant.static,
3280
- testsCompleted: mutant.testsCompleted,
3281
- description: mutant.description,
3282
- ...outcome
3283
- });
3284
- const mapRunResult = (mutant, result) => Match.value(result).pipe(Match.discriminator("status")("error", (errored) => mutantResult(mutant, "RuntimeError", { statusReason: errored.errorMessage })), Match.discriminator("status")("killed", (killed) => mutantResult(mutant, "Killed", {
3285
- testsCompleted: killed.nrOfTests,
3286
- killedBy: [...killed.killedBy],
3287
- statusReason: killed.failureMessage
3288
- })), Match.discriminator("status")("timeout", (timedOut) => {
3289
- if (timedOut.reason === void 0) return mutantResult(mutant, "Timeout");
3290
- return mutantResult(mutant, "Timeout", { statusReason: timedOut.reason });
3291
- }), Match.discriminator("status")("survived", (survived) => mutantResult(mutant, "Survived", { testsCompleted: survived.nrOfTests })), Match.exhaustive);
3292
- //#endregion
3293
- //#region src/calculate-metrics.ts
3294
- const countStatus = (mutants, status) => mutants.filter((mutant) => mutant.status === status).length;
3295
- const percentage = (numerator, denominator) => numerator / denominator * 100;
3296
- const percentOr = (emptyDenominator, numerator, denominator) => Match.value(denominator).pipe(Match.when(0, () => emptyDenominator), Match.orElse(() => percentage(numerator, denominator)));
3297
- const coveredFraction = (totalDetected, totalCovered, totalValid) => Match.value(totalValid).pipe(Match.when(0, () => NaN), Match.orElse(() => percentOr(0, totalDetected, totalCovered)));
3298
- const countMutants = (mutants) => {
3299
- const pending = countStatus(mutants, "Pending");
3300
- const killed = countStatus(mutants, "Killed");
3301
- const timeout = countStatus(mutants, "Timeout");
3302
- const survived = countStatus(mutants, "Survived");
3303
- const noCoverage = countStatus(mutants, "NoCoverage");
3304
- const runtimeErrors = countStatus(mutants, "RuntimeError");
3305
- const compileErrors = countStatus(mutants, "CompileError");
3306
- const ignored = countStatus(mutants, "Ignored");
3307
- const totalDetected = timeout + killed;
3308
- const totalUndetected = survived + noCoverage;
3309
- const totalCovered = totalDetected + survived;
3310
- const totalValid = totalUndetected + totalDetected;
3311
- const totalInvalid = runtimeErrors + compileErrors;
3312
- return {
3313
- pending,
3314
- killed,
3315
- timeout,
3316
- survived,
3317
- noCoverage,
3318
- runtimeErrors,
3319
- compileErrors,
3320
- ignored,
3321
- totalDetected,
3322
- totalUndetected,
3323
- totalCovered,
3324
- totalValid,
3325
- totalInvalid,
3326
- totalMutants: totalValid + totalInvalid + ignored + pending,
3327
- mutationScore: percentOr(NaN, totalDetected, totalValid),
3328
- mutationScoreBasedOnCoveredCode: coveredFraction(totalDetected, totalCovered, totalValid)
3329
- };
3330
- };
3331
- const segmentOf = (fileName) => {
3332
- const separator = fileName.indexOf("/");
3333
- if (separator === -1) return fileName;
3334
- return fileName.slice(0, separator);
3335
- };
3336
- const groupBySegment = (files) => Object.entries(files).reduce((groups, [fileName, file]) => {
3337
- const segment = segmentOf(fileName);
3338
- const group = groups[segment] ?? {};
3339
- group[fileName] = file;
3340
- groups[segment] = group;
3341
- return groups;
3342
- }, {});
3343
- const metricsOf = (files) => countMutants(Object.values(files).flatMap((file) => file.mutants));
3344
- const fileResultOf = (fileName, file) => ({
3345
- name: fileName,
3346
- metrics: countMutants(file.mutants),
3347
- childResults: []
3348
- });
3349
- const nestedGroupResult = (segment, entries) => {
3350
- const nested = Object.fromEntries(entries.map(([fileName, file]) => [fileName.slice(segment.length + 1), file]));
3351
- return {
3352
- name: segment,
3353
- metrics: metricsOf(nested),
3354
- childResults: childResultsOf(nested)
3355
- };
3356
- };
3357
- const soleSegmentFileResult = (segment, entries) => Option.flatMap(Arr.head(entries), ([fileName, file]) => Match.value(fileName === segment && entries.length === 1).pipe(Match.when(true, () => Option.some(fileResultOf(fileName, file))), Match.orElse(() => Option.none())));
3358
- const childResultOf = (segment, entries) => Option.getOrElse(soleSegmentFileResult(segment, entries), () => nestedGroupResult(segment, entries));
3359
- const childResultsOf = (files) => Object.entries(groupBySegment(files)).map(([segment, grouped]) => childResultOf(segment, Object.entries(grouped))).sort((left, right) => left.name.localeCompare(right.name));
3360
- const calculateMetrics = (files) => ({
3361
- name: "All files",
3362
- metrics: metricsOf(files),
3363
- childResults: childResultsOf(files)
3364
- });
3365
- //#endregion
3366
- //#region src/report-assembly.ts
3367
- const extensionOf = (fileName) => {
3368
- const base = fileName.slice(fileName.lastIndexOf("/") + 1);
3369
- const dot = base.lastIndexOf(".");
3370
- if (dot <= 0) return "";
3371
- return base.slice(dot).toLowerCase();
3372
- };
3373
- const EXTENSION_LANGUAGES = {
3374
- ".ts": "typescript",
3375
- ".tsx": "typescript",
3376
- ".html": "html",
3377
- ".vue": "html"
3378
- };
3379
- const determineLanguage = (fileName) => EXTENSION_LANGUAGES[extensionOf(fileName)] ?? "javascript";
3380
- const reportFileName = (relativePath) => Option.match(Option.fromUndefinedOr(relativePath), {
3381
- onNone: () => "",
3382
- onSome: (present) => present.replaceAll("\\", "/")
3383
- });
3384
- const testIdRemap = (testIds) => {
3385
- const positions = HashMap.fromIterable(testIds.map((id, position) => [id, position.toString()]));
3386
- const remapId = (id) => Option.getOrElse(HashMap.get(positions, id), () => id);
3387
- const remapIds = (ids) => {
3388
- if (ids === void 0) return;
3389
- return ids.map(remapId);
3390
- };
3391
- return {
3392
- testId: remapId,
3393
- testIds: remapIds
3394
- };
3395
- };
3396
- const toReportMutant = (mutant, remap) => ({
3397
- id: mutant.id,
3398
- mutatorName: mutant.mutatorName,
3399
- replacement: mutant.replacement,
3400
- status: mutant.status,
3401
- location: mutant.location,
3402
- statusReason: mutant.statusReason,
3403
- testsCompleted: mutant.testsCompleted,
3404
- description: mutant.description,
3405
- static: mutant.static,
3406
- killedBy: remap.testIds(mutant.killedBy),
3407
- coveredBy: remap.testIds(mutant.coveredBy)
3408
- });
3409
- const toReportTest = (test, remap) => {
3410
- const base = {
3411
- id: remap.testId(test.id),
3412
- name: test.name
3413
- };
3414
- if (test.startPosition === void 0) return base;
3415
- return {
3416
- ...base,
3417
- location: { start: test.startPosition }
3418
- };
3419
- };
3420
- const assembleFileResults = (input) => {
3421
- const entries = [...input.mutants.reduce((accumulator, mutant) => Option.match(HashMap.get(input.reportNames, mutant.fileName), {
3422
- onNone: () => accumulator,
3423
- onSome: (reportName) => {
3424
- const mapped = toReportMutant(mutant, input.remap);
3425
- return Option.match(HashMap.get(accumulator, reportName), {
3426
- onNone: () => HashMap.set(accumulator, reportName, {
3427
- sourceFileName: mutant.fileName,
3428
- mutants: [mapped]
3429
- }),
3430
- onSome: (existing) => HashMap.set(accumulator, reportName, {
3431
- sourceFileName: existing.sourceFileName,
3432
- mutants: [...existing.mutants, mapped]
3433
- })
3434
- });
3435
- }
3436
- }), HashMap.empty())].flatMap(([reportName, group]) => Option.match(HashMap.get(input.sources, group.sourceFileName), {
3437
- onNone: () => [],
3438
- onSome: (source) => [[reportName, {
3439
- ...source,
3440
- mutants: group.mutants
3441
- }]]
3442
- }));
3443
- return Object.fromEntries(entries);
3444
- };
3445
- const assembleTestFiles = (input) => {
3446
- const entries = [...input.tests.reduce((accumulator, test) => Option.match(Option.fromUndefinedOr(test.fileName), {
3447
- onNone: () => accumulator,
3448
- onSome: (testFileName) => Option.match(HashMap.get(input.reportNames, testFileName), {
3449
- onNone: () => accumulator,
3450
- onSome: (reportName) => {
3451
- const mapped = toReportTest(test, input.remap);
3452
- return Option.match(HashMap.get(accumulator, reportName), {
3453
- onNone: () => HashMap.set(accumulator, reportName, {
3454
- sourceFileName: testFileName,
3455
- tests: [mapped]
3456
- }),
3457
- onSome: (existing) => HashMap.set(accumulator, reportName, {
3458
- sourceFileName: existing.sourceFileName,
3459
- tests: [...existing.tests, mapped]
3460
- })
3461
- });
3462
- }
3463
- })
3464
- }), HashMap.empty())].flatMap(([reportName, group]) => Option.match(HashMap.get(input.testSources, group.sourceFileName), {
3465
- onNone: () => [],
3466
- onSome: (source) => [[reportName, {
3467
- ...source,
3468
- tests: group.tests
3469
- }]]
3470
- }));
3471
- return Object.fromEntries(entries);
3472
- };
3473
- //#endregion
3474
- //#region package.json
3475
- var package_default = {
3476
- name: "@systemfsoftware/stryker-js",
3477
- version: "5.0.0",
3478
- repository: {
3479
- "type": "git",
3480
- "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git",
3481
- "directory": "packages/stryker-js"
3482
- },
3483
- homepage: "https://github.com/systemfsoftware/stryker-js-effect",
3484
- bugs: { "url": "https://github.com/systemfsoftware/stryker-js-effect/issues" },
3485
- description: "The Stryker mutation testing tool — the stryker executable, the run engine, and the configuration surface.",
3486
- type: "module",
3487
- files: ["dist"],
3488
- bin: { "stryker": "./dist/main.mjs" },
3489
- scripts: {
3490
- "clean": "rimraf dist",
3491
- "build": "rimraf dist && tsdown --config tsdown.bin.config.ts && tsdown --config tsdown.config.ts",
3492
- "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit",
3493
- "test": "vitest run",
3494
- "attw": "attw --pack .",
3495
- "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
3496
- "api:check": "concurrently --kill-others-on-fail \"api-extractor run\" \"api-extractor run --config api-extractor.builtin-reporters.json\" \"api-extractor run --config api-extractor.plugin-loader.json\"",
3497
- "api:update": "concurrently --kill-others-on-fail \"api-extractor run --local\" \"api-extractor run --local --config api-extractor.builtin-reporters.json\" \"api-extractor run --local --config api-extractor.plugin-loader.json\"",
3498
- "lint:tsgo": "effect-tsgo diagnostics --project tsconfig.json --format ${TSGO_FORMAT:-text}"
3499
- },
3500
- license: "Apache-2.0",
3501
- engines: { "node": ">=22.18.0" },
3502
- dependencies: {
3503
- "@effect/opentelemetry": "4.0.0-rc.112",
3504
- "@effect/platform-node": "catalog:",
3505
- "@effect/platform-node-shared": "catalog:",
3506
- "@noble/hashes": "catalog:",
3507
- "@opentelemetry/api": "^1.9.0",
3508
- "@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
3509
- "@opentelemetry/sdk-trace-base": "^2.2.0",
3510
- "@systemfsoftware/effect-cell-types": "catalog:",
3511
- "@systemfsoftware/stryker-ignorer-interface": "workspace:^",
3512
- "@systemfsoftware/stryker-js-html-reporter": "workspace:^",
3513
- "@systemfsoftware/stryker-js-instrumenter": "workspace:^",
3514
- "@systemfsoftware/stryker-js-plugin-interface": "workspace:^",
3515
- "@systemfsoftware/stryker-js-plugin-runtime": "workspace:^",
3516
- "diff-match-patch": "1.0.5",
3517
- "minimatch": "~10.2.4"
3518
- },
3519
- peerDependencies: { "effect": "catalog:" },
3520
- devDependencies: {
3521
- "@effect/tsgo": "catalog:",
3522
- "@effect/vitest": "catalog:",
3523
- "@microsoft/api-extractor": "catalog:",
3524
- "@oxc-parser/binding-wasm32-wasi": "catalog:",
3525
- "@std/jsonc": "catalog:",
3526
- "@systemfsoftware/all": "catalog:",
3527
- "@systemfsoftware/arethetypeswrong-cli": "catalog:",
3528
- "@systemfsoftware/effect-gherkin-spec": "catalog:",
3529
- "@systemfsoftware/effect-schema-law": "catalog:",
3530
- "@systemfsoftware/effect-schema-vite": "catalog:",
3531
- "@systemfsoftware/stryker-ignorer-effect-schema-declarations": "workspace:^",
3532
- "@systemfsoftware/stryker-ignorer-in-source-vitest-block": "workspace:^",
3533
- "@systemfsoftware/tsconfig": "catalog:",
3534
- "@systemfsoftware/tsdown-config": "workspace:^",
3535
- "@systemfsoftware/vitest-config": "workspace:^",
3536
- "@types/diff-match-patch": "1.0.36",
3537
- "@types/node": "catalog:",
3538
- "@vitest/coverage-v8": "catalog:",
3539
- "concurrently": "catalog:",
3540
- "mutation-testing-elements": "catalog:stryker",
3541
- "oxc-parser": "catalog:",
3542
- "oxlint": "catalog:",
3543
- "oxlint-tsgolint": "catalog:",
3544
- "rimraf": "catalog:",
3545
- "tsdown": "catalog:",
3546
- "typescript": "catalog:",
3547
- "vitest": "catalog:"
3548
- },
3549
- publishConfig: {
3550
- "access": "public",
3551
- "provenance": true,
3552
- "exports": {
3553
- ".": {
3554
- "types": "./dist/index.d.mts",
3555
- "default": "./dist/index.mjs"
3556
- },
3557
- "./builtin-reporters": {
3558
- "types": "./dist/builtin-reporters.d.mts",
3559
- "default": "./dist/builtin-reporters.mjs"
3560
- },
3561
- "./config": {
3562
- "types": "./dist/config.d.mts",
3563
- "default": "./dist/config.mjs"
3564
- },
3565
- "./plugin-loader": {
3566
- "types": "./dist/plugin-loader.d.mts",
3567
- "default": "./dist/plugin-loader.mjs"
3568
- },
3569
- "./promises": {
3570
- "types": "./dist/promises.d.mts",
3571
- "default": "./dist/promises.mjs"
3572
- },
3573
- "./package.json": "./package.json"
3574
- }
3575
- },
3576
- exports: { "./package.json": "./package.json" }
3577
- };
3578
- //#endregion
3579
- //#region src/stryker-package.schema.ts
3580
- const PackageJsonSchema = Schema.Struct({ version: Schema.String });
3581
- //#endregion
3582
- //#region src/stryker-package.ts
3583
- const rawPackageJson = package_default;
3584
- const strykerVersion = Schema.decodeUnknownSync(PackageJsonSchema)(rawPackageJson).version;
3585
- //#endregion
3586
- //#region src/verdict-envelope.ts
3587
- const normalizeFileName$1 = (fileName) => fileName.replaceAll("\\", "/");
3588
- const VERDICT_ENVELOPE_SCHEMA_VERSION = "1.1";
3589
- const ACTIONABLE_STATUSES = [
3590
- "Survived",
3591
- "NoCoverage",
3592
- "Timeout",
3593
- "RuntimeError"
3594
- ];
3595
- function isActionableStatus(status) {
3596
- return ACTIONABLE_STATUSES.some((actionable) => actionable === status);
3597
- }
3598
- const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
3599
- const emitQuint = (accumulator) => {
3600
- const bits = accumulator.bits - 5;
3601
- return {
3602
- value: accumulator.value & (1 << bits) - 1,
3603
- bits,
3604
- chars: accumulator.chars + CROCKFORD_BASE32[accumulator.value >>> bits & 31]
3605
- };
3606
- };
3607
- const drainQuints = (accumulator) => Match.value(accumulator.bits >= 5).pipe(Match.when(true, () => drainQuints(emitQuint(accumulator))), Match.when(false, () => accumulator), Match.exhaustive);
3608
- const pushByte = (accumulator, byte) => drainQuints({
3609
- value: accumulator.value << 8 | byte,
3610
- bits: accumulator.bits + 8,
3611
- chars: accumulator.chars
3612
- });
3613
- function generateRunId(now) {
3614
- const bytes = /* @__PURE__ */ new Uint8Array(16);
3615
- const epoch = DateTime.toEpochMillis(now);
3616
- bytes[0] = epoch / 1099511627776 % 256;
3617
- bytes[1] = epoch / 4294967296 % 256;
3618
- bytes[2] = epoch / 16777216 % 256;
3619
- bytes[3] = epoch / 65536 % 256;
3620
- bytes[4] = epoch / 256 % 256;
3621
- bytes[5] = epoch % 256;
3622
- bytes.set(randomBytes(10), 6);
3623
- const drained = Array.from(bytes).reduce(pushByte, {
3624
- value: 0,
3625
- bits: 0,
3626
- chars: ""
3627
- });
3628
- return Match.value(drained.bits > 0).pipe(Match.when(true, () => drained.chars + CROCKFORD_BASE32[drained.value << 5 - drained.bits & 31]), Match.when(false, () => drained.chars), Match.exhaustive);
3629
- }
3630
- function embeddedConfig(report) {
3631
- const JsonReporterSchema = S.Struct({ fileName: S.String });
3632
- const EmbeddedConfigSchema = S.StructWithRest(S.Struct({ jsonReporter: S.optional(JsonReporterSchema) }), [S.Record(S.String, S.Unknown)]);
3633
- const decoded = S.decodeUnknownOption(EmbeddedConfigSchema)(report.config);
3634
- const jsonReporter = Option.flatMap(decoded, (config) => Option.fromUndefinedOr(config.jsonReporter));
3635
- return { jsonReporterFileName: Option.getOrUndefined(Option.map(jsonReporter, (reporter) => reporter.fileName)) };
3636
- }
3637
- function breakThreshold(thresholds) {
3638
- const ThresholdsBreakSchema = S.StructWithRest(S.Struct({ break: S.optional(S.Union([S.Finite, S.Null])) }), [S.Record(S.String, S.Unknown)]);
3639
- const decoded = S.decodeUnknownOption(ThresholdsBreakSchema)(thresholds);
3640
- return Option.getOrNull(Option.flatMap(decoded, (value) => Option.fromNullishOr(value.break)));
3641
- }
3642
- const actionableMutants = (files) => Object.entries(files).flatMap(([file, fileResult]) => fileResult.mutants.filter((mutant) => isActionableStatus(mutant.status)).map((mutant) => ({
3643
- id: mutant.id,
3644
- file,
3645
- location: mutant.location,
3646
- mutator: mutant.mutatorName,
3647
- replacement: mutant.replacement ?? null,
3648
- status: mutant.status
3649
- })));
3650
- function buildVerdictEnvelope(report, mode, signal, runId, basePath, pathService) {
3651
- const metrics = calculateMetrics(report.files).metrics;
3652
- const { jsonReporterFileName } = embeddedConfig(report);
3653
- return {
3654
- schemaVersion: "1.1",
3655
- runId,
3656
- mode,
3657
- signal,
3658
- score: Option.getOrNull(Option.filter(Option.some(metrics.mutationScore), (score) => metrics.totalMutants > 0 && Number.isFinite(score))),
3659
- thresholds: {
3660
- high: report.thresholds.high,
3661
- low: report.thresholds.low,
3662
- break: breakThreshold(report.thresholds)
3663
- },
3664
- counts: {
3665
- killed: metrics.killed,
3666
- timeout: metrics.timeout,
3667
- survived: metrics.survived,
3668
- noCoverage: metrics.noCoverage,
3669
- runtimeErrors: metrics.runtimeErrors,
3670
- compileErrors: metrics.compileErrors,
3671
- ignored: metrics.ignored,
3672
- pending: metrics.pending
3673
- },
3674
- reportFile: Option.getOrNull(Option.map(Option.filter(Option.fromUndefinedOr(jsonReporterFileName), () => metrics.totalMutants > 0), (fileName) => normalizeFileName$1(pathService.relative(basePath, fileName)))),
3675
- mutants: actionableMutants(report.files)
3676
- };
3677
- }
3678
- //#endregion
3679
- //#region src/mutation-reporting.ts
3680
- const STRYKER_FRAMEWORK = Object.freeze({
3681
- branding: {
3682
- homepageUrl: "https://stryker-mutator.io",
3683
- imageUrl: "https://stryker-mutator.io/assets/images/stryker-80x80.png"
3684
- },
3685
- name: "StrykerJS",
3686
- version: strykerVersion
3687
- });
3688
- Context.Service()("@systemfsoftware/stryker-js/mutation-reporting/MutationReporting");
3689
- const makeMutationReportingService = (input) => {
3690
- const reportMutantStatus = (mutant, status) => {
3691
- const location = toSchemaLocation(mutant.location);
3692
- return Effect.succeed({
3693
- _tag: "Mutant",
3694
- id: mutant.id,
3695
- fileName: mutant.fileName,
3696
- mutatorName: mutant.mutatorName,
3697
- replacement: mutant.replacement,
3698
- location,
3699
- status,
3700
- coveredBy: mutant.coveredBy,
3701
- static: mutant.static,
3702
- testsCompleted: mutant.testsCompleted,
3703
- description: mutant.description,
3704
- statusReason: mutant.statusReason
3705
- });
3706
- };
3707
- const reportCheckFailure = (mutant, result) => reportMutantStatus(mutant, checkStatusToMutantStatus(result.status));
3708
- const reportMutantRunResult = (mutant, result) => {
3709
- const mapped = mapRunResult(mutant, result);
3710
- return Effect.succeed(mapped);
3711
- };
3712
- const uniqueNames = (names) => [...new Set(names.filter((name) => name !== void 0))];
3713
- const readMutatedSources = (fileNames) => Effect.gen(function* () {
3714
- const entries = yield* Effect.forEach(fileNames, (fileName) => Effect.gen(function* () {
3715
- const language = determineLanguage(fileName);
3716
- const file = MutableHashMap.get(input.project.files, fileName);
3717
- if (Option.isNone(file)) {
3718
- yield* Effect.logWarning(`File "${fileName}" not found in input files, but did receive mutant result for it. This shouldn't happen`);
3719
- return [fileName, {
3720
- language,
3721
- mutants: [],
3722
- source: ""
3723
- }];
3724
- }
3725
- return [fileName, {
3726
- language,
3727
- mutants: [],
3728
- source: yield* readOriginal(file.value)
3729
- }];
3730
- }), { concurrency: 24 });
3731
- return HashMap.fromIterable(entries);
3732
- });
3733
- const readTestSources = (fileNames) => Effect.gen(function* () {
3734
- const entries = yield* Effect.forEach(fileNames, (fileName) => Effect.gen(function* () {
3735
- const file = MutableHashMap.get(input.project.files, fileName);
3736
- if (Option.isNone(file)) {
3737
- yield* Effect.logWarning(`Test file "${fileName}" not found in input files, but did receive test result for it. This shouldn't happen.`);
3738
- return [fileName, { tests: [] }];
3739
- }
3740
- return [fileName, {
3741
- tests: [],
3742
- source: yield* readOriginal(file.value)
3743
- }];
3744
- }), { concurrency: 24 });
3745
- return HashMap.fromIterable(entries);
3746
- });
3747
- const assembleReport = (results) => Effect.gen(function* () {
3748
- const pathService = yield* Path.Path;
3749
- const tests = [...MutableHashMap.values(input.testCoverage.testsById)];
3750
- const remap = testIdRemap(tests.map((test) => test.id));
3751
- const mutatedFileNames = uniqueNames(results.map((result) => result.fileName));
3752
- const testFileNames = uniqueNames(tests.map((test) => test.fileName));
3753
- const sources = yield* readMutatedSources(mutatedFileNames);
3754
- const testSources = yield* readTestSources(testFileNames);
3755
- const reportNames = HashMap.fromIterable([...mutatedFileNames, ...testFileNames].map((fileName) => [fileName, reportFileName(pathService.relative(input.basePath, fileName))]));
3756
- return {
3757
- files: assembleFileResults({
3758
- sources,
3759
- reportNames,
3760
- mutants: results,
3761
- remap
3762
- }),
3763
- testFiles: assembleTestFiles({
3764
- testSources,
3765
- reportNames,
3766
- tests,
3767
- remap
3768
- })
3769
- };
3770
- });
3771
- const mutationTestReport = (results) => Effect.gen(function* () {
3772
- const { files, testFiles } = yield* assembleReport(results);
3773
- const dependencies = yield* discoverDependencies();
3774
- return {
3775
- files,
3776
- schemaVersion: "1.0",
3777
- thresholds: input.options.thresholds,
3778
- testFiles,
3779
- projectRoot: input.basePath,
3780
- config: input.options,
3781
- framework: {
3782
- ...STRYKER_FRAMEWORK,
3783
- dependencies
3784
- }
3785
- };
3786
- });
3787
- const MANIFEST_SPECIFIERS = [
3788
- "@systemfsoftware/stryker-js-vitest-runner",
3789
- "@systemfsoftware/stryker-js-typescript-checker",
3790
- "@systemfsoftware/stryker-ignorer-effect-schema-declarations",
3791
- "vitest",
3792
- "karma",
3793
- "karma-chai",
3794
- "karma-chrome-launcher",
3795
- "karma-jasmine",
3796
- "karma-mocha",
3797
- "mocha",
3798
- "jasmine",
3799
- "jasmine-core",
3800
- "jest",
3801
- "react-scripts",
3802
- "typescript",
3803
- "@angular/cli",
3804
- "webpack",
3805
- "webpack-cli",
3806
- "ts-jest"
3807
- ];
3808
- const ManifestSchema = S.Struct({ version: S.optional(S.String) });
3809
- const readManifestVersion = (fs, pathService, specifier) => Effect.gen(function* () {
3810
- const resolved = yield* Effect.try(() => new URL(import.meta.resolve(`${specifier}/package.json`)));
3811
- const manifestPath = yield* pathService.fromFileUrl(resolved);
3812
- const text = yield* fs.readFileString(manifestPath);
3813
- return Result.match(S.decodeUnknownResult(S.fromJsonString(ManifestSchema))(text), {
3814
- onFailure: () => Option.none(),
3815
- onSuccess: (manifest) => Option.some(manifest.version ?? "")
3816
- });
3817
- }).pipe(Effect.orElseSucceed(() => Option.none()));
3818
- const discoverDependencies = () => Effect.gen(function* () {
3819
- const fs = yield* FileSystem.FileSystem;
3820
- const pathService = yield* Path.Path;
3821
- const pairs = yield* Effect.forEach(MANIFEST_SPECIFIERS, (specifier) => Effect.map(readManifestVersion(fs, pathService, specifier), (version) => [specifier, version]), { concurrency: 24 });
3822
- return Object.fromEntries(pairs.flatMap(([specifier, version]) => Option.match(version, {
3823
- onNone: () => [],
3824
- onSome: (present) => [[specifier, present]]
3825
- })));
3826
- });
3827
- const determineExitCode = (metrics) => Effect.gen(function* () {
3828
- const { mutationScore } = metrics.metrics;
3829
- const breaking = input.options.thresholds.break;
3830
- const formattedScore = mutationScore.toFixed(2);
3831
- const verdict = verdictExitClass(mutationScore, breaking);
3832
- if (verdict === null) {
3833
- yield* Match.value(breaking).pipe(Match.when(null, () => Effect.logDebug("No breaking threshold configured. Won't fail the build no matter how low your mutation score is. Set `thresholds.break` to change this behavior.")), Match.orElse((threshold) => Effect.logInfo(`Final mutation score of ${formattedScore} is greater than or equal to break threshold ${String(threshold)}`)));
3834
- return null;
3835
- }
3836
- yield* Effect.logError(`Final mutation score ${formattedScore} under breaking threshold ${String(breaking)}, setting exit code to 1 (failure).`);
3837
- yield* Effect.logInfo("(improve mutation score or set `thresholds.break = null` to prevent this error in the future)");
3838
- return verdict;
3839
- });
3840
- const emitVerdict = (report, pathService) => Effect.gen(function* () {
3841
- const envelope = buildVerdictEnvelope(report, input.resolvedMode.mode, input.resolvedMode.signal, input.runId, input.basePath, pathService);
3842
- const queue = yield* RunEvents;
3843
- yield* Queue.offer(queue, VerdictReached.make({
3844
- schemaVersion: envelope.schemaVersion,
3845
- runId: envelope.runId,
3846
- mode: envelope.mode,
3847
- signal: envelope.signal,
3848
- score: envelope.score,
3849
- thresholds: envelope.thresholds,
3850
- reportFile: envelope.reportFile,
3851
- counts: envelope.counts,
3852
- mutants: envelope.mutants
3853
- }));
3854
- });
3855
- const reportAll = (results) => Effect.gen(function* () {
3856
- const pathService = yield* Path.Path;
3857
- const report = yield* mutationTestReport(results);
3858
- const metrics = calculateMetrics(report.files);
3859
- yield* offerTerminalReport(input.reporterStage, report, metrics);
3860
- const terminalDrain = terminalDrainClass(yield* closeReporterStage(input.reporterStage));
3861
- const finalVerdict = highestExitClass([yield* determineExitCode(metrics), terminalDrain].filter((candidate) => candidate !== null));
3862
- yield* emitVerdict(report, pathService);
3863
- if (input.options.incremental) {
3864
- const fs = yield* FileSystem.FileSystem;
3865
- const dir = pathService.dirname(input.options.incrementalFile);
3866
- yield* fs.makeDirectory(dir, { recursive: true });
3867
- const json = yield* S.encodeEffect(S.fromJsonString(S.Unknown, { space: 2 }))(report).pipe(Effect.orDie);
3868
- yield* fs.writeFileString(input.options.incrementalFile, json);
3869
- }
3870
- return {
3871
- results,
3872
- verdict: finalVerdict
3873
- };
3874
- });
3875
- const writeAtomic = (file, content) => Effect.gen(function* () {
3876
- const fs = yield* FileSystem.FileSystem;
3877
- const pathService = yield* Path.Path;
3878
- yield* fs.makeDirectory(pathService.dirname(file), { recursive: true });
3879
- const tmp = `${file}.tmp`;
3880
- yield* fs.writeFileString(tmp, content);
3881
- yield* fs.rename(tmp, file).pipe(Effect.catch(() => fs.copyFile(tmp, file).pipe(Effect.andThen(fs.remove(tmp)))));
3882
- });
3883
- const slimIncrementalReport = (results) => Effect.gen(function* () {
3884
- const { files, testFiles } = yield* assembleReport(results);
3885
- return {
3886
- schemaVersion: "1.0",
3887
- thresholds: input.options.thresholds,
3888
- files,
3889
- testFiles
3890
- };
3891
- });
3892
- const checkpoint = (results) => Effect.gen(function* () {
3893
- if (!input.options.incremental) return;
3894
- const report = yield* slimIncrementalReport(results);
3895
- const json = yield* S.encodeEffect(S.fromJsonString(S.Unknown))(report).pipe(Effect.orDie);
3896
- yield* writeAtomic(input.options.incrementalFile, json);
3897
- });
3898
- return {
3899
- reportCheckFailure,
3900
- reportMutantRunResult,
3901
- reportAll,
3902
- checkpoint
3903
- };
3904
- };
3905
- //#endregion
3906
- //#region src/run/mutation-test.cell.ts
3907
- const readCurrentRelativeFiles = (project, basePath) => Effect.gen(function* () {
3908
- const entries = yield* Effect.forEach(MutableHashMap.values(project.files), (file) => Effect.map(readOriginal(file), (content) => [toRelativeNormalizedFileName(file.name, basePath), content]), { concurrency: 24 });
3909
- return Object.fromEntries(entries);
3910
- });
3911
- const rememberedCoveredBy = (entry) => Option.match(Option.fromNullishOr(entry.coveredBy), {
3912
- onNone: () => ({}),
3913
- onSome: (coveredBy) => ({ coveredBy: [...coveredBy] })
3914
- });
3915
- const rememberedKilledBy = (entry) => Option.match(Option.fromNullishOr(entry.killedBy), {
3916
- onNone: () => ({}),
3917
- onSome: (killedBy) => ({ killedBy: [...killedBy] })
3918
- });
3919
- const rememberedCoverage = (entry) => ({
3920
- ...rememberedCoveredBy(entry),
3921
- ...rememberedKilledBy(entry)
3922
- });
3923
- const rememberedResultOf = (mutant, entry) => Object.assign({}, mutant, {
3924
- location: toSchemaLocation(mutant.location),
3925
- status: entry.status,
3926
- statusReason: REMEMBERED_REASON,
3927
- testsCompleted: entry.testsCompleted
3928
- }, rememberedCoverage(entry));
3929
- const rememberedResultsOf = (mutants, remembered) => {
3930
- const byId = new Map(mutants.map((mutant) => [mutant.id, mutant]));
3931
- return remembered.flatMap((entry) => Option.match(Option.fromNullishOr(byId.get(entry.mutantId)), {
3932
- onNone: () => [],
3933
- onSome: (mutant) => [rememberedResultOf(mutant, entry)]
3934
- }));
3935
- };
3936
- const isRunPlan = (plan) => plan.plan === "Run";
3937
- const earlyResultOf = (plan) => Object.assign({}, plan.mutant, {
3938
- location: toSchemaLocation(plan.mutant.location),
3939
- status: plan.mutant.status ?? "Ignored"
3940
- });
3941
- const collectPlan = (plan, coveredPlans, earlyResults) => Match.value(plan).pipe(Match.when(isRunPlan, (runPlan) => {
3942
- coveredPlans.push(runPlan);
3943
- }), Match.orElse((earlyPlan) => {
3944
- earlyResults.push(earlyResultOf(earlyPlan));
3945
- }));
3946
- const partitionPlans = (plans) => {
3947
- const coveredPlans = [];
3948
- const earlyResults = [];
3949
- plans.forEach((plan) => collectPlan(plan, coveredPlans, earlyResults));
3950
- return {
3951
- coveredPlans,
3952
- earlyResults
3953
- };
3954
- };
3955
- const VALID_MUTANT_STATUS_SET = /* @__PURE__ */ new Set([
3956
- "Killed",
3957
- "Survived",
3958
- "NoCoverage",
3959
- "Timeout",
3960
- "CompileError",
3961
- "RuntimeError",
3962
- "Ignored",
3963
- "Pending"
3964
- ]);
3965
- function isMutantStatus(s) {
3966
- return VALID_MUTANT_STATUS_SET.has(s);
3967
- }
3968
- const toReportedMutant = (mutant) => Object.assign(mutant, {
3969
- coveredBy: mutant.coveredBy,
3970
- static: mutant.static
3971
- });
3972
- const makeCheckerPool = (prev, idGenerator) => Effect.gen(function* () {
3973
- const checkerName = Option.getOrUndefined(Option.fromUndefinedOr(prev.options.checkers[0]));
3974
- if (checkerName === void 0) return;
3975
- const checkerEntry = yield* resolvePluginWorkerEntry({
3976
- loaded: prev.loadedPlugins,
3977
- kind: "Checker",
3978
- name: checkerName
3979
- }).pipe(Effect.mapError(missingWorkerEntry("mutationTest", "checker", checkerName)));
3980
- return yield* Pool.make({
3981
- acquire: createCheckerFactory(prev.options, prev.project.fileDescriptions, checkerEntry.entrypoint, idGenerator, prev.sandbox.workingDirectory),
3982
- size: prev.concurrency.checkers
3983
- });
3984
- });
3985
- const passedCheck = (result) => result.status === "passed";
3986
- const reportCheckOutcome = ([plan, result], reporting) => Match.value(result).pipe(Match.when(passedCheck, () => Effect.void), Match.orElse((failed) => reporting.reportCheckFailure(toReportedMutant(plan.mutant), failed).pipe(Effect.asVoid)));
3987
- const checkPlansWithOneChecker = (checkerPool, checkerName, plans, reporting) => Effect.gen(function* () {
3988
- const checked = yield* Effect.scoped(Effect.flatMap(Pool.get(checkerPool), (checker) => checkGroupedPlans(checker, checkerName, plans).pipe(Effect.catchTags({
3989
- OutOfMemoryError: (error) => Effect.flatMap(Pool.invalidate(checkerPool, checker), () => Effect.fail(error)),
3990
- ChildProcessCrashedError: (error) => Effect.flatMap(Pool.invalidate(checkerPool, checker), () => Effect.fail(error))
3991
- }))));
3992
- yield* Effect.forEach(checked, (pair) => reportCheckOutcome(pair, reporting), {
3993
- concurrency: 1,
3994
- discard: true
3995
- });
3996
- return checked.filter(([, result]) => result.status === "passed").map(([plan]) => plan);
3997
- });
3998
- const checkPlansWithEachChecker = (prev, checkerPool, plans, reporting) => Effect.gen(function* () {
3999
- let passed = plans;
4000
- for (const checkerName of prev.options.checkers) passed = yield* checkPlansWithOneChecker(checkerPool, checkerName, passed, reporting);
4001
- return passed;
4002
- });
4003
- const checkPlansWithConfiguredCheckers = (prev, checkerPool, plans, reporting) => Option.match(Option.fromNullishOr(checkerPool), {
4004
- onNone: () => Effect.succeed(plans),
4005
- onSome: (pool) => checkPlansWithEachChecker(prev, pool, plans, reporting)
4006
- });
4007
- const mutationTestCell$1 = Cell.layer({
4008
- read: (command) => Effect.gen(function* () {
4009
- yield* Scope.Scope;
4010
- return { prev: command };
4011
- }),
4012
- decode: (raw) => Result.succeed(MutationTestCommand.make({
4013
- dryRunOnly: raw.prev.options.dryRunOnly,
4014
- allowEmpty: raw.prev.options.allowEmpty,
4015
- testCount: raw.prev.dryRunResult.tests.length,
4016
- isZero: raw.prev.dryRunResult.tests.length === 0
4017
- })),
4018
- decide: admitMutationTest,
4019
- encode: (outcome) => outcome,
4020
- write: (outcome, raw) => withPhaseSpan("mutationTest", {
4021
- mutantCount: raw.prev.mutants.length,
4022
- testCount: raw.prev.dryRunResult.tests.length
4023
- }, () => Effect.gen(function* () {
4024
- const decision = yield* Result.match(outcome, {
4025
- onFailure: (err) => Effect.fail(StageError.make({
4026
- stage: err.stage,
4027
- reason: err.reason,
4028
- cause: err
4029
- })),
4030
- onSuccess: (d) => Effect.succeed(d)
4031
- });
4032
- return yield* Match.value(decision).pipe(Match.tag("MutationTestDryRunOnly", () => Effect.gen(function* () {
4033
- const env = yield* RunEnvironment;
4034
- const queue = yield* RunEvents;
4035
- const nowEmit = yield* Clock.currentTimeMillis;
4036
- yield* Queue.offer(queue, PhaseEntered.make({
4037
- phase: "mutation-test",
4038
- elapsedMs: nowEmit - env.runStartedAt
4039
- }));
4040
- yield* Effect.logInfo("The dry-run has been completed successfully. No mutations have been executed.");
4041
- return {
4042
- results: [],
4043
- verdict: null
4044
- };
4045
- })), Match.tag("MutationTestNoTests", () => Effect.gen(function* () {
4046
- const env = yield* RunEnvironment;
4047
- const queue = yield* RunEvents;
4048
- const now = yield* Clock.currentTimeMillis;
4049
- const elapsed = Duration.millis(now - env.runStartedAt);
4050
- yield* Effect.logInfo(`Done in ${Duration.format(elapsed)}.`);
4051
- const nowEmit = yield* Clock.currentTimeMillis;
4052
- yield* Queue.offer(queue, PhaseEntered.make({
4053
- phase: "mutation-test",
4054
- elapsedMs: nowEmit - env.runStartedAt
4055
- }));
4056
- return {
4057
- results: [],
4058
- verdict: null
4059
- };
4060
- })), Match.tag("MutationTestProceed", () => Effect.gen(function* () {
4061
- const prev = raw.prev;
4062
- const env = yield* RunEnvironment;
4063
- yield* Effect.gen(function* () {
4064
- const nowEmit = yield* Clock.currentTimeMillis;
4065
- const queue = yield* RunEvents;
4066
- yield* Queue.offer(queue, PhaseEntered.make({
4067
- phase: "mutation-test",
4068
- elapsedMs: nowEmit - env.runStartedAt
4069
- }));
4070
- });
4071
- const idGenerator = yield* IdGenerator;
4072
- const checkerPool = yield* makeCheckerPool(prev, idGenerator);
4073
- const testRunnerContext = {
4074
- options: prev.options,
4075
- fileDescriptions: prev.project.fileDescriptions,
4076
- sandboxWorkingDirectory: prev.sandbox.workingDirectory,
4077
- idGenerator,
4078
- retire: Effect.void,
4079
- testFiles: prev.project.testFiles.map((file) => prev.sandbox.sandboxFileFor(file))
4080
- };
4081
- const testRunnerPool = yield* Pool.make({
4082
- acquire: buildTestRunner(testRunnerContext, Effect.suspend(() => resolvePluginWorkerEntry({
4083
- loaded: prev.loadedPlugins,
4084
- kind: "TestRunner",
4085
- name: prev.options.testRunner
4086
- }).pipe(Effect.mapError(missingWorkerEntry("mutationTest", "test runner", prev.options.testRunner)), Effect.flatMap((runnerEntry) => makeChildProcessTestRunner({
4087
- options: prev.options,
4088
- fileDescriptions: prev.project.fileDescriptions,
4089
- sandboxWorkingDirectory: prev.sandbox.workingDirectory,
4090
- workerEntrypoint: runnerEntry.entrypoint,
4091
- idGenerator
4092
- }))))),
4093
- size: prev.concurrency.testRunners
4094
- });
4095
- const reporting = makeMutationReportingService({
4096
- reporterStage: prev.reporterStage,
4097
- options: prev.options,
4098
- project: prev.project,
4099
- testCoverage: prev.testCoverage,
4100
- runId: env.runId,
4101
- resolvedMode: env.resolvedMode,
4102
- sandboxDirectory: prev.sandbox.workingDirectory,
4103
- basePath: env.basePath
4104
- });
4105
- const sandboxFileByName = Object.fromEntries([...MutableHashMap.keys(prev.project.filesToMutate)].map((name) => [name, prev.sandbox.sandboxFileFor(name)]));
4106
- const currentRelativeFiles = yield* readCurrentRelativeFiles(prev.project, env.basePath);
4107
- const incremental = incrementalDiff({
4108
- currentMutants: prev.mutants,
4109
- testCoverage: prev.testCoverage,
4110
- incrementalReport: prev.project.incrementalReport,
4111
- currentRelativeFiles,
4112
- basePath: env.basePath,
4113
- force: prev.options.force
4114
- });
4115
- const rememberedResults = rememberedResultsOf(prev.mutants, incremental.remembered);
4116
- yield* Effect.when(Effect.logInfo(`Incremental mode: reusing ${rememberedResults.length} mutant result(s), running ${incremental.mutants.length} mutant(s).`), Effect.succeed(rememberedResults.length > 0));
4117
- const { coveredPlans, earlyResults: noCoverageResults } = partitionPlans(yield* decidePlans(incremental.mutants, prev.testCoverage, {
4118
- disableBail: prev.options.disableBail,
4119
- timeoutMS: prev.options.timeoutMS,
4120
- timeoutFactor: prev.options.timeoutFactor,
4121
- ignoreStatic: prev.options.ignoreStatic
4122
- }, Duration.toMillis(prev.timeOverhead), void 0, sandboxFileByName));
4123
- const sortedPlans = [...coveredPlans].sort((a, b) => Number(a.runOptions.reloadEnvironment) - Number(b.runOptions.reloadEnvironment));
4124
- const allPlansForReporter = [...sortedPlans];
4125
- yield* offerReporterEvent(prev.reporterStage, MutationTestingPlanReady.make({
4126
- total: allPlansForReporter.length + noCoverageResults.length + rememberedResults.length,
4127
- plans: allPlansForReporter.map((plan) => ({
4128
- mutantId: plan.mutant.id,
4129
- plan: plan.plan,
4130
- netTime: plan.netTime,
4131
- reloadEnvironment: plan.runOptions.reloadEnvironment
4132
- }))
4133
- })).pipe(Effect.ignoreCause);
4134
- {
4135
- const queue2 = yield* RunEvents;
4136
- yield* Queue.offer(queue2, PlanKnown.make({ total: allPlansForReporter.length + noCoverageResults.length }));
4137
- }
4138
- const passedPlans = yield* checkPlansWithConfiguredCheckers(prev, checkerPool, sortedPlans, reporting);
4139
- const testRunnerStream = Stream.fromIterable(passedPlans);
4140
- const plannedTotal = allPlansForReporter.length + noCoverageResults.length + rememberedResults.length;
4141
- const pathService = yield* Path.Path;
4142
- const progressQueue = yield* RunEvents;
4143
- const completedRef = yield* Ref.make(0);
4144
- const preparedStreamableOf = (result) => {
4145
- if (!isMutantStatus(result.status)) return;
4146
- return {
4147
- status: result.status,
4148
- file: reportFileName(pathService.relative(env.basePath, result.fileName)),
4149
- location: toSchemaLocation(result.location)
4150
- };
4151
- };
4152
- const toStreamEvent = (result, completed, prepared) => MutantTested.make({
4153
- id: result.id,
4154
- status: prepared.status,
4155
- file: prepared.file,
4156
- location: prepared.location,
4157
- mutator: result.mutatorName,
4158
- replacement: result.replacement,
4159
- completed,
4160
- total: plannedTotal
4161
- });
4162
- const offerFinished = (result, prepared) => Effect.gen(function* () {
4163
- if (prepared === void 0) return;
4164
- const completed = yield* Ref.updateAndGet(completedRef, (n) => n + 1);
4165
- yield* Queue.offer(progressQueue, RunMutantTested.make({
4166
- id: result.id,
4167
- status: prepared.status,
4168
- file: prepared.file,
4169
- location: prepared.location,
4170
- mutator: result.mutatorName,
4171
- replacement: result.replacement,
4172
- completed,
4173
- total: plannedTotal
4174
- }));
4175
- return completed;
4176
- });
4177
- const reportStreamTested = (result, completed, prepared) => offerReporterEvent(prev.reporterStage, toStreamEvent(result, completed, prepared)).pipe(Effect.catchCause((cause) => Effect.logWarning("Reporter stream failed handling mutantTested", cause)));
4178
- const offerStreamTested = (result, completed, prepared) => Option.match(Option.all([Option.fromNullishOr(completed), Option.fromNullishOr(prepared)]), {
4179
- onNone: () => Effect.void,
4180
- onSome: ([done, streamable]) => reportStreamTested(result, done, streamable)
4181
- });
4182
- const announceSettledMutant = (result) => Effect.gen(function* () {
4183
- const prepared = preparedStreamableOf(result);
4184
- const completed = yield* offerFinished(result, prepared);
4185
- yield* offerStreamTested(result, completed, prepared);
4186
- });
4187
- yield* Effect.forEach([...rememberedResults, ...noCoverageResults], announceSettledMutant, {
4188
- concurrency: 1,
4189
- discard: true
4190
- });
4191
- const completedMutants = yield* Ref.make([...rememberedResults, ...noCoverageResults]);
4192
- const checkpointGate = yield* Semaphore.make(1);
4193
- yield* reporting.checkpoint(yield* Ref.get(completedMutants)).pipe(Effect.catchCause((cause) => Effect.logWarning("Failed to persist the mutation checkpoint", cause)));
4194
- const persist = (result) => checkpointGate.withPermits(1)(Effect.gen(function* () {
4195
- const next = yield* Ref.updateAndGet(completedMutants, (prev) => [...prev, result]);
4196
- yield* reporting.checkpoint(next).pipe(Effect.catchCause((cause) => Effect.logWarning("Failed to persist the mutation checkpoint", cause)));
4197
- }));
4198
- const runResults = yield* withPhaseSpan("mutationTest.batch", {
4199
- total: plannedTotal,
4200
- testRunners: prev.concurrency.testRunners
4201
- }, () => Stream.mapEffect(testRunnerStream, (plan) => Effect.scoped(Effect.gen(function* () {
4202
- const pool = testRunnerPool;
4203
- const runner = yield* Pool.get(pool);
4204
- const result = yield* runner.mutantRun(plan.runOptions).pipe(Effect.catchTags({
4205
- OutOfMemoryError: (error) => Effect.flatMap(Pool.invalidate(pool, runner), () => Effect.fail(error)),
4206
- ChildProcessCrashedError: (error) => Effect.flatMap(Pool.invalidate(pool, runner), () => Effect.fail(error))
4207
- }));
4208
- const reported = yield* reporting.reportMutantRunResult(toReportedMutant(plan.mutant), result);
4209
- const prepared = preparedStreamableOf(reported);
4210
- const finished = yield* offerFinished(reported, prepared);
4211
- yield* offerStreamTested(reported, finished, prepared);
4212
- yield* persist(reported);
4213
- return reported;
4214
- })), { concurrency: Math.max(1, prev.concurrency.testRunners) }).pipe(Stream.runCollect, Effect.map((chunk) => [...chunk])));
4215
- const allResults = [
4216
- ...rememberedResults,
4217
- ...noCoverageResults,
4218
- ...runResults
4219
- ];
4220
- const outcomeResult = yield* reporting.reportAll(allResults);
4221
- const doneNow = yield* Clock.currentTimeMillis;
4222
- const elapsed = Duration.millis(doneNow - env.runStartedAt);
4223
- yield* Effect.logInfo(`Done in ${Duration.format(elapsed)}.`);
4224
- return outcomeResult;
4225
- })), Match.exhaustive);
4226
- })).pipe(Effect.mapError((cause) => Match.value(cause).pipe(Match.tag("StageError", (stage) => stage), Match.tag("CheckerAnsweredUnrequested", (breach) => StageError.make({
4227
- stage: "mutationTest",
4228
- reason: `Checker "${breach.checkerName}" answered about mutants it was not asked about (${breach.phase} phase): ${breach.unrequestedIds.join(", ")}`,
4229
- cause: breach
4230
- })), Match.tag("CheckerSkippedRequested", (breach) => StageError.make({
4231
- stage: "mutationTest",
4232
- reason: `Checker "${breach.checkerName}" skipped requested mutants (${breach.phase} phase): ${breach.missingIds.join(", ")}`,
4233
- cause: breach
4234
- })), Match.tag("ChildProcessCrashedError", "OutOfMemoryError", "PlatformError", "TestRunnerFailed", () => StageError.make({
4235
- stage: "mutationTest",
4236
- reason: "Mutation testing failed",
4237
- cause
4238
- })), Match.exhaustive)))
4239
- });
4240
- //#endregion
4241
- //#region src/select-reporters.ts
4242
- const STREAM_REPORTER = "progress-stream";
4243
- const HUMAN_REPORTER = "clear-text";
4244
- const STDOUT_REPORTERS = {
4245
- "clear-text": true,
4246
- "progress": true
4247
- };
4248
- const asHumanReporter = (name) => {
4249
- if (name === STREAM_REPORTER) return HUMAN_REPORTER;
4250
- return name;
4251
- };
4252
- const selectReporters = (configured, mode) => Match.value(mode).pipe(Match.when("human", () => [...new Set(configured.map(asHumanReporter))]), Match.when("machine", () => {
4253
- const permitted = configured.filter((name) => STDOUT_REPORTERS[name] !== true);
4254
- return Match.value(permitted.includes(STREAM_REPORTER)).pipe(Match.when(true, () => permitted), Match.when(false, () => [...permitted, STREAM_REPORTER]), Match.exhaustive);
4255
- }), Match.exhaustive);
4256
- //#endregion
4257
- //#region src/run/plan-prepare.workflow.ts
4258
- const PrepareDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/PrepareDecision");
4259
- var PrepareDecoded = class extends S.Class("PrepareDecoded")({
4260
- mode: S.Literals(["human", "machine"]),
4261
- reporters: S.Array(S.String),
4262
- fileCount: S.Finite,
4263
- availableReporters: S.Array(S.String)
4264
- }) {};
4265
- var HumanReporters = class extends S.TaggedClass()("HumanReporters", { reporters: S.Array(S.String) }) {
4266
- [PrepareDecisionTypeId] = PrepareDecisionTypeId;
4267
- };
4268
- var MachineReporters = class extends S.TaggedClass()("MachineReporters", { reporters: S.Array(S.String) }) {
4269
- [PrepareDecisionTypeId] = PrepareDecisionTypeId;
4270
- };
4271
- const planPrepare = Workflow.total(PrepareDecoded, (decoded) => Match.value(decoded.mode).pipe(Match.when("human", () => Result.succeed(HumanReporters.make({ reporters: decoded.reporters }))), Match.when("machine", () => Result.succeed(MachineReporters.make({ reporters: decoded.reporters }))), Match.exhaustive));
4272
- //#endregion
4273
- //#region src/run/prepare.cell.ts
4274
- const isRecord = Predicate.isObject;
4275
- const asRecord = (value) => Match.value(value).pipe(Match.when(isRecord, (record) => Object.fromEntries(Object.entries(record))), Match.orElse(() => ({})));
4276
- const schemaPropertiesOf = (document) => asRecord(asRecord(document)["properties"]);
4277
- const buildMergedSchema = (core, contributions) => Match.value(contributions.length === 0).pipe(Match.when(true, () => core), Match.orElse(() => ({
4278
- ...core,
4279
- properties: Object.assign({}, schemaPropertiesOf(core), ...contributions.map((contribution) => schemaPropertiesOf(contribution)))
4280
- })));
4281
- const announceSummary = (env, summary) => Match.value(env.resolvedMode.mode).pipe(Match.when("human", () => announceHumanSummary(env.allowConsoleColors, summary)), Match.orElse(() => Effect.logInfo(summary)));
4282
- const announceHumanSummary = (allowConsoleColors, summary) => Match.value(allowConsoleColors).pipe(Match.when(true, () => Console.log(ansi.green(summary))), Match.orElse(() => Console.log(summary)));
4283
- const NO_PLUGIN_DESCRIPTORS = [];
4284
- const spawnPluginReporterFactory = (name, loaded, projectBasePath, options) => Effect.gen(function* () {
4285
- const entry = yield* resolvePluginWorkerEntry({
4286
- loaded,
4287
- kind: "Reporter",
4288
- name
4289
- }).pipe(Effect.mapError(missingWorkerEntry("prepare", "reporter", name)));
4290
- const client = yield* spawnReporterWorker({
4291
- entrypoint: entry.entrypoint,
4292
- projectBasePath,
4293
- execArgv: [],
4294
- options,
4295
- tempDirPrefix: "stryker-reporter-"
4296
- }).pipe(Effect.mapError((cause) => StageError.make({
4297
- stage: "prepare",
4298
- reason: `Failed to start the reporter worker "${name}"`,
4299
- cause
4300
- })));
4301
- return reporterWorkerFactory(client);
4302
- });
4303
- const reporterInputsOf = (names, choicesByName, loaded, projectBasePath, options) => Effect.forEach(selectReporterChoices(names, choicesByName), (choice) => Option.match(choice.builtinFactory, {
4304
- onSome: (builtin) => Effect.succeed({
4305
- name: choice.name,
4306
- factory: builtin
4307
- }),
4308
- onNone: () => Effect.map(spawnPluginReporterFactory(choice.name, loaded, projectBasePath, options), (factory) => ({
4309
- name: choice.name,
4310
- factory
4311
- }))
4312
- }), { concurrency: 1 });
4313
- const selectReporter = (chosen, name, choicesByName) => {
4314
- const key = name.toLowerCase();
4315
- Option.match(HashMap.get(choicesByName, key), {
4316
- onNone: () => void 0,
4317
- onSome: (choice) => {
4318
- if (!chosen.has(key)) chosen.set(key, choice);
4319
- }
4320
- });
4321
- };
4322
- const selectReporterChoices = (names, choicesByName) => {
4323
- const chosen = /* @__PURE__ */ new Map();
4324
- names.forEach((name) => selectReporter(chosen, name, choicesByName));
4325
- return [...chosen.values()];
4326
- };
4327
- const readPrepare = (command) => Effect.gen(function* () {
4328
- yield* Scope.Scope;
4329
- const env = yield* RunEnvironment;
4330
- const queue = yield* RunEvents;
4331
- const coreSchema = forkCoreSchema;
4332
- const configured = yield* readConfig(command.cliOptions, {
4333
- command: "run",
4334
- mode: env.resolvedMode.mode
4335
- }).pipe(Effect.mapError((cause) => StageError.make({
4336
- stage: "prepare",
4337
- reason: "Failed to read config",
4338
- cause
4339
- })), Effect.tapCause(() => Effect.gen(function* () {
4340
- const now = yield* Clock.currentTimeMillis;
4341
- yield* Queue.offer(queue, PhaseEntered.make({
4342
- phase: "prepare",
4343
- elapsedMs: now - env.runStartedAt
4344
- }));
4345
- }).pipe(Effect.ignore)));
4346
- const resolvedReporters = selectReporters([...configured.reporters], env.resolvedMode.mode);
4347
- const options = {
4348
- ...configured,
4349
- reporters: resolvedReporters,
4350
- allowConsoleColors: env.allowConsoleColors,
4351
- clearTextReporter: {
4352
- ...configured.clearTextReporter,
4353
- allowColor: env.allowConsoleColors
4354
- }
4355
- };
4356
- const descriptors = [...options.plugins, ...options.appendPlugins];
4357
- const loaded = yield* loadPlugins(descriptors).pipe(Effect.mapError((cause) => StageError.make({
4358
- stage: "prepare",
4359
- reason: "Failed to load plugins",
4360
- cause
4361
- })));
4362
- const mergedSchema = buildMergedSchema(coreSchema, loaded.schemaContributions);
4363
- const record = { ...options };
4364
- yield* validateOptions(record, mergedSchema).pipe(Effect.mapError((cause) => StageError.make({
4365
- stage: "prepare",
4366
- reason: "Failed to revalidate options with plugin schema",
4367
- cause
4368
- })));
4369
- const project = yield* readProject(options, command.targetMutatePatterns, env.basePath).pipe(Effect.mapError((cause) => StageError.make({
4370
- stage: "prepare",
4371
- reason: "Failed to read project",
4372
- cause
4373
- })));
4374
- const selectedIgnorers = HashSet.fromIterable(options.ignorers);
4375
- const ignorers = loaded.ignorers.filter((ignorer) => HashSet.has(selectedIgnorers, ignorer.name));
4376
- const builtinReporterFactories = {
4377
- ...makeBuiltinReporterFactories({
4378
- fileSystem: yield* FileSystem.FileSystem,
4379
- path: yield* Path.Path,
4380
- stdio: yield* Stdio.Stdio
4381
- }),
4382
- ...env.builtinReporters
4383
- };
4384
- const pluginReporterDescriptors = Option.getOrElse(HashMap.get(loaded.pluginsByKind, "Reporter"), () => NO_PLUGIN_DESCRIPTORS);
4385
- return {
4386
- env,
4387
- queue,
4388
- options,
4389
- loaded,
4390
- project,
4391
- ignorers,
4392
- builtinReporterFactories,
4393
- reporterChoicesByName: HashMap.fromIterable([...Object.entries(builtinReporterFactories).map(([name, factory]) => [name.toLowerCase(), {
4394
- name,
4395
- builtinFactory: Option.some(factory)
4396
- }]), ...pluginReporterDescriptors.map((descriptor) => [descriptor.name.toLowerCase(), {
4397
- name: descriptor.name,
4398
- builtinFactory: Option.none()
4399
- }])])
4400
- };
4401
- });
4402
- const decodePrepare = (raw) => Result.succeed(PrepareDecoded.make({
4403
- mode: raw.env.resolvedMode.mode,
4404
- reporters: [...raw.options.reporters],
4405
- fileCount: MutableHashMap.size(raw.project.files),
4406
- availableReporters: [...HashMap.values(raw.reporterChoicesByName)].map((choice) => choice.name)
4407
- }));
4408
- const encodePrepareDecision = (outcome) => outcome;
4409
- const writePrepare = (outcome, raw) => withPhaseSpan("prepare", {}, (span) => Effect.gen(function* () {
4410
- const decision = yield* Effect.fromResult(outcome);
4411
- const mutateCount = MutableHashMap.size(raw.project.filesToMutate);
4412
- yield* announceSummary(raw.env, `Found ${mutateCount} of ${MutableHashMap.size(raw.project.files)} file(s) to be mutated.`);
4413
- yield* validateReporterNames(raw.options.reporters, [...HashMap.values(raw.reporterChoicesByName)].map((choice) => choice.name)).pipe(Effect.mapError((cause) => StageError.make({
4414
- stage: "prepare",
4415
- reason: cause.message,
4416
- cause
4417
- })));
4418
- yield* Match.value(MutableHashMap.size(raw.project.files)).pipe(Match.when(0, () => Effect.fail(StageError.make({
4419
- stage: "prepare",
4420
- reason: "No input files found.",
4421
- cause: PrepareError.make({
4422
- stage: "prepare",
4423
- reason: "No input files found."
4424
- })
4425
- }))), Match.orElse(() => Effect.void));
4426
- const temporaryDirectoryPath = yield* Effect.gen(function* () {
4427
- const live = TemporaryDirectoryLive(raw.options);
4428
- return (yield* Effect.service(TemporaryDirectory).pipe(Effect.provide(live))).path;
4429
- }).pipe(Effect.mapError((cause) => StageError.make({
4430
- stage: "prepare",
4431
- reason: "Failed to create temporary directory",
4432
- cause
4433
- })));
4434
- const reporterInputs = yield* reporterInputsOf(Option.getOrElse(Match.value(decision).pipe(Match.tag("HumanReporters", (human) => Option.some(human.reporters)), Match.tag("MachineReporters", (machine) => Option.some(machine.reporters)), Match.exhaustive), () => raw.options.reporters), raw.reporterChoicesByName, raw.loaded, raw.env.basePath, raw.options);
4435
- const reporterInit = yield* currentReporterInit(span);
4436
- const reporterStage = yield* attachReporterFactories(reporterInputs, raw.options, reporterInit);
4437
- const now = yield* Clock.currentTimeMillis;
4438
- yield* Queue.offer(raw.queue, PhaseEntered.make({
4439
- phase: "prepare",
4440
- elapsedMs: now - raw.env.runStartedAt
4441
- }));
4442
- return {
4443
- project: raw.project,
4444
- loadedPlugins: raw.loaded,
4445
- ignorers: raw.ignorers,
4446
- options: raw.options,
4447
- temporaryDirectoryPath,
4448
- reporterStage
4449
- };
4450
- }));
4451
- const prepareCell = Cell.layer({
4452
- read: readPrepare,
4453
- decode: decodePrepare,
4454
- decide: planPrepare,
4455
- encode: encodePrepareDecision,
4456
- write: writePrepare
4457
- });
4458
- //#endregion
4459
- //#region src/Run.ts
4460
- const RUN_EVENTS_QUEUE_BOUND = 256;
4461
- const makeRunLayer = (env, events) => {
4462
- const eventsLayer = Match.value(events).pipe(Match.when(void 0, () => Layer.effect(RunEvents, Queue.bounded(256))), Match.orElse((queue) => Layer.succeed(RunEvents, queue)));
4463
- return Layer.mergeAll(Layer.succeed(RunEnvironment, env), eventsLayer, layer, Layer.effect(Scope.Scope, Effect.gen(function* () {
4464
- const stageScope = yield* Scope.make();
4465
- yield* Effect.addFinalizer(() => Scope.close(stageScope, Exit.void));
4466
- return stageScope;
4467
- })));
4468
- };
4469
- const mutationTestCell = Cell.andThen(Cell.andThen(Cell.andThen(prepareCell, instrumentCell), dryRunCell), mutationTestCell$1);
4470
- const shouldKeepTempDir = (exit, cleanTempDir) => Exit.isFailure(exit) && cleanTempDir !== "always";
4471
- //#endregion
4472
- //#region src/Output.ts
4473
- const TICK_INTERVAL_MS = 1e4;
4474
- var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js/Output/RunEventStreamPortTag") {};
4475
- const RunEventStreamPort = RunEventStreamPortTag;
4476
- const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.orElse(() => false));
4477
- 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);
4478
- const toWireLine = (event) => {
4479
- const fields = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_tag"));
4480
- return JSON.stringify({
4481
- kind: wireKind(event),
4482
- ...fields
4483
- });
4484
- };
4485
- function numberText(value, fallback) {
4486
- if (typeof value === "number") return String(value);
4487
- return fallback;
4488
- }
4489
- function phaseLine(phase) {
4490
- if (typeof phase === "string") return `phase ${phase}`;
4491
- return "phase ";
4492
- }
4493
- const stderrProgressLine = (event, alreadyClosed) => {
4494
- if (alreadyClosed) return;
4495
- return Match.value(event).pipe(Match.tag("plan", (e) => `plan ${numberText(e.total, "0")} mutants`), Match.tag("phase", (e) => phaseLine(e.phase)), Match.tag("tick", (e) => `${numberText(e.completed, "0")}/${numberText(e.total, "?")} elapsed ${numberText(e.elapsedMs, "0")}ms`), Match.tag("verdict", (e) => `score ${numberText(e.score, "n/a")} killed ${numberText(e.counts.killed, "0")} survived ${numberText(e.counts.survived, "0")}`), Match.tag("error", (e) => {
4496
- if (typeof e.error === "string") return `error ${e.error}`;
4497
- return "error ";
4498
- }), Match.orElse(() => void 0));
4499
- };
4500
- const writeStderr = (stdio, line) => Stream.run(Stream.succeed(`${line}\n`), stdio.stderr({ endOnDone: false })).pipe(Effect.ignore);
4501
- const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
4502
- const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, stdio)) => Effect.gen(function* () {
4503
- const startedAt = yield* Clock.currentTimeMillis;
4504
- const runId = generateRunId(DateTime.makeUnsafe(startedAt));
4505
- const queue = yield* Queue.bounded(256);
4506
- const state = {
4507
- mode: resolved.mode,
4508
- signal: resolved.signal,
4509
- headerWritten: false,
4510
- terminalWritten: false,
4511
- progress: {
4512
- completed: 0,
4513
- total: null
4514
- }
4515
- };
4516
- const queueStream = Stream.fromQueue(queue).pipe(Stream.tap((event) => Effect.sync(() => {
4517
- Match.value(event).pipe(Match.tag("plan", (e) => {
4518
- state.progress = {
4519
- ...state.progress,
4520
- total: e.total
4521
- };
4522
- }), Match.tag("mutant", (e) => {
4523
- state.progress = {
4524
- completed: e.completed,
4525
- total: e.total
4526
- };
4527
- }), Match.orElse(() => {}));
4528
- })));
4529
- const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.drop(1), Stream.filter(() => state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
4530
- const now = yield* Clock.currentTimeMillis;
4531
- return Heartbeat.make({
4532
- elapsedMs: now - startedAt,
4533
- completed: state.progress.completed,
4534
- total: state.progress.total
4535
- });
4536
- })));
4537
- let terminalSeen = false;
4538
- const noteTerminal = (event) => {
4539
- state.terminalWritten = state.terminalWritten || isTerminalEvent(event);
4540
- };
4541
- const progressTap = (event) => {
4542
- const line = stderrProgressLine(event, state.terminalWritten);
4543
- noteTerminal(event);
4544
- return Option.match(Option.fromUndefinedOr(line), {
4545
- onNone: () => Effect.void,
4546
- onSome: (text) => writeStderr(stdio, text)
4547
- });
4548
- };
4549
- const framingOpen = () => state.mode === "machine" && !terminalSeen;
4550
- const markTerminalFramed = (event) => {
4551
- terminalSeen = terminalSeen || isTerminalEvent(event);
4552
- };
4553
- const frameEvent = (event) => {
4554
- if (!framingOpen()) return false;
4555
- markTerminalFramed(event);
4556
- return true;
4557
- };
4558
- const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap(progressTap)).pipe(Stream.filter(frameEvent), Stream.map((event) => `${toWireLine(event)}\n`)));
4559
- let drainFiber = null;
4560
- const outputOpen = () => state.mode === "machine" && !state.terminalWritten;
4561
- const offerStarted = () => Effect.gen(function* () {
4562
- if (state.mode !== "machine") return;
4563
- yield* Queue.offer(queue, RunStarted.make({
4564
- schemaVersion: "1.0",
4565
- runId,
4566
- mode: state.mode,
4567
- signal: state.signal
4568
- }));
4569
- });
4570
- const openHeader = () => Effect.gen(function* () {
4571
- if (state.headerWritten) return;
4572
- state.headerWritten = true;
4573
- yield* offerStarted();
4574
- });
4575
- return {
4576
- queue,
4577
- runId,
4578
- startedAt,
4579
- isOpen: () => outputOpen() && drainFiber !== null,
4580
- ensureOpen: (openResolved) => {
4581
- if (state.headerWritten) return;
4582
- state.mode = openResolved.mode;
4583
- state.signal = openResolved.signal;
4584
- },
4585
- open: Effect.gen(function* () {
4586
- if (drainFiber !== null) return;
4587
- yield* openHeader();
4588
- drainFiber = yield* Effect.forkDetach(drain);
4589
- }),
4590
- closeAndDrain: Effect.gen(function* () {
4591
- state.terminalWritten = true;
4592
- yield* Queue.end(queue);
4593
- if (drainFiber !== null) yield* Fiber.join(drainFiber);
4594
- })
4595
- };
4596
- });
4597
- Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
4598
- const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
4599
- const decisionToResolvedMode = (decision) => Match.value(decision).pipe(Match.tag("HumanOutput", (human) => ({
4600
- mode: "human",
4601
- signal: human.signal,
4602
- stdoutIsTTY: human.stdoutIsTTY
4603
- })), Match.tag("MachineOutput", (machine) => ({
4604
- mode: "machine",
4605
- signal: machine.signal,
4606
- stdoutIsTTY: machine.stdoutIsTTY
4607
- })), Match.exhaustive);
4608
- const definedToolVars = (vars) => Object.fromEntries(Object.entries(vars ?? {}).filter((entry) => entry[1] !== void 0));
4609
- const commandFor = (input) => ResolveModeCommand.make({
4610
- stdoutIsTTY: input.stdoutIsTTY,
4611
- text: input.text,
4612
- json: input.json,
4613
- envMode: input.envMode,
4614
- agent: input.agent,
4615
- toolVars: definedToolVars(input.toolVars)
4616
- });
4617
- function isColorEnabled(resolved, noColor) {
4618
- const requested = Option.exists(Option.fromUndefinedOr(noColor), S.is(S.NonEmptyString));
4619
- return resolved.mode === "human" && !requested;
4620
- }
4621
- var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js/Output/OutputModeProbeTag") {};
4622
- const OutputModeProbe = OutputModeProbeTag;
4623
- const envToolVars = () => Effect.forEach(TOOL_VARIABLES, (variable) => Config.string(variable).pipe(Effect.option, Effect.map((value) => [variable, Option.getOrUndefined(value)]))).pipe(Effect.map((entries) => definedToolVars(Object.fromEntries(entries))));
4624
- const probeInput = (command) => Effect.gen(function* () {
4625
- const stdio = yield* Stdio.Stdio;
4626
- const envMode = yield* Config.string("STRYKER_MODE").pipe(Effect.option);
4627
- const agent = yield* Config.string("AGENT").pipe(Effect.option);
4628
- return {
4629
- stdoutIsTTY: yield* stdio.stdoutIsTerminal,
4630
- text: command.text,
4631
- json: command.json,
4632
- envMode: Option.getOrUndefined(envMode),
4633
- agent: Option.getOrUndefined(agent),
4634
- toolVars: yield* envToolVars()
4635
- };
4636
- });
4637
- const outputModeProbeCell = Cell.layer({
4638
- read: (command) => probeInput(command),
4639
- decode: (raw) => Result.succeed(commandFor(raw)),
4640
- decide: resolveOutputMode,
4641
- encode: (outcome) => Result.map(outcome, decisionToResolvedMode),
4642
- write: (outcome) => Result.match(outcome, {
4643
- onFailure: (error) => Effect.fail(error),
4644
- onSuccess: (mode) => Effect.succeed(mode)
4645
- })
4646
- });
4647
- const detectModeWithProbe = (flags = {}) => outputModeProbeCell.run(flags).pipe(Effect.mapError((error) => CliError.InvalidValue.make({
4648
- option: error.option,
4649
- value: error.value,
4650
- expected: error.expected,
4651
- kind: "flag"
4652
- })));
4653
- Layer.effect(OutputModeProbe, Effect.map(Stdio.Stdio, (stdio) => OutputModeProbe.of({ detectMode: Effect.provideService(detectModeWithProbe({}), Stdio.Stdio, stdio) })));
4654
- //#endregion
4655
- //#region src/StreamFile.ts
4656
- const DEFAULT_PROGRESS_STREAM_FILE = "reports/mutation-stream.jsonl";
4657
- const encodeUtf8 = (line) => new TextEncoder().encode(line);
4658
- const drainStdoutAndFile = (stdio, fileName, framed) => Effect.gen(function* () {
4659
- const fs = yield* FileSystem.FileSystem;
4660
- const path = yield* Path.Path;
4661
- yield* fs.makeDirectory(path.dirname(fileName), { recursive: true }).pipe(Effect.orDie);
4662
- yield* Effect.scoped(Effect.gen(function* () {
4663
- const handle = yield* fs.open(fileName, { flag: "w" });
4664
- const withFile = framed.pipe(Stream.tap((line) => handle.writeAll(encodeUtf8(line)).pipe(Effect.flatMap(() => handle.sync))));
4665
- yield* Stream.run(withFile, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
4666
- })).pipe(Effect.orDie);
4667
- });
4668
- Layer.effect(RunEventStreamPort, Effect.gen(function* () {
4669
- const stdio = yield* Stdio.Stdio;
4670
- const fs = yield* FileSystem.FileSystem;
4671
- const path = yield* Path.Path;
4672
- const fileNameRef = yield* Ref.make(DEFAULT_PROGRESS_STREAM_FILE);
4673
- const drainFramed = (framed) => Effect.gen(function* () {
4674
- const fileName = yield* Ref.get(fileNameRef);
4675
- yield* drainStdoutAndFile(stdio, fileName, framed).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
4676
- });
4677
- return RunEventStreamPort.of({ createRunEventStream: (resolved) => Effect.gen(function* () {
4678
- return {
4679
- ...yield* makeRunEventStream(stdio, resolved, drainFramed),
4680
- setProgressStreamFile: (fileName) => Ref.set(fileNameRef, fileName)
4681
- };
4682
- }) });
4683
- }));
4684
- //#endregion
4685
- //#region src/run-host.ts
4686
- const hostRunLayer = Layer.unwrap(Effect.map(Effect.all([RunEnvironment, RunEvents], { concurrency: 1 }), ([env, events]) => makeRunLayer(env, events))).pipe(Layer.provideMerge(nodePlatformLayer));
4687
- const hostMutationTest = Cell.provide(mutationTestCell, hostRunLayer);
4688
- const onHost = (host, effect) => effect.pipe(Effect.provide(hostRunLayer), Effect.provideService(RunEnvironment, host.env), Effect.provideService(RunEvents, host.events));
4689
- const runOnHost = (host, command) => onHost(host, Effect.scoped(hostMutationTest.run(command)));
4690
- const prepareCommandOf = (options, targetMutatePatterns) => ({
4691
- cliOptions: options,
4692
- targetMutatePatterns
4693
- });
4694
- const hostOptionsOf = (mode, stream, noColor) => Effect.gen(function* () {
4695
- const fs = yield* FileSystem.FileSystem;
4696
- return {
4697
- runId: stream.runId,
4698
- resolvedMode: mode,
4699
- runStartedAt: stream.startedAt,
4700
- basePath: yield* fs.realPath("."),
4701
- builtinReporters: { html: makeHtmlReporter },
4702
- allowConsoleColors: isColorEnabled(mode, noColor)
4703
- };
4704
- });
4705
- //#endregion
4706
- //#region src/index.ts
4707
- const HEADLESS_MODE = {
4708
- mode: "machine",
4709
- signal: "flag",
4710
- stdoutIsTTY: false
4711
- };
4712
- const strykerCell = (options, targetMutatePatterns) => Effect.gen(function* () {
4713
- const stdio = yield* Stdio.Stdio;
4714
- const stream = yield* makeRunEventStream(stdio, HEADLESS_MODE);
4715
- const env = yield* hostOptionsOf(HEADLESS_MODE, stream, void 0);
4716
- let patterns;
4717
- if (targetMutatePatterns !== void 0) patterns = [...targetMutatePatterns];
4718
- return yield* runOnHost({
4719
- env,
4720
- events: stream.queue
4721
- }, prepareCommandOf(options, patterns));
4722
- });
4723
- //#endregion
4724
- export { RunMutantTested as A, SUPPORTED_CONFIG_FILE_NAMES as B, RunEvents as C, PhaseEntered as D, HelpRendered as E, ExitClass$1 as F, isModuleSpecifier as G, deepFreeze as H, highestExitClass as I, isWarningEnabled as K, resolveExitCode as L, RunStarted as M, VerdictReached as N, PlanKnown as O, EXIT_CODE as P, verdictExitClass as R, toRelativeNormalizedFileName as S, Heartbeat as T, defaultOptions as U, createDefaultOptions as V, findUnserializables as W, IncrementalReportSchema as _, shouldKeepTempDir as a, reporterWorkerFactory as b, buildVerdictEnvelope as c, strykerVersion as d, calculateMetrics as f, matchesFile as g, createFileMatcher as h, mutationTestCell as i, RunPhase as j, RunFailed as k, generateRunId as l, checkGroupedPlans as m, RUN_EVENTS_QUEUE_BOUND as n, ACTIONABLE_STATUSES as o, countMutants as p, optionsPath as q, makeRunLayer as r, VERDICT_ENVELOPE_SCHEMA_VERSION as s, strykerCell as t, isActionableStatus as u, RunEnvironment as v, RunIdentity as w, spawnReporterWorker as x, REPORTER_EVENT_BATCH_BOUND as y, CONFIG_SYNTAX_HELP as z };