@ingenioz-it/caits 0.6.0 → 0.8.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/bin/caits.js +20 -7
- package/bin/init.js +16 -1
- package/package.json +8 -5
- package/quality/architecture/adapters/scss.mjs +2 -2
- package/quality/architecture/facts.mjs +39 -11
- package/quality/architecture/project.mjs +8 -6
- package/quality/dev.mjs +6 -3
- package/quality/mutation-summary.mjs +30 -20
- package/quality/mutation.mjs +170 -14
- package/src/adapter.js +94 -0
- package/src/di/Container.ts +27 -0
- package/src/di/RequestContext.ts +8 -2
- package/src/di/backend.ts +12 -0
- package/src/di/createBackend.ts +197 -25
- package/src/di/declareDependencies.js +25 -5
- package/src/di/discover.ts +19 -9
- package/src/di/discoverFrontends.ts +4 -8
- package/src/di/discovered.ts +2 -2
- package/src/di/discoveredFrontends.ts +2 -2
- package/src/messaging/BackendEventBus.ts +2 -3
- package/src/messaging/ContextEvents.ts +11 -0
- package/src/messaging/ContextMap.ts +3 -4
- package/src/messaging/Controller.ts +36 -75
- package/src/messaging/EventSubscriber.ts +1 -1
- package/src/messaging/InProcessMessageBus.ts +8 -9
- package/src/messaging/MessageBus.ts +4 -2
- package/src/messaging/Request.ts +116 -0
- package/src/messaging/RequestEvents.ts +8 -0
- package/src/messaging/StreamController.ts +261 -0
- package/src/messaging/TimerController.ts +25 -0
- package/src/messaging/sse.ts +23 -0
- package/src/server/entry.ts +37 -0
- package/src/server/kernel.ts +19 -0
- package/src/server/packages.d.ts +12 -0
- package/src/server/serve.js +121 -0
- package/src/state/createStore.ts +1 -2
- package/src/testing/backend/application.ts +1 -1
- package/src/testing/backend/applicationScenario.ts +6 -7
- package/src/testing/backend/applications.ts +4 -1
- package/src/testing/backend/context-map.ts +1 -1
- package/src/testing/backend/integration.ts +1 -1
- package/src/testing/backend/presentationApplication.ts +23 -3
- package/src/testing/backend/presentationScenario.ts +84 -7
- package/src/testing/frontend/applicationScenario.ts +2 -3
- package/src/testing/frontend/applications.ts +3 -3
- package/src/testing/frontend/frontends.ts +3 -4
- package/src/testing/shared/FakeMessageBus.ts +39 -5
- package/src/virtual-modules.d.ts +0 -2
- package/src/vite.js +2 -2
- package/src/vite.types.d.ts +7 -0
- package/template/caits/architecture.mjs +162 -113
- package/template/caits/imports.mjs +2 -2
- package/template/caits/stryker.mjs +11 -6
- package/template/caits/vite.ts +15 -5
- package/template/project/AGENTS +1 -1
- package/quality/mutation-suite.mjs +0 -82
- package/src/messaging/Input.ts +0 -4
package/bin/caits.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { relative, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { commandOf, painterOf } from "../quality/terminal.mjs";
|
|
7
|
-
import { defaultsVersions, DEFAULTS_FOLDER, EXAMPLE_SOURCE, init, install, otherVersions, SOURCE, update, VERSION } from "./init.js";
|
|
7
|
+
import { defaultsVersions, DEFAULTS_FOLDER, EXAMPLE_SOURCE, init, install, missingPackages, otherVersions, SOURCE, update, VERSION } from "./init.js";
|
|
8
8
|
|
|
9
9
|
const USAGE = `Usage: caits <command>
|
|
10
10
|
|
|
@@ -24,16 +24,17 @@ const USAGE = `Usage: caits <command>
|
|
|
24
24
|
check <script>… Runs the npm scripts of the checks at once. Shows the output of the checks that fail, and only theirs.
|
|
25
25
|
architecture Checks the rules of quality/architecture.mjs.
|
|
26
26
|
e2e Runs the end-to-end tests on the production build.
|
|
27
|
-
mutation Runs the mutation tests (Stryker),
|
|
28
|
-
code is that of Stryker, or 1 when a mutant
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
mutation Runs the mutation tests (Stryker), runs again with more time the mutants that the clock stopped, then gives
|
|
28
|
+
the score of each file and the mutants that survived. Its exit code is that of Stryker, or 1 when a mutant
|
|
29
|
+
is not judged (its tests ended in an error that is not a failed test), or when the second run puts the
|
|
30
|
+
score under the threshold.
|
|
31
|
+
mutation-summary Gives the mutation score of each file (--survivors: the mutants that survived, and those not judged).
|
|
31
32
|
|
|
32
33
|
--help, -h Shows this help, also after a command (caits init --help).
|
|
33
34
|
--version Shows the version of CAITS.`;
|
|
34
35
|
|
|
35
36
|
/** The commands that run a tool of quality/: their code is in the folder quality/ of CAITS. */
|
|
36
|
-
const TOOLS = new Set(["dev", "check", "architecture", "e2e", "mutation", "mutation-summary"
|
|
37
|
+
const TOOLS = new Set(["dev", "check", "architecture", "e2e", "mutation", "mutation-summary"]);
|
|
37
38
|
|
|
38
39
|
/** The options of `caits init`. */
|
|
39
40
|
const INIT_OPTIONS = /** @type {const} */ ({ caits: { type: "string" }, example: { type: "boolean" }, "example-source": { type: "string" }, "no-install": { type: "boolean" } });
|
|
@@ -85,7 +86,7 @@ async function main(command, args) {
|
|
|
85
86
|
if (command === "init") return runInit(args);
|
|
86
87
|
if (command === "update") return runUpdate(args);
|
|
87
88
|
if (TOOLS.has(command)) {
|
|
88
|
-
|
|
89
|
+
warnOfOldDefaults(process.cwd());
|
|
89
90
|
// The tools load Vite, Marko and the other packages of the project: only the command that runs imports its code.
|
|
90
91
|
const tool = /** @type {{ run: (args: string[]) => Promise<number | void> }} */ (await importTool(command));
|
|
91
92
|
return tool.run(args);
|
|
@@ -171,6 +172,7 @@ function runUpdate(args) {
|
|
|
171
172
|
const written = update(process.cwd());
|
|
172
173
|
console.log(paint("green", `CAITS ${VERSION}: wrote ${count(written.length, "file")} in ${DEFAULTS_FOLDER}/.`));
|
|
173
174
|
printOtherVersions(process.cwd());
|
|
175
|
+
printMissingPackages(process.cwd());
|
|
174
176
|
return 0;
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -182,6 +184,17 @@ function printOtherVersions(root) {
|
|
|
182
184
|
for (const { key, project, caits } of others) console.log(` ${key}: ${JSON.stringify(project)} (CAITS suggests ${JSON.stringify(caits)})`);
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/** @param {string} root */
|
|
188
|
+
function printMissingPackages(root) {
|
|
189
|
+
const { dependencies, devDependencies } = missingPackages(root);
|
|
190
|
+
if (dependencies.length + devDependencies.length === 0) return;
|
|
191
|
+
console.log(paint("yellow", `CAITS ${VERSION} needs packages that package.json does not have. Add them:`));
|
|
192
|
+
/** @param {string[]} names */
|
|
193
|
+
const quoted = names => names.map(name => `"${name}"`).join(" ");
|
|
194
|
+
if (dependencies.length > 0) console.log(` ${paint("cyan", `npm install ${quoted(dependencies)}`)}`);
|
|
195
|
+
if (devDependencies.length > 0) console.log(` ${paint("cyan", `npm install --save-dev ${quoted(devDependencies)}`)}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
/** @param {string} root */
|
|
186
199
|
function warnOfOldDefaults(root) {
|
|
187
200
|
const versions = defaultsVersions(root);
|
package/bin/init.js
CHANGED
|
@@ -111,7 +111,8 @@ const TOOLS = {
|
|
|
111
111
|
dependencies: ["@marko/run", "marko"],
|
|
112
112
|
devDependencies: [
|
|
113
113
|
"@eslint/js", "@marko/compiler", "@marko/type-check", "@playwright/test", "@stryker-mutator/core", "@stryker-mutator/typescript-checker",
|
|
114
|
-
"@types/node", "@vitest/coverage-v8", "eslint", "happy-dom", "sass", "typescript", "typescript-eslint",
|
|
114
|
+
"@stryker-mutator/vitest-runner", "@types/node", "@vitest/coverage-v8", "eslint", "happy-dom", "sass", "typescript", "typescript-eslint",
|
|
115
|
+
"vite", "vitest"
|
|
115
116
|
]
|
|
116
117
|
};
|
|
117
118
|
/** @param {string[]} names */
|
|
@@ -253,6 +254,20 @@ export function otherVersions(root) {
|
|
|
253
254
|
}));
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The packages that CAITS needs and that package.json does not have, in either section, as npm installs them: `name@version`. A new
|
|
259
|
+
* version of CAITS may need a package more: `caits update` tells which.
|
|
260
|
+
* @param {string} root
|
|
261
|
+
* @returns {{ dependencies: string[]; devDependencies: string[] }}
|
|
262
|
+
*/
|
|
263
|
+
export function missingPackages(root) {
|
|
264
|
+
const file = join(root, "package.json");
|
|
265
|
+
const project = existsSync(file) ? readPackage(file) : undefined;
|
|
266
|
+
/** @param {Record<string, string>} section */
|
|
267
|
+
const missing = section => project === undefined ? [] : Object.entries(section).filter(([name]) => dependencyOf(project, name) === undefined).map(([name, version]) => `${name}@${version}`);
|
|
268
|
+
return { dependencies: missing(SUGGESTED.dependencies), devDependencies: missing(SUGGESTED.devDependencies) };
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
/**
|
|
257
272
|
* Installs the packages of package.json, then the browser of the end-to-end tests. Gives false when a command fails.
|
|
258
273
|
* @param {string} root
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingenioz-it/caits",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CAITS (Clean Architecture In TypeScript): the technical base of a Marko project with a backend and a frontend.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clean-architecture",
|
|
@@ -55,12 +55,14 @@
|
|
|
55
55
|
"test": "vitest run",
|
|
56
56
|
"test:watch": "vitest",
|
|
57
57
|
"test:coverage": "vitest run --coverage",
|
|
58
|
-
"test:mutation": "
|
|
58
|
+
"test:mutation": "node test/mutation.js",
|
|
59
59
|
"test:template": "node test/template.js",
|
|
60
60
|
"check": "npm run typecheck && npm run lint && npm run test:coverage"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"
|
|
63
|
+
"compression": "^1.8.2",
|
|
64
|
+
"magic-string": "^0.30.21",
|
|
65
|
+
"serve-static": "^2.2.1"
|
|
64
66
|
},
|
|
65
67
|
"peerDependencies": {
|
|
66
68
|
"@eslint/js": "^10.0.1",
|
|
@@ -100,8 +102,9 @@
|
|
|
100
102
|
"@marko/run": "^0.11.13",
|
|
101
103
|
"@marko/type-check": "^3.2.1",
|
|
102
104
|
"@playwright/test": "^1.64.0",
|
|
103
|
-
"@stryker-mutator/core": "^10.
|
|
104
|
-
"@stryker-mutator/typescript-checker": "^10.
|
|
105
|
+
"@stryker-mutator/core": "^10.1.1",
|
|
106
|
+
"@stryker-mutator/typescript-checker": "^10.1.1",
|
|
107
|
+
"@stryker-mutator/vitest-runner": "^10.1.1",
|
|
105
108
|
"@types/node": "^26.6.4",
|
|
106
109
|
"@vitest/coverage-v8": "^5.0.3",
|
|
107
110
|
"eslint": "^10.11.0",
|
|
@@ -50,8 +50,8 @@ export function readScss(path, text, paths) {
|
|
|
50
50
|
return isIn(STYLE_FOLDER, specifier, paths) ? `/${posix.join(STYLE_FOLDER, specifier)}` : specifier;
|
|
51
51
|
});
|
|
52
52
|
return {
|
|
53
|
-
imports, imported: [], exports: [], defaultArray: null, classes: [], constructed: [], calls: [], objectTypes: [], strings: [], tags: [], comments,
|
|
54
|
-
lines: { imports: found.map(({ line }) => line), constructed: [], objectTypes: [], tags: [], comments: commentLines }
|
|
53
|
+
imports, imported: [], exports: [], defaultArray: null, classes: [], constructed: [], typeReferences: [], calls: [], objectTypes: [], strings: [], tags: [], comments,
|
|
54
|
+
lines: { imports: found.map(({ line }) => line), constructed: [], typeReferences: [], objectTypes: [], tags: [], comments: commentLines }
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
/** Each fact that a rule points at has the number of its line in the file, from 1. */
|
|
11
11
|
/** @typedef {{ name: string | null; kind: string; line: number }} Export an exported name, its kind, and its line */
|
|
12
12
|
/** @typedef {{ name: string | null; readonly: boolean; value: string | null; asConst: boolean; line: number }} Property a property of a class */
|
|
13
|
-
/** @typedef {{ name: string | null;
|
|
13
|
+
/** @typedef {{ name: string | null; text: string }} TypeName a type that a signature names: its name when it is a name (`Note`), and its text */
|
|
14
|
+
/**
|
|
15
|
+
* A method of a class, and what it returns: each type of its return type, out of `Promise<…>` and of the unions (`Promise<A | B>` names A
|
|
16
|
+
* and B), or null when it has no return type.
|
|
17
|
+
* @typedef {{ name: string | null; returns: TypeName[] | null; line: number }} Method
|
|
18
|
+
*/
|
|
19
|
+
/** @typedef {{ name: string | null; exported: boolean; extends: string | null; implements: string[]; properties: Property[]; methods: Method[]; line: number }} Class */
|
|
14
20
|
/**
|
|
15
21
|
* An item of `export default [ … ]`: the names that its chain calls, its text, the name that the first call takes first (`when(Event, …)`
|
|
16
22
|
* takes Event), the class of each `new X(…)` in it, and its line.
|
|
@@ -25,13 +31,13 @@
|
|
|
25
31
|
/**
|
|
26
32
|
* The lines of the facts that are strings, in the same order: `lines.imports[2]` is the line of `imports[2]`. A tag that Marko finds
|
|
27
33
|
* without an import counts as an import, at the line of the tag.
|
|
28
|
-
* @typedef {{ imports: number[]; constructed: number[]; objectTypes: number[]; tags: number[]; comments: number[] }} Lines
|
|
34
|
+
* @typedef {{ imports: number[]; constructed: number[]; typeReferences: number[]; objectTypes: number[]; tags: number[]; comments: number[] }} Lines
|
|
29
35
|
*/
|
|
30
36
|
/**
|
|
31
37
|
* What the rules read in a file: see factsOf.
|
|
32
38
|
* @typedef {{
|
|
33
39
|
* imports: string[]; imported: Imported[]; exports: Export[]; defaultArray: Item[] | null; classes: Class[]; constructed: (string | null)[];
|
|
34
|
-
* calls: Call[]; objectTypes: string[]; strings: string[]; tags: string[]; comments: string[]; lines: Lines
|
|
40
|
+
* typeReferences: string[]; calls: Call[]; objectTypes: string[]; strings: string[]; tags: string[]; comments: string[]; lines: Lines
|
|
35
41
|
* }} Facts
|
|
36
42
|
*/
|
|
37
43
|
/** A comment that a parser gives apart from the tree: its text, and where it starts in the file. @typedef {{ value: string; start: number }} LooseComment */
|
|
@@ -75,6 +81,8 @@ const rootOf = node => {
|
|
|
75
81
|
while (node?.type === "MemberExpression" || node?.type === "CallExpression") node = node.type === "MemberExpression" ? node.object : node.callee;
|
|
76
82
|
return node?.type === "Identifier" ? node.name : null;
|
|
77
83
|
};
|
|
84
|
+
/** The name of a type, qualified or not: `Note`, `globalThis.Request`. @type {(node: Node) => string} */
|
|
85
|
+
const typeNameOf = node => node.type === "TSQualifiedName" ? `${typeNameOf(node.left)}.${typeNameOf(node.right)}` : node.name;
|
|
78
86
|
/** @type {(node: Node) => string | null} */
|
|
79
87
|
const keyOf = node => node.key?.name ?? stringOf(node.key);
|
|
80
88
|
|
|
@@ -120,8 +128,17 @@ function exportsOf(node, lineOf) {
|
|
|
120
128
|
return [{ name: declaration.id?.name, kind: DECLARATION_KINDS[declaration.type] ?? declaration.type, line }];
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
/**
|
|
124
|
-
const
|
|
131
|
+
/** The types that a return type names: those of a union, and the type of a Promise. @type {(type: Node, text: string) => TypeName[]} */
|
|
132
|
+
const typeNamesOf = (type, text) => {
|
|
133
|
+
if (type.type === "TSUnionType") return type.types.flatMap((/** @type {Node} */ member) => typeNamesOf(member, text));
|
|
134
|
+
const reference = type.type === "TSTypeReference" && type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
135
|
+
const [promised, ...others] = (type.typeArguments ?? type.typeParameters)?.params ?? [];
|
|
136
|
+
if (reference === "Promise" && promised && others.length === 0) return typeNamesOf(promised, text);
|
|
137
|
+
return [{ name: reference, text: text.slice(type.start, type.end) }];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** @type {(node: Node, exported: boolean, lineOf: (node: Node) => number, text: string) => Class} */
|
|
141
|
+
const classOf = (node, exported, lineOf, text) => ({
|
|
125
142
|
name: node.id?.name ?? null,
|
|
126
143
|
exported,
|
|
127
144
|
extends: nameOf(node.superClass),
|
|
@@ -133,6 +150,11 @@ const classOf = (node, exported, lineOf) => ({
|
|
|
133
150
|
asConst: member.value?.type === "TSAsExpression" && member.value.typeAnnotation?.typeName?.name === "const",
|
|
134
151
|
line: lineOf(member)
|
|
135
152
|
})),
|
|
153
|
+
methods: node.body.body.filter((/** @type {Node} */ member) => (member.type === "MethodDefinition" || member.type === "ClassMethod") && member.kind === "method" && !member.static && !member.computed)
|
|
154
|
+
.map((/** @type {Node} */ member) => {
|
|
155
|
+
const returnType = (member.value ?? member).returnType?.typeAnnotation;
|
|
156
|
+
return { name: keyOf(member), returns: returnType ? typeNamesOf(returnType, text) : null, line: lineOf(member) };
|
|
157
|
+
}),
|
|
136
158
|
line: lineOf(node)
|
|
137
159
|
});
|
|
138
160
|
|
|
@@ -142,14 +164,16 @@ const classOf = (node, exported, lineOf) => ({
|
|
|
142
164
|
* - exports: each exported name, with its kind (class, interface, type, const, function, enum, specifier, default, all);
|
|
143
165
|
* - defaultArray: the items of `export default [ … ]`, with the calls of each item (`bind(X).to(Y)` calls bind, to), the name that the first
|
|
144
166
|
* call takes first (X), and the classes that the item builds, or null;
|
|
145
|
-
* - classes: the classes, with what they extend and implement, and their
|
|
167
|
+
* - classes: the classes, with what they extend and implement, their properties, and the return types of their methods;
|
|
146
168
|
* - constructed: the class of each `new X(…)`;
|
|
169
|
+
* - typeReferences: the name of each type that the file names (`Note`, `globalThis.Request`), its arguments too (`Map<string, Note>` names Map
|
|
170
|
+
* and Note);
|
|
147
171
|
* - calls: each call, with the value of its arguments that are strings (null for the others);
|
|
148
172
|
* - objectTypes: the value of each `type: "…"` of an object literal;
|
|
149
173
|
* - strings: every string of the code (comments and import paths aside);
|
|
150
174
|
* - tags: the name of each Marko tag (Marko only);
|
|
151
175
|
* - comments: the text of each comment: those that the parser gives, those that the tree holds, and the comments of a Marko template;
|
|
152
|
-
* - lines: the line of each import, `new`, `type: "…"`, tag and comment. The facts that are objects have their own line.
|
|
176
|
+
* - lines: the line of each import, `new`, type that the file names, `type: "…"`, tag and comment. The facts that are objects have their own line.
|
|
153
177
|
* A node of oxc has its offset in the text (start); a node of Babel has its line too (loc), which counts from the first line of the file.
|
|
154
178
|
* @param {Node} program
|
|
155
179
|
* @param {string} text
|
|
@@ -162,9 +186,9 @@ export function factsOf(program, text, comments = []) {
|
|
|
162
186
|
const lineOf = node => node.loc?.start?.line ?? lineAt(node.start ?? 0);
|
|
163
187
|
/** @type {Facts} */
|
|
164
188
|
const facts = {
|
|
165
|
-
imports: [], imported: [], exports: [], defaultArray: null, classes: [], constructed: [],
|
|
166
|
-
comments: comments.map(({ value }) => value),
|
|
167
|
-
lines: { imports: [], constructed: [], objectTypes: [], tags: [], comments: comments.map(({ start }) => lineAt(start)) }
|
|
189
|
+
imports: [], imported: [], exports: [], defaultArray: null, classes: [], constructed: [], typeReferences: [], calls: [], objectTypes: [], strings: [],
|
|
190
|
+
tags: [], comments: comments.map(({ value }) => value),
|
|
191
|
+
lines: { imports: [], constructed: [], typeReferences: [], objectTypes: [], tags: [], comments: comments.map(({ start }) => lineAt(start)) }
|
|
168
192
|
};
|
|
169
193
|
/** @type {(source: string, node: Node) => void} */
|
|
170
194
|
const addImport = (source, node) => {
|
|
@@ -209,12 +233,16 @@ export function factsOf(program, text, comments = []) {
|
|
|
209
233
|
});
|
|
210
234
|
}
|
|
211
235
|
}
|
|
212
|
-
if (node.type === "ClassDeclaration") facts.classes.push(classOf(node, exportedClasses.has(node), lineOf));
|
|
236
|
+
if (node.type === "ClassDeclaration") facts.classes.push(classOf(node, exportedClasses.has(node), lineOf, text));
|
|
213
237
|
if (node.type === "NewExpression") {
|
|
214
238
|
facts.constructed.push(nameOf(node.callee));
|
|
215
239
|
facts.lines.constructed.push(lineOf(node));
|
|
216
240
|
for (const { node: element, item } of items) if (element.start <= node.start && node.end <= element.end) item.constructed.push(nameOf(node.callee));
|
|
217
241
|
}
|
|
242
|
+
if (node.type === "TSTypeReference") {
|
|
243
|
+
facts.typeReferences.push(typeNameOf(node.typeName));
|
|
244
|
+
facts.lines.typeReferences.push(lineOf(node));
|
|
245
|
+
}
|
|
218
246
|
if (node.type === "CallExpression") facts.calls.push({ name: nameOf(node.callee), root: rootOf(node.callee), args: node.arguments.map(stringOf), line: lineOf(node) });
|
|
219
247
|
if (node.type === "ObjectExpression") {
|
|
220
248
|
for (const property of node.properties) {
|
|
@@ -190,16 +190,18 @@ export function portOf(path) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* An adapter, or its test: {context}/infra/{folder}/{file}.
|
|
193
|
+
* An adapter, or its test: {context}/infra/{folder}/{file}. Its port is in the folder of the same name in the domain, domain/port/{folder}/:
|
|
194
|
+
* the module of that folder that is not its contract, whatever its name. The module is null when the domain has no such port.
|
|
194
195
|
* @param {string} path
|
|
195
|
-
* @
|
|
196
|
+
* @param {Set<string>} paths
|
|
197
|
+
* @returns {{ folder: string; test: boolean; portModule: string | null } | null}
|
|
196
198
|
*/
|
|
197
|
-
export function adapterOf(path) {
|
|
199
|
+
export function adapterOf(path, paths) {
|
|
198
200
|
const match = /^(src\/(?:backend|frontend)\/[^/]+)\/infra\/([a-z0-9-]+)\/([^/]+)$/.exec(path);
|
|
199
201
|
if (!match) return null;
|
|
200
202
|
const [, context, folder, name] = match;
|
|
201
|
-
const port =
|
|
202
|
-
return { folder, test: name.endsWith(".test.ts"),
|
|
203
|
+
const port = [...paths].sort().map(portOf).find(found => found !== null && !found.contract && found.module.startsWith(`${context}/domain/port/${folder}/`));
|
|
204
|
+
return { folder, test: name.endsWith(".test.ts"), portModule: port?.module ?? null };
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
/** The contexts whose frontend a file takes: `frontendOf($global, "notes")` takes the frontend of notes. @type {(file: File) => string[]} */
|
|
@@ -216,7 +218,7 @@ export const LISTS = { domain: "domain", application: "application", presentatio
|
|
|
216
218
|
|
|
217
219
|
/**
|
|
218
220
|
* Whether a list of quality/imports.mjs holds an import of a package: the package, the module or the Node API itself, or a folder of
|
|
219
|
-
* modules that holds it (`@ingenioz-it/caits/messaging/*` holds `@ingenioz-it/caits/messaging/
|
|
221
|
+
* modules that holds it (`@ingenioz-it/caits/messaging/*` holds `@ingenioz-it/caits/messaging/EventPublisher`).
|
|
220
222
|
* @param {string} specifier
|
|
221
223
|
* @param {string[]} list
|
|
222
224
|
*/
|
package/quality/dev.mjs
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
// answer, so `marko-run dev` never starts. This command gives both ports, so @marko/run does not check them: PORT, and HMR_PORT
|
|
5
5
|
// (24678 when not set). Two projects can run their servers at the same time, each with its two ports.
|
|
6
6
|
// If a port is already in use, the server stops with EADDRINUSE.
|
|
7
|
-
// It uses the Vite configuration of the project, quality/vite.config.ts: Marko, and the plugin of CAITS.
|
|
7
|
+
// It uses the Vite configuration of the project, quality/vite.config.ts: Marko, and the plugin of CAITS. The server is that of the adapter
|
|
8
|
+
// of CAITS: it boots the backend, boots it again when its code changes, and stops it, and the command, on SIGTERM or SIGINT (Ctrl+C).
|
|
8
9
|
// @marko/run writes the entry files of the server in the build folder. `npm run build` (`npm run check` runs it) empties dist/: the
|
|
9
10
|
// server writes them in a folder of its own, else it fails at the next change of a file. @marko/vite refuses an entry file whose path
|
|
10
11
|
// starts with a dot: the folder is in node_modules/, as the cache of Vite.
|
|
11
12
|
import { existsSync } from "node:fs";
|
|
12
|
-
import adapter from "@marko/run/adapter";
|
|
13
13
|
import { loadConfigFromFile } from "vite";
|
|
14
|
+
import { adapter } from "../src/adapter.js";
|
|
15
|
+
import { stopOnSignals } from "../src/server/serve.js";
|
|
14
16
|
import { painterOf } from "./terminal.mjs";
|
|
15
17
|
|
|
16
18
|
export const VITE_CONFIG = "quality/vite.config.ts";
|
|
@@ -30,7 +32,7 @@ export async function run(args) {
|
|
|
30
32
|
const port = Number(process.env.PORT ?? 3000);
|
|
31
33
|
const hmrPort = Number(process.env.HMR_PORT ?? 24678);
|
|
32
34
|
const marko = adapter();
|
|
33
|
-
// The adapter of
|
|
35
|
+
// The adapter of CAITS has the two methods.
|
|
34
36
|
const getEntryFile = marko.getEntryFile?.bind(marko);
|
|
35
37
|
const startDev = marko.startDev?.bind(marko);
|
|
36
38
|
if (!getEntryFile || !startDev) throw new Error(`The adapter ${marko.name} of @marko/run cannot start a development server.`);
|
|
@@ -88,6 +90,7 @@ export async function run(args) {
|
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
await start();
|
|
93
|
+
stopOnSignals(async () => { await running.close(); });
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/** Whether the Vite configuration of the project loads. When it does not, says why. */
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// `caits mutation-summary`: the mutation score of each file, from the report of Stryker. With --survivors, the mutants that survived.
|
|
2
2
|
// In a terminal: the score in green when every mutant is killed, else in red; the score of each file the same way, and its path in cyan;
|
|
3
|
-
// for each survivor, its line dim, its status in red, its mutator in yellow.
|
|
3
|
+
// for each survivor, its line dim, its status in red, its mutator in yellow, and why a mutant is not judged, dim.
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { NOT_RUN } from "./mutation-suite.mjs";
|
|
6
5
|
import { painterOf } from "./terminal.mjs";
|
|
7
6
|
|
|
8
7
|
/** The report that Stryker writes in JSON (quality/caits/stryker.mjs). */
|
|
@@ -13,46 +12,57 @@ export const REPORT = "reports/mutation/mutation.json";
|
|
|
13
12
|
/** @type {(file: string) => unknown} */
|
|
14
13
|
const readJson = file => JSON.parse(readFileSync(file, "utf8"));
|
|
15
14
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Stryker leaves out of the score a mutant whose tests ended in an error that is not a failed test: an error outside the tests, or a test
|
|
17
|
+
* runner that stopped. Nothing tells whether a test sees the mutant: it is not judged. `caits mutation` kills a mutant whose test files
|
|
18
|
+
* failed to load, and nothing else. @type {(m: Mutant) => boolean}
|
|
19
|
+
*/
|
|
20
|
+
const notJudged = m => m.status === "RuntimeError";
|
|
18
21
|
|
|
19
22
|
/** @type {(m: Mutant) => string} */
|
|
20
|
-
const tag = m => (m.status === "NoCoverage" ? "[no cov]" : m.status === "Survived" ? "[survived]" : "[not
|
|
23
|
+
const tag = m => (m.status === "NoCoverage" ? "[no cov]" : m.status === "Survived" ? "[survived]" : "[not judged]");
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* @param {string[]} args
|
|
24
|
-
* @
|
|
27
|
+
* @param {{ breakAt?: number | null }} [options] `breakAt`: the score under which the run fails, the threshold `break` of Stryker
|
|
28
|
+
* @returns {number} 1 when a mutant is not judged, or when the score is under `breakAt`
|
|
25
29
|
*/
|
|
26
|
-
export function run(args) {
|
|
30
|
+
export function run(args, { breakAt = null } = {}) {
|
|
27
31
|
if (!existsSync(REPORT)) throw new Error(`${REPORT} is missing: run the mutation tests first (npm run test:mutation).`);
|
|
28
32
|
const report = /** @type {{ files: Record<string, { source: string; mutants: Mutant[] }> }} */ (readJson(REPORT));
|
|
29
33
|
const rows = [];
|
|
30
|
-
let killedTotal = 0, validTotal = 0,
|
|
34
|
+
let killedTotal = 0, validTotal = 0, notJudgedTotal = 0;
|
|
31
35
|
for (const [file, data] of Object.entries(report.files)) {
|
|
32
36
|
/** @type {(status: string) => number} */
|
|
33
37
|
const count = status => data.mutants.filter(m => m.status === status).length;
|
|
34
|
-
const
|
|
35
|
-
const killed = count("Killed")
|
|
38
|
+
const unjudged = data.mutants.filter(notJudged).length;
|
|
39
|
+
const killed = count("Killed") + count("Timeout");
|
|
36
40
|
const survived = count("Survived");
|
|
37
41
|
const noCoverage = count("NoCoverage");
|
|
38
|
-
const valid = killed + survived + noCoverage +
|
|
39
|
-
killedTotal += killed; validTotal += valid;
|
|
40
|
-
rows.push({ file, killed, survived, noCoverage,
|
|
42
|
+
const valid = killed + survived + noCoverage + unjudged;
|
|
43
|
+
killedTotal += killed; validTotal += valid; notJudgedTotal += unjudged;
|
|
44
|
+
rows.push({ file, killed, survived, noCoverage, unjudged, score: valid ? (100 * killed) / valid : 100, survivors: data.mutants.filter(m => m.status === "Survived" || m.status === "NoCoverage" || notJudged(m)), source: data.source.split("\n") });
|
|
41
45
|
}
|
|
42
46
|
rows.sort((a, b) => a.score - b.score);
|
|
43
47
|
const verbose = args.includes("--survivors");
|
|
44
48
|
const paint = painterOf(process.stdout);
|
|
45
49
|
/** @type {(killed: number, valid: number) => "green" | "red"} */
|
|
46
50
|
const colorOf = (killed, valid) => killed === valid ? "green" : "red";
|
|
47
|
-
|
|
51
|
+
const score = validTotal ? (100 * killedTotal) / validTotal : 100;
|
|
52
|
+
console.log(`${paint([colorOf(killedTotal, validTotal), "bold"], `Mutation score: ${score.toFixed(1)}% (${killedTotal} killed of ${validTotal} valid mutants)`)}\n`);
|
|
48
53
|
for (const row of rows) {
|
|
49
|
-
const valid = row.killed + row.survived + row.noCoverage + row.
|
|
50
|
-
console.log(`${paint(colorOf(row.killed, valid), `${row.score.toFixed(0).padStart(4)}%`)} ${paint("cyan", row.file)} (killed ${row.killed}, survived ${row.survived}, no coverage ${row.noCoverage}${row.
|
|
54
|
+
const valid = row.killed + row.survived + row.noCoverage + row.unjudged;
|
|
55
|
+
console.log(`${paint(colorOf(row.killed, valid), `${row.score.toFixed(0).padStart(4)}%`)} ${paint("cyan", row.file)} (killed ${row.killed}, survived ${row.survived}, no coverage ${row.noCoverage}${row.unjudged ? `, not judged ${row.unjudged}` : ""})`);
|
|
51
56
|
if (verbose && row.survivors.length) {
|
|
52
|
-
for (const m of row.survivors)
|
|
57
|
+
for (const m of row.survivors) {
|
|
58
|
+
console.log(` ${paint("dim", `L${m.location.start.line}`)} ${paint("red", tag(m))} ${paint("yellow", m.mutatorName)}: ${(row.source[m.location.start.line - 1] ?? "").trim().slice(0, 90)} ${paint("dim", "→")} ${String(m.replacement ?? "").slice(0, 60)}`);
|
|
59
|
+
if (notJudged(m) && m.statusReason) console.log(` ${paint("dim", m.statusReason.split("\n")[0].slice(0, 120))}`);
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
if (notJudgedTotal) {
|
|
64
|
+
const one = notJudgedTotal === 1;
|
|
65
|
+
console.log(`\n${paint("red", `${notJudgedTotal} ${one ? "mutant is" : "mutants are"} not judged: ${one ? "its" : "their"} tests ended in an error that is not a failed test, and Stryker leaves ${one ? "it" : "them"} out of the score. Run the mutation tests again; when ${one ? "it" : "one"} comes back, write a test that sees what it changes.`)}`);
|
|
66
|
+
}
|
|
67
|
+
return notJudgedTotal > 0 || (breakAt !== null && score < breakAt) ? 1 : 0;
|
|
58
68
|
}
|
package/quality/mutation.mjs
CHANGED
|
@@ -1,29 +1,185 @@
|
|
|
1
1
|
// `caits mutation`: the mutation tests (Stryker, with quality/stryker.config.mjs), then the score of each file and the mutants that
|
|
2
2
|
// survived, whether Stryker passed or not. Stryker alone ends with its threshold when the score is too low: the survivors say what to fix.
|
|
3
|
-
//
|
|
3
|
+
// Stryker stops the tests of a mutant that run too long, and counts the mutant as killed: the mutant may make the code loop without end.
|
|
4
|
+
// Its counter stops most of these loops, as it counts the runs of the code of the mutant, whatever the load of the machine. The clock
|
|
5
|
+
// stops the rest, but the load slows a mutant that survives, which runs the most tests, past its time: the mutants that the clock stopped
|
|
6
|
+
// run again, with more time and fewer tests at once, and their second result counts. A mutant whose test files failed to load is killed:
|
|
7
|
+
// its code throws as soon as a test imports it, every time, as a failed test does, where Stryker counts an error outside the tests. The
|
|
8
|
+
// run fails when a mutant is not judged (`caits mutation-summary`).
|
|
4
9
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { existsSync, rmSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
6
14
|
import { REPORT, run as summary } from "./mutation-summary.mjs";
|
|
15
|
+
import { painterOf } from "./terminal.mjs";
|
|
7
16
|
|
|
8
17
|
/** The configuration of Stryker in a project: it imports the defaults of CAITS (quality/caits/stryker.mjs). */
|
|
9
18
|
export const STRYKER_CONFIG = "quality/stryker.config.mjs";
|
|
10
19
|
|
|
20
|
+
/** The page of the report, which Stryker writes with the report in a line of its script (quality/caits/stryker.mjs). */
|
|
21
|
+
const PAGE = "reports/mutation/index.html";
|
|
22
|
+
|
|
23
|
+
/** The line of the page that holds the report. */
|
|
24
|
+
const PAGE_REPORT = /^( *app\.report = ).*;$/m;
|
|
25
|
+
|
|
26
|
+
/** The second run of the mutants that the clock stopped: more time for each, fewer at once, and the report in JSON only. */
|
|
27
|
+
const SECOND_RUN = ["--concurrency", "4", "--timeoutFactor", "3", "--reporters", "json"];
|
|
28
|
+
|
|
29
|
+
/** How Stryker starts the reason of an error outside the tests: then come the test files that failed to load, then the other errors. */
|
|
30
|
+
const OUTSIDE = "An error occurred outside of a test run: ";
|
|
31
|
+
|
|
32
|
+
/** A test file that failed to load, as Stryker gives it on a line of its own: `<its path>: <the message>`. */
|
|
33
|
+
const LOAD_FAILURE = /^\/.*\.(?:test|spec)\.[cm]?[jt]sx?: /;
|
|
34
|
+
|
|
35
|
+
/** The temporary folders of the runs, in the temporary folder of the system: `caits-mutation-<pid of their run>-<random>`. */
|
|
36
|
+
const TEMPORARY = /^caits-mutation-(\d+)-/;
|
|
37
|
+
|
|
38
|
+
/** @typedef {{ status: string; statusReason?: string; mutatorName: string; replacement?: string; location: { start: { line: number; column: number }; end: { line: number; column: number } } }} Mutant */
|
|
39
|
+
/** @typedef {{ files: Record<string, { mutants: Mutant[] }> }} Report */
|
|
40
|
+
|
|
11
41
|
/**
|
|
12
42
|
* @param {string[]} args the options of Stryker
|
|
13
|
-
* @
|
|
43
|
+
* @param {string} [config] the configuration of Stryker: a module, or JSON
|
|
44
|
+
* @returns {Promise<number>} the exit code of Stryker, or 1 when a mutant is not judged, or when the score is under the threshold after
|
|
45
|
+
* the second run
|
|
14
46
|
*/
|
|
15
|
-
export async function run(args) {
|
|
16
|
-
if (!existsSync(
|
|
47
|
+
export async function run(args, config = STRYKER_CONFIG) {
|
|
48
|
+
if (!existsSync(config)) throw new Error(`${config} is missing: it gives the configuration of the mutation tests. Run \`npx caits init .\` to write it.`);
|
|
17
49
|
// A run that stops before its report must not show the summary of the run before.
|
|
18
50
|
rmSync(REPORT, { force: true });
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
removeOrphans();
|
|
52
|
+
// The tests write their temporary files in a folder of this run: a test that Stryker stops cannot remove them. In WSL, the temporary
|
|
53
|
+
// folder of the system is in memory.
|
|
54
|
+
const temporary = mkdtempSync(join(tmpdir(), `caits-mutation-${process.pid}-`));
|
|
55
|
+
try {
|
|
56
|
+
const status = await stryker([config, ...args], temporary);
|
|
57
|
+
if (!existsSync(REPORT)) return status;
|
|
58
|
+
const first = readReport();
|
|
59
|
+
let report = first;
|
|
60
|
+
const late = Object.entries(first.files).flatMap(([file, { mutants }]) => mutants.filter(stoppedByTheClock).map(mutant => ({ file, mutant })));
|
|
61
|
+
if (late.length > 0) {
|
|
62
|
+
const one = late.length === 1;
|
|
63
|
+
console.log(`\n${late.length} ${one ? "mutant" : "mutants"} ran out of time on the clock, which the load of the machine slows: ${one ? "it runs" : "they run"} again, with more time and fewer tests at once.\n`);
|
|
64
|
+
rmSync(REPORT);
|
|
65
|
+
const ranges = [...new Set(late.map(({ file, mutant }) => `${file}:${mutant.location.start.line}-${mutant.location.end.line}`))];
|
|
66
|
+
await stryker([config, ...args, "--mutate", ranges.join(","), ...SECOND_RUN], temporary);
|
|
67
|
+
if (!existsSync(REPORT)) {
|
|
68
|
+
writeFileSync(REPORT, JSON.stringify(first));
|
|
69
|
+
console.log("");
|
|
70
|
+
summary(["--survivors"]);
|
|
71
|
+
console.log(`\n${painterOf(process.stdout)("red", "The second run stopped before its report: the mutants that the clock stopped count as killed, as Stryker counts them. Run the mutation tests again.")}`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
report = withSecondResults(first, readReport());
|
|
75
|
+
}
|
|
76
|
+
report = withLoadFailuresKilled(report);
|
|
77
|
+
writeFileSync(REPORT, JSON.stringify(report));
|
|
78
|
+
// The page shows the report too: Stryker breaks each < apart, so that no text of the report ends the script.
|
|
79
|
+
if (existsSync(PAGE)) writeFileSync(PAGE, readFileSync(PAGE, "utf8").replace(PAGE_REPORT, (_, start) => `${start}${JSON.stringify(report).replaceAll("<", '<"+"')};`));
|
|
80
|
+
console.log("");
|
|
81
|
+
return summary(["--survivors"], { breakAt: await breakOf(config) }) || status;
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs Stryker. Ctrl+C and `caits check` stop the whole group of processes: Stryker and its test runners stop with this command.
|
|
89
|
+
* @param {string[]} args
|
|
90
|
+
* @param {string} temporary the temporary folder of the tests
|
|
91
|
+
* @returns {Promise<number>}
|
|
92
|
+
*/
|
|
93
|
+
function stryker(args, temporary) {
|
|
94
|
+
const running = spawn("npx", ["stryker", "run", ...args], { stdio: "inherit", env: { ...process.env, TMPDIR: temporary } });
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
running.on("error", () => resolve(1));
|
|
97
|
+
running.on("close", code => resolve(code ?? 1));
|
|
25
98
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {(file: string) => unknown} */
|
|
102
|
+
const readJson = file => JSON.parse(readFileSync(file, "utf8"));
|
|
103
|
+
|
|
104
|
+
/** @returns {Report} */
|
|
105
|
+
const readReport = () => /** @type {Report} */ (readJson(REPORT));
|
|
106
|
+
|
|
107
|
+
/** The clock stopped the mutant: Stryker gives a reason only when its counter stops it. @type {(mutant: Mutant) => boolean} */
|
|
108
|
+
const stoppedByTheClock = mutant => mutant.status === "Timeout" && !mutant.statusReason?.startsWith("Hit limit reached");
|
|
109
|
+
|
|
110
|
+
/** @type {(file: string, mutant: Mutant) => string} */
|
|
111
|
+
const keyOf = (file, { mutatorName, replacement, location: { start, end } }) => [file, start.line, start.column, end.line, end.column, mutatorName, replacement].join(" ");
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The first report, with the results of the second run for the mutants that the clock stopped. The second run tests the whole lines of
|
|
115
|
+
* these mutants: its other mutants keep their first result.
|
|
116
|
+
* @param {Report} first
|
|
117
|
+
* @param {Report} second
|
|
118
|
+
* @returns {Report}
|
|
119
|
+
*/
|
|
120
|
+
function withSecondResults(first, second) {
|
|
121
|
+
const again = new Map(Object.entries(second.files).flatMap(([file, { mutants }]) => mutants.map(mutant => [keyOf(file, mutant), mutant])));
|
|
122
|
+
const files = Object.fromEntries(Object.entries(first.files).map(([file, data]) => [file, {
|
|
123
|
+
...data,
|
|
124
|
+
mutants: data.mutants.map(mutant => {
|
|
125
|
+
const result = stoppedByTheClock(mutant) ? again.get(keyOf(file, mutant)) : undefined;
|
|
126
|
+
return result === undefined ? mutant : { ...mutant, status: result.status, statusReason: result.statusReason };
|
|
127
|
+
})
|
|
128
|
+
}]));
|
|
129
|
+
return { ...first, files };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The test files of the mutant failed to load, and nothing else: each line of its reason is a test file that failed to load. An error
|
|
134
|
+
* after the tests, or a message on more than one line, has a line that is not one, and the mutant stays not judged.
|
|
135
|
+
* @type {(mutant: Mutant) => boolean}
|
|
136
|
+
*/
|
|
137
|
+
const failedToLoad = ({ status, statusReason }) => status === "RuntimeError" && typeof statusReason === "string" && statusReason.startsWith(OUTSIDE)
|
|
138
|
+
&& statusReason.slice(OUTSIDE.length).split("\n").every(line => LOAD_FAILURE.test(line));
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The report, where a mutant whose test files failed to load is killed.
|
|
142
|
+
* @param {Report} report
|
|
143
|
+
* @returns {Report}
|
|
144
|
+
*/
|
|
145
|
+
function withLoadFailuresKilled(report) {
|
|
146
|
+
const files = Object.fromEntries(Object.entries(report.files).map(([file, data]) => [file, {
|
|
147
|
+
...data,
|
|
148
|
+
mutants: data.mutants.map(mutant => failedToLoad(mutant) ? { ...mutant, status: "Killed", statusReason: `The test files that import its code failed to load. ${mutant.statusReason}` } : mutant)
|
|
149
|
+
}]));
|
|
150
|
+
return { ...report, files };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The threshold `break` of the configuration: the score under which Stryker fails, none by default.
|
|
155
|
+
* @param {string} config
|
|
156
|
+
* @returns {Promise<number | null>}
|
|
157
|
+
*/
|
|
158
|
+
async function breakOf(config) {
|
|
159
|
+
const module = config.endsWith(".json") ? { default: readJson(config) } : /** @type {{ default: unknown }} */ (await import(pathToFileURL(resolve(config)).href));
|
|
160
|
+
return /** @type {{ thresholds?: { break?: number | null } }} */ (module.default).thresholds?.break ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Removes the temporary folders whose run no longer runs: Ctrl+C and `caits check` stop a run before its end. */
|
|
164
|
+
function removeOrphans() {
|
|
165
|
+
for (const entry of readdirSync(tmpdir())) {
|
|
166
|
+
const pid = TEMPORARY.exec(entry)?.[1];
|
|
167
|
+
if (pid === undefined || runs(Number(pid))) continue;
|
|
168
|
+
try {
|
|
169
|
+
rmSync(join(tmpdir(), entry), { recursive: true, force: true });
|
|
170
|
+
} catch {
|
|
171
|
+
// The folder of another user.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {number} pid */
|
|
177
|
+
function runs(pid) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// EPERM: the process runs, but belongs to another user.
|
|
183
|
+
return /** @type {NodeJS.ErrnoException} */ (error).code === "EPERM";
|
|
184
|
+
}
|
|
29
185
|
}
|