@jsenv/test 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import os from "node:os";
1
2
  import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, readFileSync, readdirSync, existsSync } from "node:fs";
2
3
  import process$1, { memoryUsage } from "node:process";
3
4
  import v8, { takeCoverage } from "node:v8";
@@ -6,7 +7,6 @@ import { URL_META, filterV8Coverage } from "./js/v8_coverage.js";
6
7
  import { pathToFileURL, fileURLToPath } from "node:url";
7
8
  import crypto from "node:crypto";
8
9
  import { dirname } from "node:path";
9
- import os from "node:os";
10
10
  import tty from "node:tty";
11
11
  import stringWidth from "string-width";
12
12
  import { readGitHubWorkflowEnv, startGithubCheckRun } from "@jsenv/github-check-run";
@@ -2515,6 +2515,7 @@ const createLog = ({
2515
2515
  const { columns = 80, rows = 24 } = stream;
2516
2516
 
2517
2517
  const log = {
2518
+ destroyed: false,
2518
2519
  onVerticalOverflow: () => {},
2519
2520
  };
2520
2521
 
@@ -2579,6 +2580,9 @@ const createLog = ({
2579
2580
  };
2580
2581
 
2581
2582
  const write = (string, outputFromOutside = streamOutputSpy()) => {
2583
+ if (log.destroyed) {
2584
+ throw new Error("Cannot write log after destroy");
2585
+ }
2582
2586
  if (!lastOutput) {
2583
2587
  doWrite(string);
2584
2588
  return;
@@ -2599,6 +2603,7 @@ const createLog = ({
2599
2603
  };
2600
2604
 
2601
2605
  const destroy = () => {
2606
+ log.destroyed = true;
2602
2607
  if (streamOutputSpy) {
2603
2608
  streamOutputSpy(); // this uninstalls the spy
2604
2609
  streamOutputSpy = null;
@@ -2724,6 +2729,46 @@ const createTeardown = () => {
2724
2729
  };
2725
2730
  };
2726
2731
 
2732
+ const createCallOrderer = () => {
2733
+ const queue = [];
2734
+ const callWhenPreviousExecutionAreDone = (executionIndex, callback) => {
2735
+ if (queue[executionIndex]) {
2736
+ throw new Error(`${executionIndex} already used`);
2737
+ }
2738
+
2739
+ let allBeforeAreDone = true;
2740
+ if (executionIndex > 0) {
2741
+ let beforeIndex = executionIndex - 1;
2742
+ do {
2743
+ const value = queue[beforeIndex];
2744
+ if (!value) {
2745
+ allBeforeAreDone = false;
2746
+ break;
2747
+ }
2748
+ } while (beforeIndex--);
2749
+ }
2750
+ if (!allBeforeAreDone) {
2751
+ queue[executionIndex] = callback;
2752
+ return;
2753
+ }
2754
+ queue[executionIndex] = true;
2755
+ callback();
2756
+ let afterIndex = executionIndex + 1;
2757
+ while (afterIndex < queue.length) {
2758
+ const value = queue[afterIndex];
2759
+ if (value === undefined) {
2760
+ break;
2761
+ }
2762
+ if (typeof value === "function") {
2763
+ queue[afterIndex] = true;
2764
+ value();
2765
+ }
2766
+ afterIndex++;
2767
+ }
2768
+ };
2769
+ return callWhenPreviousExecutionAreDone;
2770
+ };
2771
+
2727
2772
  const readNodeV8CoverageDirectory = async ({
2728
2773
  logger,
2729
2774
  signal,
@@ -4591,7 +4636,7 @@ const ensureGlobalGc = () => {
4591
4636
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
4592
4637
  * @param {boolean} [testPlanParameters.logShortForCompletedExecutions=false] Abbreviate completed execution information to shorten terminal output
4593
4638
  * @param {boolean} [testPlanParameters.logMergeForCompletedExecutions=false] Merge completed execution logs to shorten terminal output
4594
- * @param {number} [testPlanParameters.maxExecutionsInParallel=1] Maximum amount of execution in parallel
4639
+ * @param {boolean|number} [testPlanParameters.concurrency=false] Maximum amount of execution running at the same time
4595
4640
  * @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
4596
4641
  * @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
4597
4642
  * @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
@@ -4618,7 +4663,7 @@ const executeTestPlan = async ({
4618
4663
  webServer,
4619
4664
  testPlan,
4620
4665
  updateProcessExitCode = true,
4621
- maxExecutionsInParallel = 1,
4666
+ concurrency = false,
4622
4667
  defaultMsAllocatedPerExecution = 30_000,
4623
4668
  failFast = false,
4624
4669
  // keepRunning: false to ensure runtime is stopped once executed
@@ -4686,6 +4731,13 @@ const executeTestPlan = async ({
4686
4731
  }) => {
4687
4732
  const teardown = createTeardown();
4688
4733
 
4734
+ const beforeExecutionCallbackSet = new Set();
4735
+ const afterExecutionCallbackSet = new Set();
4736
+ const afterAllExecutionCallbackSet = new Set();
4737
+ beforeExecutionCallbackSet.add(beforeExecutionCallback);
4738
+ afterExecutionCallbackSet.add(afterExecutionCallback);
4739
+ afterAllExecutionCallbackSet.add(afterAllExecutionCallback);
4740
+
4689
4741
  const operation = Abort.startOperation();
4690
4742
  operation.addAbortSignal(signal);
4691
4743
  if (handleSIGINT) {
@@ -4878,6 +4930,10 @@ To fix this warning:
4878
4930
  }
4879
4931
  }
4880
4932
  }
4933
+
4934
+ if (typeof concurrency !== "boolean" && typeof concurrency !== "number") {
4935
+ throw new TypeError(`concurrency must be a boolean or a number`);
4936
+ }
4881
4937
  }
4882
4938
 
4883
4939
  logger.debug(
@@ -4916,6 +4972,25 @@ To fix this warning:
4916
4972
  }
4917
4973
  }
4918
4974
  }
4975
+ if (cooldownBetweenExecutions) {
4976
+ if (cooldownBetweenExecutions < 0) {
4977
+ cooldownBetweenExecutions = 0;
4978
+ }
4979
+ if (cooldownBetweenExecutions === Infinity) {
4980
+ cooldownBetweenExecutions = 0;
4981
+ }
4982
+ }
4983
+ if (concurrency === true) {
4984
+ const availableCpus = os.cpus().length;
4985
+ concurrency =
4986
+ availableCpus > 2
4987
+ ? // One CPU is used to run this code so we do availableCpus - 1
4988
+ availableCpus - 1
4989
+ : 1;
4990
+ }
4991
+ if (concurrency === false) {
4992
+ concurrency = 1;
4993
+ }
4919
4994
  }
4920
4995
 
4921
4996
  testPlan = {
@@ -4943,9 +5018,7 @@ To fix this warning:
4943
5018
  checkSummary: `${executionSteps.length} files will be executed`,
4944
5019
  });
4945
5020
  const annotations = [];
4946
- const afterExecutionCallbackPrevious = afterExecutionCallback;
4947
- afterExecutionCallback = (afterExecutionInfo) => {
4948
- afterExecutionCallbackPrevious(afterExecutionInfo);
5021
+ afterExecutionCallbackSet.add((afterExecutionInfo) => {
4949
5022
  const { executionResult } = afterExecutionInfo;
4950
5023
  const { errors = [] } = executionResult;
4951
5024
  for (const error of errors) {
@@ -4955,10 +5028,8 @@ To fix this warning:
4955
5028
  });
4956
5029
  annotations.push(annotation);
4957
5030
  }
4958
- };
4959
- const afterAllExecutionCallbackPrevious = afterAllExecutionCallback;
4960
- afterAllExecutionCallback = async (returnValue) => {
4961
- afterAllExecutionCallbackPrevious(returnValue);
5031
+ });
5032
+ afterAllExecutionCallbackSet.add(async (returnValue) => {
4962
5033
  const { summary } = returnValue;
4963
5034
  const title = "Jsenv test results";
4964
5035
  const summaryText = stripAnsi(formatSummary(summary));
@@ -4975,7 +5046,7 @@ To fix this warning:
4975
5046
  summary: summaryText,
4976
5047
  annotations,
4977
5048
  });
4978
- };
5049
+ });
4979
5050
  }
4980
5051
 
4981
5052
  executionSteps = executionSteps.filter(
@@ -5059,6 +5130,7 @@ To fix this warning:
5059
5130
  const executionLogsEnabled = logger.levels.info;
5060
5131
  const executionSpinner =
5061
5132
  logRefresh &&
5133
+ concurrency === 1 &&
5062
5134
  !debugLogsEnabled &&
5063
5135
  executionLogsEnabled &&
5064
5136
  process.stdout.isTTY &&
@@ -5078,9 +5150,12 @@ To fix this warning:
5078
5150
  completed: 0,
5079
5151
  done: 0,
5080
5152
  };
5081
- await executeInParallel({
5153
+
5154
+ const callWhenPreviousExecutionAreDone = createCallOrderer();
5155
+
5156
+ await executeConcurrently({
5082
5157
  multipleExecutionsOperation,
5083
- maxExecutionsInParallel,
5158
+ concurrency,
5084
5159
  cooldownBetweenExecutions,
5085
5160
  executionSteps,
5086
5161
  start: async (paramsFromStep) => {
@@ -5133,8 +5208,9 @@ To fix this warning:
5133
5208
  },
5134
5209
  });
5135
5210
  }
5136
- beforeExecutionCallback(beforeExecutionInfo);
5137
-
5211
+ for (const beforeExecutionCallback of beforeExecutionCallbackSet) {
5212
+ beforeExecutionCallback(beforeExecutionInfo);
5213
+ }
5138
5214
  const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
5139
5215
  let executionResult;
5140
5216
  if (existsSync(new URL(fileUrl))) {
@@ -5143,7 +5219,7 @@ To fix this warning:
5143
5219
  logger,
5144
5220
  allocatedMs: executionParams.allocatedMs,
5145
5221
  keepRunning,
5146
- mirrorConsole: false, // file are executed in parallel, log would be a mess to read
5222
+ mirrorConsole: false, // might be executed in parallel: log would be a mess to read
5147
5223
  collectConsole: executionParams.collectConsole,
5148
5224
  coverageEnabled,
5149
5225
  coverageTempDirectoryUrl,
@@ -5163,7 +5239,6 @@ To fix this warning:
5163
5239
  ],
5164
5240
  };
5165
5241
  }
