@systemfsoftware/stryker-js-mutation-run 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +212 -0
- package/README.md +38 -0
- package/dist/checker-C4tGiXtZ.mjs +588 -0
- package/dist/checker-resource-DTYEKXdg.d.mts +108 -0
- package/dist/checker-worker.d.mts +16 -0
- package/dist/checker-worker.mjs +2 -0
- package/dist/child-process-proxy-worker-lx1M5Mz3.mjs +567 -0
- package/dist/child-process-proxy-worker-main.d.mts +1 -0
- package/dist/child-process-proxy-worker-main.mjs +6 -0
- package/dist/child-process-test-runner-worker-BTWRYb8V.mjs +50 -0
- package/dist/child-process-test-runner-worker.d.mts +16 -0
- package/dist/child-process-test-runner-worker.mjs +2 -0
- package/dist/config/base.d.mts +21 -0
- package/dist/config/base.mjs +47 -0
- package/dist/config/config-resolution.d.mts +80 -0
- package/dist/config/config-resolution.mjs +2 -0
- package/dist/config/fork-schema.d.mts +5 -0
- package/dist/config/fork-schema.mjs +2 -0
- package/dist/errors-BkjxkY_S.d.mts +6 -0
- package/dist/errors.d.mts +2 -0
- package/dist/errors.mjs +10 -0
- package/dist/exit-classification.d.mts +28 -0
- package/dist/exit-classification.mjs +39 -0
- package/dist/fork-schema-BHuTbRBR.mjs +62 -0
- package/dist/incremental-differ-DDVwtXBn.d.mts +72 -0
- package/dist/incremental-differ-DY8AA09Q.mjs +579 -0
- package/dist/index-CyOSz6LC.d.mts +104 -0
- package/dist/index.d.mts +87 -0
- package/dist/index.mjs +1885 -0
- package/dist/mutants/incremental-differ.d.mts +2 -0
- package/dist/mutants/incremental-differ.mjs +2 -0
- package/dist/output-mode-CfGHDpi5.d.mts +17 -0
- package/dist/output-mode.d.mts +2 -0
- package/dist/output-mode.mjs +1 -0
- package/dist/plugins-Nx86IJQW.mjs +1618 -0
- package/dist/plugins.d.mts +2 -0
- package/dist/plugins.mjs +2 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/run-event-CzRz9Gtk.d.mts +121 -0
- package/dist/run-event.d.mts +2 -0
- package/dist/run-event.mjs +1 -0
- package/dist/stryker-package-CTcsIZ4S.mjs +23 -0
- package/dist/stryker-package.d.mts +7 -0
- package/dist/stryker-package.mjs +2 -0
- package/dist/timer-BMrE1SQ3.d.mts +20 -0
- package/dist/timer.d.mts +2 -0
- package/dist/timer.mjs +48 -0
- package/dist/verdict-envelope-BWc61mHm.mjs +229 -0
- package/dist/verdict-envelope-CKLZPjxC.d.mts +94 -0
- package/dist/verdict-envelope.d.mts +2 -0
- package/dist/verdict-envelope.mjs +2 -0
- package/package.json +102 -0
- package/schema/stryker-schema.json +903 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { F as pluginModulePaths, O as loggingServerAddress, P as pluginCreator, Q as testRunnerFactory, Z as testRunnerConcurrencyTokens, h as checkerFactory, it as workerIdGenerator, m as checkerConcurrencyTokens, s as kill } from "./plugins-Nx86IJQW.mjs";
|
|
2
|
+
import { i as WorkerMessageSchema, n as ProtocolEncodeError, r as ParentMessageSchema } from "./child-process-proxy-worker-lx1M5Mz3.mjs";
|
|
3
|
+
import "@systemfsoftware/stryker-js-plugin-api/core";
|
|
4
|
+
import { PluginKind, commonTokens, tokens } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
5
|
+
import { tokens as tokens$1 } from "typed-inject";
|
|
6
|
+
import { ExpirableTask, StrykerError, Task, isErrnoException, notEmpty } from "@stryker-mutator/util";
|
|
7
|
+
import { URL, fileURLToPath } from "url";
|
|
8
|
+
import "@systemfsoftware/stryker-js-plugin-api/logging";
|
|
9
|
+
import os, { EOL } from "os";
|
|
10
|
+
import * as S from "effect/Schema";
|
|
11
|
+
import { BehaviorSubject, ReplaySubject, Subject, filter, ignoreElements, lastValueFrom, mergeMap, range, takeUntil, tap, zip } from "rxjs";
|
|
12
|
+
import childProcess from "child_process";
|
|
13
|
+
import "@systemfsoftware/stryker-js-plugin-api/test-runner";
|
|
14
|
+
import "@systemfsoftware/stryker-js-plugin-api/check";
|
|
15
|
+
import * as Exit from "effect/Exit";
|
|
16
|
+
//#region src/worker-pool/concurrency-token-provider.ts
|
|
17
|
+
var ConcurrencyTokenProvider = class {
|
|
18
|
+
log;
|
|
19
|
+
concurrencyCheckers;
|
|
20
|
+
concurrencyTestRunners;
|
|
21
|
+
testRunnerTokenSubject = new ReplaySubject();
|
|
22
|
+
get testRunnerToken$() {
|
|
23
|
+
return this.testRunnerTokenSubject;
|
|
24
|
+
}
|
|
25
|
+
checkerToken$;
|
|
26
|
+
static inject = tokens$1(commonTokens.options, commonTokens.logger);
|
|
27
|
+
constructor(options, log) {
|
|
28
|
+
this.log = log;
|
|
29
|
+
const availableParallelism = os.availableParallelism();
|
|
30
|
+
const concurrency = this.computeConcurrency(options.concurrency, availableParallelism);
|
|
31
|
+
if (options.checkers.length > 0) {
|
|
32
|
+
this.concurrencyCheckers = Math.max(Math.ceil(concurrency / 2), 1);
|
|
33
|
+
this.checkerToken$ = range(this.concurrencyCheckers);
|
|
34
|
+
this.concurrencyTestRunners = Math.max(Math.floor(concurrency / 2), 1);
|
|
35
|
+
log.info("Creating %s checker process(es) and %s test runner process(es).", this.concurrencyCheckers, this.concurrencyTestRunners);
|
|
36
|
+
} else {
|
|
37
|
+
this.concurrencyCheckers = 0;
|
|
38
|
+
this.checkerToken$ = range(1);
|
|
39
|
+
this.concurrencyTestRunners = concurrency;
|
|
40
|
+
log.info("Creating %s test runner process(es).", this.concurrencyTestRunners);
|
|
41
|
+
}
|
|
42
|
+
Array.from({ length: this.concurrencyTestRunners }).forEach(() => this.testRunnerTokenSubject.next(this.tick()));
|
|
43
|
+
}
|
|
44
|
+
computeConcurrency(concurrencyOption, availableParallelism) {
|
|
45
|
+
if (typeof concurrencyOption === "string") {
|
|
46
|
+
const percentageMatch = concurrencyOption.match(/^(100|[1-9]?[0-9])%$/);
|
|
47
|
+
if (percentageMatch && percentageMatch[1] !== void 0) {
|
|
48
|
+
const percentage = parseInt(percentageMatch[1], 10);
|
|
49
|
+
const computed = Math.max(1, Math.round(availableParallelism * percentage / 100));
|
|
50
|
+
this.log.debug("Computed concurrency %s from \"%s\" based on %s available parallelism.", computed, concurrencyOption, availableParallelism);
|
|
51
|
+
return computed;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (typeof concurrencyOption === "number") return concurrencyOption;
|
|
55
|
+
return availableParallelism > 4 ? availableParallelism - 1 : availableParallelism;
|
|
56
|
+
}
|
|
57
|
+
freeCheckers() {
|
|
58
|
+
if (this.concurrencyCheckers > 0) {
|
|
59
|
+
this.log.debug("Checking done, creating %s additional test runner process(es)", this.concurrencyCheckers);
|
|
60
|
+
for (let i = 0; i < this.concurrencyCheckers; i++) this.testRunnerTokenSubject.next(this.tick());
|
|
61
|
+
this.testRunnerTokenSubject.complete();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
count = 0;
|
|
65
|
+
tick() {
|
|
66
|
+
return this.count++;
|
|
67
|
+
}
|
|
68
|
+
dispose() {
|
|
69
|
+
this.testRunnerTokenSubject.complete();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/worker-pool/pool.ts
|
|
74
|
+
const MAX_CONCURRENT_INIT = 2;
|
|
75
|
+
createTestRunnerPool.inject = tokens$1(testRunnerFactory, testRunnerConcurrencyTokens);
|
|
76
|
+
function createTestRunnerPool(factory, concurrencyToken$) {
|
|
77
|
+
return new Pool(factory, concurrencyToken$);
|
|
78
|
+
}
|
|
79
|
+
createCheckerPool.inject = tokens$1(checkerFactory, checkerConcurrencyTokens);
|
|
80
|
+
function createCheckerPool(factory, concurrencyToken$) {
|
|
81
|
+
return new Pool(factory, concurrencyToken$);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Represents a work item: an input with a task and with a `result$` observable where the result (exactly one) will be streamed to.
|
|
85
|
+
*/
|
|
86
|
+
var WorkItem = class {
|
|
87
|
+
input;
|
|
88
|
+
task;
|
|
89
|
+
resultSubject = new Subject();
|
|
90
|
+
result$ = this.resultSubject.asObservable();
|
|
91
|
+
/**
|
|
92
|
+
* @param input The input to the ask
|
|
93
|
+
* @param task The task, where a resource and input is presented
|
|
94
|
+
*/
|
|
95
|
+
constructor(input, task) {
|
|
96
|
+
this.input = input;
|
|
97
|
+
this.task = task;
|
|
98
|
+
}
|
|
99
|
+
async execute(resource) {
|
|
100
|
+
try {
|
|
101
|
+
const output = await this.task(resource, this.input);
|
|
102
|
+
this.resultSubject.next(output);
|
|
103
|
+
this.resultSubject.complete();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.resultSubject.error(err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
reject(error) {
|
|
109
|
+
this.resultSubject.error(error);
|
|
110
|
+
}
|
|
111
|
+
complete() {
|
|
112
|
+
this.resultSubject.complete();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Represents a pool of resources. Use `schedule` to schedule work to be executed on the resources.
|
|
117
|
+
* The pool will automatically recycle the resources, but will make sure only one task is executed
|
|
118
|
+
* on one resource at any one time. Creates as many resources as the concurrency tokens allow.
|
|
119
|
+
* Also takes care of the initialing of the resources (with `init()`)
|
|
120
|
+
*/
|
|
121
|
+
var Pool = class {
|
|
122
|
+
initSubject = new ReplaySubject();
|
|
123
|
+
disposedSubject = new BehaviorSubject(false);
|
|
124
|
+
dispose$ = this.disposedSubject.pipe(filter((isDisposed) => isDisposed));
|
|
125
|
+
createdResources = [];
|
|
126
|
+
todoSubject = new ReplaySubject();
|
|
127
|
+
constructor(factory, concurrencyToken$) {
|
|
128
|
+
const resourcesSubject = new Subject();
|
|
129
|
+
zip(resourcesSubject, this.todoSubject).pipe(mergeMap(async ([resource, workItem]) => {
|
|
130
|
+
await workItem.execute(resource);
|
|
131
|
+
resourcesSubject.next(resource);
|
|
132
|
+
}), ignoreElements(), takeUntil(this.dispose$)).subscribe({ error: (error) => {
|
|
133
|
+
this.todoSubject.subscribe((workItem) => workItem.reject(error));
|
|
134
|
+
} });
|
|
135
|
+
concurrencyToken$.pipe(takeUntil(this.dispose$), mergeMap(async () => {
|
|
136
|
+
if (this.disposedSubject.value) return;
|
|
137
|
+
const resource = factory();
|
|
138
|
+
this.createdResources.push(resource);
|
|
139
|
+
await resource.init?.();
|
|
140
|
+
return resource;
|
|
141
|
+
}, MAX_CONCURRENT_INIT), filter(notEmpty), tap({
|
|
142
|
+
complete: () => {
|
|
143
|
+
this.initSubject.next();
|
|
144
|
+
this.initSubject.complete();
|
|
145
|
+
},
|
|
146
|
+
error: (err) => {
|
|
147
|
+
this.initSubject.error(err);
|
|
148
|
+
}
|
|
149
|
+
})).subscribe({
|
|
150
|
+
next: (resource) => resourcesSubject.next(resource),
|
|
151
|
+
error: (err) => resourcesSubject.error(err)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns a promise that resolves if all concurrency tokens have resulted in initialized resources.
|
|
156
|
+
* This is optional, resources will get initialized either way.
|
|
157
|
+
*/
|
|
158
|
+
async init() {
|
|
159
|
+
await lastValueFrom(this.initSubject);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Schedules a task to be executed on resources in the pool. Each input is paired with a resource, which allows async work to be done.
|
|
163
|
+
* @param input$ The inputs to pair up with a resource.
|
|
164
|
+
* @param task The task to execute on each resource
|
|
165
|
+
*/
|
|
166
|
+
schedule(input$, task) {
|
|
167
|
+
return input$.pipe(mergeMap((input) => {
|
|
168
|
+
const workItem = new WorkItem(input, task);
|
|
169
|
+
this.todoSubject.next(workItem);
|
|
170
|
+
return workItem.result$;
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Dispose the pool
|
|
175
|
+
*/
|
|
176
|
+
async dispose() {
|
|
177
|
+
if (!this.disposedSubject.value) {
|
|
178
|
+
this.disposedSubject.next(true);
|
|
179
|
+
this.todoSubject.subscribe((workItem) => workItem.complete());
|
|
180
|
+
this.todoSubject.complete();
|
|
181
|
+
await Promise.all(this.createdResources.map((resource) => resource.dispose?.()));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/worker-pool/resource-decorator.ts
|
|
187
|
+
var ResourceDecorator = class {
|
|
188
|
+
producer;
|
|
189
|
+
innerResource;
|
|
190
|
+
constructor(producer) {
|
|
191
|
+
this.producer = producer;
|
|
192
|
+
this.innerResource = producer();
|
|
193
|
+
}
|
|
194
|
+
async init() {
|
|
195
|
+
await this.innerResource.init?.();
|
|
196
|
+
}
|
|
197
|
+
async dispose() {
|
|
198
|
+
await this.innerResource.dispose?.();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Disposes the current test runner and creates a new one
|
|
202
|
+
* To be used in decorators that need recreation.
|
|
203
|
+
*/
|
|
204
|
+
async recover() {
|
|
205
|
+
await this.dispose();
|
|
206
|
+
this.innerResource = this.producer();
|
|
207
|
+
return this.init();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/checker/checker-facade.ts
|
|
212
|
+
function toMap(mutantRunPlans) {
|
|
213
|
+
return new Map(mutantRunPlans.map((mutant) => [mutant.mutant.id, mutant]));
|
|
214
|
+
}
|
|
215
|
+
var CheckerFacade = class extends ResourceDecorator {
|
|
216
|
+
async check(checkerName, mutantRunPlans) {
|
|
217
|
+
const innerCheckerResult = Object.entries(await this.innerResource.check(checkerName, mutantRunPlans.map((mr) => mr.mutant)));
|
|
218
|
+
const mutantRunPlanMap = toMap(mutantRunPlans);
|
|
219
|
+
const results = innerCheckerResult.map(([id, res]) => {
|
|
220
|
+
const mutantRunPlan = mutantRunPlanMap.get(id);
|
|
221
|
+
if (!mutantRunPlan) throw new Error(`Checker "${checkerName}" returned a check result for mutant id "${id}", but a check wasn't requested for it. Stryker asked to check mutant ids: ${mutantRunPlans.map(({ mutant }) => mutant.id).join(",")}`);
|
|
222
|
+
return [mutantRunPlan, res];
|
|
223
|
+
});
|
|
224
|
+
if (mutantRunPlans.length > results.length) {
|
|
225
|
+
const resultIds = new Set(results.map(([{ mutant }]) => mutant.id));
|
|
226
|
+
const missingIds = mutantRunPlans.map(({ mutant }) => mutant.id).filter((id) => !resultIds.has(id));
|
|
227
|
+
throw new Error(`Checker "${checkerName}" was missing check results for mutant ids "${missingIds.join(",")}", while Stryker asked to check them`);
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
async group(checkerName, mutantRunPlans) {
|
|
232
|
+
const mutantIdGroups = await this.innerResource.group(checkerName, mutantRunPlans.map((mr) => mr.mutant));
|
|
233
|
+
const mutantRunPlanMap = toMap(mutantRunPlans);
|
|
234
|
+
const groupedMutantIds = /* @__PURE__ */ new Set();
|
|
235
|
+
const groups = mutantIdGroups.map((group) => group.map((id) => {
|
|
236
|
+
const mutantRunPlan = mutantRunPlanMap.get(id);
|
|
237
|
+
groupedMutantIds.add(id);
|
|
238
|
+
if (!mutantRunPlan) throw new Error(`Checker "${checkerName}" returned a group result for mutant id "${id}", but a group wasn't requested for it. Stryker asked to group mutant ids: ${mutantRunPlans.map(({ mutant }) => mutant.id).join(",")}!`);
|
|
239
|
+
return mutantRunPlan;
|
|
240
|
+
}));
|
|
241
|
+
if (mutantRunPlans.length > groupedMutantIds.size) {
|
|
242
|
+
const missingIds = mutantRunPlans.map(({ mutant }) => mutant.id).filter((id) => !groupedMutantIds.has(id));
|
|
243
|
+
throw new Error(`Checker "${checkerName}" was missing group results for mutant ids "${missingIds.join(",")}", while Stryker asked to group them!`);
|
|
244
|
+
}
|
|
245
|
+
return groups;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/worker-pool/child-process-crashed-error.ts
|
|
250
|
+
var ChildProcessCrashedError = class ChildProcessCrashedError extends StrykerError {
|
|
251
|
+
pid;
|
|
252
|
+
exitCode;
|
|
253
|
+
signal;
|
|
254
|
+
constructor(pid, message, exitCode, signal, innerError) {
|
|
255
|
+
super(message, innerError);
|
|
256
|
+
this.pid = pid;
|
|
257
|
+
this.exitCode = exitCode;
|
|
258
|
+
this.signal = signal;
|
|
259
|
+
Error.captureStackTrace(this, ChildProcessCrashedError);
|
|
260
|
+
Object.setPrototypeOf(this, ChildProcessCrashedError.prototype);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/worker-pool/out-of-memory-error.ts
|
|
265
|
+
var OutOfMemoryError = class OutOfMemoryError extends ChildProcessCrashedError {
|
|
266
|
+
constructor(pid, exitCode) {
|
|
267
|
+
super(pid, `Process ${pid} ran out of memory`, exitCode);
|
|
268
|
+
this.message = "Process ";
|
|
269
|
+
Error.captureStackTrace(this, OutOfMemoryError);
|
|
270
|
+
Object.setPrototypeOf(this, OutOfMemoryError.prototype);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/worker-pool/string-builder.ts
|
|
275
|
+
const DEFAULT_MAX_SIZE = 8192;
|
|
276
|
+
var StringBuilder = class {
|
|
277
|
+
currentLength = 0;
|
|
278
|
+
strings = [];
|
|
279
|
+
maxSize = DEFAULT_MAX_SIZE;
|
|
280
|
+
append(str) {
|
|
281
|
+
this.strings.push(str);
|
|
282
|
+
this.currentLength += str.length;
|
|
283
|
+
while (this.currentLength > this.maxSize && this.strings.length > 1) {
|
|
284
|
+
const shifted = this.strings.shift();
|
|
285
|
+
if (shifted !== void 0) this.currentLength -= shifted.length;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
toString() {
|
|
289
|
+
return this.strings.join("");
|
|
290
|
+
}
|
|
291
|
+
static concat(...builders) {
|
|
292
|
+
return builders.map((b) => b.toString()).filter(Boolean).join(EOL);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/worker-pool/string-utils.ts
|
|
297
|
+
function padLeft(input) {
|
|
298
|
+
return input.split("\n").map((str) => " " + str).join("\n");
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/worker-pool/child-process-proxy.ts
|
|
302
|
+
/**
|
|
303
|
+
* This half's two directions, built once from the declared schemas. They take
|
|
304
|
+
* `unknown` and return an `Exit`, which is what makes them the *only* codec this
|
|
305
|
+
* half needs: the envelope performs the narrowing, so a separate `S.Json` check
|
|
306
|
+
* on `args` would guard the same bytes twice, and a decode failure on a process
|
|
307
|
+
* boundary is an expected outcome this code answers for rather than an exception
|
|
308
|
+
* that leaves the type unmarked.
|
|
309
|
+
*/
|
|
310
|
+
const encodeWorkerMessage = S.encodeUnknownExit(S.fromJsonString(WorkerMessageSchema));
|
|
311
|
+
const decodeParentMessage = S.decodeUnknownExit(S.fromJsonString(ParentMessageSchema));
|
|
312
|
+
const BROKEN_PIPE_ERROR_CODE = "EPIPE";
|
|
313
|
+
const IPC_CHANNEL_CLOSED_ERROR_CODE = "ERR_IPC_CHANNEL_CLOSED";
|
|
314
|
+
const TIMEOUT_FOR_DISPOSE = 2e3;
|
|
315
|
+
var ChildProcessProxy = class ChildProcessProxy {
|
|
316
|
+
proxy;
|
|
317
|
+
worker;
|
|
318
|
+
initTask;
|
|
319
|
+
disposeTask;
|
|
320
|
+
fatalError;
|
|
321
|
+
workerTasks = /* @__PURE__ */ new Map();
|
|
322
|
+
workerTaskCounter = 0;
|
|
323
|
+
log;
|
|
324
|
+
stdoutBuilder = new StringBuilder();
|
|
325
|
+
stderrBuilder = new StringBuilder();
|
|
326
|
+
isDisposed = false;
|
|
327
|
+
initMessage;
|
|
328
|
+
constructor(modulePath, namedExport, loggingServerAddress, options, fileDescriptions, pluginModulePaths, workingDirectory, logger, execArgv, idGenerator) {
|
|
329
|
+
const workerId = idGenerator.next().toString();
|
|
330
|
+
this.worker = childProcess.fork(fileURLToPath(new URL("./child-process-proxy-worker-main.mjs", import.meta.url)), {
|
|
331
|
+
silent: true,
|
|
332
|
+
execArgv,
|
|
333
|
+
env: {
|
|
334
|
+
STRYKER_MUTATOR_WORKER: workerId,
|
|
335
|
+
...process.env
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
this.initTask = new Task();
|
|
339
|
+
this.log = logger;
|
|
340
|
+
this.log.debug("Started %s in worker process %s with pid %s %s", namedExport, workerId, this.worker.pid, execArgv.length ? ` (using args ${execArgv.join(" ")})` : "");
|
|
341
|
+
this.worker.on("close", this.handleUnexpectedExit);
|
|
342
|
+
this.worker.on("error", this.handleError);
|
|
343
|
+
this.initMessage = {
|
|
344
|
+
kind: 0,
|
|
345
|
+
loggingServerAddress,
|
|
346
|
+
options,
|
|
347
|
+
fileDescriptions,
|
|
348
|
+
pluginModulePaths,
|
|
349
|
+
namedExport,
|
|
350
|
+
modulePath,
|
|
351
|
+
workingDirectory
|
|
352
|
+
};
|
|
353
|
+
this.listenForMessages();
|
|
354
|
+
this.listenToStdoutAndStderr();
|
|
355
|
+
this.proxy = this.initProxy();
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* @description Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process
|
|
359
|
+
*/
|
|
360
|
+
static create(modulePath, loggingServerAddress, options, fileDescriptions, pluginModulePaths, workingDirectory, injectableClass, execArgv, getLogger, idGenerator) {
|
|
361
|
+
return new ChildProcessProxy(modulePath, injectableClass.name, loggingServerAddress, options, fileDescriptions, pluginModulePaths, workingDirectory, getLogger(ChildProcessProxy.name), [...execArgv], idGenerator);
|
|
362
|
+
}
|
|
363
|
+
send(message) {
|
|
364
|
+
const encoded = encodeWorkerMessage(message);
|
|
365
|
+
if (Exit.isFailure(encoded)) throw new ProtocolEncodeError({ reason: String(encoded.cause) });
|
|
366
|
+
this.worker.send(encoded.value);
|
|
367
|
+
}
|
|
368
|
+
initProxy() {
|
|
369
|
+
return S.decodeUnknownSync(S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The runtime promisified facade of a child-process worker" }))(new Proxy({}, { get: (_, propertyKey) => {
|
|
370
|
+
if (typeof propertyKey === "string") return this.forward(propertyKey);
|
|
371
|
+
else return;
|
|
372
|
+
} }));
|
|
373
|
+
}
|
|
374
|
+
forward(methodName) {
|
|
375
|
+
return async (...args) => {
|
|
376
|
+
if (this.fatalError) return Promise.reject(this.fatalError);
|
|
377
|
+
else {
|
|
378
|
+
const workerTask = new Task();
|
|
379
|
+
const correlationId = this.workerTaskCounter++;
|
|
380
|
+
this.workerTasks.set(correlationId, workerTask);
|
|
381
|
+
this.initTask.promise.then(() => {
|
|
382
|
+
this.send({
|
|
383
|
+
args,
|
|
384
|
+
correlationId,
|
|
385
|
+
kind: 1,
|
|
386
|
+
methodName
|
|
387
|
+
});
|
|
388
|
+
}).catch((error) => {
|
|
389
|
+
workerTask.reject(error);
|
|
390
|
+
});
|
|
391
|
+
return workerTask.promise;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
listenForMessages() {
|
|
396
|
+
this.worker.on("message", (serializedMessage) => {
|
|
397
|
+
const decoded = decodeParentMessage(serializedMessage);
|
|
398
|
+
if (Exit.isFailure(decoded)) {
|
|
399
|
+
this.fatalError = new ChildProcessCrashedError(this.worker.pid, String(decoded.cause));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const message = decoded.value;
|
|
403
|
+
switch (message.kind) {
|
|
404
|
+
case 0:
|
|
405
|
+
this.send(this.initMessage);
|
|
406
|
+
break;
|
|
407
|
+
case 1:
|
|
408
|
+
this.initTask.resolve(void 0);
|
|
409
|
+
break;
|
|
410
|
+
case 3: {
|
|
411
|
+
const task = this.workerTasks.get(message.correlationId);
|
|
412
|
+
if (task !== void 0) {
|
|
413
|
+
task.resolve(message.result);
|
|
414
|
+
this.workerTasks.delete(message.correlationId);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 4: {
|
|
419
|
+
const task = this.workerTasks.get(message.correlationId);
|
|
420
|
+
if (task !== void 0) {
|
|
421
|
+
task.reject(new StrykerError(message.error));
|
|
422
|
+
this.workerTasks.delete(message.correlationId);
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case 5:
|
|
427
|
+
if (this.disposeTask) this.disposeTask.resolve(void 0);
|
|
428
|
+
break;
|
|
429
|
+
case 2:
|
|
430
|
+
this.fatalError = new StrykerError(message.error);
|
|
431
|
+
this.reportError(this.fatalError);
|
|
432
|
+
this.dispose();
|
|
433
|
+
break;
|
|
434
|
+
default: this.logUnidentifiedMessage(message);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
listenToStdoutAndStderr() {
|
|
439
|
+
const handleData = (builder) => (data) => {
|
|
440
|
+
const output = data.toString();
|
|
441
|
+
builder.append(output);
|
|
442
|
+
this.log.trace(output);
|
|
443
|
+
};
|
|
444
|
+
if (this.worker.stdout) this.worker.stdout.on("data", handleData(this.stdoutBuilder));
|
|
445
|
+
if (this.worker.stderr) this.worker.stderr.on("data", handleData(this.stderrBuilder));
|
|
446
|
+
}
|
|
447
|
+
get stdout() {
|
|
448
|
+
return this.stdoutBuilder.toString();
|
|
449
|
+
}
|
|
450
|
+
get stderr() {
|
|
451
|
+
return this.stderrBuilder.toString();
|
|
452
|
+
}
|
|
453
|
+
reportError(error) {
|
|
454
|
+
const onGoingWorkerTasks = [...this.workerTasks.values()].filter((task) => !task.isCompleted);
|
|
455
|
+
if (!this.initTask.isCompleted) onGoingWorkerTasks.push(this.initTask);
|
|
456
|
+
if (onGoingWorkerTasks.length) onGoingWorkerTasks.forEach((task) => task.reject(error));
|
|
457
|
+
}
|
|
458
|
+
handleUnexpectedExit = (code, signal) => {
|
|
459
|
+
this.isDisposed = true;
|
|
460
|
+
const output = StringBuilder.concat(this.stderrBuilder, this.stdoutBuilder);
|
|
461
|
+
if (processOutOfMemory()) {
|
|
462
|
+
const oom = new OutOfMemoryError(this.worker.pid, code);
|
|
463
|
+
this.fatalError = oom;
|
|
464
|
+
this.log.warn(`Child process [pid ${oom.pid}] ran out of memory. Stdout and stderr are logged on debug level.`);
|
|
465
|
+
this.log.debug(stdoutAndStderr());
|
|
466
|
+
} else {
|
|
467
|
+
this.fatalError = new ChildProcessCrashedError(this.worker.pid, `Child process [pid ${this.worker.pid}] exited unexpectedly with exit code ${code} (${signal || "without signal"}). ${stdoutAndStderr()}`, code, signal);
|
|
468
|
+
this.log.warn(this.fatalError.message, this.fatalError);
|
|
469
|
+
}
|
|
470
|
+
this.reportError(this.fatalError);
|
|
471
|
+
function processOutOfMemory() {
|
|
472
|
+
return output.includes("JavaScript heap out of memory") || output.includes("FatalProcessOutOfMemory");
|
|
473
|
+
}
|
|
474
|
+
function stdoutAndStderr() {
|
|
475
|
+
if (output.length) return `Last part of stdout and stderr was:${os.EOL}${padLeft(output)}`;
|
|
476
|
+
else return "Stdout and stderr were empty.";
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
handleError = (error) => {
|
|
480
|
+
if (this.innerProcessIsCrashed(error)) {
|
|
481
|
+
this.log.warn(`Child process [pid ${this.worker.pid}] has crashed. See other warning messages for more info.`, error);
|
|
482
|
+
this.reportError(new ChildProcessCrashedError(this.worker.pid, `Child process [pid ${this.worker.pid}] has crashed`, void 0, void 0, error));
|
|
483
|
+
} else this.reportError(error);
|
|
484
|
+
};
|
|
485
|
+
innerProcessIsCrashed(error) {
|
|
486
|
+
return isErrnoException(error) && (error.code === BROKEN_PIPE_ERROR_CODE || error.code === IPC_CHANNEL_CLOSED_ERROR_CODE);
|
|
487
|
+
}
|
|
488
|
+
async dispose() {
|
|
489
|
+
if (!this.isDisposed) {
|
|
490
|
+
this.worker.removeListener("close", this.handleUnexpectedExit);
|
|
491
|
+
this.isDisposed = true;
|
|
492
|
+
this.log.debug("Disposing of worker process %s", this.worker.pid);
|
|
493
|
+
this.disposeTask = new ExpirableTask(TIMEOUT_FOR_DISPOSE);
|
|
494
|
+
this.send({ kind: 2 });
|
|
495
|
+
try {
|
|
496
|
+
await this.disposeTask.promise;
|
|
497
|
+
} finally {
|
|
498
|
+
this.log.debug("Kill %s", this.worker.pid);
|
|
499
|
+
await kill(this.worker.pid);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
logUnidentifiedMessage(message) {
|
|
504
|
+
this.log.error(`Received unidentified message ${message}`);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/checker/checker-worker.ts
|
|
509
|
+
var CheckerWorker = class {
|
|
510
|
+
innerCheckers;
|
|
511
|
+
static inject = tokens(commonTokens.options, pluginCreator);
|
|
512
|
+
constructor(options, pluginCreator) {
|
|
513
|
+
this.innerCheckers = new Map(options.checkers.map((name) => [name, pluginCreator.create(PluginKind.Checker, name)]));
|
|
514
|
+
}
|
|
515
|
+
async init() {
|
|
516
|
+
for (const [name, checker] of this.innerCheckers.entries()) try {
|
|
517
|
+
await checker.init();
|
|
518
|
+
} catch (error) {
|
|
519
|
+
throw new StrykerError(`An error occurred during initialization of the "${name}" checker`, error);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async check(checkerName, mutants) {
|
|
523
|
+
return this.perform(checkerName, (checker) => checker.check(mutants));
|
|
524
|
+
}
|
|
525
|
+
async group(checkerName, mutants) {
|
|
526
|
+
return this.perform(checkerName, (checker) => checker.group?.(mutants) ?? mutants.map(({ id }) => [id]));
|
|
527
|
+
}
|
|
528
|
+
perform(checkerName, act) {
|
|
529
|
+
const checker = this.innerCheckers.get(checkerName);
|
|
530
|
+
if (checker) return act(checker);
|
|
531
|
+
else throw new Error(`Checker ${checkerName} does not exist!`);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/checker/checker-child-process-proxy.ts
|
|
536
|
+
var CheckerChildProcessProxy = class {
|
|
537
|
+
childProcess;
|
|
538
|
+
constructor(options, fileDescriptions, pluginModulePaths, loggingServerAddress, getLogger, idGenerator) {
|
|
539
|
+
this.childProcess = ChildProcessProxy.create(new URL("./checker-worker.mjs", import.meta.url).toString(), loggingServerAddress, options, fileDescriptions, pluginModulePaths, process.cwd(), CheckerWorker, options.checkerNodeArgs, getLogger, idGenerator);
|
|
540
|
+
}
|
|
541
|
+
async dispose() {
|
|
542
|
+
await this.childProcess.dispose();
|
|
543
|
+
}
|
|
544
|
+
async init() {
|
|
545
|
+
await this.childProcess.proxy.init();
|
|
546
|
+
}
|
|
547
|
+
async check(checkerName, mutants) {
|
|
548
|
+
return this.childProcess.proxy.check(checkerName, mutants);
|
|
549
|
+
}
|
|
550
|
+
async group(checkerName, mutants) {
|
|
551
|
+
return this.childProcess.proxy.group(checkerName, mutants);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/checker/checker-retry-decorator.ts
|
|
556
|
+
var CheckerRetryDecorator = class extends ResourceDecorator {
|
|
557
|
+
log;
|
|
558
|
+
constructor(producer, log) {
|
|
559
|
+
super(producer);
|
|
560
|
+
this.log = log;
|
|
561
|
+
}
|
|
562
|
+
async check(checkerName, mutants) {
|
|
563
|
+
return this.tryAction(() => this.innerResource.check(checkerName, mutants));
|
|
564
|
+
}
|
|
565
|
+
async group(checkerName, mutants) {
|
|
566
|
+
return this.tryAction(() => this.innerResource.group(checkerName, mutants));
|
|
567
|
+
}
|
|
568
|
+
async tryAction(act) {
|
|
569
|
+
try {
|
|
570
|
+
return await act();
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error instanceof ChildProcessCrashedError) {
|
|
573
|
+
if (error instanceof OutOfMemoryError) this.log.warn(`Checker process [${error.pid}] ran out of memory. Retrying in a new process.`);
|
|
574
|
+
else this.log.warn(`Checker process [${error.pid}] crashed with exit code ${error.exitCode}. Retrying in a new process.`, error);
|
|
575
|
+
await this.recover();
|
|
576
|
+
return act();
|
|
577
|
+
} else throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/checker/checker-factory.ts
|
|
583
|
+
createCheckerFactory.inject = tokens(commonTokens.options, commonTokens.fileDescriptions, loggingServerAddress, pluginModulePaths, commonTokens.getLogger, workerIdGenerator);
|
|
584
|
+
function createCheckerFactory(options, fileDescriptions, loggingServerAddress, pluginModulePaths, getLogger, idGenerator) {
|
|
585
|
+
return () => new CheckerFacade(() => new CheckerRetryDecorator(() => new CheckerChildProcessProxy(options, fileDescriptions, pluginModulePaths, loggingServerAddress, getLogger, idGenerator), getLogger(CheckerRetryDecorator.name)));
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
export { ChildProcessCrashedError as a, createTestRunnerPool as c, OutOfMemoryError as i, ConcurrencyTokenProvider as l, CheckerWorker as n, ResourceDecorator as o, ChildProcessProxy as r, createCheckerPool as s, createCheckerFactory as t };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "./index-CyOSz6LC.mjs";
|
|
2
|
+
import { FileDescriptions, Mutant, PartialStrykerOptions, StrykerOptions } from "@systemfsoftware/stryker-js-plugin-api/core";
|
|
3
|
+
import "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
4
|
+
import { Disposable, Injector as Injector$1 } from "typed-inject";
|
|
5
|
+
import { Schema } from "effect";
|
|
6
|
+
import { Logger } from "@systemfsoftware/stryker-js-plugin-api/logging";
|
|
7
|
+
import "rxjs";
|
|
8
|
+
import { TestRunner } from "@systemfsoftware/stryker-js-plugin-api/test-runner";
|
|
9
|
+
import { CheckResult } from "@systemfsoftware/stryker-js-plugin-api/check";
|
|
10
|
+
//#region src/logging/log-level.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Re-export of @systemfsoftware/stryker-js-plugin-api/core's LogLevel const enum as a local runtime object.
|
|
13
|
+
* The upstream uses `const enum` which rolldown/oxc doesn't inline at bundle time,
|
|
14
|
+
* and which has no runtime emit.
|
|
15
|
+
*
|
|
16
|
+
* Using a const object + type pattern instead of an enum keeps the type structurally
|
|
17
|
+
* compatible with the upstream's LogLevel (both are the same string literal union),
|
|
18
|
+
* so values from @systemfsoftware/stryker-js-plugin-api/core types are assignable without casting.
|
|
19
|
+
*/
|
|
20
|
+
declare const LogLevel: {
|
|
21
|
+
readonly Trace: 'trace';
|
|
22
|
+
readonly Debug: 'debug';
|
|
23
|
+
readonly Information: 'info';
|
|
24
|
+
readonly Warning: 'warn';
|
|
25
|
+
readonly Error: 'error';
|
|
26
|
+
readonly Fatal: 'fatal';
|
|
27
|
+
readonly Off: 'off';
|
|
28
|
+
};
|
|
29
|
+
type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/logging/logging-event.schema.d.ts
|
|
32
|
+
/**
|
|
33
|
+
* The wire format of a {@link LoggingEvent}; worker processes serialize
|
|
34
|
+
* events over the logging socket, the server deserializes them.
|
|
35
|
+
*/
|
|
36
|
+
declare const SerializedLoggingEventSchema: Schema.Struct<{
|
|
37
|
+
readonly startTime: Schema.String;
|
|
38
|
+
readonly categoryName: Schema.String;
|
|
39
|
+
readonly message: Schema.String;
|
|
40
|
+
readonly level: Schema.Literals<readonly ["trace", "debug", "info", "warn", "error", "fatal", "off"]>;
|
|
41
|
+
readonly pid: Schema.Finite;
|
|
42
|
+
}>;
|
|
43
|
+
type SerializedLoggingEvent = Schema.Schema.Type<typeof SerializedLoggingEventSchema>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/logging/logging-event.d.ts
|
|
46
|
+
declare class LoggingEvent {
|
|
47
|
+
#private;
|
|
48
|
+
readonly startTime: Date;
|
|
49
|
+
readonly categoryName: string;
|
|
50
|
+
readonly data: unknown[];
|
|
51
|
+
readonly level: LogLevel;
|
|
52
|
+
readonly pid: number;
|
|
53
|
+
private constructor();
|
|
54
|
+
static create(categoryName: string, level: LogLevel, data: unknown[]): LoggingEvent;
|
|
55
|
+
format(): string;
|
|
56
|
+
formatColorized(): string;
|
|
57
|
+
static deserialize(ser: SerializedLoggingEvent): LoggingEvent;
|
|
58
|
+
serialize(): SerializedLoggingEvent;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/logging/logging-sink.d.ts
|
|
62
|
+
interface LoggingSink {
|
|
63
|
+
log(event: LoggingEvent): void;
|
|
64
|
+
isEnabled(level: LogLevel): boolean;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/logging/logging-backend.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* The logging backend that handles the actual logging. So to both a file and the stdout, stderr.
|
|
70
|
+
*/
|
|
71
|
+
declare class LoggingBackend implements LoggingSink, Disposable {
|
|
72
|
+
#private;
|
|
73
|
+
activeStdoutLevel: LogLevel;
|
|
74
|
+
activeFileLevel: LogLevel;
|
|
75
|
+
showColors: boolean;
|
|
76
|
+
static readonly inject: readonly ["loggerConsoleOut", "loggerShowColors"];
|
|
77
|
+
constructor(consoleOut: NodeJS.WritableStream, showColors: boolean);
|
|
78
|
+
log(event: LoggingEvent): void;
|
|
79
|
+
isEnabled(level: LogLevel): boolean;
|
|
80
|
+
get activeLogLevel(): LogLevel;
|
|
81
|
+
get priority(): 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
82
|
+
/**
|
|
83
|
+
* `allowConsoleColors` is deliberately not read here (R8). It defaults to
|
|
84
|
+
* `true` in the schema, so honouring it would re-enable colour on every run
|
|
85
|
+
* and defeat the machine-mode contract; colour is decided once, from the
|
|
86
|
+
* resolved mode and `NO_COLOR`, and injected at construction.
|
|
87
|
+
*/
|
|
88
|
+
configure({ logLevel, fileLogLevel }: PartialStrykerOptions): void;
|
|
89
|
+
dispose(): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/logging/logging-server.d.ts
|
|
93
|
+
interface LoggingServerAddress {
|
|
94
|
+
port: number;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/worker-pool/pool.d.ts
|
|
98
|
+
interface Resource extends Partial<Disposable> {
|
|
99
|
+
init?(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/checker/checker-resource.d.ts
|
|
103
|
+
interface CheckerResource extends Resource {
|
|
104
|
+
check(checkerName: string, mutant: Mutant[]): Promise<Record<string, CheckResult>>;
|
|
105
|
+
group(checkerName: string, mutants: Mutant[]): Promise<string[][]>;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { LoggingServerAddress as n, LoggingBackend as r, CheckerResource as t };
|