@systemfsoftware/stryker-js 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +30 -0
- package/dist/Run.schema-DErVOVSj.d.mts +24 -0
- package/dist/Run.schema-DuaneDGd.mjs +57 -0
- package/dist/RunEnvironment.service-C8nNTyKa.d.mts +2927 -0
- package/dist/config.d.mts +4 -4
- package/dist/config.mjs +1 -1
- package/dist/events.d.mts +2 -2
- package/dist/events.mjs +2 -2
- package/dist/{index-C09A80iv.d.mts → index-C5FRZ4Ni.d.mts} +1694 -3074
- package/dist/index.d.mts +622 -101
- package/dist/index.mjs +1104 -26
- package/dist/main.mjs +49090 -30399
- package/dist/merge-config-DCQE_2cC.mjs +1473 -0
- package/dist/{merge-config-X_AxYxMY.d.mts → merge-config-IXgoKiix.d.mts} +4 -4
- package/dist/promises.d.mts +3 -3
- package/dist/promises.mjs +1 -1
- package/dist/run-stages-CGUzIzPZ.mjs +20687 -0
- package/package.json +15 -14
- package/dist/Run.schema-Bj7b_FMA.mjs +0 -269
- package/dist/Run.schema-BlvpjFjJ.d.mts +0 -325
- package/dist/RunEnvironment.service-BJAsZsdn.d.mts +0 -524
- package/dist/dist-BscHZmXN.mjs +0 -8089
- package/dist/merge-config-DA_v1hGx.mjs +0 -63
- package/dist/run-stages.cell-BN__EOCk.mjs +0 -8789
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as
|
|
4
|
-
import {
|
|
2
|
+
import { i as Mutant_exports, l as Report_exports, n as mergeConfigs } from "./merge-config-DCQE_2cC.mjs";
|
|
3
|
+
import { $ as WorkerLauncher, A as classifyWorkerExit, At as OutOfMemoryError, B as SurvivorRef, C as nodePlatformLayer, Ct as MergeResult, D as git_diff_schema_exports, Dt as survivorsPriorReport, E as git_diff_service_exports, Et as forkOptionsSchema, F as readConfig, Ft as CheckerAnsweredUnrequested, G as generateRunId, H as RunEvents, I as RunEnvironment, It as CheckerSkippedRequested, J as metricsResultFromFiles, K as VerdictEnvelope, L as IdGenerator, M as loadConfigCell, Mt as OutputMode_exports, N as describeErrors, Nt as RunEvent_exports$1, O as ClassifyWorkerExitCommand, Ot as checkGroupedPlans, P as forkCoreSchema, Pt as SpanTaxonomy_exports, Q as makeWorkerClient, R as surfacedSurvivorsOf, S as IncrementalReportSchema, St as MergeCommand, T as GitDiff, Tt as extendsPropertySchema, U as RunIdentity, V as StrykerPackage, W as buildVerdictEnvelope, X as reporterWorkerFactory, Y as MetricsResultFromReport, Z as spawnReporterWorker, _ as isCommandRunner, _t as defaultOptions, a as MachineConsole, at as initialExtendsStepState, b as readProjectCell, bt as ExtendsStepStateSchema, c as prepareCell, ct as ExtendsStepRead, d as buildTestRunner, dt as ConfigError, et as make, f as makeChildProcessTestRunner, ft as ConfigFileInvalidError, g as vmTestRunnerConfig, gt as createDefaultOptions, h as vmRunnerPluginUrl, ht as ConfigFileUnsupportedError, i as makeRunEventStream, it as importModule, j as validateOptions, jt as WorkerBootTimeoutError, k as ClassifyWorkerExitDecision, kt as ChildProcessCrashedError, l as mutantDetailEventsOf, lt as ExtendsStepRefused, m as testRunnerConfigOf, mt as ConfigFileUnreadableError, n as RunEventDrain, nt as FileMatcher, o as HtmlReporter_exports, ot as StrykerError, p as isVmRunner, pt as ConfigFileNotFoundError, q as staticVerdictOf, r as RunEventDrainLive, rt as decideExtendsStep, s as mutationTestCell, st as ExtendsStepDone, t as strykerCell, tt as ReproducerSchema, u as instrumentCell, ut as ExtendsStepResolve, v as import_closure_cell_exports, vt as ConfigDocumentSchema, w as layerWorkerProtocol, wt as ReadConfigCommand, x as concurrencyCell, xt as ImportedModuleSchema, y as relativeNormalizedFileName, yt as ExtendsStepDocumentSchema, z as SurfacingFields } from "./run-stages-CGUzIzPZ.mjs";
|
|
4
|
+
import { n as StageError } from "./Run.schema-DuaneDGd.mjs";
|
|
5
|
+
import { Cell, Sandwich, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
6
|
+
import * as S from "effect/Schema";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Context from "effect/Context";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Boolean from "effect/Boolean";
|
|
11
|
+
import * as Arr from "effect/Array";
|
|
12
|
+
import * as Match from "effect/Match";
|
|
13
|
+
import * as Result from "effect/Result";
|
|
14
|
+
import { dual } from "effect/Function";
|
|
15
|
+
import "effect/Cause";
|
|
16
|
+
import * as FileSystem from "effect/FileSystem";
|
|
17
|
+
import * as Path from "effect/Path";
|
|
18
|
+
import * as Stream from "effect/Stream";
|
|
19
|
+
import * as Clock from "effect/Clock";
|
|
20
|
+
import * as Layer from "effect/Layer";
|
|
21
|
+
import * as Queue from "effect/Queue";
|
|
22
|
+
import "effect/Scope";
|
|
23
|
+
import * as SocketServer from "effect/unstable/socket/SocketServer";
|
|
24
|
+
import * as Fiber from "effect/Fiber";
|
|
25
|
+
import * as DateTime from "effect/DateTime";
|
|
26
|
+
import * as Ref from "effect/Ref";
|
|
27
|
+
import * as Stdio from "effect/Stdio";
|
|
28
|
+
import { NodeSocketServer } from "@effect/platform-node";
|
|
29
|
+
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
|
|
30
|
+
import * as FiberSet from "effect/FiberSet";
|
|
31
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
32
|
+
import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai";
|
|
5
33
|
//#region src/Checker/mod.ts
|
|
6
34
|
var Checker_exports = /* @__PURE__ */ __exportAll({
|
|
7
35
|
CheckerAnsweredUnrequested: () => CheckerAnsweredUnrequested,
|
|
@@ -56,6 +84,423 @@ var Engine_exports = /* @__PURE__ */ __exportAll({
|
|
|
56
84
|
strykerCell: () => strykerCell
|
|
57
85
|
});
|
|
58
86
|
//#endregion
|
|
87
|
+
//#region src/Feedback/Feedback.schema.ts
|
|
88
|
+
var FeedbackUnusable = class extends S.TaggedError()("FeedbackUnusable", { reason: S.String }) {
|
|
89
|
+
get message() {
|
|
90
|
+
return `Cannot record feedback: ${this.reason}`;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/Feedback/read-report.ts
|
|
95
|
+
const MUTATION_REPORT_FILE = "reports/mutation/mutation.json";
|
|
96
|
+
const FEEDBACK_FILE = "reports/mutation/feedback.jsonl";
|
|
97
|
+
const SURFACING_DEFAULTS = {
|
|
98
|
+
perLine: 1,
|
|
99
|
+
perFile: 7
|
|
100
|
+
};
|
|
101
|
+
const capsOf = (fields) => ({
|
|
102
|
+
perLine: Option.getOrElse(Option.fromNullishOr(fields.perLine), () => SURFACING_DEFAULTS.perLine),
|
|
103
|
+
perFile: Option.getOrElse(Option.fromNullishOr(fields.perFile), () => SURFACING_DEFAULTS.perFile)
|
|
104
|
+
});
|
|
105
|
+
const surfacingCapsOf = (report) => Option.getOrElse(Option.map(S.decodeUnknownOption(SurfacingFields)(report.config?.["surfacing"]), capsOf), () => SURFACING_DEFAULTS);
|
|
106
|
+
const readMutationReport = (basePath) => Effect.gen(function* () {
|
|
107
|
+
const fs = yield* FileSystem.FileSystem;
|
|
108
|
+
const file = (yield* Path.Path).resolve(basePath, MUTATION_REPORT_FILE);
|
|
109
|
+
const text = yield* fs.readFileString(file).pipe(Effect.mapError(() => FeedbackUnusable.make({ reason: `cannot read the finished mutation report at ${file}; run \`stryker run\` first` })));
|
|
110
|
+
return yield* Effect.fromResult(Result.mapError(S.decodeResult(S.fromJsonString(Report_exports.MutationTestResult))(text), (error) => FeedbackUnusable.make({ reason: `cannot decode the finished mutation report at ${file}: ${error.message}` })));
|
|
111
|
+
});
|
|
112
|
+
const readSurfacedSurvivors = (basePath) => Effect.map(readMutationReport(basePath), (report) => surfacedSurvivorsOf(report, surfacingCapsOf(report)));
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/Feedback/record-feedback.workflow.ts
|
|
115
|
+
const RecordFeedbackTypeId = Symbol.for("@systemfsoftware/stryker-js/RecordFeedbackDecision");
|
|
116
|
+
var FeedbackRecorded = class extends S.TaggedClass()("FeedbackRecorded", {
|
|
117
|
+
id: Mutant_exports.MutantId,
|
|
118
|
+
judgment: RunEvent_exports$1.FeedbackJudgment,
|
|
119
|
+
reason: S.NullOr(S.String)
|
|
120
|
+
}) {
|
|
121
|
+
[RecordFeedbackTypeId] = RecordFeedbackTypeId;
|
|
122
|
+
};
|
|
123
|
+
var FeedbackRefused = class extends S.TaggedClass()("FeedbackRefused", { id: Mutant_exports.MutantId }) {
|
|
124
|
+
[RecordFeedbackTypeId] = RecordFeedbackTypeId;
|
|
125
|
+
};
|
|
126
|
+
const RecordFeedbackDecision = S.Union([FeedbackRecorded, FeedbackRefused]);
|
|
127
|
+
var RecordFeedbackCommand = class extends S.Class("RecordFeedbackCommand")({
|
|
128
|
+
id: Mutant_exports.MutantId,
|
|
129
|
+
judgment: RunEvent_exports$1.FeedbackJudgment,
|
|
130
|
+
reason: S.NullOr(S.String),
|
|
131
|
+
knownIds: S.Array(Mutant_exports.MutantId)
|
|
132
|
+
}) {
|
|
133
|
+
static [Workflow.InstrumentationBrand] = {};
|
|
134
|
+
};
|
|
135
|
+
const recordedOf = (command) => FeedbackRecorded.make({
|
|
136
|
+
id: command.id,
|
|
137
|
+
judgment: command.judgment,
|
|
138
|
+
reason: command.reason
|
|
139
|
+
});
|
|
140
|
+
const refusedOf = (command) => FeedbackRefused.make({ id: command.id });
|
|
141
|
+
const decidedOf$1 = (command) => Match.value(Arr.contains(command.knownIds, command.id)).pipe(Match.when(true, () => recordedOf(command)), Match.when(false, () => refusedOf(command)), Match.exhaustive);
|
|
142
|
+
const recordFeedback = Workflow.make({
|
|
143
|
+
command: RecordFeedbackCommand,
|
|
144
|
+
decision: RecordFeedbackDecision,
|
|
145
|
+
error: S.Never,
|
|
146
|
+
decide: (command) => Result.succeed(decidedOf$1(command))
|
|
147
|
+
});
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/Feedback/Feedback.cell.ts
|
|
150
|
+
const appendLine = (basePath, line) => Effect.gen(function* () {
|
|
151
|
+
const fs = yield* FileSystem.FileSystem;
|
|
152
|
+
const path = yield* Path.Path;
|
|
153
|
+
const file = path.resolve(basePath, FEEDBACK_FILE);
|
|
154
|
+
yield* fs.makeDirectory(path.dirname(file), { recursive: true }).pipe(Effect.orDie);
|
|
155
|
+
yield* fs.writeFileString(file, `${line}\n`, { flag: "a" }).pipe(Effect.orDie);
|
|
156
|
+
});
|
|
157
|
+
const encodeLine = (line) => Effect.orDie(S.encodeEffect(S.fromJsonString(RunEvent_exports$1.FeedbackReported))(line));
|
|
158
|
+
const mutantIdOf = (id) => Effect.fromResult(Result.mapError(S.decodeResult(Mutant_exports.MutantId)(id), () => FeedbackUnusable.make({ reason: `${id} is not a 16-character lowercase hexadecimal mutant id` })));
|
|
159
|
+
const recordFeedbackCell = (request) => Effect.gen(function* () {
|
|
160
|
+
const id = yield* mutantIdOf(request.id);
|
|
161
|
+
const survivors = yield* readSurfacedSurvivors(request.basePath);
|
|
162
|
+
const decision = Result.getOrThrow(recordFeedback(RecordFeedbackCommand.make({
|
|
163
|
+
id,
|
|
164
|
+
judgment: request.judgment,
|
|
165
|
+
reason: request.reason,
|
|
166
|
+
knownIds: survivors.map((survivor) => survivor.id)
|
|
167
|
+
})));
|
|
168
|
+
return yield* Match.value(decision).pipe(Match.tag("FeedbackRecorded", (recorded) => encodeLine(RunEvent_exports$1.FeedbackReported.make({
|
|
169
|
+
id: recorded.id,
|
|
170
|
+
judgment: recorded.judgment,
|
|
171
|
+
reason: recorded.reason
|
|
172
|
+
})).pipe(Effect.flatMap((line) => appendLine(request.basePath, line)), Effect.as(RunEvent_exports$1.FeedbackReported.make({
|
|
173
|
+
id: recorded.id,
|
|
174
|
+
judgment: recorded.judgment,
|
|
175
|
+
reason: recorded.reason
|
|
176
|
+
})))), Match.tag("FeedbackRefused", () => Effect.fail(FeedbackUnusable.make({ reason: `no surfaced survivor ${request.id} in the mutation report; run \`stryker run\` first, or read the ids from list_survivors` }))), Match.exhaustive);
|
|
177
|
+
});
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/output-mode.schema.ts
|
|
180
|
+
const ResolvedMode = S.Struct({
|
|
181
|
+
mode: OutputMode_exports.OutputMode,
|
|
182
|
+
signal: OutputMode_exports.ModeSignal,
|
|
183
|
+
stdoutIsTTY: S.Boolean
|
|
184
|
+
});
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/Rerun/admit-mutant-rerun.workflow.ts
|
|
187
|
+
const MutantRerunTypeId = Symbol.for("@systemfsoftware/stryker-js/MutantRerunAdmission");
|
|
188
|
+
const PriorMutantShape = S.Struct({
|
|
189
|
+
...Mutant_exports.Mutant.fields,
|
|
190
|
+
relativeFileName: S.String
|
|
191
|
+
});
|
|
192
|
+
var AdmitMutantRerunCommand = class extends S.Class("AdmitMutantRerunCommand")({
|
|
193
|
+
ids: S.Array(Mutant_exports.MutantId),
|
|
194
|
+
priorMutants: S.Array(PriorMutantShape),
|
|
195
|
+
priorReportPath: S.String
|
|
196
|
+
}) {
|
|
197
|
+
static [Workflow.InstrumentationBrand] = {};
|
|
198
|
+
};
|
|
199
|
+
var RerunAdmitted = class extends S.TaggedClass()("RerunAdmitted", {
|
|
200
|
+
ids: S.Array(Mutant_exports.MutantId),
|
|
201
|
+
mutateSpans: S.Array(S.String)
|
|
202
|
+
}) {
|
|
203
|
+
[MutantRerunTypeId] = MutantRerunTypeId;
|
|
204
|
+
};
|
|
205
|
+
var RerunRefused = class extends S.TaggedClass()("RerunRefused", {
|
|
206
|
+
exitClass: S.Literal("ConfigError"),
|
|
207
|
+
unknownIds: S.Array(Mutant_exports.MutantId),
|
|
208
|
+
reason: S.String
|
|
209
|
+
}) {
|
|
210
|
+
[MutantRerunTypeId] = MutantRerunTypeId;
|
|
211
|
+
};
|
|
212
|
+
const MutantRerunAdmission = S.Union([RerunAdmitted, RerunRefused]);
|
|
213
|
+
const spanOf = (mutant) => `${mutant.relativeFileName}:${mutant.location.start.line}:${mutant.location.start.column}-${mutant.location.end.line}:${mutant.location.end.column}`;
|
|
214
|
+
const requestedIdsOf = (command) => Arr.dedupe(command.ids);
|
|
215
|
+
const priorMutantOf = (command, id) => Arr.findFirst(command.priorMutants, (mutant) => mutant.id === id);
|
|
216
|
+
const unknownIdsOf = (command) => Arr.filter(requestedIdsOf(command), (id) => Option.isNone(priorMutantOf(command, id)));
|
|
217
|
+
const mutateSpansOf = (command) => Arr.dedupe(Arr.getSomes(Arr.map(requestedIdsOf(command), (id) => Option.map(priorMutantOf(command, id), spanOf))));
|
|
218
|
+
const refusalOf = (command, unknownIds) => RerunRefused.make({
|
|
219
|
+
exitClass: "ConfigError",
|
|
220
|
+
unknownIds,
|
|
221
|
+
reason: `Unknown mutant id(s): ${Arr.join(unknownIds, ", ")}. None of them appears in the prior mutation report at ${command.priorReportPath}; run \`stryker run\` to write a report that lists the ids you can re-run.`
|
|
222
|
+
});
|
|
223
|
+
const admittedOf = (command) => RerunAdmitted.make({
|
|
224
|
+
ids: requestedIdsOf(command),
|
|
225
|
+
mutateSpans: mutateSpansOf(command)
|
|
226
|
+
});
|
|
227
|
+
const decidedOf = (command) => {
|
|
228
|
+
const unknownIds = unknownIdsOf(command);
|
|
229
|
+
return Option.match(Arr.head(unknownIds), {
|
|
230
|
+
onNone: () => admittedOf(command),
|
|
231
|
+
onSome: () => refusalOf(command, unknownIds)
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const admitMutantRerun = Workflow.make({
|
|
235
|
+
command: AdmitMutantRerunCommand,
|
|
236
|
+
decision: MutantRerunAdmission,
|
|
237
|
+
error: S.Never,
|
|
238
|
+
decide: (command) => Result.succeed(decidedOf(command))
|
|
239
|
+
});
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/Survivors/Survivors.schema.ts
|
|
242
|
+
const PriorReportDocument = S.Struct({
|
|
243
|
+
config: S.optional(S.Record(S.String, S.Unknown)),
|
|
244
|
+
framework: S.optional(S.Struct({ version: S.optional(S.String) })),
|
|
245
|
+
files: S.Record(S.String, S.Struct({
|
|
246
|
+
source: S.String,
|
|
247
|
+
mutants: S.Array(S.Struct({
|
|
248
|
+
id: Mutant_exports.MutantId,
|
|
249
|
+
mutatorName: S.String,
|
|
250
|
+
replacement: S.optional(S.String),
|
|
251
|
+
status: Mutant_exports.MutantStatusSchema,
|
|
252
|
+
location: Mutant_exports.Location
|
|
253
|
+
}))
|
|
254
|
+
}))
|
|
255
|
+
});
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/Rerun/Rerun.cell.ts
|
|
258
|
+
const DEFAULT_PRIOR_REPORT = "reports/mutation/mutation.json";
|
|
259
|
+
const resolveRerunOptions = Effect.fnUntraced(function* (input) {
|
|
260
|
+
return yield* readConfig(input.cliOptions, {
|
|
261
|
+
command: "run",
|
|
262
|
+
mode: input.mode
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
const priorReportText = Effect.fnUntraced(function* (priorReportPath) {
|
|
266
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(priorReportPath).pipe(Effect.option, Effect.map(Option.getOrElse(() => "")));
|
|
267
|
+
});
|
|
268
|
+
const priorMutantsOf = (text, basePath) => Option.match(S.decodeOption(S.fromJsonString(PriorReportDocument))(text), {
|
|
269
|
+
onNone: () => [],
|
|
270
|
+
onSome: (document) => Object.entries(document.files).flatMap(([file, fileResult]) => {
|
|
271
|
+
const fileName = `${basePath}/${file}`;
|
|
272
|
+
const relativeFileName = relativeNormalizedFileName(fileName, basePath);
|
|
273
|
+
return fileResult.mutants.map((mutant) => ({
|
|
274
|
+
_tag: "Mutant",
|
|
275
|
+
id: mutant.id,
|
|
276
|
+
fileName: Mutant_exports.CanonicalFileName.make(fileName),
|
|
277
|
+
mutatorName: Mutant_exports.MutatorName.make(mutant.mutatorName),
|
|
278
|
+
replacement: mutant.replacement ?? mutant.mutatorName,
|
|
279
|
+
location: mutant.location,
|
|
280
|
+
relativeFileName
|
|
281
|
+
}));
|
|
282
|
+
})
|
|
283
|
+
});
|
|
284
|
+
const readMutantRerun = Effect.fnUntraced(function* (input) {
|
|
285
|
+
const resolvedOptions = yield* resolveRerunOptions({
|
|
286
|
+
cliOptions: input.cliOptions,
|
|
287
|
+
mode: input.mode
|
|
288
|
+
});
|
|
289
|
+
const priorReportPath = DEFAULT_PRIOR_REPORT;
|
|
290
|
+
const text = yield* priorReportText(priorReportPath);
|
|
291
|
+
return {
|
|
292
|
+
ids: [...input.ids],
|
|
293
|
+
priorMutants: priorMutantsOf(text, input.basePath),
|
|
294
|
+
priorReportPath,
|
|
295
|
+
resolvedOptions,
|
|
296
|
+
settle: input.settle
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
const mutantRerunAdmissionCell = Sandwich.named(SpanTaxonomy_exports.Spans.rerun.name)(readMutantRerun).decide(admitMutantRerun).write({
|
|
300
|
+
RerunAdmitted: (admitted, raw) => raw.settle.runAdmitted({
|
|
301
|
+
ids: admitted.ids,
|
|
302
|
+
mutateSpans: admitted.mutateSpans,
|
|
303
|
+
resolvedOptions: raw.resolvedOptions
|
|
304
|
+
}),
|
|
305
|
+
RerunRefused: (refused) => Effect.flatMap(S.decodeEffect(RerunRefused)(refused), (decoded) => Effect.fail(decoded)),
|
|
306
|
+
CommandRejected: ({ issue }) => Effect.fail(RerunRefused.make({
|
|
307
|
+
exitClass: "ConfigError",
|
|
308
|
+
unknownIds: [],
|
|
309
|
+
reason: issue
|
|
310
|
+
}))
|
|
311
|
+
});
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/Mcp/mcp-tools.schema.ts
|
|
314
|
+
const MutantDetail = S.Struct({
|
|
315
|
+
id: Mutant_exports.MutantId,
|
|
316
|
+
status: Mutant_exports.MutantStatusSchema,
|
|
317
|
+
coveringTests: S.Array(S.String),
|
|
318
|
+
killedBy: S.NullOr(S.String),
|
|
319
|
+
reproducer: S.String,
|
|
320
|
+
diff: S.NullOr(S.String)
|
|
321
|
+
});
|
|
322
|
+
var MutantUnusable = class extends S.TaggedError()("MutantUnusable", { id: Mutant_exports.MutantId }) {
|
|
323
|
+
get message() {
|
|
324
|
+
return `No mutant ${this.id} in the finished mutation report; run \`stryker run\` first, or read a known id from list_survivors.`;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
var RerunUnusable = class extends S.TaggedError()("RerunUnusable", {
|
|
328
|
+
id: Mutant_exports.MutantId,
|
|
329
|
+
reason: S.String
|
|
330
|
+
}) {
|
|
331
|
+
get message() {
|
|
332
|
+
return this.reason;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const ShowMutantFailure = S.Union([MutantUnusable, FeedbackUnusable]);
|
|
336
|
+
const RerunMutantFailure = S.Union([MutantUnusable, RerunUnusable]);
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/Mcp/mcp-tools.ts
|
|
339
|
+
const reportDependencies = [FileSystem.FileSystem, Path.Path];
|
|
340
|
+
const engineDependencies = [
|
|
341
|
+
FileSystem.FileSystem,
|
|
342
|
+
Path.Path,
|
|
343
|
+
Stdio.Stdio,
|
|
344
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
345
|
+
GitDiff,
|
|
346
|
+
WorkerLauncher
|
|
347
|
+
];
|
|
348
|
+
const ListSurvivors = Tool.make("list_survivors", {
|
|
349
|
+
description: "List the surfaced survivors of the finished mutation report, capped exactly as the CLI caps them: at most one per line and seven per file.",
|
|
350
|
+
success: S.Array(SurvivorRef),
|
|
351
|
+
failure: FeedbackUnusable,
|
|
352
|
+
dependencies: reportDependencies
|
|
353
|
+
});
|
|
354
|
+
const ShowMutant = Tool.make("show_mutant", {
|
|
355
|
+
description: "Show one mutant's status, covering tests, killing test, reproducer command, and diff against the finished mutation report.",
|
|
356
|
+
parameters: S.Struct({ id: Mutant_exports.MutantId }),
|
|
357
|
+
success: MutantDetail,
|
|
358
|
+
failure: ShowMutantFailure,
|
|
359
|
+
dependencies: reportDependencies
|
|
360
|
+
});
|
|
361
|
+
const RerunMutant = Tool.make("rerun_mutant", {
|
|
362
|
+
description: "Re-run one mutant by id through the run pipeline, restricted to that mutant, and report its fresh status, covering tests, killing test, and reproducer.",
|
|
363
|
+
parameters: S.Struct({ id: Mutant_exports.MutantId }),
|
|
364
|
+
success: MutantDetail,
|
|
365
|
+
failure: RerunMutantFailure,
|
|
366
|
+
dependencies: engineDependencies
|
|
367
|
+
});
|
|
368
|
+
const ReportUsefulness = Tool.make("report_usefulness", {
|
|
369
|
+
description: "Record a useful or not-useful judgment for one surfaced survivor. The judgment is appended to reports/mutation/feedback.jsonl as a `feedback` stream line.",
|
|
370
|
+
parameters: S.Struct({
|
|
371
|
+
id: Mutant_exports.MutantId,
|
|
372
|
+
judgment: RunEvent_exports$1.FeedbackJudgment,
|
|
373
|
+
reason: S.String.pipe(S.NullOr, S.optional)
|
|
374
|
+
}),
|
|
375
|
+
success: RunEvent_exports$1.FeedbackReported,
|
|
376
|
+
failure: FeedbackUnusable,
|
|
377
|
+
dependencies: reportDependencies
|
|
378
|
+
});
|
|
379
|
+
const mcpToolkit = Toolkit.make(ListSurvivors, ShowMutant, RerunMutant, ReportUsefulness);
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/Mcp/mcp-server.cell.ts
|
|
382
|
+
const MCP_RUN_ID = RunEvent_exports$1.RunId.make("01ARZ3NDEKTSV4RRFFQ69G5FAM");
|
|
383
|
+
const REPRODUCERS_FILE = "reports/mutation/reproducers.json";
|
|
384
|
+
const MCP_SERVER_INSTRUCTIONS = "Stryker mutation testing. Use list_survivors to read the surfaced survivors of the finished report, show_mutant to inspect one, rerun_mutant to re-run one through the run pipeline, and report_usefulness to record whether a survivor was worth acting on.";
|
|
385
|
+
const MCP_MODE = ResolvedMode.make({
|
|
386
|
+
mode: "machine",
|
|
387
|
+
signal: "tool",
|
|
388
|
+
stdoutIsTTY: false
|
|
389
|
+
});
|
|
390
|
+
const readReproducers = (basePath) => Effect.gen(function* () {
|
|
391
|
+
const fs = yield* FileSystem.FileSystem;
|
|
392
|
+
const path = yield* Path.Path;
|
|
393
|
+
const reproducerList = S.Array(ReproducerSchema).pipe(S.fromJsonString);
|
|
394
|
+
const text = yield* fs.readFileString(path.resolve(basePath, REPRODUCERS_FILE)).pipe(Effect.orElseSucceed(() => ""));
|
|
395
|
+
return Option.getOrElse(S.decodeOption(reproducerList)(text), () => []);
|
|
396
|
+
});
|
|
397
|
+
const diffOf = (reproducers, id) => Option.getOrElse(Option.map(Arr.findFirst(reproducers, (reproducer) => reproducer.id === id), (reproducer) => reproducer.diff), () => null);
|
|
398
|
+
const readDiff = (basePath, id) => Effect.map(readReproducers(basePath), (reproducers) => diffOf(reproducers, id));
|
|
399
|
+
const mutantsOf = (report) => Arr.flatMap(Object.values(report.files), (file) => file.mutants);
|
|
400
|
+
const mutantAt = (report, id) => Arr.findFirst(mutantsOf(report), (mutant) => mutant.id === id);
|
|
401
|
+
const coveringTestsOf = (mutant) => [...mutant.coveredBy ?? []];
|
|
402
|
+
const killedByOf = (mutant) => Option.getOrNull(Arr.head([...mutant.killedBy ?? []]));
|
|
403
|
+
const detailOf = (id, mutant, diff) => ({
|
|
404
|
+
id,
|
|
405
|
+
status: mutant.status,
|
|
406
|
+
coveringTests: coveringTestsOf(mutant),
|
|
407
|
+
killedBy: killedByOf(mutant),
|
|
408
|
+
reproducer: `stryker run --mutant ${id}`,
|
|
409
|
+
diff
|
|
410
|
+
});
|
|
411
|
+
const showMutant = (basePath, id) => Effect.gen(function* () {
|
|
412
|
+
const report = yield* readMutationReport(basePath);
|
|
413
|
+
const found = yield* Effect.fromOption(mutantAt(report, id)).pipe(Effect.mapError(() => MutantUnusable.make({ id })));
|
|
414
|
+
const diff = yield* readDiff(basePath, id);
|
|
415
|
+
return detailOf(id, found, diff);
|
|
416
|
+
});
|
|
417
|
+
const restrictedOptionsOf = ({ ids, mutateSpans, resolvedOptions }) => ({
|
|
418
|
+
...resolvedOptions,
|
|
419
|
+
mutate: [...mutateSpans],
|
|
420
|
+
mutantIds: [...ids],
|
|
421
|
+
incremental: true
|
|
422
|
+
});
|
|
423
|
+
const runRestricted = (basePath, options) => Effect.gen(function* () {
|
|
424
|
+
const queue = yield* Queue.bounded(RunEvent_exports$1.RunEvent.QUEUE_BOUND);
|
|
425
|
+
const environment = {
|
|
426
|
+
runId: MCP_RUN_ID,
|
|
427
|
+
resolvedMode: MCP_MODE,
|
|
428
|
+
runStartedAt: 0,
|
|
429
|
+
basePath,
|
|
430
|
+
builtinReporters: {},
|
|
431
|
+
allowConsoleColors: false
|
|
432
|
+
};
|
|
433
|
+
const runLayer = Layer.merge(RunEnvironment.stage(environment, queue), MachineConsole.captureLayer.pipe(Layer.provide(MachineConsole.layer)));
|
|
434
|
+
return yield* mutationTestCell.run({
|
|
435
|
+
cliOptions: options,
|
|
436
|
+
targetMutatePatterns: void 0
|
|
437
|
+
}).pipe(Effect.provide(runLayer), Effect.scoped, Effect.orDie);
|
|
438
|
+
});
|
|
439
|
+
const NO_RESULTS = {
|
|
440
|
+
results: [],
|
|
441
|
+
verdict: null
|
|
442
|
+
};
|
|
443
|
+
const resultsOf = (settled) => (settled ?? NO_RESULTS).results;
|
|
444
|
+
const reportedOf = (results, id) => Effect.fromOption(Arr.head(mutantDetailEventsOf({
|
|
445
|
+
requested: Option.some([id]),
|
|
446
|
+
results
|
|
447
|
+
}))).pipe(Effect.mapError(() => MutantUnusable.make({ id })));
|
|
448
|
+
const reasonOfRerun = (error) => S.is(RerunRefused)(error) ? error.reason : error.message;
|
|
449
|
+
const rerunFailureOf = (id) => (error) => S.is(MutantUnusable)(error) ? error : RerunUnusable.make({
|
|
450
|
+
id,
|
|
451
|
+
reason: reasonOfRerun(error)
|
|
452
|
+
});
|
|
453
|
+
const rerunMutant = (basePath, id) => Effect.gen(function* () {
|
|
454
|
+
const settled = yield* mutantRerunAdmissionCell.run({
|
|
455
|
+
ids: [id],
|
|
456
|
+
cliOptions: {},
|
|
457
|
+
mode: MCP_MODE.mode,
|
|
458
|
+
basePath,
|
|
459
|
+
settle: { runAdmitted: ({ ids, mutateSpans, resolvedOptions }) => runRestricted(basePath, restrictedOptionsOf({
|
|
460
|
+
ids,
|
|
461
|
+
mutateSpans,
|
|
462
|
+
resolvedOptions
|
|
463
|
+
})) }
|
|
464
|
+
}).pipe(Effect.catchTag("SchemaError", Effect.die), Effect.mapError(rerunFailureOf(id)));
|
|
465
|
+
const reported = yield* reportedOf(resultsOf(settled), id);
|
|
466
|
+
const diff = yield* readDiff(basePath, id);
|
|
467
|
+
return {
|
|
468
|
+
id: reported.id,
|
|
469
|
+
status: reported.status,
|
|
470
|
+
coveringTests: [...reported.coveringTests],
|
|
471
|
+
killedBy: reported.killedBy,
|
|
472
|
+
reproducer: reported.reproducer ?? `stryker run --mutant ${id}`,
|
|
473
|
+
diff
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
const handlers = (basePath) => mcpToolkit.toLayer({
|
|
477
|
+
list_survivors: () => readSurfacedSurvivors(basePath),
|
|
478
|
+
show_mutant: ({ id }) => showMutant(basePath, id),
|
|
479
|
+
rerun_mutant: ({ id }) => rerunMutant(basePath, id),
|
|
480
|
+
report_usefulness: ({ id, judgment, reason }) => recordFeedbackCell({
|
|
481
|
+
basePath,
|
|
482
|
+
id,
|
|
483
|
+
judgment,
|
|
484
|
+
reason: reason ?? null
|
|
485
|
+
})
|
|
486
|
+
});
|
|
487
|
+
const mcpServerLayer = ({ basePath }) => Layer.merge(McpServer.layerStdio({
|
|
488
|
+
name: "stryker",
|
|
489
|
+
version: StrykerPackage.version,
|
|
490
|
+
instructions: MCP_SERVER_INSTRUCTIONS,
|
|
491
|
+
protocols: [McpProtocol.v2025_06_18]
|
|
492
|
+
}), McpServer.toolkit(mcpToolkit)).pipe(Layer.provide(handlers(basePath)), Layer.provide(MachineConsole.layer));
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/Mcp/mod.ts
|
|
495
|
+
var Mcp_exports = /* @__PURE__ */ __exportAll({
|
|
496
|
+
MutantDetail: () => MutantDetail,
|
|
497
|
+
MutantUnusable: () => MutantUnusable,
|
|
498
|
+
RerunMutantFailure: () => RerunMutantFailure,
|
|
499
|
+
ShowMutantFailure: () => ShowMutantFailure,
|
|
500
|
+
mcpServerLayer: () => mcpServerLayer,
|
|
501
|
+
mcpToolkit: () => mcpToolkit
|
|
502
|
+
});
|
|
503
|
+
//#endregion
|
|
59
504
|
//#region src/Plugin/mod.ts
|
|
60
505
|
var Plugin_exports = /* @__PURE__ */ __exportAll({
|
|
61
506
|
REPORTER_EVENT_BATCH_BOUND: () => 128,
|
|
@@ -72,35 +517,668 @@ var Plugin_exports = /* @__PURE__ */ __exportAll({
|
|
|
72
517
|
//#endregion
|
|
73
518
|
//#region src/RunEvent/mod.ts
|
|
74
519
|
var RunEvent_exports = /* @__PURE__ */ __exportAll({
|
|
75
|
-
FormatClaimShadowingRow: () => FormatClaimShadowingRow,
|
|
76
|
-
FormatRegistryResolved: () => FormatRegistryResolved,
|
|
77
|
-
FormatRegistryRow: () => FormatRegistryRow,
|
|
78
|
-
FrameworkContributionRow: () => FrameworkContributionRow,
|
|
79
|
-
FrameworkModuleRow: () => FrameworkModuleRow,
|
|
80
|
-
Heartbeat: () => Heartbeat,
|
|
81
|
-
HelpRendered: () => HelpRendered,
|
|
82
520
|
MetricsResultFromReport: () => MetricsResultFromReport,
|
|
83
|
-
PhaseEntered: () => PhaseEntered,
|
|
84
|
-
PlanKnown: () => PlanKnown,
|
|
85
|
-
PluginLoadFailureReason: () => PluginLoadFailureReason,
|
|
86
|
-
PluginsReported: () => PluginsReported,
|
|
87
|
-
RunEvent: () => RunEvent,
|
|
88
521
|
RunEventDrain: () => RunEventDrain,
|
|
89
522
|
RunEventDrainLive: () => RunEventDrainLive,
|
|
90
|
-
RunEventWireLine: () => RunEventWireLine,
|
|
91
523
|
RunEvents: () => RunEvents,
|
|
92
|
-
RunFailed: () => RunFailed,
|
|
93
524
|
RunIdentity: () => RunIdentity,
|
|
94
|
-
RunMutantTested: () => RunMutantTested,
|
|
95
|
-
RunPhase: () => RunPhase,
|
|
96
|
-
RunStarted: () => RunStarted,
|
|
97
|
-
SkippedFileRow: () => SkippedFileRow,
|
|
98
|
-
SkippedReported: () => SkippedReported,
|
|
99
525
|
VerdictEnvelope: () => VerdictEnvelope,
|
|
100
|
-
|
|
101
|
-
VerdictReached: () => VerdictReached,
|
|
526
|
+
buildVerdictEnvelope: () => buildVerdictEnvelope,
|
|
102
527
|
makeRunEventStream: () => makeRunEventStream,
|
|
103
|
-
metricsResultFromFiles: () => metricsResultFromFiles
|
|
528
|
+
metricsResultFromFiles: () => metricsResultFromFiles,
|
|
529
|
+
staticVerdictOf: () => staticVerdictOf
|
|
530
|
+
});
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/Serve/msp-framing.schema.ts
|
|
533
|
+
var msp_framing_schema_exports = /* @__PURE__ */ __exportAll({
|
|
534
|
+
CONTENT_LENGTH_HEADER: () => CONTENT_LENGTH_HEADER,
|
|
535
|
+
FrameAction: () => FrameAction,
|
|
536
|
+
FramingBuffer: () => FramingBuffer,
|
|
537
|
+
FramingStep: () => FramingStep,
|
|
538
|
+
HeaderlessAction: () => HeaderlessAction,
|
|
539
|
+
IncompleteAction: () => IncompleteAction,
|
|
540
|
+
decodeUtf8: () => decodeUtf8,
|
|
541
|
+
emptyFraming: () => emptyFraming,
|
|
542
|
+
encodeUtf8: () => encodeUtf8,
|
|
543
|
+
frameOf: () => frameOf,
|
|
544
|
+
readFrames: () => readFrames
|
|
545
|
+
});
|
|
546
|
+
const CONTENT_LENGTH_HEADER = /content-length:\s*(\d+)/iu;
|
|
547
|
+
const textEncoder = new TextEncoder();
|
|
548
|
+
const textDecoder = new TextDecoder();
|
|
549
|
+
const encodeUtf8 = (text) => textEncoder.encode(text);
|
|
550
|
+
const decodeUtf8 = (bytes) => textDecoder.decode(bytes);
|
|
551
|
+
const FramingBuffer = S.Struct({ bytes: S.Uint8Array });
|
|
552
|
+
const emptyFraming = { bytes: /* @__PURE__ */ new Uint8Array(0) };
|
|
553
|
+
const FramingStep = S.Struct({
|
|
554
|
+
buffer: FramingBuffer,
|
|
555
|
+
frames: S.Array(S.String),
|
|
556
|
+
errors: S.Array(S.String)
|
|
557
|
+
});
|
|
558
|
+
const HEADER_END_BYTES = new Uint8Array([
|
|
559
|
+
13,
|
|
560
|
+
10,
|
|
561
|
+
13,
|
|
562
|
+
10
|
|
563
|
+
]);
|
|
564
|
+
const HEADERLESS = "a Content-Length header was expected before the message body";
|
|
565
|
+
const concat = (left, right) => {
|
|
566
|
+
if (left.length === 0) return right;
|
|
567
|
+
const joined = new Uint8Array(left.length + right.length);
|
|
568
|
+
joined.set(left, 0);
|
|
569
|
+
joined.set(right, left.length);
|
|
570
|
+
return joined;
|
|
571
|
+
};
|
|
572
|
+
const isHeaderEndAt = (bytes, index) => HEADER_END_BYTES.every((byte, offset) => bytes[index + offset] === byte);
|
|
573
|
+
const headerEndOf = (bytes) => Option.map(Option.filter(Option.fromUndefinedOr(bytes.findIndex((_byte, index) => isHeaderEndAt(bytes, index))), (found) => found >= 0), (found) => found + HEADER_END_BYTES.length);
|
|
574
|
+
const declaredLengthOf = (header) => Option.filter(Option.map(Option.fromNullishOr(CONTENT_LENGTH_HEADER.exec(header)), (match) => Number(match[1])), (size) => Number.isSafeInteger(size) && size >= 0);
|
|
575
|
+
const IncompleteAction = S.TaggedStruct("Incomplete", { rest: S.Uint8Array });
|
|
576
|
+
const FrameAction = S.TaggedStruct("Frame", {
|
|
577
|
+
rest: S.Uint8Array,
|
|
578
|
+
frame: S.String
|
|
579
|
+
});
|
|
580
|
+
const HeaderlessAction = S.TaggedStruct("Headerless", { rest: S.Uint8Array });
|
|
581
|
+
const bodyActionOf = (bytes, headerEnd, size) => Match.value(bytes.length - headerEnd < size).pipe(Match.when(true, () => IncompleteAction.make({ rest: bytes })), Match.when(false, () => FrameAction.make({
|
|
582
|
+
rest: bytes.subarray(headerEnd + size),
|
|
583
|
+
frame: decodeUtf8(bytes.subarray(headerEnd, headerEnd + size))
|
|
584
|
+
})), Match.exhaustive);
|
|
585
|
+
const withinHeader = (bytes, headerEnd) => Option.match(declaredLengthOf(decodeUtf8(bytes.subarray(0, headerEnd))), {
|
|
586
|
+
onNone: () => HeaderlessAction.make({ rest: bytes.subarray(headerEnd) }),
|
|
587
|
+
onSome: (size) => bodyActionOf(bytes, headerEnd, size)
|
|
588
|
+
});
|
|
589
|
+
const actionOf = (bytes) => Option.match(headerEndOf(bytes), {
|
|
590
|
+
onNone: () => IncompleteAction.make({ rest: bytes }),
|
|
591
|
+
onSome: (headerEnd) => withinHeader(bytes, headerEnd)
|
|
592
|
+
});
|
|
593
|
+
const readStep = (state) => Match.value(actionOf(state.rest)).pipe(Match.tag("Incomplete", () => ({
|
|
594
|
+
buffer: { bytes: state.rest },
|
|
595
|
+
frames: state.frames,
|
|
596
|
+
errors: state.errors
|
|
597
|
+
})), Match.tag("Frame", (frame) => {
|
|
598
|
+
state.frames.push(frame.frame);
|
|
599
|
+
return readStep({
|
|
600
|
+
...state,
|
|
601
|
+
rest: frame.rest
|
|
602
|
+
});
|
|
603
|
+
}), Match.tag("Headerless", (headerless) => {
|
|
604
|
+
state.errors.push(HEADERLESS);
|
|
605
|
+
return readStep({
|
|
606
|
+
...state,
|
|
607
|
+
rest: headerless.rest
|
|
608
|
+
});
|
|
609
|
+
}), Match.exhaustive);
|
|
610
|
+
const readFrames = dual(2, (buffer, chunk) => readStep({
|
|
611
|
+
rest: concat(buffer.bytes, chunk),
|
|
612
|
+
frames: [],
|
|
613
|
+
errors: []
|
|
614
|
+
}));
|
|
615
|
+
const frameOf = (payload) => {
|
|
616
|
+
const body = encodeUtf8(payload);
|
|
617
|
+
return concat(encodeUtf8(`Content-Length: ${body.length}\r\n\r\n`), body);
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/Serve/msp.schema.ts
|
|
621
|
+
/**
|
|
622
|
+
* The Mutation Server Protocol payloads this server speaks, as Effect schemas.
|
|
623
|
+
*
|
|
624
|
+
* The shapes are the published ones from `mutation-server-protocol`
|
|
625
|
+
* (stryker-mutator/editor-plugins, `packages/mutation-server-protocol`,
|
|
626
|
+
* published 0.4.1 at commit 47c1ee72b6238620474ec23082369491473d03dd —
|
|
627
|
+
* https://cdn.jsdelivr.net/npm/mutation-server-protocol@0.4.1/README.md):
|
|
628
|
+
* `ConfigureParams`/`ConfigureResult`, `DiscoverParams`/`DiscoverResult`,
|
|
629
|
+
* `MutationTestParams`/`MutationTestResult`, and the
|
|
630
|
+
* `reportMutationTestProgress` notification. Locations reuse the
|
|
631
|
+
* mutation-testing-report schema's 1-based, end-exclusive `Position`/`Location`
|
|
632
|
+
* through `Report`, which is the same base this engine already emits.
|
|
633
|
+
*
|
|
634
|
+
* `MSP_PROTOCOL_VERSION` is the version StrykerJS v10 answers `configure` with
|
|
635
|
+
* (its `StrykerServer.configure` returns `{ version: '0.4.0' }`); the protocol
|
|
636
|
+
* asks the server to name the version it supports, so the answer follows the
|
|
637
|
+
* implementation clients were written against rather than the npm release.
|
|
638
|
+
*
|
|
639
|
+
* The base protocol is JSON-RPC 2.0: a request with an `id` is answered with a
|
|
640
|
+
* matching result or error, a request without one is a notification, and the
|
|
641
|
+
* server may push the progress notification at any time.
|
|
642
|
+
*/
|
|
643
|
+
var msp_schema_exports = /* @__PURE__ */ __exportAll({
|
|
644
|
+
ConfigureParams: () => ConfigureParams,
|
|
645
|
+
ConfigureResult: () => ConfigureResult,
|
|
646
|
+
DiscoverParams: () => DiscoverParams,
|
|
647
|
+
DiscoverResult: () => DiscoverResult,
|
|
648
|
+
DiscoveredFile: () => DiscoveredFile,
|
|
649
|
+
DiscoveredFiles: () => DiscoveredFiles,
|
|
650
|
+
DiscoveredMutant: () => DiscoveredMutant,
|
|
651
|
+
FileRange: () => FileRange,
|
|
652
|
+
JsonRpcErrorObject: () => JsonRpcErrorObject,
|
|
653
|
+
JsonRpcErrorResponse: () => JsonRpcErrorResponse,
|
|
654
|
+
JsonRpcId: () => JsonRpcId,
|
|
655
|
+
JsonRpcRequest: () => JsonRpcRequest,
|
|
656
|
+
JsonRpcResponse: () => JsonRpcResponse,
|
|
657
|
+
JsonRpcResultResponse: () => JsonRpcResultResponse,
|
|
658
|
+
JsonRpcVersion: () => JsonRpcVersion,
|
|
659
|
+
MSP_INTERNAL_ERROR: () => MSP_INTERNAL_ERROR,
|
|
660
|
+
MSP_INVALID_PARAMS: () => MSP_INVALID_PARAMS,
|
|
661
|
+
MSP_INVALID_REQUEST: () => MSP_INVALID_REQUEST,
|
|
662
|
+
MSP_METHODS: () => MSP_METHODS,
|
|
663
|
+
MSP_METHOD_NOT_FOUND: () => MSP_METHOD_NOT_FOUND,
|
|
664
|
+
MSP_PARSE_ERROR: () => MSP_PARSE_ERROR,
|
|
665
|
+
MSP_PROTOCOL_VERSION: () => MSP_PROTOCOL_VERSION,
|
|
666
|
+
MSP_SERVER_BUSY: () => MSP_SERVER_BUSY,
|
|
667
|
+
MspMethod: () => MspMethod,
|
|
668
|
+
MutationTestParams: () => MutationTestParams,
|
|
669
|
+
MutationTestResult: () => MutationTestResult,
|
|
670
|
+
ReportMutationTestProgress: () => ReportMutationTestProgress,
|
|
671
|
+
ServeError: () => ServeError,
|
|
672
|
+
decodeConfigureParams: () => decodeConfigureParams,
|
|
673
|
+
decodeDiscoverParams: () => decodeDiscoverParams,
|
|
674
|
+
decodeJsonRpcRequest: () => decodeJsonRpcRequest,
|
|
675
|
+
decodeMutationTestParams: () => decodeMutationTestParams,
|
|
676
|
+
encodeJsonRpcResponse: () => encodeJsonRpcResponse,
|
|
677
|
+
encodeProgressNotification: () => encodeProgressNotification,
|
|
678
|
+
restrictPatternsOf: () => restrictPatternsOf
|
|
679
|
+
});
|
|
680
|
+
var ServeError = class extends S.TaggedError()("ServeError", {
|
|
681
|
+
reason: S.String,
|
|
682
|
+
cause: S.optionalKey(S.Unknown)
|
|
683
|
+
}) {
|
|
684
|
+
get message() {
|
|
685
|
+
return this.reason;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const MSP_PROTOCOL_VERSION = "0.4.0";
|
|
689
|
+
const MSP_METHODS = {
|
|
690
|
+
configure: "configure",
|
|
691
|
+
discover: "discover",
|
|
692
|
+
mutationTest: "mutationTest",
|
|
693
|
+
reportMutationTestProgress: "reportMutationTestProgress"
|
|
694
|
+
};
|
|
695
|
+
const MspMethod = S.Literals([
|
|
696
|
+
MSP_METHODS.configure,
|
|
697
|
+
MSP_METHODS.discover,
|
|
698
|
+
MSP_METHODS.mutationTest,
|
|
699
|
+
MSP_METHODS.reportMutationTestProgress
|
|
700
|
+
]);
|
|
701
|
+
const JsonRpcVersion = S.Literal("2.0");
|
|
702
|
+
const JsonRpcId = S.Union([
|
|
703
|
+
S.String,
|
|
704
|
+
S.Finite,
|
|
705
|
+
S.Null
|
|
706
|
+
]);
|
|
707
|
+
const JsonRpcRequest = S.Struct({
|
|
708
|
+
jsonrpc: JsonRpcVersion,
|
|
709
|
+
id: S.optionalKey(JsonRpcId),
|
|
710
|
+
method: S.String,
|
|
711
|
+
params: S.optionalKey(S.Json)
|
|
712
|
+
});
|
|
713
|
+
const JsonRpcErrorObject = S.Struct({
|
|
714
|
+
code: S.Int,
|
|
715
|
+
message: S.String,
|
|
716
|
+
data: S.optionalKey(S.Json)
|
|
717
|
+
});
|
|
718
|
+
const MSP_PARSE_ERROR = -32700;
|
|
719
|
+
const MSP_INVALID_REQUEST = -32600;
|
|
720
|
+
const MSP_METHOD_NOT_FOUND = -32601;
|
|
721
|
+
const MSP_INVALID_PARAMS = -32602;
|
|
722
|
+
const MSP_INTERNAL_ERROR = -32603;
|
|
723
|
+
const MSP_SERVER_BUSY = -32e3;
|
|
724
|
+
const JsonRpcErrorResponse = S.Struct({
|
|
725
|
+
jsonrpc: JsonRpcVersion,
|
|
726
|
+
id: S.NullOr(JsonRpcId),
|
|
727
|
+
error: JsonRpcErrorObject
|
|
728
|
+
});
|
|
729
|
+
const JsonRpcResultResponse = S.Struct({
|
|
730
|
+
jsonrpc: JsonRpcVersion,
|
|
731
|
+
id: JsonRpcId,
|
|
732
|
+
result: S.Unknown
|
|
733
|
+
});
|
|
734
|
+
const JsonRpcResponse = S.Union([JsonRpcResultResponse, JsonRpcErrorResponse]);
|
|
735
|
+
const FileRange = S.Struct({
|
|
736
|
+
path: S.String,
|
|
737
|
+
range: S.optionalKey(Report_exports.Location)
|
|
738
|
+
});
|
|
739
|
+
const ConfigureParams = S.Struct({ configFilePath: S.optionalKey(S.String) });
|
|
740
|
+
const ConfigureResult = S.Struct({ version: S.String });
|
|
741
|
+
const DiscoveredMutant = S.Struct({
|
|
742
|
+
id: S.String,
|
|
743
|
+
location: Report_exports.Location,
|
|
744
|
+
description: S.optionalKey(S.String),
|
|
745
|
+
mutatorName: S.String,
|
|
746
|
+
replacement: S.optionalKey(S.String)
|
|
747
|
+
});
|
|
748
|
+
const DiscoveredFile = S.Struct({ mutants: S.Array(DiscoveredMutant) });
|
|
749
|
+
const DiscoveredFiles = S.Record(S.String, DiscoveredFile);
|
|
750
|
+
const FileRanges = S.Array(FileRange);
|
|
751
|
+
const DiscoverParams = S.Struct({ files: FileRanges.pipe(S.optionalKey) });
|
|
752
|
+
const DiscoverResult = S.Struct({ files: DiscoveredFiles });
|
|
753
|
+
const MutationTestParams = S.Struct({
|
|
754
|
+
files: FileRanges.pipe(S.optionalKey),
|
|
755
|
+
mutants: S.optionalKey(DiscoveredFiles)
|
|
756
|
+
});
|
|
757
|
+
const MutationTestResult = S.Struct({ files: S.Record(S.String, S.Struct({ mutants: S.Array(Report_exports.MutantResult) })) });
|
|
758
|
+
const ReportMutationTestProgress = S.Struct({
|
|
759
|
+
jsonrpc: JsonRpcVersion,
|
|
760
|
+
method: S.Literal(MSP_METHODS.reportMutationTestProgress),
|
|
761
|
+
params: MutationTestResult
|
|
762
|
+
});
|
|
763
|
+
const encodeJsonRpcResponse = S.encodeResult(S.fromJsonString(JsonRpcResponse));
|
|
764
|
+
const encodeProgressNotification = S.encodeResult(S.fromJsonString(ReportMutationTestProgress));
|
|
765
|
+
const decodeJsonRpcRequest = S.decodeUnknownResult(S.fromJsonString(JsonRpcRequest));
|
|
766
|
+
const decodeConfigureParams = S.decodeUnknownResult(ConfigureParams);
|
|
767
|
+
const decodeDiscoverParams = S.decodeUnknownResult(DiscoverParams);
|
|
768
|
+
const decodeMutationTestParams = S.decodeUnknownResult(MutationTestParams);
|
|
769
|
+
/**
|
|
770
|
+
* The `targetMutatePatterns` this engine takes for a `FileRange` list, or
|
|
771
|
+
* `undefined` when no restriction was asked for. A trailing slash means a
|
|
772
|
+
* directory, matching the protocol.
|
|
773
|
+
*
|
|
774
|
+
* The positions pass through verbatim. This engine compares a range's `end`
|
|
775
|
+
* against a mutant location's `end`, and both of those ends are the report
|
|
776
|
+
* schema's 1-based, exclusive end — the same coordinate space MSP defines for
|
|
777
|
+
* `Location` — so MSP's start-inclusive, end-exclusive range needs no
|
|
778
|
+
* arithmetic here. (StrykerJS v10 subtracts one from both columns instead,
|
|
779
|
+
* because its own mutate ranges are exclusive at the start; copying that would
|
|
780
|
+
* shift this engine's start by a character.)
|
|
781
|
+
*/
|
|
782
|
+
const restrictPatternsOf = (files) => Option.getOrUndefined(Option.map(Option.filter(Option.fromUndefinedOr(files), (scopes) => scopes.length > 0), (scopes) => scopes.map(patternOf)));
|
|
783
|
+
const patternOf = (file) => Option.match(Option.fromUndefinedOr(file.range), {
|
|
784
|
+
onNone: () => basePatternOf(file.path),
|
|
785
|
+
onSome: (range) => `${basePatternOf(file.path)}:${rangeTextOf(range)}`
|
|
786
|
+
});
|
|
787
|
+
const basePatternOf = (path) => Match.value(path.endsWith("/")).pipe(Match.when(true, () => `${path}**/*`), Match.when(false, () => path), Match.exhaustive);
|
|
788
|
+
const rangeTextOf = (range) => `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`;
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/Serve/msp-protocol.workflow.ts
|
|
791
|
+
const MspDecisionTypeId = Symbol.for("@systemfsoftware/stryker-js/MspDecision");
|
|
792
|
+
var MspConfigureRequested = class extends S.TaggedClass()("MspConfigureRequested", {
|
|
793
|
+
id: JsonRpcId,
|
|
794
|
+
configFilePath: S.optionalKey(S.String)
|
|
795
|
+
}) {
|
|
796
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
797
|
+
};
|
|
798
|
+
var MspDiscoverRequested = class extends S.TaggedClass()("MspDiscoverRequested", {
|
|
799
|
+
id: JsonRpcId,
|
|
800
|
+
params: DiscoverParams
|
|
801
|
+
}) {
|
|
802
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
803
|
+
};
|
|
804
|
+
var MspMutationTestRequested = class extends S.TaggedClass()("MspMutationTestRequested", {
|
|
805
|
+
id: JsonRpcId,
|
|
806
|
+
params: MutationTestParams
|
|
807
|
+
}) {
|
|
808
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
809
|
+
};
|
|
810
|
+
var MspMethodNotFound = class extends S.TaggedClass()("MspMethodNotFound", {
|
|
811
|
+
id: JsonRpcId,
|
|
812
|
+
method: S.String
|
|
813
|
+
}) {
|
|
814
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
815
|
+
};
|
|
816
|
+
var MspInvalidParams = class extends S.TaggedClass()("MspInvalidParams", {
|
|
817
|
+
id: JsonRpcId,
|
|
818
|
+
method: S.String,
|
|
819
|
+
issue: S.String
|
|
820
|
+
}) {
|
|
821
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
822
|
+
};
|
|
823
|
+
var MspRequestBusy = class extends S.TaggedClass()("MspRequestBusy", {
|
|
824
|
+
id: JsonRpcId,
|
|
825
|
+
method: S.String
|
|
826
|
+
}) {
|
|
827
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
828
|
+
};
|
|
829
|
+
var MspNotificationIgnored = class extends S.TaggedClass()("MspNotificationIgnored", { method: S.String }) {
|
|
830
|
+
[MspDecisionTypeId] = MspDecisionTypeId;
|
|
831
|
+
};
|
|
832
|
+
var MspRequestCommand = class extends S.TaggedClass()("MspRequestCommand", {
|
|
833
|
+
id: S.optionalKey(JsonRpcId),
|
|
834
|
+
method: S.String,
|
|
835
|
+
params: S.optionalKey(S.Json),
|
|
836
|
+
busy: S.Boolean
|
|
837
|
+
}) {
|
|
838
|
+
static [Workflow.InstrumentationBrand] = {};
|
|
839
|
+
};
|
|
840
|
+
const refusedParams = (id, method, issue) => MspInvalidParams.make({
|
|
841
|
+
id,
|
|
842
|
+
method,
|
|
843
|
+
issue: issue.message
|
|
844
|
+
});
|
|
845
|
+
const configured = (id, params) => Result.match(decodeConfigureParams(params), {
|
|
846
|
+
onFailure: (issue) => Result.succeed(refusedParams(id, MSP_METHODS.configure, issue)),
|
|
847
|
+
onSuccess: (decoded) => Result.succeed(MspConfigureRequested.make({
|
|
848
|
+
id,
|
|
849
|
+
...configFieldsOf(decoded.configFilePath)
|
|
850
|
+
}))
|
|
851
|
+
});
|
|
852
|
+
const configFieldsOf = (configFilePath) => Option.match(Option.fromUndefinedOr(configFilePath), {
|
|
853
|
+
onNone: () => ({}),
|
|
854
|
+
onSome: (path) => ({ configFilePath: path })
|
|
855
|
+
});
|
|
856
|
+
const discovered = (id, params) => Result.match(decodeDiscoverParams(params), {
|
|
857
|
+
onFailure: (issue) => Result.succeed(refusedParams(id, MSP_METHODS.discover, issue)),
|
|
858
|
+
onSuccess: (decoded) => Result.succeed(MspDiscoverRequested.make({
|
|
859
|
+
id,
|
|
860
|
+
params: decoded
|
|
861
|
+
}))
|
|
862
|
+
});
|
|
863
|
+
const mutationTest = (id, params) => Result.match(decodeMutationTestParams(params), {
|
|
864
|
+
onFailure: (issue) => Result.succeed(refusedParams(id, MSP_METHODS.mutationTest, issue)),
|
|
865
|
+
onSuccess: (decoded) => Result.succeed(MspMutationTestRequested.make({
|
|
866
|
+
id,
|
|
867
|
+
params: decoded
|
|
868
|
+
}))
|
|
869
|
+
});
|
|
870
|
+
const methodOf = (name) => S.decodeUnknownOption(MspMethod)(name);
|
|
871
|
+
const dispatchMethod = (method, command, id) => Match.value(method).pipe(Match.when(MSP_METHODS.configure, () => configured(id, command.params)), Match.when(MSP_METHODS.discover, () => discovered(id, command.params)), Match.when(MSP_METHODS.mutationTest, () => mutationTest(id, command.params)), Match.when(MSP_METHODS.reportMutationTestProgress, () => Result.succeed(MspMethodNotFound.make({
|
|
872
|
+
id,
|
|
873
|
+
method: command.method
|
|
874
|
+
}))), Match.exhaustive);
|
|
875
|
+
const decidedForMethod = (command, id) => Option.match(methodOf(command.method), {
|
|
876
|
+
onNone: () => Result.succeed(MspMethodNotFound.make({
|
|
877
|
+
id,
|
|
878
|
+
method: command.method
|
|
879
|
+
})),
|
|
880
|
+
onSome: (method) => dispatchMethod(method, command, id)
|
|
881
|
+
});
|
|
882
|
+
const whileRunning = (command, id) => Boolean.match(command.busy, {
|
|
883
|
+
onTrue: () => Result.succeed(MspRequestBusy.make({
|
|
884
|
+
id,
|
|
885
|
+
method: command.method
|
|
886
|
+
})),
|
|
887
|
+
onFalse: () => decidedForMethod(command, id)
|
|
888
|
+
});
|
|
889
|
+
const decideMspRequest = (command) => Option.match(Option.fromUndefinedOr(command.id), {
|
|
890
|
+
onNone: () => Result.succeed(MspNotificationIgnored.make({ method: command.method })),
|
|
891
|
+
onSome: (id) => whileRunning(command, id)
|
|
892
|
+
});
|
|
893
|
+
const mspProtocol = Workflow.make({
|
|
894
|
+
command: MspRequestCommand,
|
|
895
|
+
decision: S.Union([
|
|
896
|
+
MspConfigureRequested,
|
|
897
|
+
MspDiscoverRequested,
|
|
898
|
+
MspMutationTestRequested,
|
|
899
|
+
MspMethodNotFound,
|
|
900
|
+
MspInvalidParams,
|
|
901
|
+
MspRequestBusy,
|
|
902
|
+
MspNotificationIgnored
|
|
903
|
+
]),
|
|
904
|
+
error: S.Never,
|
|
905
|
+
decide: decideMspRequest
|
|
906
|
+
});
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/Serve/Serve.cell.ts
|
|
909
|
+
/**
|
|
910
|
+
* The Mutation Server Protocol server: stdio and socket transports, one run at
|
|
911
|
+
* a time.
|
|
912
|
+
*
|
|
913
|
+
* Each decoded request runs the same engine the CLI runs, restricted by the
|
|
914
|
+
* `mutate` ranges or mutant ids the request named. The socket transport binds
|
|
915
|
+
* loopback unless an explicit `--address` names another interface: the protocol
|
|
916
|
+
* has no authentication and `mutationTest` executes the project's own test
|
|
917
|
+
* code.
|
|
918
|
+
*
|
|
919
|
+
* Only framed JSON-RPC reaches stdout. Engine logging goes to stderr and the
|
|
920
|
+
* protocol run disables reporters, because a reporter would otherwise write
|
|
921
|
+
* human text onto the stream the client is parsing.
|
|
922
|
+
*/
|
|
923
|
+
const HEADLESS_MODE = {
|
|
924
|
+
mode: "human",
|
|
925
|
+
signal: "flag",
|
|
926
|
+
stdoutIsTTY: false
|
|
927
|
+
};
|
|
928
|
+
const PROTOCOL_CLI_OPTIONS = {
|
|
929
|
+
logLevel: "warn",
|
|
930
|
+
reporters: []
|
|
931
|
+
};
|
|
932
|
+
const LOOPBACK = "localhost";
|
|
933
|
+
const LOOPBACK_NAMES = [
|
|
934
|
+
LOOPBACK,
|
|
935
|
+
"127.0.0.1",
|
|
936
|
+
"::1",
|
|
937
|
+
"[::1]"
|
|
938
|
+
];
|
|
939
|
+
const serveEnvironment = (basePath, startedAt) => ({
|
|
940
|
+
runId: generateRunId(DateTime.makeUnsafe(startedAt)),
|
|
941
|
+
resolvedMode: HEADLESS_MODE,
|
|
942
|
+
runStartedAt: startedAt,
|
|
943
|
+
basePath,
|
|
944
|
+
builtinReporters: { html: HtmlReporter_exports.makeHtmlReporter },
|
|
945
|
+
allowConsoleColors: false
|
|
946
|
+
});
|
|
947
|
+
const describe = (failure, fallback) => Option.getOrElse(Option.filter(Option.fromUndefinedOr(failure.message), S.is(S.NonEmptyString)), () => fallback);
|
|
948
|
+
const optionalFieldsOf = (entries) => entries.reduce((fields, [key, value]) => {
|
|
949
|
+
if (value !== void 0) fields[key] = value;
|
|
950
|
+
return fields;
|
|
951
|
+
}, {});
|
|
952
|
+
const copiedOrUndefined = (tests) => Option.getOrUndefined(Option.map(Option.fromUndefinedOr(tests), (present) => [...present]));
|
|
953
|
+
const withoutUndefined = (value) => value;
|
|
954
|
+
const bucketOf = (grouped, key) => Option.getOrElse(Option.fromUndefinedOr(grouped.get(key)), () => {
|
|
955
|
+
const fresh = [];
|
|
956
|
+
grouped.set(key, fresh);
|
|
957
|
+
return fresh;
|
|
958
|
+
});
|
|
959
|
+
const withEngine = (consume, use) => Effect.gen(function* () {
|
|
960
|
+
const basePath = yield* (yield* FileSystem.FileSystem).realPath(".").pipe(Effect.mapError(() => ServeError.make({ reason: "cannot resolve the working directory" })));
|
|
961
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
962
|
+
const env = serveEnvironment(basePath, startedAt);
|
|
963
|
+
const events = yield* Queue.bounded(RunEvent_exports$1.RunEvent.QUEUE_BOUND);
|
|
964
|
+
const consumer = yield* Stream.fromQueue(events).pipe(Stream.runForEach(consume), Effect.forkChild);
|
|
965
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
966
|
+
const context = yield* Layer.build(RunEnvironment.stage(env, events));
|
|
967
|
+
return yield* use({
|
|
968
|
+
env,
|
|
969
|
+
context
|
|
970
|
+
});
|
|
971
|
+
})).pipe(Effect.ensuring(Queue.end(events).pipe(Effect.andThen(Fiber.join(consumer)), Effect.ignore)));
|
|
972
|
+
});
|
|
973
|
+
const discoverStageCell = Cell.andThen(Cell.andThen(Cell.andThen(loadConfigCell, readProjectCell), prepareCell), Cell.andThen(concurrencyCell, instrumentCell));
|
|
974
|
+
const discoveredFilesOf = (mutants, relativeFileName) => {
|
|
975
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
976
|
+
for (const mutant of mutants) bucketOf(grouped, relativeFileName(mutant.fileName)).push({
|
|
977
|
+
id: mutant.id,
|
|
978
|
+
location: mutant.location,
|
|
979
|
+
mutatorName: mutant.mutatorName,
|
|
980
|
+
...optionalFieldsOf([["replacement", mutant.replacement], ["description", withoutUndefined(mutant.description)]])
|
|
981
|
+
});
|
|
982
|
+
return Object.fromEntries([...grouped].map(([fileName, groupedMutants]) => [fileName, { mutants: groupedMutants }]));
|
|
983
|
+
};
|
|
984
|
+
const reportedMutantOf = (result) => Object.assign({
|
|
985
|
+
id: result.id,
|
|
986
|
+
location: result.location,
|
|
987
|
+
mutatorName: result.mutatorName,
|
|
988
|
+
replacement: result.replacement,
|
|
989
|
+
status: result.status
|
|
990
|
+
}, optionalFieldsOf([
|
|
991
|
+
["static", withoutUndefined(result.static)],
|
|
992
|
+
["coveredBy", copiedOrUndefined(result.coveredBy)],
|
|
993
|
+
["killedBy", copiedOrUndefined(result.killedBy)],
|
|
994
|
+
["statusReason", withoutUndefined(result.statusReason)],
|
|
995
|
+
["testsCompleted", withoutUndefined(result.testsCompleted)],
|
|
996
|
+
["duration", Option.getOrUndefined(Option.map(Option.fromNullishOr(result.cost), (cost) => cost.testBodyMs))]
|
|
997
|
+
]));
|
|
998
|
+
const mutationTestFilesOf = (results, relativeFileName) => {
|
|
999
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1000
|
+
for (const result of results) bucketOf(grouped, relativeFileName(result.fileName)).push(reportedMutantOf(result));
|
|
1001
|
+
return Object.fromEntries([...grouped].map(([fileName, groupedMutants]) => [fileName, { mutants: groupedMutants }]));
|
|
1002
|
+
};
|
|
1003
|
+
const progressOf = (tested) => ({
|
|
1004
|
+
jsonrpc: "2.0",
|
|
1005
|
+
method: MSP_METHODS.reportMutationTestProgress,
|
|
1006
|
+
params: { files: { [tested.fileName]: { mutants: [Object.assign({
|
|
1007
|
+
id: tested.id,
|
|
1008
|
+
location: tested.location,
|
|
1009
|
+
mutatorName: tested.mutatorName,
|
|
1010
|
+
status: tested.status,
|
|
1011
|
+
static: tested.static
|
|
1012
|
+
}, optionalFieldsOf([
|
|
1013
|
+
["replacement", withoutUndefined(tested.replacement)],
|
|
1014
|
+
["duration", Option.getOrUndefined(Option.map(Option.fromNullishOr(tested.cost), (cost) => cost.testBodyMs))],
|
|
1015
|
+
["testsCompleted", Option.getOrUndefined(Option.map(Option.fromNullishOr(tested.cost), (cost) => cost.testsExecuted))]
|
|
1016
|
+
]))] } } }
|
|
1017
|
+
});
|
|
1018
|
+
const encodedOr = (encoded, what) => Result.match(encoded, {
|
|
1019
|
+
onFailure: (issue) => ServeError.make({
|
|
1020
|
+
reason: `cannot encode ${what}`,
|
|
1021
|
+
cause: issue
|
|
1022
|
+
}),
|
|
1023
|
+
onSuccess: Effect.succeed
|
|
1024
|
+
});
|
|
1025
|
+
const optionsOf = (invocation, configFilePath) => Object.assign({
|
|
1026
|
+
...invocation.cliOptions,
|
|
1027
|
+
...PROTOCOL_CLI_OPTIONS
|
|
1028
|
+
}, optionalFieldsOf([["configFile", withoutUndefined(configFilePath)]]));
|
|
1029
|
+
const mutantIdsOf = (mutants) => Object.values(mutants).flatMap((file) => file.mutants.map((mutant) => mutant.id));
|
|
1030
|
+
const withPatterns = (options, patterns) => Option.getOrElse(Option.map(Option.fromUndefinedOr(patterns), (present) => ({
|
|
1031
|
+
...options,
|
|
1032
|
+
mutate: [...present]
|
|
1033
|
+
})), () => options);
|
|
1034
|
+
const mutationTestOptionsOf = (invocation, configFilePath, params) => Option.match(Option.fromUndefinedOr(params.mutants), {
|
|
1035
|
+
onNone: () => withPatterns(optionsOf(invocation, configFilePath), restrictPatternsOf(params.files)),
|
|
1036
|
+
onSome: (mutants) => ({
|
|
1037
|
+
...optionsOf(invocation, configFilePath),
|
|
1038
|
+
mutate: Object.keys(mutants),
|
|
1039
|
+
mutantIds: mutantIdsOf(mutants)
|
|
1040
|
+
})
|
|
1041
|
+
});
|
|
1042
|
+
const runDiscover = (options, targetMutatePatterns) => withEngine(() => Effect.void, ({ env, context }) => Effect.gen(function* () {
|
|
1043
|
+
const path = yield* Path.Path;
|
|
1044
|
+
const done = yield* Cell.provideContext(discoverStageCell, context).run({
|
|
1045
|
+
cliOptions: options,
|
|
1046
|
+
targetMutatePatterns: targetMutatePatterns === void 0 ? void 0 : [...targetMutatePatterns]
|
|
1047
|
+
});
|
|
1048
|
+
return { files: discoveredFilesOf(done.mutants, (fileName) => path.relative(env.basePath, fileName)) };
|
|
1049
|
+
}).pipe(Effect.mapError((failure) => ServeError.make({
|
|
1050
|
+
reason: describe(failure, "discovery failed"),
|
|
1051
|
+
cause: failure
|
|
1052
|
+
}))));
|
|
1053
|
+
const runMutationTest = (options, targetMutatePatterns, notify) => withEngine((event) => Match.value(event).pipe(Match.tag("mutantTested", (tested) => notify(tested)), Match.orElse(() => Effect.void)), ({ env, context }) => Effect.gen(function* () {
|
|
1054
|
+
const path = yield* Path.Path;
|
|
1055
|
+
const done = yield* Cell.provideContext(mutationTestCell, context).run({
|
|
1056
|
+
cliOptions: options,
|
|
1057
|
+
targetMutatePatterns: targetMutatePatterns === void 0 ? void 0 : [...targetMutatePatterns]
|
|
1058
|
+
});
|
|
1059
|
+
return { files: mutationTestFilesOf(done.results, (fileName) => path.relative(env.basePath, fileName)) };
|
|
1060
|
+
}).pipe(Effect.mapError((failure) => ServeError.make({
|
|
1061
|
+
reason: describe(failure, "the mutation test failed"),
|
|
1062
|
+
cause: failure
|
|
1063
|
+
}))));
|
|
1064
|
+
const respond = (connection, id, result) => Effect.flatMap(encodedOr(encodeJsonRpcResponse({
|
|
1065
|
+
jsonrpc: "2.0",
|
|
1066
|
+
id,
|
|
1067
|
+
result
|
|
1068
|
+
}), "a response"), (payload) => connection.write(payload));
|
|
1069
|
+
const respondError = (connection, id, code, message) => Effect.flatMap(encodedOr(encodeJsonRpcResponse({
|
|
1070
|
+
jsonrpc: "2.0",
|
|
1071
|
+
id,
|
|
1072
|
+
error: {
|
|
1073
|
+
code,
|
|
1074
|
+
message
|
|
1075
|
+
}
|
|
1076
|
+
}), "an error response"), (payload) => connection.write(payload));
|
|
1077
|
+
const notifyProgress = (connection, notification) => Effect.flatMap(encodedOr(encodeProgressNotification(notification), "a progress notification"), (payload) => connection.write(payload));
|
|
1078
|
+
const runRequested = (decision, connection, stateRef, invocation) => {
|
|
1079
|
+
const state = Effect.map(Ref.get(stateRef), (current) => current.configFilePath);
|
|
1080
|
+
return Match.value(decision).pipe(Match.tag("MspConfigureRequested", ({ id, configFilePath }) => Effect.andThen(Ref.set(stateRef, { configFilePath }), respond(connection, id, { version: MSP_PROTOCOL_VERSION }))), Match.tag("MspDiscoverRequested", ({ id, params }) => Effect.gen(function* () {
|
|
1081
|
+
const result = yield* Effect.result(runDiscover(optionsOf(invocation, yield* state), restrictPatternsOf(params.files)));
|
|
1082
|
+
yield* Result.match(result, {
|
|
1083
|
+
onFailure: (failure) => respondError(connection, id, MSP_INTERNAL_ERROR, failure.reason),
|
|
1084
|
+
onSuccess: (files) => respond(connection, id, files)
|
|
1085
|
+
});
|
|
1086
|
+
})), Match.tag("MspMutationTestRequested", ({ id, params }) => Effect.gen(function* () {
|
|
1087
|
+
const result = yield* Effect.result(runMutationTest(mutationTestOptionsOf(invocation, yield* state, params), void 0, (tested) => notifyProgress(connection, progressOf(tested))));
|
|
1088
|
+
yield* Result.match(result, {
|
|
1089
|
+
onFailure: (failure) => respondError(connection, id, MSP_INTERNAL_ERROR, failure.reason),
|
|
1090
|
+
onSuccess: (files) => respond(connection, id, files)
|
|
1091
|
+
});
|
|
1092
|
+
})), Match.tag("MspMethodNotFound", ({ id, method }) => respondError(connection, id, MSP_METHOD_NOT_FOUND, `unknown method "${method}"`)), Match.tag("MspInvalidParams", ({ id, method, issue }) => respondError(connection, id, MSP_INVALID_PARAMS, `invalid params for "${method}": ${issue}`)), Match.tag("MspRequestBusy", ({ id }) => respondError(connection, id, MSP_SERVER_BUSY, "a mutation test is already running")), Match.tag("MspNotificationIgnored", () => Effect.void), Match.exhaustive);
|
|
1093
|
+
};
|
|
1094
|
+
const startsRunOf = (decision) => Match.value(decision).pipe(Match.tag("MspMutationTestRequested", () => true), Match.tag("MspConfigureRequested", () => false), Match.tag("MspDiscoverRequested", () => false), Match.tag("MspMethodNotFound", () => false), Match.tag("MspInvalidParams", () => false), Match.tag("MspRequestBusy", () => false), Match.tag("MspNotificationIgnored", () => false), Match.exhaustive);
|
|
1095
|
+
const releaseRun = (busy, startsRun) => Match.value(startsRun).pipe(Match.when(true, () => Ref.set(busy, false)), Match.when(false, () => Effect.void), Match.exhaustive);
|
|
1096
|
+
const dispatchRequest = (request, id, connection, stateRef, busy, invocation) => Effect.gen(function* () {
|
|
1097
|
+
const running = yield* Ref.get(busy);
|
|
1098
|
+
const decision = Result.getOrThrow(mspProtocol(MspRequestCommand.make({
|
|
1099
|
+
id,
|
|
1100
|
+
method: request.method,
|
|
1101
|
+
params: request.params,
|
|
1102
|
+
busy: running
|
|
1103
|
+
})));
|
|
1104
|
+
const startsRun = startsRunOf(decision);
|
|
1105
|
+
const wasRunning = yield* Ref.modify(busy, (current) => [current, current || startsRun]);
|
|
1106
|
+
yield* Match.value(wasRunning && startsRun).pipe(Match.when(true, () => respondError(connection, id, MSP_SERVER_BUSY, "a mutation test is already running")), Match.when(false, () => runRequested(decision, connection, stateRef, invocation).pipe(Effect.ensuring(releaseRun(busy, startsRun)))), Match.exhaustive);
|
|
1107
|
+
});
|
|
1108
|
+
const handleFrame = (raw, connection, stateRef, busy, invocation) => Result.match(decodeJsonRpcRequest(raw), {
|
|
1109
|
+
onFailure: () => respondError(connection, null, MSP_PARSE_ERROR, "the message is not a JSON-RPC 2.0 request"),
|
|
1110
|
+
onSuccess: (request) => Option.match(Option.fromUndefinedOr(request.id), {
|
|
1111
|
+
onNone: () => Effect.void,
|
|
1112
|
+
onSome: (id) => dispatchRequest({
|
|
1113
|
+
method: request.method,
|
|
1114
|
+
params: request.params
|
|
1115
|
+
}, id, connection, stateRef, busy, invocation)
|
|
1116
|
+
})
|
|
1117
|
+
});
|
|
1118
|
+
const serveConnection = (connection, invocation) => Effect.gen(function* () {
|
|
1119
|
+
const stateRef = yield* Ref.make({ configFilePath: void 0 });
|
|
1120
|
+
const busy = yield* Ref.make(false);
|
|
1121
|
+
const decoder = yield* Ref.make(emptyFraming);
|
|
1122
|
+
const handlers = yield* FiberSet.make();
|
|
1123
|
+
yield* connection.chunks.pipe(Stream.runForEach((chunk) => Effect.gen(function* () {
|
|
1124
|
+
const step = yield* Ref.modify(decoder, (buffer) => {
|
|
1125
|
+
const read = readFrames(buffer, chunk);
|
|
1126
|
+
return [read, read.buffer];
|
|
1127
|
+
});
|
|
1128
|
+
yield* Effect.forEach(step.errors, (error) => respondError(connection, null, MSP_PARSE_ERROR, error), { discard: true });
|
|
1129
|
+
yield* Effect.forEach(step.frames, (frame) => FiberSet.run(handlers, handleFrame(frame, connection, stateRef, busy, invocation)), { discard: true });
|
|
1130
|
+
})));
|
|
1131
|
+
yield* FiberSet.join(handlers);
|
|
1132
|
+
});
|
|
1133
|
+
const stdioConnection = (stdio) => ({
|
|
1134
|
+
chunks: stdio.stdin.pipe(Stream.catch(() => Stream.empty)),
|
|
1135
|
+
write: (payload) => Stream.run(Stream.succeed(frameOf(payload)), stdio.stdout({ endOnDone: false })).pipe(Effect.mapError(() => ServeError.make({ reason: "cannot write to standard output" })))
|
|
1136
|
+
});
|
|
1137
|
+
const socketConnection = (socket) => Effect.gen(function* () {
|
|
1138
|
+
const writer = yield* socket.writer;
|
|
1139
|
+
const pull = yield* Socket.readerBytes(socket);
|
|
1140
|
+
return {
|
|
1141
|
+
chunks: Stream.fromEffectRepeat(pull).pipe(Stream.flatMap((batch) => Stream.fromIterable(batch)), Stream.catch(() => Stream.empty)),
|
|
1142
|
+
write: (payload) => writer.writeAll([frameOf(payload)]).pipe(Effect.mapError(() => ServeError.make({ reason: "cannot write to the socket" })))
|
|
1143
|
+
};
|
|
1144
|
+
}).pipe(Effect.orDie);
|
|
1145
|
+
const writeStderr = (stdio, line) => Stream.run(Stream.succeed(`${line}\n`), stdio.stderr({ endOnDone: false })).pipe(Effect.mapError(() => ServeError.make({ reason: "cannot write to standard error" })));
|
|
1146
|
+
const exposureWarning = (address) => `MSP socket mode is listening on ${address}, which is not loopback: the mutation server has no authentication and runs this project's test code, so anyone who can reach that interface can drive it.`;
|
|
1147
|
+
const portOf = (invocation) => Option.match(Option.fromUndefinedOr(invocation.port), {
|
|
1148
|
+
onNone: () => ServeError.make({ reason: "the socket channel needs --port" }),
|
|
1149
|
+
onSome: Effect.succeed
|
|
1150
|
+
});
|
|
1151
|
+
const exposureIfNeeded = (stdio, address) => Match.value(LOOPBACK_NAMES.includes(address)).pipe(Match.when(true, () => Effect.void), Match.when(false, () => writeStderr(stdio, exposureWarning(address))), Match.exhaustive);
|
|
1152
|
+
const bindListener = (address, port) => Layer.build(NodeSocketServer.layer({
|
|
1153
|
+
port,
|
|
1154
|
+
host: address
|
|
1155
|
+
})).pipe(Effect.mapError((failure) => ServeError.make({
|
|
1156
|
+
reason: `cannot bind ${address}:${port}: ${describe(failure, "the listener failed")}`,
|
|
1157
|
+
cause: failure
|
|
1158
|
+
})));
|
|
1159
|
+
const serveStdio = (invocation) => Effect.flatMap(Stdio.Stdio, (stdio) => serveConnection(stdioConnection(stdio), invocation));
|
|
1160
|
+
const serveSocket = (invocation) => Effect.gen(function* () {
|
|
1161
|
+
const stdio = yield* Stdio.Stdio;
|
|
1162
|
+
const address = Option.getOrElse(Option.fromUndefinedOr(invocation.address), () => LOOPBACK);
|
|
1163
|
+
const port = yield* portOf(invocation);
|
|
1164
|
+
yield* exposureIfNeeded(stdio, address);
|
|
1165
|
+
const serverContext = yield* bindListener(address, port);
|
|
1166
|
+
const server = Context.get(serverContext, SocketServer.SocketServer);
|
|
1167
|
+
yield* writeStderr(stdio, `msp:listening ${String(server.address)}`);
|
|
1168
|
+
const connections = yield* FiberSet.make();
|
|
1169
|
+
return yield* server.run((socket) => FiberSet.run(connections, Effect.scoped(Effect.flatMap(socketConnection(socket), (connection) => serveConnection(connection, invocation))))).pipe(Effect.onInterrupt(() => FiberSet.clear(connections)), Effect.ensuring(FiberSet.clear(connections)), Effect.mapError((failure) => ServeError.make({
|
|
1170
|
+
reason: `the listener failed: ${describe(failure, "unknown cause")}`,
|
|
1171
|
+
cause: failure
|
|
1172
|
+
})));
|
|
1173
|
+
});
|
|
1174
|
+
const serveMutationServer = (invocation) => Match.value(invocation.channel).pipe(Match.when("stdio", () => serveStdio(invocation)), Match.orElse(() => serveSocket(invocation)));
|
|
1175
|
+
//#endregion
|
|
1176
|
+
//#region src/Serve/mod.ts
|
|
1177
|
+
var Serve_exports = /* @__PURE__ */ __exportAll({
|
|
1178
|
+
Framing: () => msp_framing_schema_exports,
|
|
1179
|
+
Protocol: () => msp_schema_exports,
|
|
1180
|
+
ServeError: () => ServeError,
|
|
1181
|
+
serveMutationServer: () => serveMutationServer
|
|
104
1182
|
});
|
|
105
1183
|
//#endregion
|
|
106
1184
|
//#region src/Worker/mod.ts
|
|
@@ -117,4 +1195,4 @@ var Worker_exports = /* @__PURE__ */ __exportAll({
|
|
|
117
1195
|
makeWorkerClient: () => makeWorkerClient
|
|
118
1196
|
});
|
|
119
1197
|
//#endregion
|
|
120
|
-
export { Checker_exports as Checker, Configuration_exports as Configuration, Engine_exports as Engine, Plugin_exports as Plugin, RunEvent_exports as RunEvent, Worker_exports as Worker };
|
|
1198
|
+
export { Checker_exports as Checker, Configuration_exports as Configuration, Engine_exports as Engine, git_diff_service_exports as GitDiff, git_diff_schema_exports as GitDiffSchema, import_closure_cell_exports as ImportClosure, Mcp_exports as Mcp, Plugin_exports as Plugin, RunEvent_exports as RunEvent, Serve_exports as Serve, Worker_exports as Worker };
|