5166
- counters.done++;
5167
5242
  const fileReport = report[fileRelativeUrl];
5168
5243
  if (fileReport) {
5169
5244
  fileReport[executionName] = executionResult;
@@ -5180,58 +5255,65 @@ To fix this warning:
5180
5255
  executionResult,
5181
5256
  };
5182
5257
 
5183
- if (executionResult.status === "aborted") {
5184
- counters.aborted++;
5185
- } else if (executionResult.status === "timedout") {
5186
- counters.timedout++;
5187
- } else if (executionResult.status === "failed") {
5188
- counters.failed++;
5189
- } else if (executionResult.status === "completed") {
5190
- counters.completed++;
5191
- }
5192
5258
  if (gcBetweenExecutions) {
5193
5259
  global.gc();
5194
5260
  }
5195
- if (executionLogsEnabled) {
5196
- const log = createExecutionLog(afterExecutionInfo, {
5197
- logShortForCompletedExecutions,
5198
- logRuntime,
5199
- logEachDuration,
5200
- ...(logTimeUsage
5201
- ? {
5202
- timeEllapsed: Date.now() - startMs,
5203
- }
5204
- : {}),
5205
- ...(logMemoryHeapUsage
5206
- ? { memoryHeap: memoryUsage().heapUsed }
5207
- : {}),
5208
- });
5209
- // replace spinner with this execution result
5210
- if (spinner) spinner.stop();
5211
- executionLog.write(log);
5212
- rawOutput += stripAnsi(log);
5213
-
5214
- const canOverwriteLog = canOverwriteLogGetter({
5215
- logMergeForCompletedExecutions,
5216
- executionResult,
5217
- });
5218
- if (canOverwriteLog) {
5219
- // nothing to do, we reuse the current executionLog object
5220
- } else {
5221
- executionLog.destroy();
5222
- executionLog = createLog({ newLine: "" });
5261
+
5262
+ const timeEllapsed = Date.now() - startMs;
5263
+ const memoryHeap = memoryUsage().heapUsed;
5264
+ callWhenPreviousExecutionAreDone(executionIndex, () => {
5265
+ counters.done++;
5266
+ if (executionResult.status === "aborted") {
5267
+ counters.aborted++;
5268
+ } else if (executionResult.status === "timedout") {
5269
+ counters.timedout++;
5270
+ } else if (executionResult.status === "failed") {
5271
+ counters.failed++;
5272
+ } else if (executionResult.status === "completed") {
5273
+ counters.completed++;
5274
+ }
5275
+
5276
+ if (executionLogsEnabled) {
5277
+ // replace spinner with this execution result
5278
+ if (spinner) {
5279
+ spinner.stop();
5280
+ spinner = null;
5281
+ }
5282
+
5283
+ const log = createExecutionLog(afterExecutionInfo, {
5284
+ logShortForCompletedExecutions,
5285
+ logRuntime,
5286
+ logEachDuration,
5287
+ ...(logTimeUsage ? { timeEllapsed } : {}),
5288
+ ...(logMemoryHeapUsage ? { memoryHeap } : {}),
5289
+ });
5290
+
5291
+ executionLog.write(log);
5292
+ rawOutput += stripAnsi(log);
5293
+ const canOverwriteLog = canOverwriteLogGetter({
5294
+ logMergeForCompletedExecutions,
5295
+ executionResult,
5296
+ });
5297
+ if (canOverwriteLog) {
5298
+ // nothing to do, we reuse the current executionLog object
5299
+ } else {
5300
+ executionLog.destroy();
5301
+ executionLog = createLog({ newLine: "" });
5302
+ }
5303
+ const isLastExecutionLog =
5304
+ executionIndex === executionSteps.length - 1;
5305
+ if (isLastExecutionLog && logger.levels.info) {
5306
+ executionLog.write("\n");
5307
+ }
5223
5308
  }
5309
+ });
5310
+ for (const afterExecutionCallback of afterExecutionCallbackSet) {
5311
+ afterExecutionCallback(afterExecutionInfo);
5224
5312
  }
5225
- afterExecutionCallback(afterExecutionInfo);
5226
- const isLastExecutionLog = executionIndex === executionSteps.length - 1;
5227
5313
  const cancelRemaining =
5228
5314
  failFast &&
5229
5315
  executionResult.status !== "completed" &&
5230
5316
  counters.done < counters.total;
5231
- if (isLastExecutionLog && logger.levels.info) {
5232
- executionLog.write("\n");
5233
- }
5234
-
5235
5317
  if (cancelRemaining) {
5236
5318
  logger.info(`"failFast" enabled -> cancel remaining executions`);
5237
5319
  failFastAbortController.abort();
@@ -5320,7 +5402,9 @@ To fix this warning:
5320
5402
  await Promise.all(promises);
5321
5403
  }
5322
5404
 
5323
- await afterAllExecutionCallback(returnValue);
5405
+ for (const afterAllExecutionCallback of afterAllExecutionCallbackSet) {
5406
+ await afterAllExecutionCallback(returnValue);
5407
+ }
5324
5408
  return returnValue;
5325
5409
  };
5326
5410
 
@@ -5344,55 +5428,50 @@ const canOverwriteLogGetter = ({
5344
5428
  return true;
5345
5429
  };
5346
5430
 
5347
- const executeInParallel = async ({
5431
+ const executeConcurrently = async ({
5348
5432
  multipleExecutionsOperation,
5349
- maxExecutionsInParallel,
5433
+ concurrency,
5350
5434
  cooldownBetweenExecutions,
5351
5435
  executionSteps,
5352
5436
  start,
5353
5437
  }) => {
5354
- const executionResults = [];
5355
5438
  let progressionIndex = 0;
5356
- let remainingExecutionCount = executionSteps.length;
5439
+ let remainingExecutionToStart = executionSteps.length;
5357
5440
 
5358
- const nextChunk = async () => {
5359
- if (multipleExecutionsOperation.signal.aborted) {
5441
+ const startNext = async () => {
5442
+ if (remainingExecutionToStart === 0) {
5360
5443
  return;
5361
5444
  }
5362
- const outputPromiseArray = [];
5363
- while (
5364
- remainingExecutionCount > 0 &&
5365
- outputPromiseArray.length < maxExecutionsInParallel
5366
- ) {
5367
- remainingExecutionCount--;
5368
- const outputPromise = executeOne(progressionIndex);
5369
- progressionIndex++;
5370
- outputPromiseArray.push(outputPromise);
5371
- }
5372
- if (outputPromiseArray.length) {
5373
- await Promise.all(outputPromiseArray);
5374
- if (remainingExecutionCount > 0) {
5375
- await nextChunk();
5376
- }
5377
- }
5378
- };
5379
-
5380
- const executeOne = async (index) => {
5381
- const input = executionSteps[index];
5382
- const output = await start(input);
5383
- if (!multipleExecutionsOperation.signal.aborted) {
5384
- executionResults[index] = output;
5445
+ remainingExecutionToStart--;
5446
+ const input = executionSteps[progressionIndex];
5447
+ progressionIndex++;
5448
+ await start(input);
5449
+ if (multipleExecutionsOperation.signal.aborted) {
5450
+ return;
5385
5451
  }
5386
5452
  if (cooldownBetweenExecutions) {
5387
- await new Promise((resolve) =>
5388
- setTimeout(resolve, cooldownBetweenExecutions),
5389
- );
5453
+ await new Promise((resolve) => {
5454
+ const timeoutId = setTimeout(() => {
5455
+ removeClearTimeoutOnAbort();
5456
+ resolve();
5457
+ }, cooldownBetweenExecutions);
5458
+ const removeClearTimeoutOnAbort =
5459
+ multipleExecutionsOperation.signal.addAbortCallback(() => {
5460
+ clearTimeout(timeoutId);
5461
+ });
5462
+ });
5463
+ if (multipleExecutionsOperation.signal.aborted) {
5464
+ return;
5465
+ }
5390
5466
  }
5467
+ await startNext();
5391
5468
  };
5392
5469
 
5393
- await nextChunk();
5394
-
5395
- return executionResults;
5470
+ const promises = [];
5471
+ while (concurrency--) {
5472
+ promises.push(startNext());
5473
+ }
5474
+ await Promise.all(promises);
5396
5475
  };
5397
5476
 
5398
5477
  const memoize = (compute) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,12 +36,12 @@
36
36
  "@jsenv/abort": "4.2.4",
37
37
  "@jsenv/ast": "6.0.1",
38
38
  "@jsenv/filesystem": "4.3.2",
39
- "@jsenv/log": "3.4.2",
39
+ "@jsenv/log": "3.4.3",
40
40
  "@jsenv/plugin-supervisor": "1.3.9",
41
41
  "@jsenv/urls": "2.2.1",
42
42
  "@jsenv/url-meta": "8.1.0",
43
43
  "@jsenv/utils": "2.0.1",
44
- "@jsenv/github-check-run": "0.1.3",
44
+ "@jsenv/github-check-run": "0.1.4",
45
45
  "v8-to-istanbul": "9.1.3",
46
46
  "istanbul-lib-coverage": "3.2.2",
47
47
  "istanbul-lib-instrument": "6.0.1",
@@ -1,3 +1,4 @@
1
+ import os from "node:os";
1
2
  import { existsSync } from "node:fs";
2
3
  import { memoryUsage } from "node:process";
3
4
  import { takeCoverage } from "node:v8";
@@ -24,6 +25,7 @@ import {
24
25
  } from "@jsenv/github-check-run";
25
26
 
26
27
  import { createTeardown } from "../helpers/teardown.js";
28
+ import { createCallOrderer } from "../helpers/call_orderer.js";
27
29
  import { reportToCoverage } from "../coverage/report_to_coverage.js";
28
30
  import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
29
31
  import { generateCoverageHtmlDirectory } from "../coverage/coverage_reporter_html_directory.js";
@@ -47,7 +49,7 @@ import { ensureGlobalGc } from "./gc.js";
47
49
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
48
50
  * @param {boolean} [testPlanParameters.logShortForCompletedExecutions=false] Abbreviate completed execution information to shorten terminal output
49
51
  * @param {boolean} [testPlanParameters.logMergeForCompletedExecutions=false] Merge completed execution logs to shorten terminal output
50
- * @param {number} [testPlanParameters.maxExecutionsInParallel=1] Maximum amount of execution in parallel
52
+ * @param {boolean|number} [testPlanParameters.concurrency=false] Maximum amount of execution running at the same time
51
53
  * @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
52
54
  * @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
53
55
  * @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
@@ -74,7 +76,7 @@ export const executeTestPlan = async ({
74
76
  webServer,
75
77
  testPlan,
76
78
  updateProcessExitCode = true,
77
- maxExecutionsInParallel = 1,
79
+ concurrency = false,
78
80
  defaultMsAllocatedPerExecution = 30_000,
79
81
  failFast = false,
80
82
  // keepRunning: false to ensure runtime is stopped once executed
@@ -142,6 +144,13 @@ export const executeTestPlan = async ({
142
144
  }) => {
143
145
  const teardown = createTeardown();
144
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);
153
+
145
154
  const operation = Abort.startOperation();
146
155
  operation.addAbortSignal(signal);
147
156
  if (handleSIGINT) {
@@ -334,6 +343,10 @@ To fix this warning:
334
343
  }
335
344
  }
336
345
  }
346
+
347
+ if (typeof concurrency !== "boolean" && typeof concurrency !== "number") {
348
+ throw new TypeError(`concurrency must be a boolean or a number`);
349
+ }
337
350
  }
338
351
 
339
352
  logger.debug(
@@ -372,6 +385,25 @@ To fix this warning:
372
385
  }
373
386
  }
374
387
  }
388
+ if (cooldownBetweenExecutions) {
389
+ if (cooldownBetweenExecutions < 0) {
390
+ cooldownBetweenExecutions = 0;
391
+ }
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
+ }
404
+ if (concurrency === false) {
405
+ concurrency = 1;
406
+ }
375
407
  }
376
408
 
377
409
  testPlan = {
@@ -399,9 +431,7 @@ To fix this warning:
399
431
  checkSummary: `${executionSteps.length} files will be executed`,
400
432
  });
401
433
  const annotations = [];
402
- const afterExecutionCallbackPrevious = afterExecutionCallback;
403
- afterExecutionCallback = (afterExecutionInfo) => {
404
- afterExecutionCallbackPrevious(afterExecutionInfo);
434
+ afterExecutionCallbackSet.add((afterExecutionInfo) => {
405
435
  const { executionResult } = afterExecutionInfo;
406
436
  const { errors = [] } = executionResult;
407
437
  for (const error of errors) {
@@ -411,10 +441,8 @@ To fix this warning:
411
441
  });
412
442
  annotations.push(annotation);
413
443
  }
414
- };
415
- const afterAllExecutionCallbackPrevious = afterAllExecutionCallback;
416
- afterAllExecutionCallback = async (returnValue) => {
417
- afterAllExecutionCallbackPrevious(returnValue);
444
+ });
445
+ afterAllExecutionCallbackSet.add(async (returnValue) => {
418
446
  const { summary } = returnValue;
419
447
  const title = "Jsenv test results";
420
448
  const summaryText = stripAnsi(formatSummary(summary));
@@ -431,7 +459,7 @@ To fix this warning:
431
459
  summary: summaryText,
432
460
  annotations,
433
461
  });
434
- };
462
+ });
435
463
  }
