@jsenv/test 2.1.3 → 3.0.1
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/README.md +2 -1
- package/dist/js/exception.js +908 -0
- package/dist/js/execute_using_dynamic_import.js +76 -167
- package/dist/js/v8_coverage.js +4 -482
- package/dist/jsenv_test.js +4608 -2917
- package/dist/node_child_process_controlled.mjs +95 -76
- package/dist/node_worker_thread_controlled.mjs +72 -54
- package/package.json +23 -14
- package/src/coverage/generate_coverage.js +186 -0
- package/src/coverage/list_files_not_covered.js +2 -2
- package/src/coverage/missing_coverage.js +2 -2
- package/src/coverage/report_coverage_as_html.js +45 -0
- package/src/coverage/report_coverage_as_json.js +20 -0
- package/src/coverage/{coverage_reporter_text_log.js → report_coverage_in_console.js} +13 -7
- package/src/coverage/v8_and_istanbul.js +4 -4
- package/src/coverage/v8_coverage.js +2 -2
- package/src/execution/exception.js +151 -68
- package/src/execution/execute.js +24 -11
- package/src/execution/execute_test_plan.js +834 -750
- package/src/execution/format_error_for_terminal.js +187 -0
- package/src/execution/github_annotation_from_error.js +3 -3
- package/src/execution/junit_xml_file/report_as_junit_xml.js +149 -0
- package/src/execution/junit_xml_file/xml_generator.js +141 -0
- package/src/execution/reporters/reporter_list.js +687 -0
- package/src/execution/run.js +92 -30
- package/src/execution/web_server/start_using_command.js +2 -2
- package/src/execution/web_server_param.js +5 -5
- package/src/helpers/cpu_usage.js +142 -0
- package/src/helpers/cpu_usage.test_manual.js +7 -0
- package/src/main.js +8 -0
- package/src/runtime_browsers/chromium.js +1 -3
- package/src/runtime_browsers/firefox.js +0 -3
- package/src/runtime_browsers/middleware_istanbul.js +2 -2
- package/src/runtime_browsers/using_playwright.js +296 -220
- package/src/runtime_browsers/webkit.js +0 -3
- package/src/runtime_node/execute_using_dynamic_import.js +75 -28
- package/src/runtime_node/node_child_process.js +250 -139
- package/src/runtime_node/node_child_process_controlled.mjs +94 -75
- package/src/runtime_node/node_worker_thread.js +206 -120
- package/src/runtime_node/node_worker_thread_controlled.mjs +70 -53
- package/src/coverage/coverage_reporter_html_directory.js +0 -32
- package/src/coverage/coverage_reporter_json_file.js +0 -17
- package/src/coverage/report_to_coverage.js +0 -198
- package/src/execution/execution_colors.js +0 -10
- package/src/execution/execution_steps.js +0 -65
- package/src/execution/logs_file_execution.js +0 -495
|
@@ -1,45 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
|
+
*
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cpus, availableParallelism, freemem, totalmem } from "node:os";
|
|
2
6
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { memoryUsage } from "node:process";
|
|
4
7
|
import { takeCoverage } from "node:v8";
|
|
5
8
|
import stripAnsi from "strip-ansi";
|
|
6
9
|
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
7
10
|
import { URL_META } from "@jsenv/url-meta";
|
|
8
|
-
import { urlToFileSystemPath, urlToRelativeUrl } from "@jsenv/urls";
|
|
9
11
|
import {
|
|
10
12
|
ensureEmptyDirectory,
|
|
11
13
|
assertAndNormalizeDirectoryUrl,
|
|
12
|
-
|
|
13
|
-
writeFileSync,
|
|
14
|
+
collectFiles,
|
|
14
15
|
} from "@jsenv/filesystem";
|
|
15
|
-
import {
|
|
16
|
-
createLogger,
|
|
17
|
-
createDetailedMessage,
|
|
18
|
-
UNICODE,
|
|
19
|
-
createLog,
|
|
20
|
-
startSpinner,
|
|
21
|
-
} from "@jsenv/log";
|
|
16
|
+
import { createLogger, createDetailedMessage, UNICODE } from "@jsenv/log";
|
|
22
17
|
import {
|
|
23
18
|
startGithubCheckRun,
|
|
24
19
|
readGitHubWorkflowEnv,
|
|
25
20
|
} from "@jsenv/github-check-run";
|
|
26
21
|
|
|
27
|
-
import {
|
|
22
|
+
import { startMeasuringCpuUsage } from "../helpers/cpu_usage.js";
|
|
28
23
|
import { createCallOrderer } from "../helpers/call_orderer.js";
|
|
29
|
-
import {
|
|
30
|
-
import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
|
|
31
|
-
import { generateCoverageHtmlDirectory } from "../coverage/coverage_reporter_html_directory.js";
|
|
32
|
-
import { generateCoverageTextLog } from "../coverage/coverage_reporter_text_log.js";
|
|
24
|
+
import { generateCoverage } from "../coverage/generate_coverage.js";
|
|
33
25
|
import { assertAndNormalizeWebServer } from "./web_server_param.js";
|
|
34
|
-
import { executionStepsFromTestPlan } from "./execution_steps.js";
|
|
35
|
-
import {
|
|
36
|
-
createExecutionLog,
|
|
37
|
-
formatSummaryLog,
|
|
38
|
-
formatSummary,
|
|
39
|
-
} from "./logs_file_execution.js";
|
|
40
26
|
import { githubAnnotationFromError } from "./github_annotation_from_error.js";
|
|
41
27
|
import { run } from "./run.js";
|
|
42
|
-
import {
|
|
28
|
+
import { reporterList, renderOutro } from "./reporters/reporter_list.js";
|
|
43
29
|
|
|
44
30
|
/**
|
|
45
31
|
* Execute a list of files and log how it goes.
|
|
@@ -47,58 +33,29 @@ import { ensureGlobalGc } from "./gc.js";
|
|
|
47
33
|
* @param {string|url} testPlanParameters.rootDirectoryUrl Directory containing test files;
|
|
48
34
|
* @param {Object} [testPlanParameters.webServer] Web server info; required when executing test on browsers
|
|
49
35
|
* @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {boolean} [testPlanParameters.logMergeForCompletedExecutions=false] Merge completed execution logs to shorten terminal output
|
|
52
|
-
* @param {boolean|number} [testPlanParameters.concurrency=false] Maximum amount of execution running at the same time
|
|
36
|
+
* @param {Object|false} [testPlanParameters.parallel] Maximum amount of execution running at the same time
|
|
53
37
|
* @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
|
|
54
38
|
* @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
|
|
55
|
-
* @param {
|
|
56
|
-
* @param {boolean} [testPlanParameters.logMemoryHeapUsage=false] Add memory heap usage during logs
|
|
57
|
-
* @param {boolean} [testPlanParameters.coverageEnabled=false] Controls if coverage is collected during files executions
|
|
58
|
-
* @param {boolean} [testPlanParameters.coverageV8ConflictWarning=true] Warn when coverage from 2 executions cannot be merged
|
|
39
|
+
* @param {Object|false} [testPlanParameters.coverage=false] Controls if coverage is collected during files executions
|
|
59
40
|
* @return {Object} An object containing the result of all file executions
|
|
60
41
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
updateProcessExitCode = true,
|
|
79
|
-
concurrency = false,
|
|
80
|
-
defaultMsAllocatedPerExecution = 30_000,
|
|
81
|
-
failFast = false,
|
|
82
|
-
// keepRunning: false to ensure runtime is stopped once executed
|
|
83
|
-
// because we have what we wants: execution is completed and
|
|
84
|
-
// we have associated coverage and console output
|
|
85
|
-
// passsing true means all node process and browsers launched stays opened
|
|
86
|
-
// (can eventually be used for debug)
|
|
87
|
-
keepRunning = false,
|
|
88
|
-
cooldownBetweenExecutions = 0,
|
|
89
|
-
gcBetweenExecutions = logMemoryHeapUsage,
|
|
90
|
-
|
|
91
|
-
githubCheckEnabled = Boolean(process.env.GITHUB_WORKFLOW),
|
|
92
|
-
githubCheckLogLevel,
|
|
93
|
-
githubCheckName = "Jsenv tests",
|
|
94
|
-
githubCheckTitle = "Tests executions",
|
|
95
|
-
githubCheckToken,
|
|
96
|
-
githubCheckRepositoryOwner,
|
|
97
|
-
githubCheckRepositoryName,
|
|
98
|
-
githubCheckCommitSha,
|
|
99
|
-
|
|
100
|
-
coverageEnabled = process.argv.includes("--coverage"),
|
|
101
|
-
coverageConfig = {
|
|
42
|
+
const logsDefault = {
|
|
43
|
+
level: "info",
|
|
44
|
+
type: "list",
|
|
45
|
+
animated: true,
|
|
46
|
+
fileUrl: undefined,
|
|
47
|
+
};
|
|
48
|
+
const githubCheckDefault = {
|
|
49
|
+
logLevel: "info",
|
|
50
|
+
name: "Jsenv tests",
|
|
51
|
+
title: "Tests execution",
|
|
52
|
+
token: undefined,
|
|
53
|
+
repositoryOwner: undefined,
|
|
54
|
+
repositoryName: undefined,
|
|
55
|
+
commitSha: undefined,
|
|
56
|
+
};
|
|
57
|
+
const coverageDefault = {
|
|
58
|
+
include: {
|
|
102
59
|
"file:///**/node_modules/": false,
|
|
103
60
|
"./**/.*": false,
|
|
104
61
|
"./**/.*/": false,
|
|
@@ -112,9 +69,9 @@ export const executeTestPlan = async ({
|
|
|
112
69
|
"./**/*.test.js": false,
|
|
113
70
|
"./**/*.test.mjs": false,
|
|
114
71
|
},
|
|
115
|
-
|
|
116
|
-
coverageAndExecutionAllowed
|
|
117
|
-
|
|
72
|
+
includeMissing: true,
|
|
73
|
+
coverageAndExecutionAllowed: false,
|
|
74
|
+
methodForNodeJs: process.env.NODE_V8_COVERAGE
|
|
118
75
|
? "NODE_V8_COVERAGE"
|
|
119
76
|
: "Profiler",
|
|
120
77
|
// - When chromium only -> coverage generated by v8
|
|
@@ -124,765 +81,892 @@ export const executeTestPlan = async ({
|
|
|
124
81
|
// -> by default only coverage from chromium is used
|
|
125
82
|
// and a warning is logged according to coverageV8ConflictWarning
|
|
126
83
|
// -> to collect coverage from both browsers, pass coverageMethodForBrowsers: "istanbul"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
84
|
+
methodForBrowsers: undefined, // undefined | "playwright" | "istanbul"
|
|
85
|
+
v8ConflictWarning: true,
|
|
86
|
+
tempDirectoryUrl: undefined,
|
|
87
|
+
};
|
|
88
|
+
const parallelDefault = {
|
|
89
|
+
max: "80%", // percentage resolved against the available cpus
|
|
90
|
+
maxCpu: "90%",
|
|
91
|
+
maxMemory: "90%",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const executeTestPlan = async ({
|
|
95
|
+
logs = logsDefault,
|
|
96
|
+
|
|
97
|
+
rootDirectoryUrl,
|
|
98
|
+
webServer,
|
|
99
|
+
testPlan,
|
|
100
|
+
|
|
101
|
+
signal = new AbortController().signal,
|
|
102
|
+
handleSIGINT = true,
|
|
103
|
+
handleSIGUP = true,
|
|
104
|
+
handleSIGTERM = true,
|
|
105
|
+
updateProcessExitCode = true,
|
|
106
|
+
parallel = parallelDefault,
|
|
107
|
+
defaultMsAllocatedPerExecution = 30_000,
|
|
108
|
+
failFast = false,
|
|
109
|
+
// keepRunning: false to ensure runtime is stopped once executed
|
|
110
|
+
// because we have what we wants: execution is completed and
|
|
111
|
+
// we have associated coverage and console output
|
|
112
|
+
// passsing true means all node process and browsers launched stays opened
|
|
113
|
+
// (can eventually be used for debug)
|
|
114
|
+
keepRunning = false,
|
|
115
|
+
|
|
116
|
+
githubCheck = process.env.GITHUB_WORKFLOW ? githubCheckDefault : null,
|
|
117
|
+
coverage = process.argv.includes("--coverage") ? coverageDefault : null,
|
|
118
|
+
|
|
119
|
+
reporters = [],
|
|
143
120
|
...rest
|
|
144
121
|
}) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
const beforeExecutionCallbackSet = new Set();
|
|
148
|
-
const afterExecutionCallbackSet = new Set();
|
|
149
|
-
const afterAllExecutionCallbackSet = new Set();
|
|
150
|
-
beforeExecutionCallbackSet.add(beforeExecutionCallback);
|
|
151
|
-
afterExecutionCallbackSet.add(afterExecutionCallback);
|
|
152
|
-
afterAllExecutionCallbackSet.add(afterAllExecutionCallback);
|
|
122
|
+
const teardownCallbackSet = new Set();
|
|
153
123
|
|
|
154
124
|
const operation = Abort.startOperation();
|
|
155
125
|
operation.addAbortSignal(signal);
|
|
156
|
-
if (handleSIGINT) {
|
|
126
|
+
if (handleSIGINT || handleSIGUP || handleSIGTERM) {
|
|
157
127
|
operation.addAbortSource((abort) => {
|
|
158
128
|
return raceProcessTeardownEvents(
|
|
159
129
|
{
|
|
160
|
-
SIGINT:
|
|
130
|
+
SIGINT: handleSIGINT,
|
|
131
|
+
SIGHUP: handleSIGUP,
|
|
132
|
+
SIGTERM: handleSIGTERM,
|
|
161
133
|
},
|
|
162
|
-
() => {
|
|
163
|
-
|
|
134
|
+
({ name }) => {
|
|
135
|
+
console.log(`${name} -> abort`);
|
|
164
136
|
abort();
|
|
165
137
|
},
|
|
166
138
|
);
|
|
167
139
|
});
|
|
168
140
|
}
|
|
169
141
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
184
|
-
rootDirectoryUrl,
|
|
185
|
-
"rootDirectoryUrl",
|
|
186
|
-
);
|
|
187
|
-
if (!existsSync(new URL(rootDirectoryUrl))) {
|
|
188
|
-
throw new Error(`ENOENT on rootDirectoryUrl at ${rootDirectoryUrl}`);
|
|
189
|
-
}
|
|
190
|
-
if (typeof testPlan !== "object") {
|
|
191
|
-
throw new Error(`testPlan must be an object, got ${testPlan}`);
|
|
192
|
-
}
|
|
142
|
+
const timingOrigin = Date.now();
|
|
143
|
+
const takeTiming = () => {
|
|
144
|
+
return Date.now() - timingOrigin;
|
|
145
|
+
};
|
|
146
|
+
const testPlanResult = {
|
|
147
|
+
rootDirectoryUrl: String(rootDirectoryUrl),
|
|
148
|
+
groups: {},
|
|
149
|
+
counters: {
|
|
150
|
+
planified: 0,
|
|
151
|
+
remaining: 0,
|
|
152
|
+
waiting: 0,
|
|
153
|
+
executing: 0,
|
|
154
|
+
executed: 0,
|
|
193
155
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
156
|
+
aborted: 0,
|
|
157
|
+
cancelled: 0,
|
|
158
|
+
timedout: 0,
|
|
159
|
+
failed: 0,
|
|
160
|
+
completed: 0,
|
|
161
|
+
},
|
|
162
|
+
aborted: false,
|
|
163
|
+
failed: false,
|
|
164
|
+
timings: {
|
|
165
|
+
origin: timingOrigin,
|
|
166
|
+
executionStart: null,
|
|
167
|
+
executionEnd: null,
|
|
168
|
+
teardownEnd: null,
|
|
169
|
+
coverageTeardownEnd: null,
|
|
170
|
+
end: null,
|
|
171
|
+
},
|
|
172
|
+
coverage: null,
|
|
173
|
+
results: {},
|
|
174
|
+
};
|
|
175
|
+
const timings = testPlanResult.timings;
|
|
176
|
+
const groups = testPlanResult.groups;
|
|
177
|
+
const counters = testPlanResult.counters;
|
|
178
|
+
const countersInOrder = { ...counters };
|
|
179
|
+
const results = testPlanResult.results;
|
|
180
|
+
|
|
181
|
+
const beforeEachCallbackSet = new Set();
|
|
182
|
+
const beforeEachInOrderCallbackSet = new Set();
|
|
183
|
+
const afterEachCallbackSet = new Set();
|
|
184
|
+
const afterEachInOrderCallbackSet = new Set();
|
|
185
|
+
const afterAllCallbackSet = new Set();
|
|
186
|
+
let finalizeCoverage;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const cpuUsage = startMeasuringCpuUsage();
|
|
190
|
+
operation.addEndCallback(cpuUsage.stop);
|
|
191
|
+
|
|
192
|
+
let logger;
|
|
193
|
+
const runtimeInfo = {
|
|
194
|
+
someNeedsServer: false,
|
|
195
|
+
someHasCoverageV8: false,
|
|
196
|
+
someNodeRuntime: false,
|
|
197
|
+
};
|
|
198
|
+
// param validation and normalization
|
|
199
|
+
{
|
|
200
|
+
const unexpectedParamNames = Object.keys(rest);
|
|
201
|
+
if (unexpectedParamNames.length > 0) {
|
|
202
|
+
throw new TypeError(`${unexpectedParamNames.join(",")}: no such param`);
|
|
203
|
+
}
|
|
204
|
+
// logs
|
|
205
|
+
{
|
|
206
|
+
if (typeof logs !== "object") {
|
|
207
|
+
throw new TypeError(`logs must be an object, got ${logs}`);
|
|
208
|
+
}
|
|
209
|
+
const unexpectedLogsKeys = Object.keys(logs).filter(
|
|
210
|
+
(key) => !Object.hasOwn(logsDefault, key),
|
|
211
|
+
);
|
|
212
|
+
if (unexpectedLogsKeys.length > 0) {
|
|
213
|
+
throw new TypeError(
|
|
214
|
+
`${unexpectedLogsKeys.join(",")}: no such key on logs`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
logs = { ...logsDefault, ...logs };
|
|
218
|
+
logger = createLogger({ logLevel: logs.level });
|
|
219
|
+
|
|
220
|
+
if (logs.type === "list" && logger.levels.info) {
|
|
221
|
+
const listReporterOptions = {
|
|
222
|
+
animated: logger.levels.debug ? false : logs.animated,
|
|
223
|
+
fileUrl:
|
|
224
|
+
logs.fileUrl === undefined
|
|
225
|
+
? new URL("./.jsenv/jsenv_tests_output.txt", rootDirectoryUrl)
|
|
226
|
+
: logs.fileUrl,
|
|
227
|
+
};
|
|
228
|
+
reporters.push(reporterList(listReporterOptions));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// rootDirectoryUrl
|
|
232
|
+
{
|
|
233
|
+
rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
234
|
+
rootDirectoryUrl,
|
|
235
|
+
"rootDirectoryUrl",
|
|
236
|
+
);
|
|
237
|
+
if (!existsSync(new URL(rootDirectoryUrl))) {
|
|
238
|
+
throw new Error(`ENOENT on rootDirectoryUrl at ${rootDirectoryUrl}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// parallel
|
|
242
|
+
{
|
|
243
|
+
if (parallel === true) {
|
|
244
|
+
parallel = {};
|
|
245
|
+
}
|
|
246
|
+
if (parallel === false) {
|
|
247
|
+
parallel = { max: 1 };
|
|
248
|
+
}
|
|
249
|
+
if (typeof parallel !== "object") {
|
|
250
|
+
throw new TypeError(`parallel must be an object, got ${parallel}`);
|
|
251
|
+
}
|
|
252
|
+
const unexpectedParallelKeys = Object.keys(parallel).filter(
|
|
253
|
+
(key) => !Object.hasOwn(parallelDefault, key),
|
|
254
|
+
);
|
|
255
|
+
if (unexpectedParallelKeys.length > 0) {
|
|
256
|
+
throw new TypeError(
|
|
257
|
+
`${unexpectedParallelKeys.join(",")}: no such key on parallel`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
parallel = { ...parallelDefault, ...parallel };
|
|
261
|
+
const assertPercentageAndConvertToRatio = (string) => {
|
|
262
|
+
const lastChar = string[string.length - 1];
|
|
263
|
+
if (lastChar !== "%") {
|
|
264
|
+
throw new TypeError(`string is not a percentage, got ${string}`);
|
|
265
|
+
}
|
|
266
|
+
const percentageString = string.slice(0, -1);
|
|
267
|
+
const percentageNumber = parseInt(percentageString);
|
|
268
|
+
if (percentageNumber <= 0) {
|
|
269
|
+
return 0;
|
|
209
270
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
271
|
+
if (percentageNumber >= 100) {
|
|
272
|
+
return 1;
|
|
212
273
|
}
|
|
274
|
+
const ratio = percentageNumber / 100;
|
|
275
|
+
return ratio;
|
|
276
|
+
};
|
|
277
|
+
const max = parallel.max;
|
|
278
|
+
if (typeof max === "string") {
|
|
279
|
+
const maxAsRatio = assertPercentageAndConvertToRatio(max);
|
|
280
|
+
const availableCpus = countAvailableCpus();
|
|
281
|
+
parallel.max = Math.round(maxAsRatio * availableCpus) || 1;
|
|
282
|
+
} else if (typeof max === "number") {
|
|
283
|
+
if (max < 1) {
|
|
284
|
+
parallel.max = 1;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
throw new TypeError(
|
|
288
|
+
`parallel.max must be a number or a percentage, got ${max}`,
|
|
289
|
+
);
|
|
213
290
|
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (someNeedsServer) {
|
|
218
|
-
await assertAndNormalizeWebServer(webServer, {
|
|
219
|
-
signal: operation.signal,
|
|
220
|
-
teardown,
|
|
221
|
-
logger,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
291
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
292
|
+
const maxMemory = parallel.maxMemory;
|
|
293
|
+
if (typeof maxMemory === "string") {
|
|
294
|
+
const maxMemoryAsRatio = assertPercentageAndConvertToRatio(maxMemory);
|
|
295
|
+
parallel.maxMemory = Math.round(maxMemoryAsRatio * totalmem());
|
|
296
|
+
} else if (typeof maxMemory !== "number") {
|
|
297
|
+
throw new TypeError(
|
|
298
|
+
`parallel.maxMemory must be a number or a percentage, got ${maxMemory}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// testPlan
|
|
303
|
+
{
|
|
304
|
+
if (typeof testPlan !== "object") {
|
|
305
|
+
throw new Error(`testPlan must be an object, got ${testPlan}`);
|
|
306
|
+
}
|
|
307
|
+
for (const filePattern of Object.keys(testPlan)) {
|
|
308
|
+
const filePlan = testPlan[filePattern];
|
|
309
|
+
if (!filePlan) continue;
|
|
310
|
+
for (const executionName of Object.keys(filePlan)) {
|
|
311
|
+
const executionConfig = filePlan[executionName];
|
|
312
|
+
if (executionConfig === null) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const { runtime } = executionConfig;
|
|
316
|
+
if (!runtime || runtime.disabled) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (runtime.type === "browser") {
|
|
320
|
+
if (runtime.capabilities && runtime.capabilities.coverageV8) {
|
|
321
|
+
runtimeInfo.someHasCoverageV8 = true;
|
|
322
|
+
}
|
|
323
|
+
runtimeInfo.someNeedsServer = true;
|
|
324
|
+
}
|
|
325
|
+
if (runtime.type === "node") {
|
|
326
|
+
runtimeInfo.someNodeRuntime = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
testPlan = {
|
|
331
|
+
"file:///**/node_modules/": null,
|
|
332
|
+
"**/*./": null,
|
|
333
|
+
...testPlan,
|
|
334
|
+
"**/.jsenv/": null, // ensure it's impossible to look for ".jsenv/"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// webServer
|
|
338
|
+
if (runtimeInfo.someNeedsServer) {
|
|
339
|
+
await assertAndNormalizeWebServer(webServer, {
|
|
340
|
+
signal: operation.signal,
|
|
341
|
+
teardownCallbackSet,
|
|
342
|
+
logger,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// githubCheck
|
|
346
|
+
{
|
|
347
|
+
if (githubCheck && !process.env.GITHUB_TOKEN) {
|
|
348
|
+
githubCheck = false;
|
|
349
|
+
const suggestions = [];
|
|
350
|
+
if (process.env.GITHUB_WORKFLOW_REF) {
|
|
351
|
+
const workflowFileRef = process.env.GITHUB_WORKFLOW_REF;
|
|
352
|
+
const refsIndex = workflowFileRef.indexOf("@refs/");
|
|
353
|
+
// see "GITHUB_WORKFLOW_REF" in https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
354
|
+
const workflowFilePath =
|
|
355
|
+
refsIndex === -1
|
|
356
|
+
? workflowFileRef
|
|
357
|
+
: workflowFileRef.slice(0, refsIndex);
|
|
358
|
+
suggestions.push(`Pass github token in ${workflowFilePath} during job "${process.env.GITHUB_JOB}"
|
|
237
359
|
\`\`\`yml
|
|
238
360
|
env:
|
|
239
361
|
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
240
362
|
\`\`\``);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
UNICODE.WARNING
|
|
246
|
-
} githubCheckEnabled but process.env.GITHUB_TOKEN is missing.
|
|
363
|
+
}
|
|
364
|
+
suggestions.push(`Disable github check with githubCheck: false`);
|
|
365
|
+
logger.warn(
|
|
366
|
+
`${UNICODE.WARNING} githubCheck requires process.env.GITHUB_TOKEN.
|
|
247
367
|
Integration with Github check API is disabled
|
|
248
368
|
To fix this warning:
|
|
249
369
|
- ${suggestions.join("\n- ")}
|
|
250
370
|
`,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (githubCheck) {
|
|
374
|
+
if (githubCheck === true) {
|
|
375
|
+
githubCheck = {};
|
|
376
|
+
}
|
|
377
|
+
if (typeof githubCheck !== "object") {
|
|
378
|
+
throw new TypeError(
|
|
379
|
+
`githubCheck must be an object, got ${githubCheck}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const unexpectedKeys = Object.keys(githubCheck).filter(
|
|
383
|
+
(key) => !Object.hasOwn(githubCheckDefault, key),
|
|
384
|
+
);
|
|
385
|
+
if (unexpectedKeys.length > 0) {
|
|
386
|
+
throw new TypeError(
|
|
387
|
+
`${unexpectedKeys.join(",")}: no such key on githubCheck`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
265
390
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
391
|
+
const githubCheckInfoFromEnv = process.env.GITHUB_WORKFLOW
|
|
392
|
+
? readGitHubWorkflowEnv()
|
|
393
|
+
: {};
|
|
394
|
+
githubCheck = { ...githubCheckDefault, githubCheck };
|
|
395
|
+
if (githubCheck.token === undefined) {
|
|
396
|
+
githubCheck.token = githubCheckInfoFromEnv.githubToken;
|
|
397
|
+
}
|
|
398
|
+
if (githubCheck.repositoryOwner === undefined) {
|
|
399
|
+
githubCheck.repositoryOwner =
|
|
400
|
+
githubCheckInfoFromEnv.repositoryOwner;
|
|
401
|
+
}
|
|
402
|
+
if (githubCheck.repositoryName === undefined) {
|
|
403
|
+
githubCheck.repositoryName = githubCheckInfoFromEnv.repositoryName;
|
|
404
|
+
}
|
|
405
|
+
if (githubCheck.commitSha === undefined) {
|
|
406
|
+
githubCheck.commitSha = githubCheckInfoFromEnv.commitSha;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
276
409
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
410
|
+
// coverage
|
|
411
|
+
if (coverage) {
|
|
412
|
+
if (coverage === true) {
|
|
413
|
+
coverage = {};
|
|
414
|
+
}
|
|
415
|
+
if (typeof coverage !== "object") {
|
|
416
|
+
throw new TypeError(`coverage must be an object, got ${coverage}`);
|
|
417
|
+
}
|
|
418
|
+
const unexpectedKeys = Object.keys(coverage).filter(
|
|
419
|
+
(key) => !Object.hasOwn(coverageDefault, key),
|
|
285
420
|
);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const { cover } = URL_META.applyAssociations({
|
|
290
|
-
url: testPlanPattern,
|
|
291
|
-
associations: associationsForCover,
|
|
292
|
-
});
|
|
293
|
-
return cover;
|
|
294
|
-
});
|
|
295
|
-
if (patternsMatchingCoverAndExecute.length) {
|
|
296
|
-
// It would be strange, for a given file to be both covered and executed
|
|
297
|
-
throw new Error(
|
|
298
|
-
createDetailedMessage(
|
|
299
|
-
`some file will be both covered and executed`,
|
|
300
|
-
{
|
|
301
|
-
patterns: patternsMatchingCoverAndExecute,
|
|
302
|
-
},
|
|
303
|
-
),
|
|
421
|
+
if (unexpectedKeys.length > 0) {
|
|
422
|
+
throw new TypeError(
|
|
423
|
+
`${unexpectedKeys.join(",")}: no such key on coverage`,
|
|
304
424
|
);
|
|
305
425
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"./.coverage/tmp/",
|
|
311
|
-
rootDirectoryUrl,
|
|
312
|
-
);
|
|
313
|
-
} else {
|
|
314
|
-
coverageTempDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
315
|
-
coverageTempDirectoryUrl,
|
|
316
|
-
"coverageTempDirectoryUrl",
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
if (coverageReportJson) {
|
|
320
|
-
if (coverageReportJsonFileUrl === undefined) {
|
|
321
|
-
coverageReportJsonFileUrl = new URL(
|
|
322
|
-
"./.coverage/coverage.json",
|
|
323
|
-
rootDirectoryUrl,
|
|
426
|
+
coverage = { ...coverageDefault, ...coverage };
|
|
427
|
+
if (typeof coverage.include !== "object") {
|
|
428
|
+
throw new TypeError(
|
|
429
|
+
`coverage.include must be an object, got ${coverage.include}`,
|
|
324
430
|
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
431
|
+
}
|
|
432
|
+
if (Object.keys(coverage.include).length === 0) {
|
|
433
|
+
logger.warn(
|
|
434
|
+
`coverage.include is an empty object. Nothing will be instrumented for coverage so your coverage will be empty`,
|
|
329
435
|
);
|
|
330
436
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
437
|
+
if (coverage.methodForBrowsers === undefined) {
|
|
438
|
+
coverage.methodForBrowsers = runtimeInfo.someHasCoverageV8
|
|
439
|
+
? "playwright"
|
|
440
|
+
: "istanbul";
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
runtimeInfo.someNodeRuntime &&
|
|
444
|
+
coverage.methodForNodeJs === "NODE_V8_COVERAGE"
|
|
445
|
+
) {
|
|
446
|
+
if (process.env.NODE_V8_COVERAGE) {
|
|
447
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
448
|
+
await ensureEmptyDirectory(process.env.NODE_V8_COVERAGE);
|
|
449
|
+
} else {
|
|
450
|
+
coverage.methodForNodeJs = "Profiler";
|
|
451
|
+
logger.warn(
|
|
452
|
+
createDetailedMessage(
|
|
453
|
+
`process.env.NODE_V8_COVERAGE is required to generate coverage for Node.js subprocesses`,
|
|
454
|
+
{
|
|
455
|
+
"suggestion": `set process.env.NODE_V8_COVERAGE`,
|
|
456
|
+
"suggestion 2": `use coverage.methodForNodeJs: "Profiler". But it means coverage for child_process and worker_thread cannot be collected`,
|
|
457
|
+
},
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!coverage.coverageAndExecutionAllowed) {
|
|
463
|
+
const associationsForExecute = URL_META.resolveAssociations(
|
|
464
|
+
{ execute: testPlan },
|
|
465
|
+
"file:///",
|
|
466
|
+
);
|
|
467
|
+
const associationsForCover = URL_META.resolveAssociations(
|
|
468
|
+
{ cover: coverage.include },
|
|
469
|
+
"file:///",
|
|
470
|
+
);
|
|
471
|
+
const patternsMatchingCoverAndExecute = Object.keys(
|
|
472
|
+
associationsForExecute.execute,
|
|
473
|
+
).filter((testPlanPattern) => {
|
|
474
|
+
const { cover } = URL_META.applyAssociations({
|
|
475
|
+
url: testPlanPattern,
|
|
476
|
+
associations: associationsForCover,
|
|
477
|
+
});
|
|
478
|
+
return cover;
|
|
479
|
+
});
|
|
480
|
+
if (patternsMatchingCoverAndExecute.length) {
|
|
481
|
+
// It would be strange, for a given file to be both covered and executed
|
|
482
|
+
throw new Error(
|
|
483
|
+
createDetailedMessage(
|
|
484
|
+
`some file will be both covered and executed`,
|
|
485
|
+
{
|
|
486
|
+
patterns: patternsMatchingCoverAndExecute,
|
|
487
|
+
},
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (coverage.tempDirectoryUrl === undefined) {
|
|
493
|
+
coverage.tempDirectoryUrl = new URL(
|
|
494
|
+
"./.coverage/tmp/",
|
|
336
495
|
rootDirectoryUrl,
|
|
337
496
|
);
|
|
338
497
|
} else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
"
|
|
498
|
+
coverage.tempDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
499
|
+
coverage.tempDirectoryUrl,
|
|
500
|
+
"coverageTempDirectoryUrl",
|
|
342
501
|
);
|
|
343
502
|
}
|
|
344
503
|
}
|
|
345
504
|
}
|
|
346
505
|
|
|
347
|
-
|
|
348
|
-
throw new TypeError(`concurrency must be a boolean or a number`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
506
|
+
const executionPlanifiedSet = new Set();
|
|
351
507
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
508
|
+
// collect files to execute + fill executionPlanifiedSet
|
|
509
|
+
{
|
|
510
|
+
const fileResultArray = await collectFiles({
|
|
511
|
+
signal,
|
|
512
|
+
directoryUrl: rootDirectoryUrl,
|
|
513
|
+
associations: { testPlan },
|
|
514
|
+
predicate: ({ testPlan }) => testPlan,
|
|
515
|
+
});
|
|
516
|
+
let index = 0;
|
|
517
|
+
let lastExecution;
|
|
518
|
+
const fileExecutionCountMap = new Map();
|
|
519
|
+
for (const { relativeUrl, meta } of fileResultArray) {
|
|
520
|
+
const filePlan = meta.testPlan;
|
|
521
|
+
for (const groupName of Object.keys(filePlan)) {
|
|
522
|
+
const stepConfig = filePlan[groupName];
|
|
523
|
+
if (stepConfig === null || stepConfig === undefined) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (typeof stepConfig !== "object") {
|
|
527
|
+
throw new TypeError(
|
|
528
|
+
createDetailedMessage(
|
|
529
|
+
`found unexpected value in plan, they must be object`,
|
|
530
|
+
{
|
|
531
|
+
["file relative path"]: relativeUrl,
|
|
532
|
+
["group"]: groupName,
|
|
533
|
+
["value"]: stepConfig,
|
|
534
|
+
},
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (stepConfig.runtime?.disabled) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const {
|
|
543
|
+
runtime,
|
|
544
|
+
runtimeParams,
|
|
545
|
+
allocatedMs = defaultMsAllocatedPerExecution,
|
|
546
|
+
} = stepConfig;
|
|
547
|
+
const params = {
|
|
548
|
+
measureMemoryUsage: true,
|
|
549
|
+
measurePerformance: false,
|
|
550
|
+
collectPerformance: false,
|
|
551
|
+
collectConsole: true,
|
|
552
|
+
allocatedMs,
|
|
553
|
+
runtime,
|
|
554
|
+
runtimeParams: {
|
|
555
|
+
rootDirectoryUrl,
|
|
556
|
+
webServer,
|
|
557
|
+
teardownCallbackSet,
|
|
558
|
+
|
|
559
|
+
coverageEnabled: Boolean(coverage),
|
|
560
|
+
coverageInclude: coverage?.include,
|
|
561
|
+
coverageMethodForBrowsers: coverage?.methodForBrowsers,
|
|
562
|
+
coverageMethodForNodeJs: coverage?.methodForNodeJs,
|
|
563
|
+
isTestPlan: true,
|
|
564
|
+
fileRelativeUrl: relativeUrl,
|
|
565
|
+
...runtimeParams,
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
const runtimeType = runtime.type;
|
|
569
|
+
const runtimeName = runtime.name;
|
|
570
|
+
const runtimeVersion = runtime.version;
|
|
571
|
+
|
|
572
|
+
let fileExecutionCount;
|
|
573
|
+
if (fileExecutionCountMap.has(relativeUrl)) {
|
|
574
|
+
fileExecutionCount = fileExecutionCountMap.get(relativeUrl) + 1;
|
|
575
|
+
fileExecutionCountMap.set(relativeUrl, fileExecutionCount);
|
|
576
|
+
} else {
|
|
577
|
+
fileExecutionCount = 1;
|
|
578
|
+
fileExecutionCountMap.set(relativeUrl, fileExecutionCount);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const execution = {
|
|
582
|
+
counters,
|
|
583
|
+
countersInOrder,
|
|
584
|
+
index,
|
|
585
|
+
isLast: false,
|
|
586
|
+
group: groupName,
|
|
587
|
+
rootDirectoryUrl: String(rootDirectoryUrl),
|
|
588
|
+
fileRelativeUrl: relativeUrl,
|
|
589
|
+
fileExecutionIndex: fileExecutionCount - 1,
|
|
590
|
+
fileExecutionCount: null,
|
|
591
|
+
runtimeType,
|
|
592
|
+
runtimeName,
|
|
593
|
+
runtimeVersion,
|
|
594
|
+
params,
|
|
595
|
+
|
|
596
|
+
// will be set by run()
|
|
597
|
+
status: "planified",
|
|
598
|
+
result: {},
|
|
599
|
+
};
|
|
600
|
+
if (typeof params.allocatedMs === "function") {
|
|
601
|
+
params.allocatedMs = params.allocatedMs(execution);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
lastExecution = execution;
|
|
605
|
+
executionPlanifiedSet.add(execution);
|
|
606
|
+
const existingResults = results[relativeUrl];
|
|
607
|
+
if (existingResults) {
|
|
608
|
+
existingResults[groupName] = execution.result;
|
|
609
|
+
} else {
|
|
610
|
+
results[relativeUrl] = {
|
|
611
|
+
[groupName]: execution.result,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const existingGroup = groups[groupName];
|
|
615
|
+
if (existingGroup) {
|
|
616
|
+
groups[groupName].count++;
|
|
617
|
+
} else {
|
|
618
|
+
groups[groupName] = {
|
|
619
|
+
count: 1,
|
|
620
|
+
runtimeType,
|
|
621
|
+
runtimeName,
|
|
622
|
+
runtimeVersion,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
index++;
|
|
385
626
|
}
|
|
386
627
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
628
|
+
fileResultArray.length = 0;
|
|
629
|
+
fileExecutionCountMap.clear();
|
|
630
|
+
if (lastExecution) {
|
|
631
|
+
lastExecution.isLast = true;
|
|
391
632
|
}
|
|
392
|
-
if (cooldownBetweenExecutions === Infinity) {
|
|
393
|
-
cooldownBetweenExecutions = 0;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (concurrency === true) {
|
|
397
|
-
const availableCpus = os.cpus().length;
|
|
398
|
-
concurrency =
|
|
399
|
-
availableCpus > 2
|
|
400
|
-
? // One CPU is used to run this code so we do availableCpus - 1
|
|
401
|
-
availableCpus - 1
|
|
402
|
-
: 1;
|
|
403
633
|
}
|
|
404
|
-
|
|
405
|
-
|
|
634
|
+
|
|
635
|
+
counters.planified =
|
|
636
|
+
counters.remaining =
|
|
637
|
+
counters.waiting =
|
|
638
|
+
executionPlanifiedSet.size;
|
|
639
|
+
countersInOrder.planified =
|
|
640
|
+
countersInOrder.remaining =
|
|
641
|
+
countersInOrder.waiting =
|
|
642
|
+
executionPlanifiedSet.size;
|
|
643
|
+
if (githubCheck) {
|
|
644
|
+
const githubCheckRun = await startGithubCheckRun({
|
|
645
|
+
logLevel: githubCheck.logLevel,
|
|
646
|
+
githubToken: githubCheck.token,
|
|
647
|
+
repositoryOwner: githubCheck.repositoryOwner,
|
|
648
|
+
repositoryName: githubCheck.repositoryName,
|
|
649
|
+
commitSha: githubCheck.commitSha,
|
|
650
|
+
checkName: githubCheck.name,
|
|
651
|
+
checkTitle: githubCheck.title,
|
|
652
|
+
checkSummary: `${executionPlanifiedSet.size} files will be executed`,
|
|
653
|
+
});
|
|
654
|
+
const annotations = [];
|
|
655
|
+
reporters.push({
|
|
656
|
+
beforeAll: (testPlanResult) => {
|
|
657
|
+
return {
|
|
658
|
+
afterEach: (execution) => {
|
|
659
|
+
const { result } = execution;
|
|
660
|
+
const { errors = [] } = result;
|
|
661
|
+
for (const error of errors) {
|
|
662
|
+
const annotation = githubAnnotationFromError(error, {
|
|
663
|
+
rootDirectoryUrl,
|
|
664
|
+
execution,
|
|
665
|
+
});
|
|
666
|
+
annotations.push(annotation);
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
afterAll: async () => {
|
|
670
|
+
const title = "Jsenv test results";
|
|
671
|
+
const summaryText = stripAnsi(renderOutro(testPlanResult));
|
|
672
|
+
if (testPlanResult.failed) {
|
|
673
|
+
await githubCheckRun.fail({
|
|
674
|
+
title,
|
|
675
|
+
summary: summaryText,
|
|
676
|
+
annotations,
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
await githubCheckRun.pass({
|
|
681
|
+
title,
|
|
682
|
+
summary: summaryText,
|
|
683
|
+
annotations,
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
});
|
|
406
689
|
}
|
|
407
|
-
}
|
|
408
690
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
logger.debug(`Generate executions`);
|
|
416
|
-
let executionSteps = await executionStepsFromTestPlan({
|
|
417
|
-
signal,
|
|
418
|
-
testPlan,
|
|
419
|
-
rootDirectoryUrl,
|
|
420
|
-
});
|
|
421
|
-
logger.debug(`${executionSteps.length} executions planned`);
|
|
422
|
-
if (githubCheckEnabled) {
|
|
423
|
-
const githubCheckRun = await startGithubCheckRun({
|
|
424
|
-
logLevel: githubCheckLogLevel,
|
|
425
|
-
githubToken: githubCheckToken,
|
|
426
|
-
repositoryOwner: githubCheckRepositoryOwner,
|
|
427
|
-
repositoryName: githubCheckRepositoryName,
|
|
428
|
-
commitSha: githubCheckCommitSha,
|
|
429
|
-
checkName: githubCheckName,
|
|
430
|
-
checkTitle: githubCheckTitle,
|
|
431
|
-
checkSummary: `${executionSteps.length} files will be executed`,
|
|
432
|
-
});
|
|
433
|
-
const annotations = [];
|
|
434
|
-
afterExecutionCallbackSet.add((afterExecutionInfo) => {
|
|
435
|
-
const { executionResult } = afterExecutionInfo;
|
|
436
|
-
const { errors = [] } = executionResult;
|
|
437
|
-
for (const error of errors) {
|
|
438
|
-
const annotation = githubAnnotationFromError(error, {
|
|
439
|
-
rootDirectoryUrl,
|
|
440
|
-
executionInfo: afterExecutionInfo,
|
|
441
|
-
});
|
|
442
|
-
annotations.push(annotation);
|
|
691
|
+
timings.executionStart = takeTiming();
|
|
692
|
+
// execute all
|
|
693
|
+
{
|
|
694
|
+
const failFastAbortController = new AbortController();
|
|
695
|
+
if (failFast) {
|
|
696
|
+
operation.addAbortSignal(failFastAbortController.signal);
|
|
443
697
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
698
|
+
|
|
699
|
+
if (coverage) {
|
|
700
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
701
|
+
await ensureEmptyDirectory(coverage.tempDirectoryUrl);
|
|
702
|
+
finalizeCoverage = async () => {
|
|
703
|
+
if (operation.signal.aborted) {
|
|
704
|
+
// don't try to do the coverage stuff
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
if (coverage.methodForNodeJs === "NODE_V8_COVERAGE") {
|
|
709
|
+
takeCoverage();
|
|
710
|
+
// conceptually we don't need coverage anymore so it would be
|
|
711
|
+
// good to call v8.stopCoverage()
|
|
712
|
+
// but it logs a strange message about "result is not an object"
|
|
713
|
+
}
|
|
714
|
+
const testPlanCoverage = await generateCoverage(testPlanResult, {
|
|
715
|
+
signal: operation.signal,
|
|
716
|
+
logger,
|
|
717
|
+
rootDirectoryUrl,
|
|
718
|
+
coverage,
|
|
719
|
+
});
|
|
720
|
+
testPlanResult.coverage = testPlanCoverage;
|
|
721
|
+
} catch (e) {
|
|
722
|
+
if (Abort.isAbortError(e)) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
throw e;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
456
728
|
}
|
|
457
|
-
await githubCheckRun.pass({
|
|
458
|
-
title,
|
|
459
|
-
summary: summaryText,
|
|
460
|
-
annotations,
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
729
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
730
|
+
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
731
|
+
|
|
732
|
+
const executionRemainingSet = new Set(executionPlanifiedSet);
|
|
733
|
+
const executionExecutingSet = new Set();
|
|
734
|
+
const start = async (execution) => {
|
|
735
|
+
execution.fileExecutionCount = Object.keys(
|
|
736
|
+
testPlanResult.results[execution.fileRelativeUrl],
|
|
737
|
+
).length;
|
|
738
|
+
mutateCountersBeforeExecutionStarts(counters, execution);
|
|
739
|
+
mutateCountersBeforeExecutionStarts(countersInOrder, execution);
|
|
740
|
+
|
|
741
|
+
execution.status = "executing";
|
|
742
|
+
executionRemainingSet.delete(execution);
|
|
743
|
+
executionExecutingSet.add(execution);
|
|
744
|
+
for (const beforeEachCallback of beforeEachCallbackSet) {
|
|
745
|
+
const returnValue = beforeEachCallback(execution, testPlanResult);
|
|
746
|
+
if (typeof returnValue === "function") {
|
|
747
|
+
const callback = (...args) => {
|
|
748
|
+
afterEachCallbackSet.delete(callback);
|
|
749
|
+
return returnValue(...args);
|
|
750
|
+
};
|
|
751
|
+
afterEachCallbackSet.add(callback);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
468
754
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
755
|
+
for (const beforeEachInOrderCallback of beforeEachInOrderCallbackSet) {
|
|
756
|
+
const returnValue = beforeEachInOrderCallback(
|
|
757
|
+
execution,
|
|
758
|
+
testPlanResult,
|
|
759
|
+
);
|
|
760
|
+
if (typeof returnValue === "function") {
|
|
761
|
+
const callback = (...args) => {
|
|
762
|
+
afterEachInOrderCallbackSet.delete(callback);
|
|
763
|
+
return returnValue(...args);
|
|
764
|
+
};
|
|
765
|
+
afterEachInOrderCallbackSet.add(callback);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const executionResult = await run({
|
|
769
|
+
...execution.params,
|
|
770
|
+
signal: operation.signal,
|
|
771
|
+
logger,
|
|
772
|
+
keepRunning,
|
|
773
|
+
mirrorConsole: false, // might be executed in parallel: log would be a mess to read
|
|
774
|
+
coverageEnabled: Boolean(coverage),
|
|
775
|
+
coverageTempDirectoryUrl: coverage?.tempDirectoryUrl,
|
|
776
|
+
});
|
|
777
|
+
Object.assign(execution.result, executionResult);
|
|
778
|
+
execution.status = "executed";
|
|
779
|
+
executionExecutingSet.delete(execution);
|
|
780
|
+
mutateCountersAfterExecutionEnds(counters, execution);
|
|
781
|
+
if (execution.result.status !== "completed") {
|
|
782
|
+
testPlanResult.failed = true;
|
|
783
|
+
if (updateProcessExitCode) {
|
|
784
|
+
process.exitCode = 1;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
484
787
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
788
|
+
for (const afterEachCallback of afterEachCallbackSet) {
|
|
789
|
+
afterEachCallback(execution, testPlanResult);
|
|
790
|
+
}
|
|
791
|
+
callWhenPreviousExecutionAreDone(execution.index, () => {
|
|
792
|
+
mutateCountersAfterExecutionEnds(countersInOrder, execution);
|
|
793
|
+
for (const afterEachInOrderCallback of afterEachInOrderCallbackSet) {
|
|
794
|
+
afterEachInOrderCallback(execution, testPlanResult);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
489
797
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
callbacks.push(async () => {
|
|
494
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
495
|
-
// don't try to do the coverage stuff
|
|
798
|
+
if (testPlanResult.failed && failFast && counters.remaining) {
|
|
799
|
+
logger.info(`"failFast" enabled -> cancel remaining executions`);
|
|
800
|
+
failFastAbortController.abort();
|
|
496
801
|
return;
|
|
497
802
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
803
|
+
await startAsMuchAsPossible();
|
|
804
|
+
};
|
|
805
|
+
const startAsMuchAsPossible = async () => {
|
|
806
|
+
const promises = [];
|
|
807
|
+
// eslint-disable-next-line no-constant-condition
|
|
808
|
+
while (true) {
|
|
809
|
+
operation.throwIfAborted();
|
|
810
|
+
if (executionRemainingSet.size === 0) {
|
|
811
|
+
break;
|
|
504
812
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
logger,
|
|
508
|
-
rootDirectoryUrl,
|
|
509
|
-
coverageConfig,
|
|
510
|
-
coverageIncludeMissing,
|
|
511
|
-
coverageMethodForNodeJs,
|
|
512
|
-
coverageV8ConflictWarning,
|
|
513
|
-
});
|
|
514
|
-
returnValue.coverage = coverage;
|
|
515
|
-
} catch (e) {
|
|
516
|
-
if (Abort.isAbortError(e)) {
|
|
517
|
-
return;
|
|
813
|
+
if (executionExecutingSet.size >= parallel.max) {
|
|
814
|
+
break;
|
|
518
815
|
}
|
|
519
|
-
throw e;
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const runtimeParams = {
|
|
525
|
-
rootDirectoryUrl,
|
|
526
|
-
webServer,
|
|
527
816
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
817
|
+
if (
|
|
818
|
+
// starting execution in parallel is limited by
|
|
819
|
+
// cpu and memory only when trying to parallelize
|
|
820
|
+
// if nothing is executing these limitations don't apply
|
|
821
|
+
executionExecutingSet.size > 0
|
|
822
|
+
) {
|
|
823
|
+
const availableMemory = freemem();
|
|
824
|
+
const totalMemory = totalmem();
|
|
825
|
+
const usedMemory = totalMemory - availableMemory;
|
|
826
|
+
if (usedMemory > parallel.maxMemory) {
|
|
827
|
+
// retry after Xms in case memory usage decreases
|
|
828
|
+
const promise = (async () => {
|
|
829
|
+
await operation.wait(200);
|
|
830
|
+
await startAsMuchAsPossible();
|
|
831
|
+
})();
|
|
832
|
+
promises.push(promise);
|
|
833
|
+
}
|
|
535
834
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
concurrency === 1 &&
|
|
547
|
-
!debugLogsEnabled &&
|
|
548
|
-
executionLogsEnabled &&
|
|
549
|
-
process.stdout.isTTY &&
|
|
550
|
-
// if there is an error during execution npm will mess up the output
|
|
551
|
-
// (happens when npm runs several command in a workspace)
|
|
552
|
-
// so we enable spinner only when !process.exitCode (no error so far)
|
|
553
|
-
process.exitCode !== 1;
|
|
554
|
-
|
|
555
|
-
const startMs = Date.now();
|
|
556
|
-
let rawOutput = "";
|
|
557
|
-
let executionLog = createLog({ newLine: "" });
|
|
558
|
-
const counters = {
|
|
559
|
-
total: executionSteps.length,
|
|
560
|
-
aborted: 0,
|
|
561
|
-
timedout: 0,
|
|
562
|
-
failed: 0,
|
|
563
|
-
completed: 0,
|
|
564
|
-
done: 0,
|
|
565
|
-
};
|
|
835
|
+
if (cpuUsage.overall.active > parallel.maxCpu) {
|
|
836
|
+
// retry after Xms in case cpu usage decreases
|
|
837
|
+
const promise = (async () => {
|
|
838
|
+
await operation.wait(200);
|
|
839
|
+
await startAsMuchAsPossible();
|
|
840
|
+
})();
|
|
841
|
+
promises.push(promise);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
566
845
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const runtimeType = runtime.type;
|
|
578
|
-
const runtimeName = runtime.name;
|
|
579
|
-
const runtimeVersion = runtime.version;
|
|
580
|
-
const executionParams = {
|
|
581
|
-
measurePerformance: false,
|
|
582
|
-
collectPerformance: false,
|
|
583
|
-
collectConsole: true,
|
|
584
|
-
allocatedMs: defaultMsAllocatedPerExecution,
|
|
585
|
-
...paramsFromStep,
|
|
586
|
-
runtimeParams: {
|
|
587
|
-
fileRelativeUrl,
|
|
588
|
-
...paramsFromStep.runtimeParams,
|
|
589
|
-
},
|
|
590
|
-
};
|
|
591
|
-
const beforeExecutionInfo = {
|
|
592
|
-
fileRelativeUrl,
|
|
593
|
-
runtimeType,
|
|
594
|
-
runtimeName,
|
|
595
|
-
runtimeVersion,
|
|
596
|
-
executionIndex,
|
|
597
|
-
executionParams,
|
|
598
|
-
startMs: Date.now(),
|
|
599
|
-
executionResult: {
|
|
600
|
-
status: "executing",
|
|
601
|
-
},
|
|
602
|
-
counters,
|
|
603
|
-
timeEllapsed: Date.now() - startMs,
|
|
604
|
-
memoryHeap: memoryUsage().heapUsed,
|
|
605
|
-
};
|
|
606
|
-
if (typeof executionParams.allocatedMs === "function") {
|
|
607
|
-
executionParams.allocatedMs =
|
|
608
|
-
executionParams.allocatedMs(beforeExecutionInfo);
|
|
609
|
-
}
|
|
610
|
-
let spinner;
|
|
611
|
-
if (executionSpinner) {
|
|
612
|
-
spinner = startSpinner({
|
|
613
|
-
log: executionLog,
|
|
614
|
-
render: () => {
|
|
615
|
-
return createExecutionLog(beforeExecutionInfo, {
|
|
616
|
-
logRuntime,
|
|
617
|
-
logEachDuration,
|
|
618
|
-
logTimeUsage,
|
|
619
|
-
logMemoryHeapUsage,
|
|
620
|
-
});
|
|
621
|
-
},
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
for (const beforeExecutionCallback of beforeExecutionCallbackSet) {
|
|
625
|
-
beforeExecutionCallback(beforeExecutionInfo);
|
|
626
|
-
}
|
|
627
|
-
const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
|
|
628
|
-
let executionResult;
|
|
629
|
-
if (existsSync(new URL(fileUrl))) {
|
|
630
|
-
executionResult = await run({
|
|
631
|
-
signal: multipleExecutionsOperation.signal,
|
|
632
|
-
logger,
|
|
633
|
-
allocatedMs: executionParams.allocatedMs,
|
|
634
|
-
keepRunning,
|
|
635
|
-
mirrorConsole: false, // might be executed in parallel: log would be a mess to read
|
|
636
|
-
collectConsole: executionParams.collectConsole,
|
|
637
|
-
coverageEnabled,
|
|
638
|
-
coverageTempDirectoryUrl,
|
|
639
|
-
runtime: executionParams.runtime,
|
|
640
|
-
runtimeParams: {
|
|
641
|
-
...runtimeParams,
|
|
642
|
-
...executionParams.runtimeParams,
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
} else {
|
|
646
|
-
executionResult = {
|
|
647
|
-
status: "failed",
|
|
648
|
-
errors: [
|
|
649
|
-
new Error(
|
|
650
|
-
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
651
|
-
),
|
|
652
|
-
],
|
|
653
|
-
};
|
|
846
|
+
let execution;
|
|
847
|
+
for (const executionCandidate of executionRemainingSet) {
|
|
848
|
+
// TODO: this is where we'll check if it can be executed
|
|
849
|
+
// according to upcoming "using" execution param
|
|
850
|
+
execution = executionCandidate;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (execution) {
|
|
854
|
+
promises.push(start(execution));
|
|
855
|
+
}
|
|
654
856
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
} else {
|
|
659
|
-
report[fileRelativeUrl] = {
|
|
660
|
-
[executionName]: executionResult,
|
|
661
|
-
};
|
|
857
|
+
if (promises.length) {
|
|
858
|
+
await Promise.all(promises);
|
|
859
|
+
promises.length = 0;
|
|
662
860
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (executionLogsEnabled) {
|
|
690
|
-
// replace spinner with this execution result
|
|
691
|
-
if (spinner) {
|
|
692
|
-
spinner.stop();
|
|
693
|
-
spinner = null;
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
reporters = reporters.flat(Infinity);
|
|
864
|
+
for (const reporter of reporters) {
|
|
865
|
+
const {
|
|
866
|
+
beforeAll,
|
|
867
|
+
// TODO: if defined add them too
|
|
868
|
+
// beforeEach,
|
|
869
|
+
// afterEach,
|
|
870
|
+
// beforeEachInOrder,
|
|
871
|
+
// afterEachInOrder,
|
|
872
|
+
// afterAll,
|
|
873
|
+
} = reporter;
|
|
874
|
+
if (beforeAll) {
|
|
875
|
+
const returnValue = await beforeAll(testPlanResult);
|
|
876
|
+
if (returnValue) {
|
|
877
|
+
const {
|
|
878
|
+
beforeEach,
|
|
879
|
+
beforeEachInOrder,
|
|
880
|
+
afterEach,
|
|
881
|
+
afterEachInOrder,
|
|
882
|
+
afterAll,
|
|
883
|
+
} = returnValue;
|
|
884
|
+
if (beforeEach) {
|
|
885
|
+
beforeEachCallbackSet.add(beforeEach);
|
|
694
886
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
executionLog.write(log);
|
|
705
|
-
rawOutput += stripAnsi(log);
|
|
706
|
-
const canOverwriteLog = canOverwriteLogGetter({
|
|
707
|
-
logMergeForCompletedExecutions,
|
|
708
|
-
executionResult,
|
|
709
|
-
});
|
|
710
|
-
if (canOverwriteLog) {
|
|
711
|
-
// nothing to do, we reuse the current executionLog object
|
|
712
|
-
} else {
|
|
713
|
-
executionLog.destroy();
|
|
714
|
-
executionLog = createLog({ newLine: "" });
|
|
887
|
+
if (beforeEachInOrder) {
|
|
888
|
+
beforeEachInOrderCallbackSet.add(beforeEachInOrder);
|
|
889
|
+
}
|
|
890
|
+
if (afterEach) {
|
|
891
|
+
afterEachCallbackSet.add(afterEach);
|
|
892
|
+
}
|
|
893
|
+
if (afterEachInOrder) {
|
|
894
|
+
afterEachInOrderCallbackSet.add(afterEachInOrder);
|
|
715
895
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (isLastExecutionLog && logger.levels.info) {
|
|
719
|
-
executionLog.write("\n");
|
|
896
|
+
if (afterAll) {
|
|
897
|
+
afterAllCallbackSet.add(afterAll);
|
|
720
898
|
}
|
|
721
899
|
}
|
|
722
|
-
});
|
|
723
|
-
for (const afterExecutionCallback of afterExecutionCallbackSet) {
|
|
724
|
-
afterExecutionCallback(afterExecutionInfo);
|
|
725
|
-
}
|
|
726
|
-
const cancelRemaining =
|
|
727
|
-
failFast &&
|
|
728
|
-
executionResult.status !== "completed" &&
|
|
729
|
-
counters.done < counters.total;
|
|
730
|
-
if (cancelRemaining) {
|
|
731
|
-
logger.info(`"failFast" enabled -> cancel remaining executions`);
|
|
732
|
-
failFastAbortController.abort();
|
|
733
900
|
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (!keepRunning) {
|
|
737
|
-
logger.debug("trigger test plan teardown");
|
|
738
|
-
await teardown.trigger();
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
counters.cancelled = counters.total - counters.done;
|
|
742
|
-
const summary = {
|
|
743
|
-
counters,
|
|
744
|
-
// when execution is aborted, the remaining executions are "cancelled"
|
|
745
|
-
duration: Date.now() - startMs,
|
|
746
|
-
};
|
|
747
|
-
if (logSummary) {
|
|
748
|
-
const summaryLog = formatSummaryLog(summary);
|
|
749
|
-
rawOutput += stripAnsi(summaryLog);
|
|
750
|
-
logger.info(summaryLog);
|
|
751
|
-
}
|
|
752
|
-
if (summary.counters.total !== summary.counters.completed) {
|
|
753
|
-
const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href;
|
|
754
|
-
writeFileSync(logFileUrl, rawOutput);
|
|
755
|
-
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`);
|
|
901
|
+
}
|
|
902
|
+
await startAsMuchAsPossible();
|
|
756
903
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
904
|
+
timings.executionEnd = takeTiming();
|
|
905
|
+
} catch (e) {
|
|
906
|
+
if (Abort.isAbortError(e)) {
|
|
907
|
+
} else {
|
|
908
|
+
throw e;
|
|
762
909
|
}
|
|
763
910
|
} finally {
|
|
764
|
-
|
|
765
|
-
|
|
911
|
+
testPlanResult.aborted = operation.signal.aborted;
|
|
912
|
+
if (testPlanResult.aborted) {
|
|
913
|
+
// when execution is aborted, the remaining executions are "cancelled"
|
|
914
|
+
counters.cancelled = counters.planified - counters.executed;
|
|
915
|
+
counters.remaining = 0;
|
|
916
|
+
countersInOrder.cancelled =
|
|
917
|
+
countersInOrder.planified - countersInOrder.executed;
|
|
918
|
+
countersInOrder.remaining = 0;
|
|
919
|
+
}
|
|
766
920
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
const coverage = returnValue.coverage;
|
|
774
|
-
// planCoverage can be null when execution is aborted
|
|
775
|
-
if (coverage) {
|
|
776
|
-
const promises = [];
|
|
777
|
-
// keep this one first because it does ensureEmptyDirectory
|
|
778
|
-
// and in case coverage json file gets written in the same directory
|
|
779
|
-
// it must be done before
|
|
780
|
-
if (coverageEnabled && coverageReportHtml) {
|
|
781
|
-
await ensureEmptyDirectory(coverageReportHtmlDirectoryUrl);
|
|
782
|
-
const htmlCoverageDirectoryIndexFileUrl = `${coverageReportHtmlDirectoryUrl}index.html`;
|
|
783
|
-
logger.info(
|
|
784
|
-
`-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`,
|
|
785
|
-
);
|
|
786
|
-
promises.push(
|
|
787
|
-
generateCoverageHtmlDirectory(coverage, {
|
|
788
|
-
rootDirectoryUrl,
|
|
789
|
-
coverageHtmlDirectoryRelativeUrl: urlToRelativeUrl(
|
|
790
|
-
coverageReportHtmlDirectoryUrl,
|
|
791
|
-
rootDirectoryUrl,
|
|
792
|
-
),
|
|
793
|
-
coverageReportSkipEmpty,
|
|
794
|
-
coverageReportSkipFull,
|
|
795
|
-
}),
|
|
796
|
-
);
|
|
921
|
+
if (!keepRunning) {
|
|
922
|
+
for (const teardownCallback of teardownCallbackSet) {
|
|
923
|
+
await teardownCallback();
|
|
924
|
+
}
|
|
925
|
+
teardownCallbackSet.clear();
|
|
797
926
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
coverageJsonFileUrl: coverageReportJsonFileUrl,
|
|
803
|
-
logger,
|
|
804
|
-
}),
|
|
805
|
-
);
|
|
927
|
+
timings.teardownEnd = takeTiming();
|
|
928
|
+
|
|
929
|
+
if (finalizeCoverage) {
|
|
930
|
+
await finalizeCoverage();
|
|
806
931
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
);
|
|
932
|
+
timings.coverageTeardownEnd = takeTiming();
|
|
933
|
+
timings.end = takeTiming();
|
|
934
|
+
|
|
935
|
+
afterEachCallbackSet.clear();
|
|
936
|
+
afterEachInOrderCallbackSet.clear();
|
|
937
|
+
for (const afterAllCallback of afterAllCallbackSet) {
|
|
938
|
+
await afterAllCallback(testPlanResult);
|
|
814
939
|
}
|
|
815
|
-
|
|
940
|
+
afterAllCallbackSet.clear();
|
|
941
|
+
await operation.end();
|
|
816
942
|
}
|
|
817
943
|
|
|
818
|
-
|
|
819
|
-
await afterAllExecutionCallback(returnValue);
|
|
820
|
-
}
|
|
821
|
-
return returnValue;
|
|
944
|
+
return testPlanResult;
|
|
822
945
|
};
|
|
823
946
|
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}) => {
|
|
828
|
-
if (!logMergeForCompletedExecutions) {
|
|
829
|
-
return false;
|
|
947
|
+
const countAvailableCpus = () => {
|
|
948
|
+
if (typeof availableParallelism === "function") {
|
|
949
|
+
return availableParallelism();
|
|
830
950
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
if (executionResult.status !== "completed") {
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
const { consoleCalls = [] } = executionResult;
|
|
838
|
-
if (consoleCalls.length > 0) {
|
|
839
|
-
return false;
|
|
840
|
-
}
|
|
841
|
-
return true;
|
|
951
|
+
const cpuArray = cpus();
|
|
952
|
+
return cpuArray.length || 1;
|
|
842
953
|
};
|
|
843
954
|
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
progressionIndex++;
|
|
861
|
-
await start(input);
|
|
862
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (cooldownBetweenExecutions) {
|
|
866
|
-
await new Promise((resolve) => {
|
|
867
|
-
const timeoutId = setTimeout(() => {
|
|
868
|
-
removeClearTimeoutOnAbort();
|
|
869
|
-
resolve();
|
|
870
|
-
}, cooldownBetweenExecutions);
|
|
871
|
-
const removeClearTimeoutOnAbort =
|
|
872
|
-
multipleExecutionsOperation.signal.addAbortCallback(() => {
|
|
873
|
-
clearTimeout(timeoutId);
|
|
874
|
-
});
|
|
875
|
-
});
|
|
876
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
await startNext();
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
const promises = [];
|
|
884
|
-
while (concurrency--) {
|
|
885
|
-
promises.push(startNext());
|
|
955
|
+
const mutateCountersBeforeExecutionStarts = (counters) => {
|
|
956
|
+
counters.executing++;
|
|
957
|
+
counters.waiting--;
|
|
958
|
+
};
|
|
959
|
+
const mutateCountersAfterExecutionEnds = (counters, execution) => {
|
|
960
|
+
counters.executing--;
|
|
961
|
+
counters.executed++;
|
|
962
|
+
counters.remaining--;
|
|
963
|
+
if (execution.result.status === "aborted") {
|
|
964
|
+
counters.aborted++;
|
|
965
|
+
} else if (execution.result.status === "timedout") {
|
|
966
|
+
counters.timedout++;
|
|
967
|
+
} else if (execution.result.status === "failed") {
|
|
968
|
+
counters.failed++;
|
|
969
|
+
} else if (execution.result.status === "completed") {
|
|
970
|
+
counters.completed++;
|
|
886
971
|
}
|
|
887
|
-
await Promise.all(promises);
|
|
888
972
|
};
|