@systemfsoftware/stryker-js-vitest-runner 1.0.0 → 2.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 +49 -0
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +792 -238
- package/dist/stryker-setup.mjs +1 -1
- package/package.json +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @systemfsoftware/stryker-js-vitest-runner
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Three packages are renamed. `plugin-api` is now `@systemfsoftware/stryker-js`, the
|
|
8
|
+
language every plugin is written against. `mutation-run` is now
|
|
9
|
+
`@systemfsoftware/stryker-js-platform-node`, the Node host that runs a mutation
|
|
10
|
+
test. `mutation-report` is now `@systemfsoftware/stryker-js-html-reporter`.
|
|
11
|
+
Install the new names and change your imports.
|
|
12
|
+
|
|
13
|
+
Options types moved. `StrykerOptions`, `PartialStrykerOptions` and `LogLevel` are
|
|
14
|
+
imported from the `Schema` export; `Mutant`, `MutantStatus`, `Position` and
|
|
15
|
+
`Location` from the `Mutant` export. Point a config's `extends` at the language
|
|
16
|
+
package's `Schema` export.
|
|
17
|
+
|
|
18
|
+
`MutantStatus` accepts one spelling per outcome: `Killed`, `Survived`,
|
|
19
|
+
`NoCoverage`, `Timeout`, `CompileError`, `RuntimeError`, `Ignored` and `Pending`.
|
|
20
|
+
The lowercase and abbreviated forms — `killed`, `timedOut`, `noCoverage` and the
|
|
21
|
+
rest — are gone. A comparison against a removed spelling never matched the value
|
|
22
|
+
the reporter actually produced, so check any status comparison you wrote.
|
|
23
|
+
|
|
24
|
+
Statuses, plugin kinds, exit classes and AST formats are string literal unions
|
|
25
|
+
rather than enums, so read them as their string values. Member access such as
|
|
26
|
+
`ExitClass.VerdictFail` no longer resolves.
|
|
27
|
+
|
|
28
|
+
A plugin no longer receives a logger, and the logger port is gone. Plugins log
|
|
29
|
+
through Effect, and the host decides where that output goes.
|
|
30
|
+
|
|
31
|
+
The bundled base preset is gone. A config inherits from the language package's
|
|
32
|
+
`Schema` export and states the thresholds, reporters and plugins it wants; you no
|
|
33
|
+
longer silently inherit a package manager, a plugin list or a break threshold.
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- Machine-readable mutation progress is a newline-delimited JSON file next to the HTML and JSON reports, not the console.
|
|
38
|
+
|
|
39
|
+
The console prints bounded progress prose: phase names, a count line, at most twenty surviving mutants, and a verdict. Killed mutants now advance that count. Child test runs during mutation no longer print per-test output or GitHub workflow commands.
|
|
40
|
+
|
|
41
|
+
If you parsed the console as JSON lines, read the stream file instead. A hard kill can leave that file without a closing verdict line.
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- A mutation run writes the JSON report the `json` reporter is configured to produce.
|
|
46
|
+
|
|
47
|
+
Vitest no longer reprints its full summary for every mutant.
|
|
48
|
+
|
|
49
|
+
- Updated dependencies:
|
|
50
|
+
- @systemfsoftware/effect-cell-types@5.0.0
|
|
51
|
+
|
|
3
52
|
## 1.0.0
|
|
4
53
|
|
|
5
54
|
### Major Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { PluginKind } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
2
1
|
//#region src/index.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* The `vitest` test runner, as the plugin the engine loads.
|
|
@@ -7,7 +6,7 @@ import { PluginKind } from "@systemfsoftware/stryker-js-plugin-api/plugin";
|
|
|
7
6
|
* in, so the requirement is visible in the type and an engine that does not
|
|
8
7
|
* provide it fails to compile.
|
|
9
8
|
*/
|
|
10
|
-
declare const strykerPlugins: import("@systemfsoftware/stryker-js
|
|
9
|
+
declare const strykerPlugins: import("@systemfsoftware/stryker-js/Plugin").PluginContribution<"TestRunner">[];
|
|
11
10
|
/**
|
|
12
11
|
* The `vitest` option section as a JSON Schema document, for Stryker's option
|
|
13
12
|
* validation — derived from the declaration, never read from a file. It is built
|
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RunConfiguration, SandboxDirectory, declarePlugin } from "@systemfsoftware/stryker-js/Plugin";
|
|
2
2
|
import * as Effect$1 from "effect/Effect";
|
|
3
3
|
import * as Layer from "effect/Layer";
|
|
4
4
|
import * as S from "effect/Schema";
|
|
5
|
-
import { Effect, Option } from "effect";
|
|
6
|
-
import { INSTRUMENTER_CONSTANTS, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js-plugin-api/core";
|
|
7
|
-
import { DryRunStatus, TestRunner, TestRunnerFailed, TestStatus, determineHitLimitReached, testFilesProvided, toMutantRunResult } from "@systemfsoftware/stryker-js-plugin-api/test-runner";
|
|
8
|
-
import * as FileSystem from "effect/FileSystem";
|
|
9
|
-
import * as Option$1 from "effect/Option";
|
|
10
|
-
import * as Path from "effect/Path";
|
|
11
|
-
import * as Ref from "effect/Ref";
|
|
12
|
-
import { fileURLToPath } from "url";
|
|
13
5
|
import { createRequire } from "module";
|
|
14
6
|
import { pathToFileURL } from "node:url";
|
|
15
7
|
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
16
9
|
import { createVitest } from "vitest/node";
|
|
17
|
-
|
|
10
|
+
import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
11
|
+
import { INSTRUMENTER_CONSTANTS, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js/Mutant";
|
|
12
|
+
import { TestRunner, TestRunnerFailed, testFilesProvided } from "@systemfsoftware/stryker-js/TestRunner";
|
|
13
|
+
import * as FileSystem from "effect/FileSystem";
|
|
14
|
+
import { pipe } from "effect/Function";
|
|
15
|
+
import * as Option from "effect/Option";
|
|
16
|
+
import * as Path from "effect/Path";
|
|
17
|
+
import * as Ref from "effect/Ref";
|
|
18
|
+
import * as Result from "effect/Result";
|
|
19
|
+
import { Effect } from "effect";
|
|
20
|
+
import * as Match from "effect/Match";
|
|
21
|
+
//#region src/Runner.schema.ts
|
|
22
|
+
/**
|
|
23
|
+
* Runner schemas — declarations for the vitest runner capability.
|
|
24
|
+
*
|
|
25
|
+
* Houses every Schema/Wire type this capability publishes or decodes
|
|
26
|
+
* internally: option validation, coverage/task metadata, sandbox
|
|
27
|
+
* manifest, and the dynamic vitest module shape.
|
|
28
|
+
*/
|
|
18
29
|
/**
|
|
19
30
|
* The `vitest` option section of the Stryker options document. `related`
|
|
20
31
|
* defaults to `true` at decode, the other members are optional.
|
|
@@ -29,24 +40,527 @@ const VitestRunnerOptionsSchema = S.Struct({
|
|
|
29
40
|
* document, or present as a partial, and decoded into the section defaults.
|
|
30
41
|
*/
|
|
31
42
|
const VitestSectionSchema = S.optional(VitestRunnerOptionsSchema).pipe(S.withDecodingDefault(Effect.succeed({ related: true })));
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
|
|
44
|
+
const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
|
|
45
|
+
static: S.Record(S.String, S.Finite),
|
|
46
|
+
perTest: S.Record(S.String, S.Record(S.String, S.Finite))
|
|
47
|
+
})) });
|
|
48
|
+
const MutantCoverageShapeSchema = S.Struct({
|
|
49
|
+
static: S.Record(S.String, S.Finite),
|
|
50
|
+
perTest: S.Record(S.String, S.Record(S.String, S.Finite))
|
|
51
|
+
});
|
|
52
|
+
var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
|
|
34
53
|
const ExportEntry = S.Union([S.String, S.Record(S.String, S.Unknown)]);
|
|
35
54
|
const PackageManifest = S.StructWithRest(S.Struct({
|
|
36
55
|
name: S.optional(S.String),
|
|
37
56
|
exports: S.optional(S.Record(S.String, ExportEntry))
|
|
38
57
|
}), [S.Record(S.String, S.Unknown)]);
|
|
58
|
+
/**
|
|
59
|
+
* The dynamically imported project-local `vitest/node` module. The runtime
|
|
60
|
+
* check only asserts object-likeness: the module namespace is whatever the
|
|
61
|
+
* resolved package exports, and the consumers tolerate a missing
|
|
62
|
+
* `createVitest` via their own fallbacks.
|
|
63
|
+
*/
|
|
64
|
+
const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
|
|
65
|
+
/** The `package.json` document of a resolved vitest package. */
|
|
66
|
+
const VitestPackageSchema = S.Struct({ version: S.String });
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/VitestDryRun.workflow.ts
|
|
69
|
+
/**
|
|
70
|
+
* VitestDryRun workflow — pure result-mapping for the dry-run phase.
|
|
71
|
+
*
|
|
72
|
+
* Every function here is pure: it maps raw vitest task payloads to
|
|
73
|
+
* Stryker run results without touching the filesystem or spawning
|
|
74
|
+
* processes. Impure orchestration lives in `Runner.ts`.
|
|
75
|
+
*/
|
|
76
|
+
var VitestDryRunCommand$1 = class extends S.TaggedClass()("VitestDryRunCommand", {
|
|
77
|
+
rawTests: S.Array(S.Unknown),
|
|
78
|
+
projectRoot: S.String,
|
|
79
|
+
hasExternalError: S.Boolean,
|
|
80
|
+
externalErrorText: S.String
|
|
81
|
+
}) {};
|
|
82
|
+
var VitestDryRunOutput$1 = class extends S.TaggedClass()("VitestDryRunOutput", {
|
|
83
|
+
status: S.Literals(["Complete", "Error"]),
|
|
84
|
+
testsJson: S.String,
|
|
85
|
+
errorMessage: S.optional(S.String)
|
|
86
|
+
}) {};
|
|
87
|
+
S.TaggedError()("VitestDryRunError", { message: S.String });
|
|
88
|
+
const recordOption$1 = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
|
|
89
|
+
const getStringField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
|
|
90
|
+
const getNumberField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
|
|
91
|
+
const getSuite$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["suite"])));
|
|
92
|
+
const getFile$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["file"])));
|
|
93
|
+
const getResult$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["result"])));
|
|
94
|
+
const getErrors$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["errors"])), Option.filter((v) => Array.isArray(v)));
|
|
95
|
+
const getMessage$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["message"])), Option.filter((v) => typeof v === "string"));
|
|
96
|
+
const getName$1 = (value) => Option.match(recordOption$1(value), {
|
|
97
|
+
onNone: () => "",
|
|
98
|
+
onSome: (rec) => Option.getOrElse(getStringField$1(rec, "name"), () => "")
|
|
99
|
+
});
|
|
100
|
+
const getMode$1 = (value) => Option.match(recordOption$1(value), {
|
|
101
|
+
onNone: () => "run",
|
|
102
|
+
onSome: (rec) => Option.getOrElse(getStringField$1(rec, "mode"), () => "run")
|
|
103
|
+
});
|
|
104
|
+
const getState$1 = (value) => Match.value(value).pipe(Match.when("pass", () => "pass"), Match.when("fail", () => "fail"), Match.when("skip", () => "skip"), Match.when("todo", () => "todo"), Match.when("run", () => "run"), Match.when("queued", () => "queued"), Match.when("only", () => "only"), Match.when(void 0, () => void 0), Match.orElse(() => void 0));
|
|
105
|
+
const getDuration$1 = (value) => Option.match(recordOption$1(value), {
|
|
106
|
+
onNone: () => 0,
|
|
107
|
+
onSome: (rec) => Option.getOrElse(getNumberField$1(rec, "duration"), () => 0)
|
|
108
|
+
});
|
|
109
|
+
const getFilepath$1 = (value) => Option.match(recordOption$1(value), {
|
|
110
|
+
onNone: () => void 0,
|
|
111
|
+
onSome: (rec) => Option.getOrUndefined(Option.fromNullishOr(rec["filepath"]).pipe(Option.filter((v) => typeof v === "string")))
|
|
112
|
+
});
|
|
113
|
+
const collectSuiteNames$1 = (suite) => Option.match(Option.fromNullishOr(suite), {
|
|
114
|
+
onNone: () => [],
|
|
115
|
+
onSome: (current) => Option.match(recordOption$1(current), {
|
|
116
|
+
onNone: () => [],
|
|
117
|
+
onSome: (rec) => {
|
|
118
|
+
const name = Option.getOrElse(getStringField$1(rec, "name"), () => "");
|
|
119
|
+
const hasName = name.length > 0;
|
|
120
|
+
const parentNames = collectSuiteNames$1(rec["suite"]);
|
|
121
|
+
return Match.value(hasName).pipe(Match.when(true, () => [...parentNames, name]), Match.when(false, () => parentNames), Match.exhaustive);
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
const collectTestNameRaw$1 = (test) => {
|
|
126
|
+
const name = getName$1(test);
|
|
127
|
+
const suite = Option.getOrUndefined(getSuite$1(test));
|
|
128
|
+
return [...collectSuiteNames$1(suite), name].join(" ").trim();
|
|
129
|
+
};
|
|
130
|
+
const toRawTestIdRaw$1 = (test) => {
|
|
131
|
+
return `${Option.match(getFile$1(test), {
|
|
132
|
+
onNone: () => "unknown.js",
|
|
133
|
+
onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath$1(file)), () => "unknown.js")
|
|
134
|
+
})}#${collectTestNameRaw$1(test)}`;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* A test id is `<file>#<test name>`, and the file is reported relative to the
|
|
138
|
+
* project root so an id is stable across machines and sandbox directories.
|
|
139
|
+
* Vitest reports an absolute path, so the root prefix is stripped here rather
|
|
140
|
+
* than resolved — a decision body has no path service and needs none.
|
|
141
|
+
*/
|
|
142
|
+
const normalizeTestIdRaw$1 = (id, projectRoot) => {
|
|
143
|
+
const hash = id.indexOf("#");
|
|
144
|
+
if (hash === -1) return id;
|
|
145
|
+
const file = id.slice(0, hash);
|
|
146
|
+
const rest = id.slice(hash + 1);
|
|
147
|
+
return `${(() => {
|
|
148
|
+
if (file.startsWith(projectRoot)) return file.slice(projectRoot.length);
|
|
149
|
+
return file;
|
|
150
|
+
})().replace(/^[/\\]+/, "").replaceAll("\\", "/")}#${rest}`;
|
|
151
|
+
};
|
|
152
|
+
const toTestStatus$1 = (taskState, mode) => Match.value(mode === "skip").pipe(Match.when(true, () => "skipped"), Match.when(false, () => Match.value(taskState).pipe(Match.when("pass", () => "success"), Match.when("fail", () => "failed"), Match.when("skip", () => "skipped"), Match.when("todo", () => "skipped"), Match.when(void 0, () => "failed"), Match.when("queued", () => "failed"), Match.when("run", () => "failed"), Match.when("only", () => "failed"), Match.orElse(() => "failed"))), Match.exhaustive);
|
|
153
|
+
const findSuiteErrorRaw$1 = (suite) => Option.match(Option.fromNullishOr(suite), {
|
|
154
|
+
onNone: () => void 0,
|
|
155
|
+
onSome: (current) => Option.match(recordOption$1(current), {
|
|
156
|
+
onNone: () => void 0,
|
|
157
|
+
onSome: (rec) => {
|
|
158
|
+
const maybeError = Option.flatMap(getResult$1(rec), (result) => Option.flatMap(getErrors$1(result), (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.flatMap(Option.fromNullishOr(errs[0]), (first) => getMessage$1(first))), Match.when(false, () => Option.none()), Match.exhaustive)));
|
|
159
|
+
return Option.match(maybeError, {
|
|
160
|
+
onNone: () => findSuiteErrorRaw$1(rec["suite"]),
|
|
161
|
+
onSome: (msg) => msg
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
const extractStatus$1 = (test) => {
|
|
167
|
+
const result = Option.getOrUndefined(getResult$1(test));
|
|
168
|
+
const mode = getMode$1(test);
|
|
169
|
+
const state = Option.match(Option.fromNullishOr(result), {
|
|
170
|
+
onNone: () => void 0,
|
|
171
|
+
onSome: (r) => Option.match(recordOption$1(r), {
|
|
172
|
+
onNone: () => void 0,
|
|
173
|
+
onSome: (rec) => getState$1(rec["state"])
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
return toTestStatus$1(state, mode);
|
|
177
|
+
};
|
|
178
|
+
const extractDuration$1 = (test) => Option.match(getResult$1(test), {
|
|
179
|
+
onNone: () => 0,
|
|
180
|
+
onSome: (result) => Option.match(recordOption$1(result), {
|
|
181
|
+
onNone: () => 0,
|
|
182
|
+
onSome: (rec) => getDuration$1(rec)
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
const extractFileName$1 = (test) => Option.match(getFile$1(test), {
|
|
186
|
+
onNone: () => void 0,
|
|
187
|
+
onSome: (file) => getFilepath$1(file)
|
|
188
|
+
});
|
|
189
|
+
const extractRawId$1 = (test, projectRoot) => normalizeTestIdRaw$1(toRawTestIdRaw$1(test), projectRoot);
|
|
190
|
+
const extractName$1 = (test) => collectTestNameRaw$1(test);
|
|
191
|
+
const extractFailureMessage$1 = (test) => Option.match(getResult$1(test), {
|
|
192
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
193
|
+
onSome: (result) => Option.match(getErrors$1(result), {
|
|
194
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
195
|
+
onSome: (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.match(Option.fromNullishOr(errs[0]), {
|
|
196
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
197
|
+
onSome: (first) => Option.getOrElse(getMessage$1(first), () => "StrykerJS: Unknown test failure")
|
|
198
|
+
})), Match.when(false, () => "StrykerJS: Unknown test failure"), Match.exhaustive)
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
const convertTestRaw$1 = (test, projectRoot) => {
|
|
202
|
+
const status = extractStatus$1(test);
|
|
203
|
+
const base = {
|
|
204
|
+
id: extractRawId$1(test, projectRoot),
|
|
205
|
+
name: extractName$1(test),
|
|
206
|
+
timeSpentMs: extractDuration$1(test),
|
|
207
|
+
fileName: extractFileName$1(test),
|
|
208
|
+
status
|
|
209
|
+
};
|
|
210
|
+
return Match.value(status).pipe(Match.when("failed", () => ({
|
|
211
|
+
...base,
|
|
212
|
+
status,
|
|
213
|
+
failureMessage: extractFailureMessage$1(test)
|
|
214
|
+
})), Match.when("skipped", () => Match.value(findSuiteErrorRaw$1(Option.getOrUndefined(getSuite$1(test)))).pipe(Match.when(Match.defined, (suiteError) => ({
|
|
215
|
+
...base,
|
|
216
|
+
status: "failed",
|
|
217
|
+
failureMessage: suiteError
|
|
218
|
+
})), Match.orElse(() => ({
|
|
219
|
+
...base,
|
|
220
|
+
status
|
|
221
|
+
})))), Match.orElse(() => ({
|
|
222
|
+
...base,
|
|
223
|
+
status
|
|
224
|
+
})));
|
|
225
|
+
};
|
|
226
|
+
const decideVitestDryRun$1 = (command) => {
|
|
227
|
+
const tests = command.rawTests.map((t) => convertTestRaw$1(t, command.projectRoot));
|
|
228
|
+
const hasFailure = tests.some((t) => t.status === "failed");
|
|
229
|
+
return Match.value({
|
|
230
|
+
hasFailure,
|
|
231
|
+
hasExternalError: command.hasExternalError
|
|
232
|
+
}).pipe(Match.when({
|
|
233
|
+
hasFailure: false,
|
|
234
|
+
hasExternalError: true
|
|
235
|
+
}, () => Result.succeed(VitestDryRunOutput$1.make({
|
|
236
|
+
status: "Error",
|
|
237
|
+
testsJson: JSON.stringify(tests),
|
|
238
|
+
errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
|
|
239
|
+
}))), Match.orElse(() => Result.succeed(VitestDryRunOutput$1.make({
|
|
240
|
+
status: "Complete",
|
|
241
|
+
testsJson: JSON.stringify(tests),
|
|
242
|
+
errorMessage: void 0
|
|
243
|
+
}))));
|
|
244
|
+
};
|
|
245
|
+
const vitestDryRunWorkflow = Workflow.make(VitestDryRunCommand$1, decideVitestDryRun$1);
|
|
39
246
|
//#endregion
|
|
40
|
-
//#region src/
|
|
247
|
+
//#region src/VitestMutantRun.workflow.ts
|
|
248
|
+
/**
|
|
249
|
+
* VitestMutantRun workflow — pure result-mapping for the mutant-run phase.
|
|
250
|
+
*/
|
|
251
|
+
var VitestMutantRunCommand = class extends S.TaggedClass()("VitestMutantRunCommand", {
|
|
252
|
+
rawTests: S.Array(S.Unknown),
|
|
253
|
+
projectRoot: S.String,
|
|
254
|
+
hasExternalError: S.Boolean,
|
|
255
|
+
externalErrorText: S.String,
|
|
256
|
+
hitCount: S.optional(S.Finite),
|
|
257
|
+
hitLimit: S.optional(S.Finite),
|
|
258
|
+
reportAllKillers: S.Boolean
|
|
259
|
+
}) {};
|
|
260
|
+
var VitestMutantRunOutput = class extends S.TaggedClass()("VitestMutantRunOutput", {
|
|
261
|
+
status: S.Literals([
|
|
262
|
+
"Killed",
|
|
263
|
+
"Survived",
|
|
264
|
+
"Timeout",
|
|
265
|
+
"Error"
|
|
266
|
+
]),
|
|
267
|
+
testsJson: S.String,
|
|
268
|
+
errorMessage: S.optional(S.String),
|
|
269
|
+
killerIds: S.optional(S.Array(S.String)),
|
|
270
|
+
failureMessage: S.optional(S.String),
|
|
271
|
+
reason: S.optional(S.String)
|
|
272
|
+
}) {};
|
|
273
|
+
var VitestMutantRunError = class extends S.TaggedError()("VitestMutantRunError", { message: S.String }) {};
|
|
274
|
+
var VitestDryRunCommand = class extends S.TaggedClass()("VitestDryRunCommand", {
|
|
275
|
+
rawTests: S.Array(S.Unknown),
|
|
276
|
+
projectRoot: S.String,
|
|
277
|
+
hasExternalError: S.Boolean,
|
|
278
|
+
externalErrorText: S.String
|
|
279
|
+
}) {};
|
|
280
|
+
var VitestDryRunOutput = class extends S.TaggedClass()("VitestDryRunOutput", {
|
|
281
|
+
status: S.Literals(["Complete", "Error"]),
|
|
282
|
+
testsJson: S.String,
|
|
283
|
+
errorMessage: S.optional(S.String)
|
|
284
|
+
}) {};
|
|
285
|
+
const recordOption = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
|
|
286
|
+
const getStringField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
|
|
287
|
+
const getNumberField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
|
|
288
|
+
const getSuite = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["suite"])));
|
|
289
|
+
const getFile = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["file"])));
|
|
290
|
+
const getResult = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["result"])));
|
|
291
|
+
const getErrors = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["errors"])), Option.filter((v) => Array.isArray(v)));
|
|
292
|
+
const getMessage = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["message"])), Option.filter((v) => typeof v === "string"));
|
|
293
|
+
const getName = (value) => Option.match(recordOption(value), {
|
|
294
|
+
onNone: () => "",
|
|
295
|
+
onSome: (rec) => Option.getOrElse(getStringField(rec, "name"), () => "")
|
|
296
|
+
});
|
|
297
|
+
const getMode = (value) => Option.match(recordOption(value), {
|
|
298
|
+
onNone: () => "run",
|
|
299
|
+
onSome: (rec) => Option.getOrElse(getStringField(rec, "mode"), () => "run")
|
|
300
|
+
});
|
|
301
|
+
const getState = (value) => Match.value(value).pipe(Match.when("pass", () => "pass"), Match.when("fail", () => "fail"), Match.when("skip", () => "skip"), Match.when("todo", () => "todo"), Match.when("run", () => "run"), Match.when("queued", () => "queued"), Match.when("only", () => "only"), Match.when(void 0, () => void 0), Match.orElse(() => void 0));
|
|
302
|
+
const getDuration = (value) => Option.match(recordOption(value), {
|
|
303
|
+
onNone: () => 0,
|
|
304
|
+
onSome: (rec) => Option.getOrElse(getNumberField(rec, "duration"), () => 0)
|
|
305
|
+
});
|
|
306
|
+
const getFilepath = (value) => Option.match(recordOption(value), {
|
|
307
|
+
onNone: () => void 0,
|
|
308
|
+
onSome: (rec) => Option.getOrUndefined(Option.fromNullishOr(rec["filepath"]).pipe(Option.filter((v) => typeof v === "string")))
|
|
309
|
+
});
|
|
310
|
+
const collectSuiteNames = (suite) => Option.match(Option.fromNullishOr(suite), {
|
|
311
|
+
onNone: () => [],
|
|
312
|
+
onSome: (current) => Option.match(recordOption(current), {
|
|
313
|
+
onNone: () => [],
|
|
314
|
+
onSome: (rec) => {
|
|
315
|
+
const name = Option.getOrElse(getStringField(rec, "name"), () => "");
|
|
316
|
+
const hasName = name.length > 0;
|
|
317
|
+
const parentNames = collectSuiteNames(rec["suite"]);
|
|
318
|
+
return Match.value(hasName).pipe(Match.when(true, () => [...parentNames, name]), Match.when(false, () => parentNames), Match.exhaustive);
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
});
|
|
322
|
+
const collectTestNameRaw = (test) => {
|
|
323
|
+
const name = getName(test);
|
|
324
|
+
const suite = Option.getOrUndefined(getSuite(test));
|
|
325
|
+
return [...collectSuiteNames(suite), name].join(" ").trim();
|
|
326
|
+
};
|
|
327
|
+
const toRawTestIdRaw = (test) => {
|
|
328
|
+
return `${Option.match(getFile(test), {
|
|
329
|
+
onNone: () => "unknown.js",
|
|
330
|
+
onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath(file)), () => "unknown.js")
|
|
331
|
+
})}#${collectTestNameRaw(test)}`;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* A test id is `<file>#<test name>`, and the file is reported relative to the
|
|
335
|
+
* project root so an id is stable across machines and sandbox directories.
|
|
336
|
+
* Vitest reports an absolute path, so the root prefix is stripped here rather
|
|
337
|
+
* than resolved — a decision body has no path service and needs none.
|
|
338
|
+
*/
|
|
339
|
+
const normalizeTestIdRaw = (id, projectRoot) => {
|
|
340
|
+
const hash = id.indexOf("#");
|
|
341
|
+
if (hash === -1) return id;
|
|
342
|
+
const file = id.slice(0, hash);
|
|
343
|
+
const rest = id.slice(hash + 1);
|
|
344
|
+
return `${(() => {
|
|
345
|
+
if (file.startsWith(projectRoot)) return file.slice(projectRoot.length);
|
|
346
|
+
return file;
|
|
347
|
+
})().replace(/^[/\\]+/, "").replaceAll("\\", "/")}#${rest}`;
|
|
348
|
+
};
|
|
349
|
+
const toTestStatus = (taskState, mode) => Match.value(mode === "skip").pipe(Match.when(true, () => "skipped"), Match.when(false, () => Match.value(taskState).pipe(Match.when("pass", () => "success"), Match.when("fail", () => "failed"), Match.when("skip", () => "skipped"), Match.when("todo", () => "skipped"), Match.when(void 0, () => "failed"), Match.when("queued", () => "failed"), Match.when("run", () => "failed"), Match.when("only", () => "failed"), Match.orElse(() => "failed"))), Match.exhaustive);
|
|
350
|
+
const findSuiteErrorRaw = (suite) => Option.match(Option.fromNullishOr(suite), {
|
|
351
|
+
onNone: () => void 0,
|
|
352
|
+
onSome: (current) => Option.match(recordOption(current), {
|
|
353
|
+
onNone: () => void 0,
|
|
354
|
+
onSome: (rec) => {
|
|
355
|
+
const maybeError = Option.flatMap(getResult(rec), (result) => Option.flatMap(getErrors(result), (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.flatMap(Option.fromNullishOr(errs[0]), (first) => getMessage(first))), Match.when(false, () => Option.none()), Match.exhaustive)));
|
|
356
|
+
return Option.match(maybeError, {
|
|
357
|
+
onNone: () => findSuiteErrorRaw(rec["suite"]),
|
|
358
|
+
onSome: (msg) => msg
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
const extractStatus = (test) => {
|
|
364
|
+
const result = Option.getOrUndefined(getResult(test));
|
|
365
|
+
const mode = getMode(test);
|
|
366
|
+
const state = Option.match(Option.fromNullishOr(result), {
|
|
367
|
+
onNone: () => void 0,
|
|
368
|
+
onSome: (r) => Option.match(recordOption(r), {
|
|
369
|
+
onNone: () => void 0,
|
|
370
|
+
onSome: (rec) => getState(rec["state"])
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
return toTestStatus(state, mode);
|
|
374
|
+
};
|
|
375
|
+
const extractDuration = (test) => Option.match(getResult(test), {
|
|
376
|
+
onNone: () => 0,
|
|
377
|
+
onSome: (result) => Option.match(recordOption(result), {
|
|
378
|
+
onNone: () => 0,
|
|
379
|
+
onSome: (rec) => getDuration(rec)
|
|
380
|
+
})
|
|
381
|
+
});
|
|
382
|
+
const extractFileName = (test) => Option.match(getFile(test), {
|
|
383
|
+
onNone: () => void 0,
|
|
384
|
+
onSome: (file) => getFilepath(file)
|
|
385
|
+
});
|
|
386
|
+
const extractRawId = (test, projectRoot) => normalizeTestIdRaw(toRawTestIdRaw(test), projectRoot);
|
|
387
|
+
const extractName = (test) => collectTestNameRaw(test);
|
|
388
|
+
const extractFailureMessage = (test) => Option.match(getResult(test), {
|
|
389
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
390
|
+
onSome: (result) => Option.match(getErrors(result), {
|
|
391
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
392
|
+
onSome: (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.match(Option.fromNullishOr(errs[0]), {
|
|
393
|
+
onNone: () => "StrykerJS: Unknown test failure",
|
|
394
|
+
onSome: (first) => Option.getOrElse(getMessage(first), () => "StrykerJS: Unknown test failure")
|
|
395
|
+
})), Match.when(false, () => "StrykerJS: Unknown test failure"), Match.exhaustive)
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
const convertTestRaw = (test, projectRoot) => {
|
|
399
|
+
const status = extractStatus(test);
|
|
400
|
+
const base = {
|
|
401
|
+
id: extractRawId(test, projectRoot),
|
|
402
|
+
name: extractName(test),
|
|
403
|
+
timeSpentMs: extractDuration(test),
|
|
404
|
+
fileName: extractFileName(test),
|
|
405
|
+
status
|
|
406
|
+
};
|
|
407
|
+
return Match.value(status).pipe(Match.when("failed", () => ({
|
|
408
|
+
...base,
|
|
409
|
+
status,
|
|
410
|
+
failureMessage: extractFailureMessage(test)
|
|
411
|
+
})), Match.when("skipped", () => Match.value(findSuiteErrorRaw(Option.getOrUndefined(getSuite(test)))).pipe(Match.when(Match.defined, (suiteError) => ({
|
|
412
|
+
...base,
|
|
413
|
+
status: "failed",
|
|
414
|
+
failureMessage: suiteError
|
|
415
|
+
})), Match.orElse(() => ({
|
|
416
|
+
...base,
|
|
417
|
+
status
|
|
418
|
+
})))), Match.orElse(() => ({
|
|
419
|
+
...base,
|
|
420
|
+
status
|
|
421
|
+
})));
|
|
422
|
+
};
|
|
423
|
+
const decideVitestDryRun = (command) => Match.value(command.rawTests.map((t) => convertTestRaw(t, command.projectRoot))).pipe(Match.when((tests) => tests.some((t) => t.status === "failed") === false && command.hasExternalError === true, (tests) => Result.succeed(VitestDryRunOutput.make({
|
|
424
|
+
status: "Error",
|
|
425
|
+
testsJson: JSON.stringify(tests),
|
|
426
|
+
errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
|
|
427
|
+
}))), Match.orElse((tests) => Result.succeed(VitestDryRunOutput.make({
|
|
428
|
+
status: "Complete",
|
|
429
|
+
testsJson: JSON.stringify(tests),
|
|
430
|
+
errorMessage: void 0
|
|
431
|
+
}))));
|
|
432
|
+
/**
|
|
433
|
+
* The hit-limit cutoff: `Some` iff both numbers are present AND the count
|
|
434
|
+
* strictly exceeds the limit. The boundary is `>` and not `>=` on purpose —
|
|
435
|
+
* `hitCount === hitLimit` is the last permitted hit, not one too many.
|
|
436
|
+
*/
|
|
437
|
+
const hitLimitReason = (hitCount, hitLimit) => {
|
|
438
|
+
if (hitCount === void 0 || hitLimit === void 0) return Option.none();
|
|
439
|
+
if (hitCount > hitLimit) return Option.some(`Hit limit reached (${hitCount}/${hitLimit})`);
|
|
440
|
+
return Option.none();
|
|
441
|
+
};
|
|
442
|
+
const decideVitestMutantRun = (command) => Match.value(hitLimitReason(command.hitCount, command.hitLimit)).pipe(Match.when(Option.isSome, (hit) => Result.succeed(VitestMutantRunOutput.make({
|
|
443
|
+
status: "Timeout",
|
|
444
|
+
testsJson: "[]",
|
|
445
|
+
errorMessage: void 0,
|
|
446
|
+
killerIds: void 0,
|
|
447
|
+
failureMessage: void 0,
|
|
448
|
+
reason: hit.value
|
|
449
|
+
}))), Match.when(Option.isNone, () => {
|
|
450
|
+
const dryOut = decideVitestDryRun(VitestDryRunCommand.make({
|
|
451
|
+
rawTests: command.rawTests,
|
|
452
|
+
projectRoot: command.projectRoot,
|
|
453
|
+
hasExternalError: command.hasExternalError,
|
|
454
|
+
externalErrorText: command.externalErrorText
|
|
455
|
+
}));
|
|
456
|
+
return Match.value(Result.isFailure(dryOut)).pipe(Match.when(true, () => Result.fail(new VitestMutantRunError({ message: "dry run mapping failed" }))), Match.when(false, () => {
|
|
457
|
+
const dry = Result.getOrElse(dryOut, () => VitestDryRunOutput.make({
|
|
458
|
+
status: "Complete",
|
|
459
|
+
testsJson: "[]",
|
|
460
|
+
errorMessage: void 0
|
|
461
|
+
}));
|
|
462
|
+
return Match.value(dry.status === "Error").pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
|
|
463
|
+
status: "Error",
|
|
464
|
+
testsJson: "[]",
|
|
465
|
+
errorMessage: dry.errorMessage,
|
|
466
|
+
killerIds: void 0,
|
|
467
|
+
failureMessage: void 0
|
|
468
|
+
}))), Match.when(false, () => {
|
|
469
|
+
const testsOption = Option.liftThrowable((input) => JSON.parse(input))(dry.testsJson).pipe(Option.filter((v) => Array.isArray(v)));
|
|
470
|
+
const killed = Option.getOrElse(testsOption, () => []).filter((t) => t.status === "failed");
|
|
471
|
+
return Match.value(killed.length > 0).pipe(Match.when(true, () => Match.value(command.reportAllKillers).pipe(Match.when(true, () => {
|
|
472
|
+
const firstKiller = Option.fromNullishOr(killed[0]);
|
|
473
|
+
const failureMessage = Option.match(firstKiller, {
|
|
474
|
+
onNone: () => void 0,
|
|
475
|
+
onSome: (k) => k.failureMessage
|
|
476
|
+
});
|
|
477
|
+
return Result.succeed(VitestMutantRunOutput.make({
|
|
478
|
+
status: "Killed",
|
|
479
|
+
testsJson: dry.testsJson,
|
|
480
|
+
errorMessage: void 0,
|
|
481
|
+
killerIds: killed.map((t) => t.id),
|
|
482
|
+
failureMessage
|
|
483
|
+
}));
|
|
484
|
+
}), Match.when(false, () => {
|
|
485
|
+
const first = Option.fromNullishOr(killed[0]);
|
|
486
|
+
const failureMessage = Option.match(first, {
|
|
487
|
+
onNone: () => void 0,
|
|
488
|
+
onSome: (k) => k.failureMessage
|
|
489
|
+
});
|
|
490
|
+
return Match.value(Option.isSome(first)).pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
|
|
491
|
+
status: "Killed",
|
|
492
|
+
testsJson: dry.testsJson,
|
|
493
|
+
errorMessage: void 0,
|
|
494
|
+
killerIds: Option.match(first, {
|
|
495
|
+
onNone: () => void 0,
|
|
496
|
+
onSome: (k) => [k.id]
|
|
497
|
+
}),
|
|
498
|
+
failureMessage
|
|
499
|
+
}))), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
|
|
500
|
+
status: "Killed",
|
|
501
|
+
testsJson: dry.testsJson,
|
|
502
|
+
errorMessage: void 0,
|
|
503
|
+
killerIds: Option.getOrUndefined(Option.match(first, {
|
|
504
|
+
onNone: () => Option.none(),
|
|
505
|
+
onSome: (k) => Option.some([k.id])
|
|
506
|
+
})),
|
|
507
|
+
failureMessage
|
|
508
|
+
}))), Match.exhaustive);
|
|
509
|
+
}), Match.exhaustive)), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
|
|
510
|
+
status: "Survived",
|
|
511
|
+
testsJson: dry.testsJson,
|
|
512
|
+
errorMessage: void 0,
|
|
513
|
+
killerIds: void 0,
|
|
514
|
+
failureMessage: void 0
|
|
515
|
+
}))), Match.exhaustive);
|
|
516
|
+
}), Match.exhaustive);
|
|
517
|
+
}), Match.exhaustive);
|
|
518
|
+
}), Match.exhaustive);
|
|
519
|
+
const vitestMutantRunWorkflow = Workflow.make(VitestMutantRunCommand, decideVitestMutantRun);
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/Runner.ts
|
|
522
|
+
function fromTestId(id) {
|
|
523
|
+
const [file, ...name] = id.split("#");
|
|
524
|
+
return {
|
|
525
|
+
file,
|
|
526
|
+
test: name.join("#")
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function normalizeTestId(id, projectRoot, pathService) {
|
|
530
|
+
const { file, test } = fromTestId(id);
|
|
531
|
+
return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
|
|
532
|
+
}
|
|
533
|
+
function normalizeCoverage(rawCoverage, projectRoot, pathService) {
|
|
534
|
+
return {
|
|
535
|
+
perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
|
|
536
|
+
static: rawCoverage.static
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function collectTestsFromSuite(suite) {
|
|
540
|
+
return suite.tasks.flatMap((task) => {
|
|
541
|
+
if (task.type === "suite") return collectTestsFromSuite(task);
|
|
542
|
+
return task;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function isErrorCodeError(error) {
|
|
546
|
+
if (error instanceof Error && "code" in error) return typeof Reflect.get(error, "code") === "string";
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
/** @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/errors.ts */
|
|
550
|
+
const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
|
|
41
551
|
const SOURCE_CONDITION = "@systemfsoftware/source";
|
|
42
552
|
const sourceTargetOf = (entry) => {
|
|
43
|
-
if (typeof entry === "string") return
|
|
553
|
+
if (typeof entry === "string") return (() => {
|
|
554
|
+
if (entry.endsWith(".ts") || entry.endsWith(".tsx") || entry.endsWith(".mts")) return entry;
|
|
555
|
+
})();
|
|
44
556
|
const source = entry[SOURCE_CONDITION];
|
|
45
|
-
return
|
|
557
|
+
return (() => {
|
|
558
|
+
if (typeof source === "string") return source;
|
|
559
|
+
})();
|
|
46
560
|
};
|
|
47
561
|
const specifierForExport = (packageName, exportKey) => {
|
|
48
562
|
if (exportKey === ".") return packageName;
|
|
49
|
-
if (exportKey === "./package.json" || !exportKey.startsWith("./")) return;
|
|
563
|
+
if (exportKey === "./package.json" || !exportKey.startsWith("./")) return void 0;
|
|
50
564
|
return `${packageName}/${exportKey.slice(2)}`;
|
|
51
565
|
};
|
|
52
566
|
const sandboxSelfAliases = (manifest, projectRoot, pathService) => {
|
|
@@ -87,117 +601,13 @@ const readSandboxSelfAliases = (projectRoot) => Effect$1.gen(function* () {
|
|
|
87
601
|
* zero tests. Returning the sandbox source path from `resolveId` makes the
|
|
88
602
|
* dep a real filesystem path related-mode can walk.
|
|
89
603
|
*/
|
|
90
|
-
const sandboxSelfPlugin = (aliases) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (const alias of aliases) if (alias.find.test(source)) return alias.replacement;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region src/test-identity.ts
|
|
101
|
-
function collectTestName({ name, suite }) {
|
|
102
|
-
const nameParts = [name];
|
|
103
|
-
let currentSuite = suite;
|
|
104
|
-
while (currentSuite) {
|
|
105
|
-
nameParts.unshift(currentSuite.name);
|
|
106
|
-
currentSuite = currentSuite.suite;
|
|
107
|
-
}
|
|
108
|
-
return nameParts.join(" ").trim();
|
|
109
|
-
}
|
|
110
|
-
function toRawTestId(test) {
|
|
111
|
-
return `${test.file?.filepath ?? "unknown.js"}#${collectTestName(test)}`;
|
|
112
|
-
}
|
|
113
|
-
//#endregion
|
|
114
|
-
//#region src/vitest-task-mapping.ts
|
|
115
|
-
function convertTaskStateToTestStatus(taskState, testMode) {
|
|
116
|
-
if (testMode === "skip") return TestStatus.Skipped;
|
|
117
|
-
switch (taskState) {
|
|
118
|
-
case "pass": return TestStatus.Success;
|
|
119
|
-
case "fail": return TestStatus.Failed;
|
|
120
|
-
case "skip":
|
|
121
|
-
case "todo": return TestStatus.Skipped;
|
|
122
|
-
case void 0:
|
|
123
|
-
case "queued":
|
|
124
|
-
case "run":
|
|
125
|
-
case "only": return TestStatus.Failed;
|
|
604
|
+
const sandboxSelfPlugin = (aliases) => ({
|
|
605
|
+
name: "stryker-sandbox-self-exports",
|
|
606
|
+
enforce: "pre",
|
|
607
|
+
resolveId(source) {
|
|
608
|
+
for (const alias of aliases) if (alias.find.test(source)) return alias.replacement;
|
|
126
609
|
}
|
|
127
|
-
}
|
|
128
|
-
function convertTestToTestResult(test, projectRoot, pathService) {
|
|
129
|
-
const status = convertTaskStateToTestStatus(test.result?.state, test.mode);
|
|
130
|
-
const baseTestResult = {
|
|
131
|
-
id: normalizeTestId(toRawTestId(test), projectRoot, pathService),
|
|
132
|
-
name: collectTestName(test),
|
|
133
|
-
timeSpentMs: test.result?.duration ?? 0,
|
|
134
|
-
fileName: test.file?.filepath && pathService.resolve(test.file.filepath)
|
|
135
|
-
};
|
|
136
|
-
if (status === TestStatus.Failed) return {
|
|
137
|
-
...baseTestResult,
|
|
138
|
-
status,
|
|
139
|
-
failureMessage: test.result?.errors?.[0]?.message ?? "StrykerJS: Unknown test failure"
|
|
140
|
-
};
|
|
141
|
-
else if (status === TestStatus.Skipped) {
|
|
142
|
-
const suiteError = findSuiteError(test.suite);
|
|
143
|
-
if (suiteError) return {
|
|
144
|
-
...baseTestResult,
|
|
145
|
-
status: TestStatus.Failed,
|
|
146
|
-
failureMessage: suiteError
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
...baseTestResult,
|
|
151
|
-
status
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
function findSuiteError(suite) {
|
|
155
|
-
if (!suite) return;
|
|
156
|
-
if (suite.result?.state === "fail") return suite.result?.errors?.[0]?.message ?? "StrykerJS: Suite execution failed";
|
|
157
|
-
return findSuiteError(suite.suite);
|
|
158
|
-
}
|
|
159
|
-
function fromTestId(id) {
|
|
160
|
-
const [file, ...name] = id.split("#");
|
|
161
|
-
return {
|
|
162
|
-
file,
|
|
163
|
-
test: name.join("#")
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function normalizeTestId(id, projectRoot, pathService) {
|
|
167
|
-
const { file, test } = fromTestId(id);
|
|
168
|
-
return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
|
|
169
|
-
}
|
|
170
|
-
function normalizeCoverage(rawCoverage, projectRoot, pathService) {
|
|
171
|
-
return {
|
|
172
|
-
perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
|
|
173
|
-
static: rawCoverage.static
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function collectTestsFromSuite(suite) {
|
|
177
|
-
return suite.tasks.flatMap((task) => {
|
|
178
|
-
if (task.type === "suite") return collectTestsFromSuite(task);
|
|
179
|
-
else if (task.type === "test") return task;
|
|
180
|
-
else return [];
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
function isErrorCodeError(error) {
|
|
184
|
-
return error instanceof Error && "code" in error && typeof error.code === "string";
|
|
185
|
-
}
|
|
186
|
-
/** @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/errors.ts */
|
|
187
|
-
const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/vitest-wrapper.schema.ts
|
|
190
|
-
/**
|
|
191
|
-
* The dynamically imported project-local `vitest/node` module. The runtime
|
|
192
|
-
* check only asserts object-likeness: the module namespace is whatever the
|
|
193
|
-
* resolved package exports, and the consumers tolerate a missing
|
|
194
|
-
* `createVitest` via their own fallbacks.
|
|
195
|
-
*/
|
|
196
|
-
const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
|
|
197
|
-
/** The `package.json` document of a resolved vitest package. */
|
|
198
|
-
const VitestPackageSchema = S.Struct({ version: S.String });
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region src/vitest-wrapper.ts
|
|
610
|
+
});
|
|
201
611
|
const readVitestVersion = (readPackageJson) => S.decodeUnknownSync(VitestPackageSchema)(readPackageJson()).version;
|
|
202
612
|
/** Falls back to the Vitest bundled with this package when `dir` has none. */
|
|
203
613
|
const resolveVitest = async (dir) => {
|
|
@@ -206,7 +616,7 @@ const resolveVitest = async (dir) => {
|
|
|
206
616
|
const vitestNodePath = projectRequire.resolve("vitest/node");
|
|
207
617
|
const vitestNodeUrl = pathToFileURL(vitestNodePath).href;
|
|
208
618
|
return {
|
|
209
|
-
createVitest: S.decodeUnknownSync(VitestNodeModuleSchema)(await import(vitestNodeUrl)).createVitest
|
|
619
|
+
createVitest: S.decodeUnknownSync(VitestNodeModuleSchema)(await import(vitestNodeUrl)).createVitest,
|
|
210
620
|
version: readVitestVersion(() => projectRequire(projectRequire.resolve("vitest/package.json")))
|
|
211
621
|
};
|
|
212
622
|
} catch {
|
|
@@ -217,20 +627,6 @@ const resolveVitest = async (dir) => {
|
|
|
217
627
|
};
|
|
218
628
|
}
|
|
219
629
|
};
|
|
220
|
-
//#endregion
|
|
221
|
-
//#region src/vitest-runner-coverage.schema.ts
|
|
222
|
-
const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
|
|
223
|
-
const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
|
|
224
|
-
static: S.Record(S.String, S.Finite),
|
|
225
|
-
perTest: S.Record(S.String, S.Record(S.String, S.Finite))
|
|
226
|
-
})) });
|
|
227
|
-
const MutantCoverageShapeSchema = S.Struct({
|
|
228
|
-
static: S.Record(S.String, S.Finite),
|
|
229
|
-
perTest: S.Record(S.String, S.Record(S.String, S.Finite))
|
|
230
|
-
});
|
|
231
|
-
var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
|
|
232
|
-
//#endregion
|
|
233
|
-
//#region src/vitest-test-runner.ts
|
|
234
630
|
const isRunnerTestSuite = (value) => typeof value === "object" && value !== null && "tasks" in value && Array.isArray(Reflect.get(value, "tasks"));
|
|
235
631
|
const STRYKER_SETUP = fileURLToPath(new URL("./stryker-setup.mjs", import.meta.url));
|
|
236
632
|
const shouldUseSuiteMetaSecondArg = (version) => {
|
|
@@ -262,7 +658,10 @@ const experimentalStateHasExternalErrors = (vitest) => {
|
|
|
262
658
|
if (errorsSet instanceof Set) return errorsSet.size > 0;
|
|
263
659
|
if (typeof errorsSet === "object" && errorsSet !== null && "size" in errorsSet) {
|
|
264
660
|
const size = Reflect.get(errorsSet, "size");
|
|
265
|
-
return
|
|
661
|
+
return (() => {
|
|
662
|
+
if (typeof size === "number") return size > 0;
|
|
663
|
+
return false;
|
|
664
|
+
})();
|
|
266
665
|
}
|
|
267
666
|
}
|
|
268
667
|
}
|
|
@@ -289,7 +688,10 @@ const applySetupFilesToProjects = (vitest, localSetupFile) => {
|
|
|
289
688
|
if (typeof browser === "object" && browser !== null) Reflect.set(browser, "screenshotFailures", false);
|
|
290
689
|
for (const project of vitest.projects) {
|
|
291
690
|
const setupFilesRaw = Reflect.get(project.config, "setupFiles");
|
|
292
|
-
const files =
|
|
691
|
+
const files = (() => {
|
|
692
|
+
if (Array.isArray(setupFilesRaw)) return setupFilesRaw.filter((x) => typeof x === "string");
|
|
693
|
+
return [];
|
|
694
|
+
})();
|
|
293
695
|
Reflect.set(project.config, "setupFiles", [localSetupFile, ...files]);
|
|
294
696
|
const pBrowser = Reflect.get(project.config, "browser");
|
|
295
697
|
if (typeof pBrowser === "object" && pBrowser !== null) Reflect.set(pBrowser, "screenshotFailures", false);
|
|
@@ -308,14 +710,17 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
308
710
|
if (state.ctx === void 0) return yield* new TestRunnerFailed({
|
|
309
711
|
runnerName: "vitest",
|
|
310
712
|
phase: "dryRun",
|
|
311
|
-
cause: /* @__PURE__ */ new Error("Vitest runner is not initialized; call init() before running tests")
|
|
713
|
+
cause: errorToString(/* @__PURE__ */ new Error("Vitest runner is not initialized; call init() before running tests"))
|
|
312
714
|
});
|
|
313
715
|
return state.ctx;
|
|
314
716
|
});
|
|
315
|
-
const decodedOptionsEffect = (raw) => S.decodeUnknownEffect(VitestSectionSchema)(raw).pipe(Effect$1.map((decoded) =>
|
|
717
|
+
const decodedOptionsEffect = (raw) => S.decodeUnknownEffect(VitestSectionSchema)(raw).pipe(Effect$1.map((decoded) => (() => {
|
|
718
|
+
if (decoded === void 0) return { related: true };
|
|
719
|
+
return decoded;
|
|
720
|
+
})()), Effect$1.mapError((cause) => new TestRunnerFailed({
|
|
316
721
|
runnerName: "vitest",
|
|
317
722
|
phase: "init",
|
|
318
|
-
cause
|
|
723
|
+
cause: errorToString(cause)
|
|
319
724
|
})));
|
|
320
725
|
const optionsEffect = decodedOptionsEffect(Reflect.get(input.options, "vitest")).pipe(Effect$1.map((vitestOptions) => ({
|
|
321
726
|
...input.options,
|
|
@@ -337,7 +742,7 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
337
742
|
yield* fsService.copyFile(input.setupFilePath ?? STRYKER_SETUP, localSetupFile).pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
|
|
338
743
|
runnerName: "vitest",
|
|
339
744
|
phase: "init",
|
|
340
|
-
cause
|
|
745
|
+
cause: errorToString(cause)
|
|
341
746
|
})));
|
|
342
747
|
const resolver = input.resolveVitestFor ?? resolveVitest;
|
|
343
748
|
const { createVitest, version } = yield* Effect$1.tryPromise({
|
|
@@ -345,11 +750,13 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
345
750
|
catch: (cause) => new TestRunnerFailed({
|
|
346
751
|
runnerName: "vitest",
|
|
347
752
|
phase: "init",
|
|
348
|
-
cause
|
|
753
|
+
cause: errorToString(cause)
|
|
349
754
|
})
|
|
350
755
|
});
|
|
351
756
|
const namespace = input.globalNamespace ?? INSTRUMENTER_CONSTANTS.NAMESPACE;
|
|
352
|
-
const scanDir =
|
|
757
|
+
const scanDir = (() => {
|
|
758
|
+
if (typeof options.vitest.dir === "string") return pathService.resolve(projectRoot, options.vitest.dir);
|
|
759
|
+
})();
|
|
353
760
|
const aliases = yield* readSandboxSelfAliases(projectRoot).pipe(Effect$1.provideService(FileSystem.FileSystem, fsService), Effect$1.provideService(Path.Path, pathService));
|
|
354
761
|
const plugin = sandboxSelfPlugin(aliases);
|
|
355
762
|
const ctx = yield* Effect$1.tryPromise({
|
|
@@ -360,9 +767,17 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
360
767
|
maxConcurrency: 1,
|
|
361
768
|
watch: false,
|
|
362
769
|
root: projectRoot,
|
|
363
|
-
...
|
|
364
|
-
|
|
365
|
-
|
|
770
|
+
...(() => {
|
|
771
|
+
if (scanDir === void 0) return {};
|
|
772
|
+
return { dir: scanDir };
|
|
773
|
+
})(),
|
|
774
|
+
bail: (() => {
|
|
775
|
+
if (options.disableBail) return 0;
|
|
776
|
+
return 1;
|
|
777
|
+
})(),
|
|
778
|
+
onConsoleLog: () => false,
|
|
779
|
+
silent: true,
|
|
780
|
+
reporters: [{ onInit(_vitest) {} }]
|
|
366
781
|
}, {
|
|
367
782
|
resolve: {
|
|
368
783
|
alias: [...aliases],
|
|
@@ -373,29 +788,31 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
373
788
|
catch: (cause) => new TestRunnerFailed({
|
|
374
789
|
runnerName: "vitest",
|
|
375
790
|
phase: "init",
|
|
376
|
-
cause
|
|
791
|
+
cause: errorToString(cause)
|
|
377
792
|
})
|
|
378
793
|
});
|
|
379
794
|
ctx.provide("globalNamespace", namespace);
|
|
380
795
|
ctx.provide("isGreaterThanVitest4Point1", shouldUseSuiteMetaSecondArg(version));
|
|
381
796
|
applySetupFilesToProjects(ctx, localSetupFile);
|
|
382
|
-
yield* Effect$1.logDebug(`vitest final config: ${JSON.stringify(ctx.config, null, 2)}`);
|
|
383
797
|
yield* Ref.update(stateRef, (s) => ({
|
|
384
798
|
...s,
|
|
385
799
|
ctx
|
|
386
800
|
}));
|
|
387
|
-
}).pipe(Effect$1.mapError((cause) =>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
801
|
+
}).pipe(Effect$1.mapError((cause) => (() => {
|
|
802
|
+
if (cause instanceof TestRunnerFailed) return cause;
|
|
803
|
+
return new TestRunnerFailed({
|
|
804
|
+
runnerName: "vitest",
|
|
805
|
+
phase: "init",
|
|
806
|
+
cause
|
|
807
|
+
});
|
|
808
|
+
})()));
|
|
392
809
|
const resetContext = Effect$1.gen(function* () {
|
|
393
810
|
const ctx = yield* requireCtx;
|
|
394
811
|
experimentalStateClearFiles(ctx);
|
|
395
812
|
});
|
|
396
|
-
const getFileMeta = (file) => {
|
|
813
|
+
const getFileMeta = (file) => (() => {
|
|
397
814
|
if (file !== null && typeof file === "object" && "meta" in file) return Reflect.get(file, "meta");
|
|
398
|
-
};
|
|
815
|
+
})();
|
|
399
816
|
const readHitCount = Effect$1.gen(function* () {
|
|
400
817
|
const ctx = yield* requireCtx.pipe(Effect$1.mapError((cause) => new CoverageDecodeFailed({ cause })));
|
|
401
818
|
const files = experimentalStateGetFiles(ctx);
|
|
@@ -412,11 +829,21 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
412
829
|
const files = experimentalStateGetFiles(ctx);
|
|
413
830
|
const deduped = {};
|
|
414
831
|
for (const file of files) {
|
|
415
|
-
const projectNameValue =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
832
|
+
const projectNameValue = (() => {
|
|
833
|
+
if (typeof file === "object" && file !== null && "projectName" in file) return Reflect.get(file, "projectName");
|
|
834
|
+
})();
|
|
835
|
+
const projectName = (() => {
|
|
836
|
+
if (typeof projectNameValue === "string") return projectNameValue;
|
|
837
|
+
return "";
|
|
838
|
+
})();
|
|
839
|
+
const nameValue = (() => {
|
|
840
|
+
if (typeof file === "object" && file !== null && "name" in file) return Reflect.get(file, "name");
|
|
841
|
+
})();
|
|
842
|
+
const name = (() => {
|
|
843
|
+
if (typeof nameValue === "string") return nameValue;
|
|
844
|
+
return "";
|
|
845
|
+
})();
|
|
846
|
+
deduped[`${projectName}-${name}`] = file;
|
|
420
847
|
}
|
|
421
848
|
const coverages = [];
|
|
422
849
|
for (const file of Object.values(deduped)) {
|
|
@@ -428,36 +855,36 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
428
855
|
coverages.push(validated);
|
|
429
856
|
}
|
|
430
857
|
}
|
|
431
|
-
if (coverages.length === 0) return;
|
|
858
|
+
if (coverages.length === 0) return void 0;
|
|
432
859
|
if (coverages.length === 1) return coverages[0];
|
|
433
860
|
const first = coverages[0];
|
|
434
|
-
if (first === void 0) return;
|
|
435
861
|
return coverages.slice(1).reduce((acc, projectCoverage) => {
|
|
436
|
-
for (const [testId, testCoverage] of Object.entries(projectCoverage.perTest))
|
|
437
|
-
|
|
438
|
-
if (existing !== void 0) mergeCoverage(existing, testCoverage);
|
|
439
|
-
else acc.perTest[testId] = testCoverage;
|
|
440
|
-
}
|
|
862
|
+
for (const [testId, testCoverage] of Object.entries(projectCoverage.perTest)) if (testId in acc.perTest) mergeCoverage(acc.perTest[testId], testCoverage);
|
|
863
|
+
else acc.perTest[testId] = testCoverage;
|
|
441
864
|
mergeCoverage(acc.static, projectCoverage.static);
|
|
442
865
|
return acc;
|
|
443
866
|
}, first);
|
|
444
867
|
});
|
|
445
|
-
const
|
|
868
|
+
const collectRaw = (filter) => Effect$1.gen(function* () {
|
|
446
869
|
const ctx = yield* requireCtx;
|
|
447
870
|
const options = yield* optionsEffect;
|
|
448
871
|
yield* resetContext.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
|
|
449
872
|
runnerName: "vitest",
|
|
450
873
|
phase: "dryRun",
|
|
451
|
-
cause
|
|
874
|
+
cause: errorToString(cause)
|
|
452
875
|
})));
|
|
453
876
|
const vitestInRun = Reflect.get(options, "vitest");
|
|
454
|
-
const
|
|
455
|
-
|
|
877
|
+
const relatedValue = Reflect.get(vitestInRun, "related");
|
|
878
|
+
const related = (() => {
|
|
879
|
+
if (relatedValue !== false && filter.relatedFiles !== void 0) return filter.relatedFiles.map(normalizeFileName);
|
|
880
|
+
})();
|
|
881
|
+
let testFilesToRun = (() => {
|
|
882
|
+
if (filter.testFiles !== void 0) return [...filter.testFiles];
|
|
883
|
+
})();
|
|
456
884
|
let pattern;
|
|
457
885
|
if ((filter.testIds ?? []).length > 0) {
|
|
458
886
|
const parsedTests = (filter.testIds ?? []).map(fromTestId);
|
|
459
|
-
|
|
460
|
-
pattern = new RegExp(regexTestNameFilter);
|
|
887
|
+
pattern = new RegExp(parsedTests.map(({ test: name }) => RegExp.escape(name)).join("|"));
|
|
461
888
|
testFilesToRun = parsedTests.map(({ file }) => pathService.resolve(input.sandboxDirectory, file));
|
|
462
889
|
}
|
|
463
890
|
applyRunFilterToConfig(ctx, {
|
|
@@ -469,77 +896,201 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
469
896
|
catch: (cause) => new TestRunnerFailed({
|
|
470
897
|
runnerName: "vitest",
|
|
471
898
|
phase: "dryRun",
|
|
472
|
-
cause
|
|
899
|
+
cause: errorToString(cause)
|
|
473
900
|
})
|
|
474
|
-
}).pipe(Effect$1.catchIf((error) => isErrorCodeError(error.cause) && error.cause
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return testResult;
|
|
481
|
-
});
|
|
482
|
-
if (!failure && experimentalStateHasExternalErrors(ctx)) {
|
|
483
|
-
const errorText = experimentalStateGetExternalErrorText(ctx);
|
|
484
|
-
return {
|
|
485
|
-
status: DryRunStatus.Error,
|
|
486
|
-
errorMessage: `An error occurred outside of a test run: ${errorText}`
|
|
487
|
-
};
|
|
488
|
-
}
|
|
901
|
+
}).pipe(Effect$1.catchIf((error) => isErrorCodeError(error.cause) && typeof error.cause === "string" && error.cause.includes(VITEST_ERROR_CODES.FILES_NOT_FOUND), () => Effect$1.void));
|
|
902
|
+
const rawTests = experimentalStateGetFiles(ctx).flatMap((file) => (() => {
|
|
903
|
+
if (isRunnerTestSuite(file)) return collectTestsFromSuite(file);
|
|
904
|
+
return [];
|
|
905
|
+
})()).filter((test) => test.result !== void 0);
|
|
906
|
+
const hasExternalError = experimentalStateHasExternalErrors(ctx);
|
|
489
907
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
908
|
+
rawTests,
|
|
909
|
+
hasExternalError,
|
|
910
|
+
externalErrorText: (() => {
|
|
911
|
+
if (hasExternalError) return experimentalStateGetExternalErrorText(ctx);
|
|
912
|
+
return "";
|
|
913
|
+
})()
|
|
492
914
|
};
|
|
493
915
|
});
|
|
494
|
-
const
|
|
916
|
+
const dryRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
|
|
495
917
|
(yield* requireCtx).provide("mode", "dry-run");
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
918
|
+
const hasTestFiles = testFilesProvided(command);
|
|
919
|
+
const filter = (() => {
|
|
920
|
+
if (hasTestFiles) return {
|
|
921
|
+
testFiles: [...command.testFiles ?? []],
|
|
922
|
+
relatedFiles: (() => {
|
|
923
|
+
if (command.files !== void 0) return [...command.files];
|
|
924
|
+
})()
|
|
925
|
+
};
|
|
926
|
+
return { relatedFiles: (() => {
|
|
927
|
+
if (command.files !== void 0) return [...command.files];
|
|
928
|
+
})() };
|
|
929
|
+
})();
|
|
930
|
+
const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw(filter);
|
|
931
|
+
return {
|
|
932
|
+
rawTests,
|
|
933
|
+
projectRoot: input.sandboxDirectory,
|
|
934
|
+
hasExternalError,
|
|
935
|
+
externalErrorText
|
|
936
|
+
};
|
|
937
|
+
})), Cell.decode((raw) => Result.succeed(new VitestDryRunCommand$1({
|
|
938
|
+
rawTests: raw.rawTests,
|
|
939
|
+
projectRoot: raw.projectRoot,
|
|
940
|
+
hasExternalError: raw.hasExternalError,
|
|
941
|
+
externalErrorText: raw.externalErrorText
|
|
942
|
+
}))), Cell.decide(vitestDryRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
|
|
943
|
+
onFailure: (e) => ({
|
|
944
|
+
status: "error",
|
|
945
|
+
errorMessage: e.message
|
|
946
|
+
}),
|
|
947
|
+
onSuccess: (out) => {
|
|
948
|
+
const raw = JSON.parse(out.testsJson);
|
|
949
|
+
let tests;
|
|
950
|
+
if (Array.isArray(raw)) tests = raw.filter(isTestResultLike);
|
|
951
|
+
else tests = [];
|
|
952
|
+
if (out.status === "Error") return {
|
|
953
|
+
status: "error",
|
|
954
|
+
errorMessage: out.errorMessage ?? "unknown"
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
status: "complete",
|
|
958
|
+
tests
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
})), Cell.write((output) => Effect$1.gen(function* () {
|
|
962
|
+
if (output.status === "complete") {
|
|
502
963
|
const mutantCoverage = yield* readMutantCoverage.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
|
|
503
964
|
runnerName: "vitest",
|
|
504
965
|
phase: "dryRun",
|
|
505
|
-
cause
|
|
966
|
+
cause: errorToString(cause)
|
|
506
967
|
})));
|
|
507
|
-
if (mutantCoverage
|
|
508
|
-
|
|
509
|
-
...testResult,
|
|
968
|
+
if (mutantCoverage !== void 0) return {
|
|
969
|
+
...output,
|
|
510
970
|
mutantCoverage
|
|
511
971
|
};
|
|
512
972
|
}
|
|
513
|
-
return
|
|
514
|
-
}).pipe(Effect$1.mapError((cause) => cause instanceof TestRunnerFailed ? cause : new TestRunnerFailed({
|
|
515
|
-
runnerName: "vitest",
|
|
516
|
-
phase: "dryRun",
|
|
517
|
-
cause
|
|
973
|
+
return output;
|
|
518
974
|
})));
|
|
519
|
-
const
|
|
975
|
+
const mutantRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
|
|
520
976
|
const ctx = yield* requireCtx;
|
|
521
977
|
ctx.provide("mode", "mutant");
|
|
522
|
-
ctx.provide("hitLimit",
|
|
523
|
-
ctx.provide("mutantActivation",
|
|
524
|
-
ctx.provide("activeMutant",
|
|
525
|
-
const
|
|
526
|
-
testIds:
|
|
527
|
-
|
|
978
|
+
ctx.provide("hitLimit", command.hitLimit);
|
|
979
|
+
ctx.provide("mutantActivation", command.mutantActivation);
|
|
980
|
+
ctx.provide("activeMutant", command.activeMutant.id);
|
|
981
|
+
const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw({
|
|
982
|
+
testIds: (() => {
|
|
983
|
+
if (command.testFilter !== void 0) return [...command.testFilter];
|
|
984
|
+
})(),
|
|
985
|
+
relatedFiles: [command.sandboxFileName]
|
|
528
986
|
});
|
|
529
987
|
const hitCount = yield* readHitCount.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
|
|
530
988
|
runnerName: "vitest",
|
|
531
989
|
phase: "mutantRun",
|
|
532
|
-
cause
|
|
533
|
-
})));
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
990
|
+
cause: errorToString(cause)
|
|
991
|
+
})), Effect$1.option, Effect$1.map(Option.getOrUndefined));
|
|
992
|
+
const reportAllKillers = (() => {
|
|
993
|
+
if (typeof input.options.disableBail === "boolean") return input.options.disableBail;
|
|
994
|
+
return false;
|
|
995
|
+
})();
|
|
996
|
+
if (hitCount === void 0) return {
|
|
997
|
+
rawTests,
|
|
998
|
+
projectRoot: input.sandboxDirectory,
|
|
999
|
+
hasExternalError,
|
|
1000
|
+
externalErrorText,
|
|
1001
|
+
hitLimit: command.hitLimit,
|
|
1002
|
+
reportAllKillers
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
rawTests,
|
|
1006
|
+
projectRoot: input.sandboxDirectory,
|
|
1007
|
+
hasExternalError,
|
|
1008
|
+
externalErrorText,
|
|
1009
|
+
hitCount,
|
|
1010
|
+
hitLimit: command.hitLimit,
|
|
1011
|
+
reportAllKillers
|
|
1012
|
+
};
|
|
1013
|
+
})), Cell.decode((raw) => {
|
|
1014
|
+
const base = {
|
|
1015
|
+
rawTests: raw.rawTests,
|
|
1016
|
+
projectRoot: raw.projectRoot,
|
|
1017
|
+
hasExternalError: raw.hasExternalError,
|
|
1018
|
+
externalErrorText: raw.externalErrorText,
|
|
1019
|
+
reportAllKillers: raw.reportAllKillers
|
|
1020
|
+
};
|
|
1021
|
+
if (raw.hitCount !== void 0) {
|
|
1022
|
+
if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
|
|
1023
|
+
...base,
|
|
1024
|
+
hitCount: raw.hitCount,
|
|
1025
|
+
hitLimit: raw.hitLimit
|
|
1026
|
+
}));
|
|
1027
|
+
return Result.succeed(new VitestMutantRunCommand({
|
|
1028
|
+
...base,
|
|
1029
|
+
hitCount: raw.hitCount
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
|
|
1033
|
+
...base,
|
|
1034
|
+
hitLimit: raw.hitLimit
|
|
1035
|
+
}));
|
|
1036
|
+
return Result.succeed(new VitestMutantRunCommand(base));
|
|
1037
|
+
}), Cell.decide(vitestMutantRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
|
|
1038
|
+
onFailure: (e) => ({
|
|
1039
|
+
status: "error",
|
|
1040
|
+
errorMessage: e.message
|
|
1041
|
+
}),
|
|
1042
|
+
onSuccess: (out) => {
|
|
1043
|
+
let parsed;
|
|
1044
|
+
try {
|
|
1045
|
+
const raw = JSON.parse(out.testsJson);
|
|
1046
|
+
if (Array.isArray(raw)) parsed = raw.filter(isIdRecord);
|
|
1047
|
+
else parsed = [];
|
|
1048
|
+
} catch {
|
|
1049
|
+
parsed = [];
|
|
1050
|
+
}
|
|
1051
|
+
const nrOfTests = parsed.length;
|
|
1052
|
+
if (out.status === "Error") return {
|
|
1053
|
+
status: "error",
|
|
1054
|
+
errorMessage: out.errorMessage ?? "unknown"
|
|
1055
|
+
};
|
|
1056
|
+
if (out.status === "Timeout") {
|
|
1057
|
+
if (out.reason === void 0) return { status: "timeout" };
|
|
1058
|
+
return {
|
|
1059
|
+
status: "timeout",
|
|
1060
|
+
reason: out.reason
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
if (out.status === "Killed") return {
|
|
1064
|
+
status: "killed",
|
|
1065
|
+
failureMessage: out.failureMessage ?? "",
|
|
1066
|
+
killedBy: (() => {
|
|
1067
|
+
if (out.killerIds !== void 0) return [...out.killerIds];
|
|
1068
|
+
return [];
|
|
1069
|
+
})(),
|
|
1070
|
+
nrOfTests
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
status: "survived",
|
|
1074
|
+
nrOfTests
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
})), Cell.write((output) => Effect$1.succeed(output)));
|
|
1078
|
+
const dryRun = (options) => Cell.apply(dryRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
|
|
1079
|
+
if (cause instanceof TestRunnerFailed) return cause;
|
|
1080
|
+
return new TestRunnerFailed({
|
|
1081
|
+
runnerName: "vitest",
|
|
1082
|
+
phase: "dryRun",
|
|
1083
|
+
cause: errorToString(cause)
|
|
1084
|
+
});
|
|
1085
|
+
})()));
|
|
1086
|
+
const mutantRun = (options) => Cell.apply(mutantRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
|
|
1087
|
+
if (cause instanceof TestRunnerFailed) return cause;
|
|
1088
|
+
return new TestRunnerFailed({
|
|
1089
|
+
runnerName: "vitest",
|
|
1090
|
+
phase: "mutantRun",
|
|
1091
|
+
cause: errorToString(cause)
|
|
1092
|
+
});
|
|
1093
|
+
})()));
|
|
543
1094
|
const dispose = Effect$1.gen(function* () {
|
|
544
1095
|
const state = yield* getState;
|
|
545
1096
|
if (state.ctx !== void 0) {
|
|
@@ -554,7 +1105,7 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
554
1105
|
catch: (cause) => new TestRunnerFailed({
|
|
555
1106
|
runnerName: "vitest",
|
|
556
1107
|
phase: "dispose",
|
|
557
|
-
cause
|
|
1108
|
+
cause: errorToString(cause)
|
|
558
1109
|
})
|
|
559
1110
|
});
|
|
560
1111
|
}
|
|
@@ -567,12 +1118,15 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
|
|
|
567
1118
|
dispose
|
|
568
1119
|
});
|
|
569
1120
|
}));
|
|
1121
|
+
function isTestResultLike(value) {
|
|
1122
|
+
return typeof value === "object" && value !== null && "id" in value && typeof Reflect.get(value, "id") === "string";
|
|
1123
|
+
}
|
|
1124
|
+
function isIdRecord(value) {
|
|
1125
|
+
return typeof value === "object" && value !== null && "id" in value && typeof Reflect.get(value, "id") === "string";
|
|
1126
|
+
}
|
|
570
1127
|
function mergeCoverage(to, from) {
|
|
571
|
-
for (const [mutantId, hitCount] of Object.entries(from))
|
|
572
|
-
|
|
573
|
-
if (existing !== void 0) to[mutantId] = existing + hitCount;
|
|
574
|
-
else to[mutantId] = hitCount;
|
|
575
|
-
}
|
|
1128
|
+
for (const [mutantId, hitCount] of Object.entries(from)) if (mutantId in to) to[mutantId] = to[mutantId] + hitCount;
|
|
1129
|
+
else to[mutantId] = hitCount;
|
|
576
1130
|
}
|
|
577
1131
|
//#endregion
|
|
578
1132
|
//#region src/index.ts
|
|
@@ -583,7 +1137,7 @@ function mergeCoverage(to, from) {
|
|
|
583
1137
|
* in, so the requirement is visible in the type and an engine that does not
|
|
584
1138
|
* provide it fails to compile.
|
|
585
1139
|
*/
|
|
586
|
-
const strykerPlugins = [declarePlugin(
|
|
1140
|
+
const strykerPlugins = [declarePlugin("TestRunner", "vitest", Layer.unwrap(Effect$1.gen(function* () {
|
|
587
1141
|
const options = yield* RunConfiguration;
|
|
588
1142
|
const sandboxDirectory = yield* SandboxDirectory;
|
|
589
1143
|
return makeVitestRunnerLayer({
|
package/dist/stryker-setup.mjs
CHANGED
|
@@ -45,7 +45,7 @@ function collectTestName({ name, suite }) {
|
|
|
45
45
|
return nameParts.join(" ").trim();
|
|
46
46
|
}
|
|
47
47
|
function toRawTestId(test) {
|
|
48
|
-
return `${test.file
|
|
48
|
+
return `${test.file.filepath}#${collectTestName(test)}`;
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
51
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/stryker-js-vitest-runner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
|
|
@@ -31,24 +31,26 @@
|
|
|
31
31
|
"coverageAnalysis": "perTest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@systemfsoftware/
|
|
34
|
+
"@systemfsoftware/effect-cell-types": "^5.0.0",
|
|
35
|
+
"@systemfsoftware/stryker-js": "^0.1.0"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"effect": "^4.0.0-rc.
|
|
38
|
+
"effect": "^4.0.0-rc.112",
|
|
38
39
|
"vitest": ">=2.0.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@effect/platform-node": "^4.0.0-rc.
|
|
42
|
-
"@effect/vitest": "4.0.0-rc.
|
|
42
|
+
"@effect/platform-node": "^4.0.0-rc.112",
|
|
43
|
+
"@effect/vitest": "4.0.0-rc.112",
|
|
43
44
|
"@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
|
|
44
45
|
"@types/node": "^24",
|
|
45
46
|
"@vitest/browser-playwright": "^4",
|
|
46
|
-
"effect": "^4.0.0-rc.
|
|
47
|
+
"effect": "^4.0.0-rc.112",
|
|
47
48
|
"oxlint": "^1.77.0",
|
|
48
49
|
"rimraf": "^6.1.3",
|
|
49
50
|
"tsdown": "^0.22.14",
|
|
50
51
|
"typescript": "^7",
|
|
51
52
|
"vitest": "^4.1.10",
|
|
53
|
+
"@systemfsoftware/all": "^1.0.0",
|
|
52
54
|
"@systemfsoftware/effect-gherkin-spec": "^2.0.1",
|
|
53
55
|
"@systemfsoftware/oxlint-config": "^0.1.0",
|
|
54
56
|
"@systemfsoftware/tsconfig": "^1.3.3",
|