436
464
 
437
465
  executionSteps = executionSteps.filter(
@@ -515,6 +543,7 @@ To fix this warning:
515
543
  const executionLogsEnabled = logger.levels.info;
516
544
  const executionSpinner =
517
545
  logRefresh &&
546
+ concurrency === 1 &&
518
547
  !debugLogsEnabled &&
519
548
  executionLogsEnabled &&
520
549
  process.stdout.isTTY &&
@@ -534,9 +563,12 @@ To fix this warning:
534
563
  completed: 0,
535
564
  done: 0,
536
565
  };
537
- await executeInParallel({
566
+
567
+ const callWhenPreviousExecutionAreDone = createCallOrderer();
568
+
569
+ await executeConcurrently({
538
570
  multipleExecutionsOperation,
539
- maxExecutionsInParallel,
571
+ concurrency,
540
572
  cooldownBetweenExecutions,
541
573
  executionSteps,
542
574
  start: async (paramsFromStep) => {
@@ -589,8 +621,9 @@ To fix this warning:
589
621
  },
590
622
  });
591
623
  }
592
- beforeExecutionCallback(beforeExecutionInfo);
593
-
624
+ for (const beforeExecutionCallback of beforeExecutionCallbackSet) {
625
+ beforeExecutionCallback(beforeExecutionInfo);
626
+ }
594
627
  const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
595
628
  let executionResult;
596
629
  if (existsSync(new URL(fileUrl))) {
@@ -599,7 +632,7 @@ To fix this warning:
599
632
  logger,
600
633
  allocatedMs: executionParams.allocatedMs,
601
634
  keepRunning,
602
- mirrorConsole: false, // file are executed in parallel, log would be a mess to read
635
+ mirrorConsole: false, // might be executed in parallel: log would be a mess to read
603
636
  collectConsole: executionParams.collectConsole,
604
637
  coverageEnabled,
605
638
  coverageTempDirectoryUrl,
@@ -619,7 +652,6 @@ To fix this warning:
619
652
  ],
620
653
  };
