@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.
- package/dist/jsenv_test.js +175 -96
- package/package.json +3 -3
- package/src/execution/execute_test_plan.js +131 -95
- package/src/helpers/call_orderer.js +39 -0
package/dist/jsenv_test.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5153
|
+
|
|
5154
|
+
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
5155
|
+
|
|
5156
|
+
await executeConcurrently({
|
|
5082
5157
|
multipleExecutionsOperation,
|
|
5083
|
-
|
|
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
|
|
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, //
|
|
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
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
}
|
|
5209
|
-
|
|
5210
|
-
if (
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
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
|
-
|
|
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
|
|
5431
|
+
const executeConcurrently = async ({
|
|
5348
5432
|
multipleExecutionsOperation,
|
|
5349
|
-
|
|
5433
|
+
concurrency,
|
|
5350
5434
|
cooldownBetweenExecutions,
|
|
5351
5435
|
executionSteps,
|
|
5352
5436
|
start,
|
|
5353
5437
|
}) => {
|
|
5354
|
-
const executionResults = [];
|
|
5355
5438
|
let progressionIndex = 0;
|
|
5356
|
-
let
|
|
5439
|
+
let remainingExecutionToStart = executionSteps.length;
|
|
5357
5440
|
|
|
5358
|
-
const
|
|
5359
|
-
if (
|
|
5441
|
+
const startNext = async () => {
|
|
5442
|
+
if (remainingExecutionToStart === 0) {
|
|
5360
5443
|
return;
|
|
5361
5444
|
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
) {
|
|
5367
|
-
|
|
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(
|
|
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
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
+
|
|
567
|
+
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
568
|
+
|
|
569
|
+
await executeConcurrently({
|
|
538
570
|
multipleExecutionsOperation,
|
|
539
|
-
|
|
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
|
|
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, //
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
|
844
|
+
const executeConcurrently = async ({
|
|
804
845
|
multipleExecutionsOperation,
|
|
805
|
-
|
|
846
|
+
concurrency,
|
|
806
847
|
cooldownBetweenExecutions,
|
|
807
848
|
executionSteps,
|
|
808
849
|
start,
|
|
809
850
|
}) => {
|
|
810
|
-
const executionResults = [];
|
|
811
851
|
let progressionIndex = 0;
|
|
812
|
-
let
|
|
852
|
+
let remainingExecutionToStart = executionSteps.length;
|
|
813
853
|
|
|
814
|
-
const
|
|
815
|
-
if (
|
|
854
|
+
const startNext = async () => {
|
|
855
|
+
if (remainingExecutionToStart === 0) {
|
|
816
856
|
return;
|
|
817
857
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
) {
|
|
823
|
-
|
|
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(
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
+
};
|