@jsenv/test 1.7.9 → 1.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.
@@ -1,409 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { memoryUsage } from "node:process";
3
- import { takeCoverage } from "node:v8";
4
- import stripAnsi from "strip-ansi";
5
- import { urlToFileSystemPath } from "@jsenv/urls";
6
- import { createLog, startSpinner } from "@jsenv/log";
7
- import { Abort } from "@jsenv/abort";
8
- import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem";
9
-
10
- import { reportToCoverage } from "../coverage/report_to_coverage.js";
11
- import { run } from "./run.js";
12
- import { ensureGlobalGc } from "./gc.js";
13
- import { createExecutionLog, formatSummaryLog } from "./logs_file_execution.js";
14
-
15
- export const executeSteps = async (
16
- executionSteps,
17
- {
18
- signal,
19
- teardown,
20
- logger,
21
- logRefresh,
22
- logRuntime,
23
- logEachDuration,
24
- logSummary,
25
- logTimeUsage,
26
- logMemoryHeapUsage,
27
- logFileRelativeUrl,
28
- logMergeForCompletedExecutions,
29
- logShortForCompletedExecutions,
30
- rootDirectoryUrl,
31
- webServer,
32
-
33
- keepRunning,
34
- defaultMsAllocatedPerExecution,
35
- maxExecutionsInParallel,
36
- failFast,
37
- gcBetweenExecutions,
38
- cooldownBetweenExecutions,
39
-
40
- coverageEnabled,
41
- coverageConfig,
42
- coverageIncludeMissing,
43
- coverageMethodForBrowsers,
44
- coverageMethodForNodeJs,
45
- coverageV8ConflictWarning,
46
- coverageTempDirectoryUrl,
47
-
48
- beforeExecutionCallback = () => {},
49
- afterExecutionCallback = () => {},
50
- } = {},
51
- ) => {
52
- executionSteps = executionSteps.filter(
53
- (executionStep) => !executionStep.runtime?.disabled,
54
- );
55
-
56
- const executePlanReturnValue = {};
57
- const report = {};
58
- const callbacks = [];
59
-
60
- const multipleExecutionsOperation = Abort.startOperation();
61
- multipleExecutionsOperation.addAbortSignal(signal);
62
- const failFastAbortController = new AbortController();
63
- if (failFast) {
64
- multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
65
- }
66
-
67
- try {
68
- if (gcBetweenExecutions) {
69
- ensureGlobalGc();
70
- }
71
-
72
- if (coverageEnabled) {
73
- // when runned multiple times, we don't want to keep previous files in this directory
74
- await ensureEmptyDirectory(coverageTempDirectoryUrl);
75
- callbacks.push(async () => {
76
- if (multipleExecutionsOperation.signal.aborted) {
77
- // don't try to do the coverage stuff
78
- return;
79
- }
80
- try {
81
- if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
82
- takeCoverage();
83
- // conceptually we don't need coverage anymore so it would be
84
- // good to call v8.stopCoverage()
85
- // but it logs a strange message about "result is not an object"
86
- }
87
- const planCoverage = await reportToCoverage(report, {
88
- signal: multipleExecutionsOperation.signal,
89
- logger,
90
- rootDirectoryUrl,
91
- coverageConfig,
92
- coverageIncludeMissing,
93
- coverageMethodForNodeJs,
94
- coverageV8ConflictWarning,
95
- });
96
- executePlanReturnValue.planCoverage = planCoverage;
97
- } catch (e) {
98
- if (Abort.isAbortError(e)) {
99
- return;
100
- }
101
- throw e;
102
- }
103
- });
104
- }
105
-
106
- const runtimeParams = {
107
- rootDirectoryUrl,
108
- webServer,
109
-
110
- coverageEnabled,
111
- coverageConfig,
112
- coverageMethodForBrowsers,
113
- coverageMethodForNodeJs,
114
- isTestPlan: true,
115
- teardown,
116
- };
117
-
118
- if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
119
- logMergeForCompletedExecutions = false;
120
- logger.debug(
121
- `Force logMergeForCompletedExecutions to false because process.stdout.isTTY is false`,
122
- );
123
- }
124
- const debugLogsEnabled = logger.levels.debug;
125
- const executionLogsEnabled = logger.levels.info;
126
- const executionSpinner =
127
- logRefresh &&
128
- !debugLogsEnabled &&
129
- executionLogsEnabled &&
130
- process.stdout.isTTY &&
131
- // if there is an error during execution npm will mess up the output
132
- // (happens when npm runs several command in a workspace)
133
- // so we enable spinner only when !process.exitCode (no error so far)
134
- process.exitCode !== 1;
135
-
136
- const startMs = Date.now();
137
- let rawOutput = "";
138
- let executionLog = createLog({ newLine: "" });
139
- const counters = {
140
- total: executionSteps.length,
141
- aborted: 0,
142
- timedout: 0,
143
- failed: 0,
144
- completed: 0,
145
- done: 0,
146
- };
147
- await executeInParallel({
148
- multipleExecutionsOperation,
149
- maxExecutionsInParallel,
150
- cooldownBetweenExecutions,
151
- executionSteps,
152
- start: async (paramsFromStep) => {
153
- const executionIndex = executionSteps.indexOf(paramsFromStep);
154
- const { executionName, fileRelativeUrl, runtime } = paramsFromStep;
155
- const runtimeType = runtime.type;
156
- const runtimeName = runtime.name;
157
- const runtimeVersion = runtime.version;
158
- const executionParams = {
159
- measurePerformance: false,
160
- collectPerformance: false,
161
- collectConsole: true,
162
- allocatedMs: defaultMsAllocatedPerExecution,
163
- ...paramsFromStep,
164
- runtimeParams: {
165
- fileRelativeUrl,
166
- ...paramsFromStep.runtimeParams,
167
- },
168
- };
169
- const beforeExecutionInfo = {
170
- fileRelativeUrl,
171
- runtimeType,
172
- runtimeName,
173
- runtimeVersion,
174
- executionIndex,
175
- executionParams,
176
- startMs: Date.now(),
177
- executionResult: {
178
- status: "executing",
179
- },
180
- counters,
181
- timeEllapsed: Date.now() - startMs,
182
- memoryHeap: memoryUsage().heapUsed,
183
- };
184
- if (typeof executionParams.allocatedMs === "function") {
185
- executionParams.allocatedMs =
186
- executionParams.allocatedMs(beforeExecutionInfo);
187
- }
188
- let spinner;
189
- if (executionSpinner) {
190
- spinner = startSpinner({
191
- log: executionLog,
192
- render: () => {
193
- return createExecutionLog(beforeExecutionInfo, {
194
- logRuntime,
195
- logEachDuration,
196
- logTimeUsage,
197
- logMemoryHeapUsage,
198
- });
199
- },
200
- });
201
- }
202
- beforeExecutionCallback(beforeExecutionInfo);
203
-
204
- const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
205
- let executionResult;
206
- if (existsSync(new URL(fileUrl))) {
207
- executionResult = await run({
208
- signal: multipleExecutionsOperation.signal,
209
- logger,
210
- allocatedMs: executionParams.allocatedMs,
211
- keepRunning,
212
- mirrorConsole: false, // file are executed in parallel, log would be a mess to read
213
- collectConsole: executionParams.collectConsole,
214
- coverageEnabled,
215
- coverageTempDirectoryUrl,
216
- runtime: executionParams.runtime,
217
- runtimeParams: {
218
- ...runtimeParams,
219
- ...executionParams.runtimeParams,
220
- },
221
- });
222
- } else {
223
- executionResult = {
224
- status: "failed",
225
- errors: [
226
- new Error(
227
- `No file at ${fileRelativeUrl} for execution "${executionName}"`,
228
- ),
229
- ],
230
- };
231
- }
232
- counters.done++;
233
- const fileReport = report[fileRelativeUrl];
234
- if (fileReport) {
235
- fileReport[executionName] = executionResult;
236
- } else {
237
- report[fileRelativeUrl] = {
238
- [executionName]: executionResult,
239
- };
240
- }
241
-
242
- const afterExecutionInfo = {
243
- ...beforeExecutionInfo,
244
- runtimeVersion: runtime.version,
245
- endMs: Date.now(),
246
- executionResult,
247
- };
248
-
249
- if (executionResult.status === "aborted") {
250
- counters.aborted++;
251
- } else if (executionResult.status === "timedout") {
252
- counters.timedout++;
253
- } else if (executionResult.status === "failed") {
254
- counters.failed++;
255
- } else if (executionResult.status === "completed") {
256
- counters.completed++;
257
- }
258
- if (gcBetweenExecutions) {
259
- global.gc();
260
- }
261
- if (executionLogsEnabled) {
262
- const log = createExecutionLog(afterExecutionInfo, {
263
- logShortForCompletedExecutions,
264
- logRuntime,
265
- logEachDuration,
266
- ...(logTimeUsage
267
- ? {
268
- timeEllapsed: Date.now() - startMs,
269
- }
270
- : {}),
271
- ...(logMemoryHeapUsage
272
- ? { memoryHeap: memoryUsage().heapUsed }
273
- : {}),
274
- });
275
- // replace spinner with this execution result
276
- if (spinner) spinner.stop();
277
- executionLog.write(log);
278
- rawOutput += stripAnsi(log);
279
-
280
- const canOverwriteLog = canOverwriteLogGetter({
281
- logMergeForCompletedExecutions,
282
- executionResult,
283
- });
284
- if (canOverwriteLog) {
285
- // nothing to do, we reuse the current executionLog object
286
- } else {
287
- executionLog.destroy();
288
- executionLog = createLog({ newLine: "" });
289
- }
290
- }
291
- afterExecutionCallback(afterExecutionInfo);
292
- const isLastExecutionLog = executionIndex === executionSteps.length - 1;
293
- const cancelRemaining =
294
- failFast &&
295
- executionResult.status !== "completed" &&
296
- counters.done < counters.total;
297
- if (isLastExecutionLog && logger.levels.info) {
298
- executionLog.write("\n");
299
- }
300
-
301
- if (cancelRemaining) {
302
- logger.info(`"failFast" enabled -> cancel remaining executions`);
303
- failFastAbortController.abort();
304
- }
305
- },
306
- });
307
- if (!keepRunning) {
308
- logger.debug("trigger test plan teardown");
309
- await teardown.trigger();
310
- }
311
-
312
- counters.cancelled = counters.total - counters.done;
313
- const summary = {
314
- counters,
315
- // when execution is aborted, the remaining executions are "cancelled"
316
- duration: Date.now() - startMs,
317
- };
318
- if (logSummary) {
319
- const summaryLog = formatSummaryLog(summary);
320
- rawOutput += stripAnsi(summaryLog);
321
- logger.info(summaryLog);
322
- }
323
- if (summary.counters.total !== summary.counters.completed) {
324
- const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href;
325
- writeFileSync(logFileUrl, rawOutput);
326
- logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`);
327
- }
328
- executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted;
329
- executePlanReturnValue.planSummary = summary;
330
- executePlanReturnValue.planReport = report;
331
- for (const callback of callbacks) {
332
- await callback();
333
- }
334
- return executePlanReturnValue;
335
- } finally {
336
- await multipleExecutionsOperation.end();
337
- }
338
- };
339
-
340
- const canOverwriteLogGetter = ({
341
- logMergeForCompletedExecutions,
342
- executionResult,
343
- }) => {
344
- if (!logMergeForCompletedExecutions) {
345
- return false;
346
- }
347
- if (executionResult.status === "aborted") {
348
- return true;
349
- }
350
- if (executionResult.status !== "completed") {
351
- return false;
352
- }
353
- const { consoleCalls = [] } = executionResult;
354
- if (consoleCalls.length > 0) {
355
- return false;
356
- }
357
- return true;
358
- };
359
-
360
- const executeInParallel = async ({
361
- multipleExecutionsOperation,
362
- maxExecutionsInParallel,
363
- cooldownBetweenExecutions,
364
- executionSteps,
365
- start,
366
- }) => {
367
- const executionResults = [];
368
- let progressionIndex = 0;
369
- let remainingExecutionCount = executionSteps.length;
370
-
371
- const nextChunk = async () => {
372
- if (multipleExecutionsOperation.signal.aborted) {
373
- return;
374
- }
375
- const outputPromiseArray = [];
376
- while (
377
- remainingExecutionCount > 0 &&
378
- outputPromiseArray.length < maxExecutionsInParallel
379
- ) {
380
- remainingExecutionCount--;
381
- const outputPromise = executeOne(progressionIndex);
382
- progressionIndex++;
383
- outputPromiseArray.push(outputPromise);
384
- }
385
- if (outputPromiseArray.length) {
386
- await Promise.all(outputPromiseArray);
387
- if (remainingExecutionCount > 0) {
388
- await nextChunk();
389
- }
390
- }
391
- };
392
-
393
- const executeOne = async (index) => {
394
- const input = executionSteps[index];
395
- const output = await start(input);
396
- if (!multipleExecutionsOperation.signal.aborted) {
397
- executionResults[index] = output;
398
- }
399
- if (cooldownBetweenExecutions) {
400
- await new Promise((resolve) =>
401
- setTimeout(resolve, cooldownBetweenExecutions),
402
- );
403
- }
404
- };
405
-
406
- await nextChunk();
407
-
408
- return executionResults;
409
- };