621
654
  }
622
- counters.done++;
623
655
  const fileReport = report[fileRelativeUrl];
624
656
  if (fileReport) {
625
657
  fileReport[executionName] = executionResult;
@@ -636,58 +668,65 @@ To fix this warning:
636
668
  executionResult,
637
669
  };
638
670
 
639
- if (executionResult.status === "aborted") {
640
- counters.aborted++;
641
- } else if (executionResult.status === "timedout") {
642
- counters.timedout++;
643
- } else if (executionResult.status === "failed") {
644
- counters.failed++;
645
- } else if (executionResult.status === "completed") {
646
- counters.completed++;
647
- }
648
671
  if (gcBetweenExecutions) {
649
672
  global.gc();
650
673
  }
651
- if (executionLogsEnabled) {
652
- const log = createExecutionLog(afterExecutionInfo, {
653
- logShortForCompletedExecutions,
654
- logRuntime,
655
- logEachDuration,
656
- ...(logTimeUsage
657
- ? {
658
- timeEllapsed: Date.now() - startMs,
659
- }
660
- : {}),
661
- ...(logMemoryHeapUsage
662
- ? { memoryHeap: memoryUsage().heapUsed }
663
- : {}),
664
- });
665
- // replace spinner with this execution result
666
- if (spinner) spinner.stop();
667
- executionLog.write(log);
668
- rawOutput += stripAnsi(log);
669
-
670
- const canOverwriteLog = canOverwriteLogGetter({
671
- logMergeForCompletedExecutions,
672
- executionResult,
673
- });
674
- if (canOverwriteLog) {
675
- // nothing to do, we reuse the current executionLog object
676
- } else {
677
- executionLog.destroy();
678
- executionLog = createLog({ newLine: "" });
674
+
675
+ const timeEllapsed = Date.now() - startMs;
676
+ const memoryHeap = memoryUsage().heapUsed;
677
+ callWhenPreviousExecutionAreDone(executionIndex, () => {
678
+ counters.done++;
679
+ if (executionResult.status === "aborted") {
680
+ counters.aborted++;
681
+ } else if (executionResult.status === "timedout") {
682
+ counters.timedout++;
683
+ } else if (executionResult.status === "failed") {
684
+ counters.failed++;
685
+ } else if (executionResult.status === "completed") {
686
+ counters.completed++;
687
+ }
688
+
689
+ if (executionLogsEnabled) {
690
+ // replace spinner with this execution result
691
+ if (spinner) {
692
+ spinner.stop();
693
+ spinner = null;
694
+ }
695
+
696
+ const log = createExecutionLog(afterExecutionInfo, {
697
+ logShortForCompletedExecutions,
698
+ logRuntime,
699
+ logEachDuration,
700
+ ...(logTimeUsage ? { timeEllapsed } : {}),
701
+ ...(logMemoryHeapUsage ? { memoryHeap } : {}),
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: "" });
715
+ }
716
+ const isLastExecutionLog =
717
+ executionIndex === executionSteps.length - 1;
718
+ if (isLastExecutionLog && logger.levels.info) {
719
+ executionLog.write("\n");
720
+ }
679
721
  }
722
+ });
723
+ for (const afterExecutionCallback of afterExecutionCallbackSet) {
724
+ afterExecutionCallback(afterExecutionInfo);
680
725
  }
681
- afterExecutionCallback(afterExecutionInfo);
682
- const isLastExecutionLog = executionIndex === executionSteps.length - 1;
683
726
  const cancelRemaining =
684
727
  failFast &&
685
728
  executionResult.status !== "completed" &&
686
729
  counters.done < counters.total;
687
- if (isLastExecutionLog && logger.levels.info) {
688
- executionLog.write("\n");
689
- }
690
-
691
730
  if (cancelRemaining) {
692
731
  logger.info(`"failFast" enabled -> cancel remaining executions`);
693
732
  failFastAbortController.abort();
@@ -776,7 +815,9 @@ To fix this warning:
776
815
  await Promise.all(promises);
777
816
  }
778
817
 
779
- await afterAllExecutionCallback(returnValue);
818
+ for (const afterAllExecutionCallback of afterAllExecutionCallbackSet) {
819
+ await afterAllExecutionCallback(returnValue);
820
+ }
780
821
  return returnValue;
781
822
  };
782
823
 
@@ -800,53 +841,48 @@ const canOverwriteLogGetter = ({
800
841
  return true;
801
842
  };
802
843
 
803
- const executeInParallel = async ({
844
+ const executeConcurrently = async ({
804
845
  multipleExecutionsOperation,
805
- maxExecutionsInParallel,
846
+ concurrency,
806
847
  cooldownBetweenExecutions,
807
848
  executionSteps,
808
849
  start,
809
850
  }) => {
810
- const executionResults = [];
811
851
  let progressionIndex = 0;
812
- let remainingExecutionCount = executionSteps.length;
852
+ let remainingExecutionToStart = executionSteps.length;
813
853
 
814
- const nextChunk = async () => {
815
- if (multipleExecutionsOperation.signal.aborted) {
854
+ const startNext = async () => {
855
+ if (remainingExecutionToStart === 0) {
816
856
  return;
817
857
  }
818
- const outputPromiseArray = [];
819
- while (
820
- remainingExecutionCount > 0 &&
821
- outputPromiseArray.length < maxExecutionsInParallel
822
- ) {
823
- remainingExecutionCount--;
824
- const outputPromise = executeOne(progressionIndex);
825
- progressionIndex++;
826
- outputPromiseArray.push(outputPromise);
827
- }
828
- if (outputPromiseArray.length) {
829
- await Promise.all(outputPromiseArray);
830
- if (remainingExecutionCount > 0) {
831
- await nextChunk();
832
- }
833
- }
834
- };
835
-
836
- const executeOne = async (index) => {
837
- const input = executionSteps[index];
838
- const output = await start(input);
839
- if (!multipleExecutionsOperation.signal.aborted) {
840
- executionResults[index] = output;
858
+ remainingExecutionToStart--;
859
+ const input = executionSteps[progressionIndex];
860
+ progressionIndex++;
861
+ await start(input);
862
+ if (multipleExecutionsOperation.signal.aborted) {
863
+ return;
841
864
  }
842
865
  if (cooldownBetweenExecutions) {
843
- await new Promise((resolve) =>
844
- setTimeout(resolve, cooldownBetweenExecutions),
845
- );
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
+ }
846
879
  }
880
+ await startNext();
847
881
  };
848
882
 
849
- await nextChunk();
850
-
851
- return executionResults;
883
+ const promises = [];
884
+ while (concurrency--) {
885
+ promises.push(startNext());
886
+ }
887
+ await Promise.all(promises);
852
888
  };
@@ -0,0 +1,39 @@
1
+ export const createCallOrderer = () => {
2
+ const queue = [];
3
+ const callWhenPreviousExecutionAreDone = (executionIndex, callback) => {
4
+ if (queue[executionIndex]) {
5
+ throw new Error(`${executionIndex} already used`);
6
+ }
7
+
8
+ let allBeforeAreDone = true;
9
+ if (executionIndex > 0) {
10
+ let beforeIndex = executionIndex - 1;
11
+ do {
12
+ const value = queue[beforeIndex];
13
+ if (!value) {
14
+ allBeforeAreDone = false;
15
+ break;
16
+ }
17
+ } while (beforeIndex--);
18
+ }
19
+ if (!allBeforeAreDone) {
20
+ queue[executionIndex] = callback;
21
+ return;
22
+ }
23
+ queue[executionIndex] = true;
24
+ callback();
25
+ let afterIndex = executionIndex + 1;
26
+ while (afterIndex < queue.length) {
27
+ const value = queue[afterIndex];
28
+ if (value === undefined) {
29
+ break;
30
+ }
31
+ if (typeof value === "function") {
32
+ queue[afterIndex] = true;
33
+ value();
34
+ }
35
+ afterIndex++;
36
+ }
37
+ };
38
+ return callWhenPreviousExecutionAreDone;
39
+ };