@jsenv/test 1.7.9 → 1.8.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/dist/jsenv_test.js +1660 -1675
- package/package.json +3 -3
- package/src/execution/execute_test_plan.js +426 -74
- package/src/helpers/call_orderer.js +39 -0
- package/src/execution/execute_steps.js +0 -409
package/dist/jsenv_test.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, readFileSync,
|
|
1
|
+
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
|
+
import process$1, { memoryUsage } from "node:process";
|
|
3
|
+
import v8, { takeCoverage } from "node:v8";
|
|
2
4
|
import stripAnsi from "strip-ansi";
|
|
3
5
|
import { URL_META, filterV8Coverage } from "./js/v8_coverage.js";
|
|
4
6
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
5
7
|
import crypto from "node:crypto";
|
|
6
8
|
import { dirname } from "node:path";
|
|
7
|
-
import process$1, { memoryUsage } from "node:process";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import tty from "node:tty";
|
|
10
11
|
import stringWidth from "string-width";
|
|
11
12
|
import { readGitHubWorkflowEnv, startGithubCheckRun } from "@jsenv/github-check-run";
|
|
12
13
|
import { createRequire } from "node:module";
|
|
14
|
+
import { applyBabelPlugins } from "@jsenv/ast";
|
|
13
15
|
import { spawn, spawnSync, fork } from "node:child_process";
|
|
14
16
|
import { createServer } from "node:net";
|
|
15
|
-
import v8, { takeCoverage } from "node:v8";
|
|
16
|
-
import { applyBabelPlugins } from "@jsenv/ast";
|
|
17
|
-
import { runInNewContext } from "node:vm";
|
|
18
17
|
import wrapAnsi from "wrap-ansi";
|
|
18
|
+
import { runInNewContext } from "node:vm";
|
|
19
19
|
import { injectSupervisorIntoHTML, supervisorFileUrl } from "@jsenv/plugin-supervisor";
|
|
20
20
|
import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap";
|
|
21
21
|
import { findFreePort } from "@jsenv/server";
|
|
@@ -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,1238 +2729,1113 @@ const createTeardown = () => {
|
|
|
2724
2729
|
};
|
|
2725
2730
|
};
|
|
2726
2731
|
|
|
2727
|
-
const
|
|
2728
|
-
|
|
2729
|
-
|
|
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
|
+
|
|
2772
|
+
const readNodeV8CoverageDirectory = async ({
|
|
2730
2773
|
logger,
|
|
2774
|
+
signal,
|
|
2775
|
+
onV8Coverage,
|
|
2776
|
+
maxMsWaitingForNodeToWriteCoverageFile = 2000,
|
|
2731
2777
|
}) => {
|
|
2732
|
-
const
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
Buffer.byteLength(coverageAsText),
|
|
2736
|
-
)})`,
|
|
2737
|
-
);
|
|
2738
|
-
await writeFile(coverageJsonFileUrl, coverageAsText);
|
|
2739
|
-
};
|
|
2778
|
+
const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE;
|
|
2779
|
+
const operation = Abort.startOperation();
|
|
2780
|
+
operation.addAbortSignal(signal);
|
|
2740
2781
|
|
|
2741
|
-
|
|
2782
|
+
let timeSpentTrying = 0;
|
|
2783
|
+
const tryReadDirectory = async () => {
|
|
2784
|
+
const dirContent = readdirSync(NODE_V8_COVERAGE);
|
|
2785
|
+
if (dirContent.length > 0) {
|
|
2786
|
+
return dirContent;
|
|
2787
|
+
}
|
|
2788
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
2789
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2790
|
+
timeSpentTrying += 200;
|
|
2791
|
+
logger.debug("retry to read coverage directory");
|
|
2792
|
+
return tryReadDirectory();
|
|
2793
|
+
}
|
|
2794
|
+
logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`);
|
|
2795
|
+
return dirContent;
|
|
2796
|
+
};
|
|
2742
2797
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2798
|
+
try {
|
|
2799
|
+
operation.throwIfAborted();
|
|
2800
|
+
const dirContent = await tryReadDirectory();
|
|
2745
2801
|
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
path: key.slice(2),
|
|
2751
|
-
};
|
|
2752
|
-
});
|
|
2802
|
+
const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
2803
|
+
NODE_V8_COVERAGE,
|
|
2804
|
+
"NODE_V8_COVERAGE",
|
|
2805
|
+
);
|
|
2753
2806
|
|
|
2754
|
-
|
|
2755
|
-
|
|
2807
|
+
await dirContent.reduce(async (previous, dirEntry) => {
|
|
2808
|
+
operation.throwIfAborted();
|
|
2809
|
+
await previous;
|
|
2810
|
+
|
|
2811
|
+
const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl);
|
|
2812
|
+
const tryReadJsonFile = async () => {
|
|
2813
|
+
const fileContent = String(readFileSync(dirEntryUrl));
|
|
2814
|
+
if (fileContent === "") {
|
|
2815
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
2816
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2817
|
+
timeSpentTrying += 200;
|
|
2818
|
+
return tryReadJsonFile();
|
|
2819
|
+
}
|
|
2820
|
+
console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`);
|
|
2821
|
+
return null;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
try {
|
|
2825
|
+
const fileAsJson = JSON.parse(fileContent);
|
|
2826
|
+
return fileAsJson;
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
2829
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2830
|
+
timeSpentTrying += 200;
|
|
2831
|
+
return tryReadJsonFile();
|
|
2832
|
+
}
|
|
2833
|
+
console.warn(
|
|
2834
|
+
createDetailedMessage(`Error while reading coverage file`, {
|
|
2835
|
+
"error stack": e.stack,
|
|
2836
|
+
"file": dirEntryUrl,
|
|
2837
|
+
}),
|
|
2838
|
+
);
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
const fileContent = await tryReadJsonFile();
|
|
2844
|
+
if (fileContent) {
|
|
2845
|
+
await onV8Coverage(fileContent);
|
|
2846
|
+
}
|
|
2847
|
+
}, Promise.resolve());
|
|
2848
|
+
} finally {
|
|
2849
|
+
await operation.end();
|
|
2850
|
+
}
|
|
2756
2851
|
};
|
|
2757
2852
|
|
|
2758
|
-
const
|
|
2759
|
-
coverage,
|
|
2760
|
-
{
|
|
2761
|
-
rootDirectoryUrl,
|
|
2762
|
-
coverageHtmlDirectoryRelativeUrl,
|
|
2763
|
-
coverageReportSkipEmpty,
|
|
2764
|
-
coverageReportSkipFull,
|
|
2765
|
-
},
|
|
2766
|
-
) => {
|
|
2767
|
-
const libReport = importWithRequire("istanbul-lib-report");
|
|
2768
|
-
const reports = importWithRequire("istanbul-reports");
|
|
2853
|
+
const importWithRequire = createRequire(import.meta.url);
|
|
2769
2854
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
|
|
2775
|
-
});
|
|
2855
|
+
const composeTwoV8Coverages = (firstV8Coverage, secondV8Coverage) => {
|
|
2856
|
+
if (secondV8Coverage.result.length === 0) {
|
|
2857
|
+
return firstV8Coverage;
|
|
2858
|
+
}
|
|
2776
2859
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2860
|
+
// eslint-disable-next-line import/no-unresolved
|
|
2861
|
+
const { mergeProcessCovs } = importWithRequire("@c88/v8-coverage");
|
|
2862
|
+
// "mergeProcessCovs" do not preserves source-map-cache during the merge
|
|
2863
|
+
// so we store sourcemap cache now
|
|
2864
|
+
const sourceMapCache = {};
|
|
2865
|
+
const visit = (coverageReport) => {
|
|
2866
|
+
if (coverageReport["source-map-cache"]) {
|
|
2867
|
+
Object.assign(sourceMapCache, coverageReport["source-map-cache"]);
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
visit(firstV8Coverage);
|
|
2871
|
+
visit(secondV8Coverage);
|
|
2872
|
+
const v8Coverage = mergeProcessCovs([firstV8Coverage, secondV8Coverage]);
|
|
2873
|
+
v8Coverage["source-map-cache"] = sourceMapCache;
|
|
2874
|
+
|
|
2875
|
+
return v8Coverage;
|
|
2783
2876
|
};
|
|
2784
2877
|
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2878
|
+
const composeTwoFileByFileIstanbulCoverages = (
|
|
2879
|
+
firstFileByFileIstanbulCoverage,
|
|
2880
|
+
secondFileByFileIstanbulCoverage,
|
|
2788
2881
|
) => {
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
const context = libReport.createContext({
|
|
2793
|
-
coverageMap: istanbulCoverageMapFromCoverage(coverage),
|
|
2882
|
+
const fileByFileIstanbulCoverage = {};
|
|
2883
|
+
Object.keys(firstFileByFileIstanbulCoverage).forEach((key) => {
|
|
2884
|
+
fileByFileIstanbulCoverage[key] = firstFileByFileIstanbulCoverage[key];
|
|
2794
2885
|
});
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2886
|
+
Object.keys(secondFileByFileIstanbulCoverage).forEach((key) => {
|
|
2887
|
+
const firstCoverage = firstFileByFileIstanbulCoverage[key];
|
|
2888
|
+
const secondCoverage = secondFileByFileIstanbulCoverage[key];
|
|
2889
|
+
fileByFileIstanbulCoverage[key] = firstCoverage
|
|
2890
|
+
? merge(firstCoverage, secondCoverage)
|
|
2891
|
+
: secondCoverage;
|
|
2798
2892
|
});
|
|
2799
|
-
report.execute(context);
|
|
2800
|
-
};
|
|
2801
2893
|
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
const { hostname, port } = new URL(url);
|
|
2894
|
+
return fileByFileIstanbulCoverage;
|
|
2895
|
+
};
|
|
2805
2896
|
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
server.listen(port, hostname);
|
|
2813
|
-
});
|
|
2814
|
-
} catch (error) {
|
|
2815
|
-
if (error && error.code === "EADDRINUSE") {
|
|
2816
|
-
return true;
|
|
2817
|
-
}
|
|
2818
|
-
if (error && error.code === "EACCES") {
|
|
2819
|
-
return true;
|
|
2820
|
-
}
|
|
2821
|
-
throw error;
|
|
2822
|
-
}
|
|
2823
|
-
await new Promise((resolve, reject) => {
|
|
2824
|
-
server.on("error", reject);
|
|
2825
|
-
server.on("close", resolve);
|
|
2826
|
-
server.close();
|
|
2827
|
-
});
|
|
2828
|
-
return false;
|
|
2897
|
+
const merge = (firstIstanbulCoverage, secondIstanbulCoverage) => {
|
|
2898
|
+
const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
|
|
2899
|
+
const istanbulFileCoverageObject = createFileCoverage(firstIstanbulCoverage);
|
|
2900
|
+
istanbulFileCoverageObject.merge(secondIstanbulCoverage);
|
|
2901
|
+
const istanbulCoverage = istanbulFileCoverageObject.toJSON();
|
|
2902
|
+
return istanbulCoverage;
|
|
2829
2903
|
};
|
|
2830
2904
|
|
|
2831
|
-
const
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
) => {
|
|
2835
|
-
const spawnedProcess = spawn(webServer.command, [], {
|
|
2836
|
-
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
|
2837
|
-
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
|
2838
|
-
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
|
2839
|
-
detached: process.platform !== "win32",
|
|
2840
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2841
|
-
shell: true,
|
|
2842
|
-
cwd: webServer.cwd,
|
|
2843
|
-
});
|
|
2844
|
-
if (!spawnedProcess.pid) {
|
|
2845
|
-
await new Promise((resolve, reject) => {
|
|
2846
|
-
spawnedProcess.once("error", (error) => {
|
|
2847
|
-
reject(new Error(`Failed to launch: ${error}`));
|
|
2848
|
-
});
|
|
2849
|
-
});
|
|
2850
|
-
}
|
|
2905
|
+
const v8CoverageToIstanbul = async (v8Coverage, { signal }) => {
|
|
2906
|
+
const operation = Abort.startOperation();
|
|
2907
|
+
operation.addAbortSignal(signal);
|
|
2851
2908
|
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
reject(e);
|
|
2857
|
-
});
|
|
2858
|
-
});
|
|
2909
|
+
try {
|
|
2910
|
+
const v8ToIstanbul = importWithRequire("v8-to-istanbul");
|
|
2911
|
+
const sourcemapCache = v8Coverage["source-map-cache"];
|
|
2912
|
+
let istanbulCoverageComposed = null;
|
|
2859
2913
|
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
const closedPromise = new Promise((resolve) => {
|
|
2870
|
-
spawnedProcess.once("exit", (exitCode, signal) => {
|
|
2871
|
-
logger.debug(
|
|
2872
|
-
`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
|
|
2873
|
-
);
|
|
2874
|
-
processClosed = true;
|
|
2875
|
-
resolve();
|
|
2876
|
-
});
|
|
2877
|
-
});
|
|
2878
|
-
const killProcess = async () => {
|
|
2879
|
-
logger.debug(`[pid=${spawnedProcess.pid}] <kill>`);
|
|
2880
|
-
if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
|
|
2881
|
-
logger.debug(
|
|
2882
|
-
`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
|
|
2883
|
-
);
|
|
2884
|
-
return;
|
|
2885
|
-
}
|
|
2886
|
-
logger.debug(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
|
2887
|
-
// Force kill the browser.
|
|
2888
|
-
try {
|
|
2889
|
-
if (process.platform === "win32") {
|
|
2890
|
-
const taskkillProcess = spawnSync(
|
|
2891
|
-
`taskkill /pid ${spawnedProcess.pid} /T /F`,
|
|
2892
|
-
{ shell: true },
|
|
2893
|
-
);
|
|
2894
|
-
const [stdout, stderr] = [
|
|
2895
|
-
taskkillProcess.stdout.toString(),
|
|
2896
|
-
taskkillProcess.stderr.toString(),
|
|
2897
|
-
];
|
|
2898
|
-
if (stdout)
|
|
2899
|
-
logger.debug(
|
|
2900
|
-
`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`,
|
|
2901
|
-
);
|
|
2902
|
-
if (stderr)
|
|
2903
|
-
logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
|
|
2904
|
-
} else {
|
|
2905
|
-
process.kill(-spawnedProcess.pid, "SIGKILL");
|
|
2914
|
+
await v8Coverage.result.reduce(async (previous, fileV8Coverage) => {
|
|
2915
|
+
operation.throwIfAborted();
|
|
2916
|
+
await previous;
|
|
2917
|
+
|
|
2918
|
+
const { source } = fileV8Coverage;
|
|
2919
|
+
let sources;
|
|
2920
|
+
// when v8 coverage comes from playwright (chromium) v8Coverage.source is set
|
|
2921
|
+
if (typeof source === "string") {
|
|
2922
|
+
sources = { source };
|
|
2906
2923
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2924
|
+
// when v8 coverage comes from Node.js, the source can be read from sourcemapCache
|
|
2925
|
+
else if (sourcemapCache) {
|
|
2926
|
+
sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache);
|
|
2927
|
+
}
|
|
2928
|
+
const path = urlToFileSystemPath(fileV8Coverage.url);
|
|
2929
|
+
|
|
2930
|
+
const converter = v8ToIstanbul(
|
|
2931
|
+
path,
|
|
2932
|
+
// wrapperLength is undefined we don't need it
|
|
2933
|
+
// https://github.com/istanbuljs/v8-to-istanbul/blob/2b54bc97c5edf8a37b39a171ec29134ba9bfd532/lib/v8-to-istanbul.js#L27
|
|
2934
|
+
undefined,
|
|
2935
|
+
sources,
|
|
2910
2936
|
);
|
|
2911
|
-
|
|
2912
|
-
}
|
|
2913
|
-
await closedPromise;
|
|
2914
|
-
};
|
|
2937
|
+
await converter.load();
|
|
2915
2938
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
2919
|
-
startOperation.addAbortCallback(killProcess);
|
|
2920
|
-
teardown.addCallback(killProcess);
|
|
2939
|
+
converter.applyCoverage(fileV8Coverage.functions);
|
|
2940
|
+
const istanbulCoverage = converter.toIstanbul();
|
|
2921
2941
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
const connected = await pingServer(webServer.origin);
|
|
2930
|
-
if (connected) {
|
|
2931
|
-
break;
|
|
2932
|
-
}
|
|
2933
|
-
startOperation.throwIfAborted();
|
|
2934
|
-
const delay = logScale.shift() || 1000;
|
|
2935
|
-
logger.debug(`Waiting ${delay}ms`);
|
|
2936
|
-
await new Promise((x) => setTimeout(x, delay));
|
|
2937
|
-
}
|
|
2938
|
-
})();
|
|
2942
|
+
istanbulCoverageComposed = istanbulCoverageComposed
|
|
2943
|
+
? composeTwoFileByFileIstanbulCoverages(
|
|
2944
|
+
istanbulCoverageComposed,
|
|
2945
|
+
istanbulCoverage,
|
|
2946
|
+
)
|
|
2947
|
+
: istanbulCoverage;
|
|
2948
|
+
}, Promise.resolve());
|
|
2939
2949
|
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
} catch (e) {
|
|
2943
|
-
if (Abort.isAbortError(e)) {
|
|
2944
|
-
if (timeoutAbortSource.signal.aborted) {
|
|
2945
|
-
// aborted by timeout
|
|
2946
|
-
throw new Error(
|
|
2947
|
-
`"${webServer.command}" command did not start a server in less than ${allocatedMs}ms`,
|
|
2948
|
-
);
|
|
2949
|
-
}
|
|
2950
|
-
if (signal.aborted) {
|
|
2951
|
-
// aborted from outside
|
|
2952
|
-
return;
|
|
2953
|
-
}
|
|
2950
|
+
if (!istanbulCoverageComposed) {
|
|
2951
|
+
return {};
|
|
2954
2952
|
}
|
|
2955
|
-
|
|
2953
|
+
istanbulCoverageComposed = markAsConvertedFromV8(istanbulCoverageComposed);
|
|
2954
|
+
return istanbulCoverageComposed;
|
|
2956
2955
|
} finally {
|
|
2957
|
-
await
|
|
2956
|
+
await operation.end();
|
|
2958
2957
|
}
|
|
2959
2958
|
};
|
|
2960
2959
|
|
|
2961
|
-
const
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
);
|
|
2960
|
+
const markAsConvertedFromV8 = (fileByFileCoverage) => {
|
|
2961
|
+
const fileByFileMarked = {};
|
|
2962
|
+
Object.keys(fileByFileCoverage).forEach((key) => {
|
|
2963
|
+
const fileCoverage = fileByFileCoverage[key];
|
|
2964
|
+
fileByFileMarked[key] = {
|
|
2965
|
+
...fileCoverage,
|
|
2966
|
+
fromV8: true,
|
|
2967
|
+
};
|
|
2968
|
+
});
|
|
2969
|
+
return fileByFileMarked;
|
|
2972
2970
|
};
|
|
2973
2971
|
|
|
2974
|
-
const
|
|
2975
|
-
url
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
let requestModule;
|
|
2979
|
-
if (url.startsWith("http:")) {
|
|
2980
|
-
requestModule = await import("node:http");
|
|
2981
|
-
} else {
|
|
2982
|
-
requestModule = await import("node:https");
|
|
2972
|
+
const sourcesFromSourceMapCache = (url, sourceMapCache) => {
|
|
2973
|
+
const sourceMapAndLineLengths = sourceMapCache[url];
|
|
2974
|
+
if (!sourceMapAndLineLengths) {
|
|
2975
|
+
return {};
|
|
2983
2976
|
}
|
|
2984
|
-
const { request } = requestModule;
|
|
2985
2977
|
|
|
2986
|
-
const
|
|
2978
|
+
const { data, lineLengths } = sourceMapAndLineLengths;
|
|
2979
|
+
// See: https://github.com/nodejs/node/pull/34305
|
|
2980
|
+
if (!data) {
|
|
2981
|
+
return undefined;
|
|
2982
|
+
}
|
|
2987
2983
|
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
status: response.statusCode,
|
|
3000
|
-
headers: response.headers,
|
|
3001
|
-
json: () => {
|
|
3002
|
-
req.setTimeout(0);
|
|
3003
|
-
req.destroy();
|
|
3004
|
-
return new Promise((resolve) => {
|
|
3005
|
-
if (response.headers["content-type"] !== "application/json") {
|
|
3006
|
-
console.warn("not json");
|
|
3007
|
-
}
|
|
3008
|
-
let responseBody = "";
|
|
3009
|
-
response.setEncoding("utf8");
|
|
3010
|
-
response.on("data", (chunk) => {
|
|
3011
|
-
responseBody += chunk;
|
|
3012
|
-
});
|
|
3013
|
-
response.on("end", () => {
|
|
3014
|
-
resolve(JSON.parse(responseBody));
|
|
3015
|
-
});
|
|
3016
|
-
response.on("error", (e) => {
|
|
3017
|
-
reject(e);
|
|
3018
|
-
});
|
|
3019
|
-
});
|
|
3020
|
-
},
|
|
3021
|
-
});
|
|
3022
|
-
});
|
|
3023
|
-
req.on("error", reject);
|
|
3024
|
-
req.end();
|
|
2984
|
+
const sources = {
|
|
2985
|
+
sourcemap: data,
|
|
2986
|
+
...(lineLengths ? { source: sourcesFromLineLengths(lineLengths) } : {}),
|
|
2987
|
+
};
|
|
2988
|
+
return sources;
|
|
2989
|
+
};
|
|
2990
|
+
|
|
2991
|
+
const sourcesFromLineLengths = (lineLengths) => {
|
|
2992
|
+
let source = "";
|
|
2993
|
+
lineLengths.forEach((length) => {
|
|
2994
|
+
source += `${"".padEnd(length, ".")}\n`;
|
|
3025
2995
|
});
|
|
2996
|
+
return source;
|
|
3026
2997
|
};
|
|
3027
2998
|
|
|
3028
|
-
const
|
|
3029
|
-
|
|
3030
|
-
|
|
2999
|
+
const composeV8AndIstanbul = (
|
|
3000
|
+
v8FileByFileCoverage,
|
|
3001
|
+
istanbulFileByFileCoverage,
|
|
3002
|
+
{ coverageV8ConflictWarning },
|
|
3031
3003
|
) => {
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
return ![
|
|
3039
|
-
"origin",
|
|
3040
|
-
"moduleUrl",
|
|
3041
|
-
"command",
|
|
3042
|
-
"cwd",
|
|
3043
|
-
"rootDirectoryUrl",
|
|
3044
|
-
].includes(key);
|
|
3004
|
+
const fileByFileCoverage = {};
|
|
3005
|
+
const v8Files = Object.keys(v8FileByFileCoverage);
|
|
3006
|
+
const istanbulFiles = Object.keys(istanbulFileByFileCoverage);
|
|
3007
|
+
|
|
3008
|
+
v8Files.forEach((key) => {
|
|
3009
|
+
fileByFileCoverage[key] = v8FileByFileCoverage[key];
|
|
3045
3010
|
});
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
rejectUnauthorized: false,
|
|
3064
|
-
headers: {
|
|
3065
|
-
"x-server-inspect": "1",
|
|
3066
|
-
},
|
|
3067
|
-
});
|
|
3068
|
-
if (String(headers["server"]).includes("jsenv_dev_server")) {
|
|
3069
|
-
webServer.isJsenvDevServer = true;
|
|
3070
|
-
const { json } = await basicFetch(`${webServer.origin}/__params__.json`, {
|
|
3071
|
-
rejectUnauthorized: false,
|
|
3072
|
-
});
|
|
3073
|
-
if (webServer.rootDirectoryUrl === undefined) {
|
|
3074
|
-
const jsenvDevServerParams = await json();
|
|
3075
|
-
webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl;
|
|
3011
|
+
istanbulFiles.forEach((key) => {
|
|
3012
|
+
const v8Coverage = v8FileByFileCoverage[key];
|
|
3013
|
+
if (v8Coverage) {
|
|
3014
|
+
if (coverageV8ConflictWarning) {
|
|
3015
|
+
console.warn(
|
|
3016
|
+
createDetailedMessage(
|
|
3017
|
+
`Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`,
|
|
3018
|
+
{
|
|
3019
|
+
"details": `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
|
|
3020
|
+
"suggestion":
|
|
3021
|
+
"disable this warning with coverageV8ConflictWarning: false",
|
|
3022
|
+
"suggestion 2": `force coverage using istanbul with coverageMethodForBrowsers: "istanbul"`,
|
|
3023
|
+
},
|
|
3024
|
+
),
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
fileByFileCoverage[key] = v8Coverage;
|
|
3076
3028
|
} else {
|
|
3077
|
-
|
|
3078
|
-
webServer.rootDirectoryUrl,
|
|
3079
|
-
"webServer.rootDirectoryUrl",
|
|
3080
|
-
);
|
|
3029
|
+
fileByFileCoverage[key] = istanbulFileByFileCoverage[key];
|
|
3081
3030
|
}
|
|
3082
|
-
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
"webServer.rootDirectoryUrl",
|
|
3086
|
-
);
|
|
3087
|
-
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
return fileByFileCoverage;
|
|
3088
3034
|
};
|
|
3089
3035
|
|
|
3090
|
-
const
|
|
3091
|
-
|
|
3092
|
-
|
|
3036
|
+
const normalizeFileByFileCoveragePaths = (
|
|
3037
|
+
fileByFileCoverage,
|
|
3038
|
+
rootDirectoryUrl,
|
|
3093
3039
|
) => {
|
|
3094
|
-
const
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
|
-
await startServerUsingCommand(webServer, {
|
|
3109
|
-
signal,
|
|
3110
|
-
allocatedMs,
|
|
3111
|
-
teardown,
|
|
3112
|
-
logger,
|
|
3113
|
-
});
|
|
3114
|
-
return;
|
|
3115
|
-
}
|
|
3116
|
-
throw new TypeError(
|
|
3117
|
-
`webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
|
|
3118
|
-
);
|
|
3040
|
+
const fileByFileNormalized = {};
|
|
3041
|
+
Object.keys(fileByFileCoverage).forEach((key) => {
|
|
3042
|
+
const fileCoverage = fileByFileCoverage[key];
|
|
3043
|
+
const { path } = fileCoverage;
|
|
3044
|
+
const url = isFileSystemPath(path)
|
|
3045
|
+
? fileSystemPathToUrl(path)
|
|
3046
|
+
: new URL(path, rootDirectoryUrl).href;
|
|
3047
|
+
const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
|
|
3048
|
+
fileByFileNormalized[`./${relativeUrl}`] = {
|
|
3049
|
+
...fileCoverage,
|
|
3050
|
+
path: `./${relativeUrl}`,
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
return fileByFileNormalized;
|
|
3119
3054
|
};
|
|
3120
3055
|
|
|
3121
|
-
const
|
|
3056
|
+
const listRelativeFileUrlToCover = async ({
|
|
3122
3057
|
signal,
|
|
3123
3058
|
rootDirectoryUrl,
|
|
3124
|
-
|
|
3059
|
+
coverageConfig,
|
|
3125
3060
|
}) => {
|
|
3061
|
+
const matchingFileResultArray = await collectFiles({
|
|
3062
|
+
signal,
|
|
3063
|
+
directoryUrl: rootDirectoryUrl,
|
|
3064
|
+
associations: { cover: coverageConfig },
|
|
3065
|
+
predicate: ({ cover }) => cover,
|
|
3066
|
+
});
|
|
3067
|
+
return matchingFileResultArray.map(({ relativeUrl }) => relativeUrl);
|
|
3068
|
+
};
|
|
3069
|
+
|
|
3070
|
+
// https://github.com/istanbuljs/babel-plugin-istanbul/blob/321740f7b25d803f881466ea819d870f7ed6a254/src/index.js
|
|
3071
|
+
|
|
3072
|
+
const babelPluginInstrument = (api, { useInlineSourceMaps = false }) => {
|
|
3073
|
+
const { programVisitor } = importWithRequire("istanbul-lib-instrument");
|
|
3074
|
+
const { types } = api;
|
|
3075
|
+
|
|
3076
|
+
return {
|
|
3077
|
+
name: "transform-instrument",
|
|
3078
|
+
visitor: {
|
|
3079
|
+
Program: {
|
|
3080
|
+
enter(path, state) {
|
|
3081
|
+
const { file } = state;
|
|
3082
|
+
const { opts } = file;
|
|
3083
|
+
let inputSourceMap;
|
|
3084
|
+
if (useInlineSourceMaps) {
|
|
3085
|
+
// https://github.com/istanbuljs/babel-plugin-istanbul/commit/a9e15643d249a2985e4387e4308022053b2cd0ad#diff-1fdf421c05c1140f6d71444ea2b27638R65
|
|
3086
|
+
inputSourceMap =
|
|
3087
|
+
opts.inputSourceMap || file.inputMap
|
|
3088
|
+
? file.inputMap.sourcemap
|
|
3089
|
+
: null;
|
|
3090
|
+
} else {
|
|
3091
|
+
inputSourceMap = opts.inputSourceMap;
|
|
3092
|
+
}
|
|
3093
|
+
const __dv__ = programVisitor(
|
|
3094
|
+
types,
|
|
3095
|
+
opts.filenameRelative || opts.filename,
|
|
3096
|
+
{
|
|
3097
|
+
coverageVariable: "__coverage__",
|
|
3098
|
+
inputSourceMap,
|
|
3099
|
+
},
|
|
3100
|
+
);
|
|
3101
|
+
__dv__.enter(path);
|
|
3102
|
+
file.metadata.__dv__ = __dv__;
|
|
3103
|
+
},
|
|
3104
|
+
|
|
3105
|
+
exit(path, state) {
|
|
3106
|
+
const { __dv__ } = state.file.metadata;
|
|
3107
|
+
if (!__dv__) {
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const object = __dv__.exit(path);
|
|
3111
|
+
// object got two properties: fileCoverage and sourceMappingURL
|
|
3112
|
+
state.file.metadata.coverage = object.fileCoverage;
|
|
3113
|
+
},
|
|
3114
|
+
},
|
|
3115
|
+
},
|
|
3116
|
+
};
|
|
3117
|
+
};
|
|
3118
|
+
|
|
3119
|
+
const relativeUrlToEmptyCoverage = async (
|
|
3120
|
+
relativeUrl,
|
|
3121
|
+
{ signal, rootDirectoryUrl },
|
|
3122
|
+
) => {
|
|
3123
|
+
const operation = Abort.startOperation();
|
|
3124
|
+
operation.addAbortSignal(signal);
|
|
3125
|
+
|
|
3126
3126
|
try {
|
|
3127
|
-
const
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3127
|
+
const fileUrl = resolveUrl(relativeUrl, rootDirectoryUrl);
|
|
3128
|
+
const content = await readFile(fileUrl, { as: "string" });
|
|
3129
|
+
|
|
3130
|
+
operation.throwIfAborted();
|
|
3131
|
+
const { metadata } = await applyBabelPlugins({
|
|
3132
|
+
babelPlugins: [babelPluginInstrument],
|
|
3133
|
+
input: content,
|
|
3134
|
+
inputIsJsModule: true,
|
|
3135
|
+
inputUrl: fileUrl,
|
|
3132
3136
|
});
|
|
3133
|
-
const
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3137
|
+
const { coverage } = metadata;
|
|
3138
|
+
if (!coverage) {
|
|
3139
|
+
throw new Error(`missing coverage for file`);
|
|
3140
|
+
}
|
|
3141
|
+
// https://github.com/gotwarlost/istanbul/blob/bc84c315271a5dd4d39bcefc5925cfb61a3d174a/lib/command/common/run-with-cover.js#L229
|
|
3142
|
+
Object.keys(coverage.s).forEach(function (key) {
|
|
3143
|
+
coverage.s[key] = 0;
|
|
3140
3144
|
});
|
|
3141
|
-
return
|
|
3145
|
+
return coverage;
|
|
3142
3146
|
} catch (e) {
|
|
3143
|
-
if (
|
|
3144
|
-
return
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
planReport: {},
|
|
3148
|
-
planCoverage: null,
|
|
3149
|
-
};
|
|
3147
|
+
if (e && e.code === "PARSE_ERROR") {
|
|
3148
|
+
// return an empty coverage for that file when
|
|
3149
|
+
// it contains a syntax error
|
|
3150
|
+
return createEmptyCoverage(relativeUrl);
|
|
3150
3151
|
}
|
|
3151
3152
|
throw e;
|
|
3153
|
+
} finally {
|
|
3154
|
+
await operation.end();
|
|
3152
3155
|
}
|
|
3153
3156
|
};
|
|
3154
3157
|
|
|
3155
|
-
const
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
const stepConfig = filePlan[executionName];
|
|
3159
|
-
if (stepConfig === null || stepConfig === undefined) {
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
if (typeof stepConfig !== "object") {
|
|
3163
|
-
throw new TypeError(
|
|
3164
|
-
createDetailedMessage(
|
|
3165
|
-
`found unexpected value in plan, they must be object`,
|
|
3166
|
-
{
|
|
3167
|
-
["file relative path"]: fileRelativeUrl,
|
|
3168
|
-
["execution name"]: executionName,
|
|
3169
|
-
["value"]: stepConfig,
|
|
3170
|
-
},
|
|
3171
|
-
),
|
|
3172
|
-
);
|
|
3173
|
-
}
|
|
3174
|
-
fileExecutionSteps.push({
|
|
3175
|
-
executionName,
|
|
3176
|
-
fileRelativeUrl,
|
|
3177
|
-
...stepConfig,
|
|
3178
|
-
});
|
|
3179
|
-
});
|
|
3180
|
-
return fileExecutionSteps;
|
|
3158
|
+
const createEmptyCoverage = (relativeUrl) => {
|
|
3159
|
+
const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
|
|
3160
|
+
return createFileCoverage(relativeUrl).toJSON();
|
|
3181
3161
|
};
|
|
3182
3162
|
|
|
3183
|
-
const
|
|
3184
|
-
logger,
|
|
3163
|
+
const getMissingFileByFileCoverage = async ({
|
|
3185
3164
|
signal,
|
|
3186
|
-
|
|
3187
|
-
|
|
3165
|
+
rootDirectoryUrl,
|
|
3166
|
+
coverageConfig,
|
|
3167
|
+
fileByFileCoverage,
|
|
3188
3168
|
}) => {
|
|
3189
|
-
const
|
|
3169
|
+
const relativeUrlsToCover = await listRelativeFileUrlToCover({
|
|
3170
|
+
signal,
|
|
3171
|
+
rootDirectoryUrl,
|
|
3172
|
+
coverageConfig,
|
|
3173
|
+
});
|
|
3174
|
+
const relativeUrlsMissing = relativeUrlsToCover.filter((relativeUrlToCover) =>
|
|
3175
|
+
Object.keys(fileByFileCoverage).every((key) => {
|
|
3176
|
+
return key !== `./${relativeUrlToCover}`;
|
|
3177
|
+
}),
|
|
3178
|
+
);
|
|
3179
|
+
|
|
3190
3180
|
const operation = Abort.startOperation();
|
|
3191
3181
|
operation.addAbortSignal(signal);
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
const tryReadDirectory = async () => {
|
|
3195
|
-
const dirContent = readdirSync(NODE_V8_COVERAGE);
|
|
3196
|
-
if (dirContent.length > 0) {
|
|
3197
|
-
return dirContent;
|
|
3198
|
-
}
|
|
3199
|
-
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
3200
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3201
|
-
timeSpentTrying += 200;
|
|
3202
|
-
logger.debug("retry to read coverage directory");
|
|
3203
|
-
return tryReadDirectory();
|
|
3204
|
-
}
|
|
3205
|
-
logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`);
|
|
3206
|
-
return dirContent;
|
|
3207
|
-
};
|
|
3208
|
-
|
|
3209
|
-
try {
|
|
3182
|
+
const missingFileByFileCoverage = {};
|
|
3183
|
+
await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
|
|
3210
3184
|
operation.throwIfAborted();
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3185
|
+
await previous;
|
|
3186
|
+
await operation.withSignal(async (signal) => {
|
|
3187
|
+
const emptyCoverage = await relativeUrlToEmptyCoverage(
|
|
3188
|
+
relativeUrlMissing,
|
|
3189
|
+
{
|
|
3190
|
+
signal,
|
|
3191
|
+
rootDirectoryUrl,
|
|
3192
|
+
},
|
|
3193
|
+
);
|
|
3194
|
+
missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage;
|
|
3195
|
+
});
|
|
3196
|
+
}, Promise.resolve());
|
|
3197
|
+
return missingFileByFileCoverage;
|
|
3198
|
+
};
|
|
3217
3199
|
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3200
|
+
const reportToCoverage = async (
|
|
3201
|
+
report,
|
|
3202
|
+
{
|
|
3203
|
+
signal,
|
|
3204
|
+
logger,
|
|
3205
|
+
rootDirectoryUrl,
|
|
3206
|
+
coverageConfig,
|
|
3207
|
+
coverageIncludeMissing,
|
|
3208
|
+
coverageMethodForNodeJs,
|
|
3209
|
+
coverageV8ConflictWarning,
|
|
3210
|
+
},
|
|
3211
|
+
) => {
|
|
3212
|
+
// collect v8 and istanbul coverage from executions
|
|
3213
|
+
let { v8Coverage, fileByFileIstanbulCoverage } = await getCoverageFromReport({
|
|
3214
|
+
signal,
|
|
3215
|
+
report,
|
|
3216
|
+
onMissing: ({ file, executionResult, executionName }) => {
|
|
3217
|
+
// several reasons not to have coverage here:
|
|
3218
|
+
// 1. the file we executed did not import an instrumented file.
|
|
3219
|
+
// - a test file without import
|
|
3220
|
+
// - a test file importing only file excluded from coverage
|
|
3221
|
+
// - a coverDescription badly configured so that we don't realize
|
|
3222
|
+
// a file should be covered
|
|
3221
3223
|
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
3227
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3228
|
-
timeSpentTrying += 200;
|
|
3229
|
-
return tryReadJsonFile();
|
|
3230
|
-
}
|
|
3231
|
-
console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`);
|
|
3232
|
-
return null;
|
|
3233
|
-
}
|
|
3224
|
+
// 2. the file we wanted to executed timedout
|
|
3225
|
+
// - infinite loop
|
|
3226
|
+
// - too extensive operation
|
|
3227
|
+
// - a badly configured or too low allocatedMs for that execution.
|
|
3234
3228
|
|
|
3235
|
-
|
|
3236
|
-
const fileAsJson = JSON.parse(fileContent);
|
|
3237
|
-
return fileAsJson;
|
|
3238
|
-
} catch (e) {
|
|
3239
|
-
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
3240
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3241
|
-
timeSpentTrying += 200;
|
|
3242
|
-
return tryReadJsonFile();
|
|
3243
|
-
}
|
|
3244
|
-
console.warn(
|
|
3245
|
-
createDetailedMessage(`Error while reading coverage file`, {
|
|
3246
|
-
"error stack": e.stack,
|
|
3247
|
-
"file": dirEntryUrl,
|
|
3248
|
-
}),
|
|
3249
|
-
);
|
|
3250
|
-
return null;
|
|
3251
|
-
}
|
|
3252
|
-
};
|
|
3229
|
+
// 3. the file we wanted to execute contains syntax-error
|
|
3253
3230
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3231
|
+
// in any scenario we are fine because
|
|
3232
|
+
// coverDescription will generate empty coverage for files
|
|
3233
|
+
// that were suppose to be coverage but were not.
|
|
3234
|
+
if (
|
|
3235
|
+
executionResult.status === "completed" &&
|
|
3236
|
+
executionResult.type === "node" &&
|
|
3237
|
+
coverageMethodForNodeJs !== "NODE_V8_COVERAGE"
|
|
3238
|
+
) {
|
|
3239
|
+
logger.warn(
|
|
3240
|
+
`"${executionName}" execution of ${file} did not properly write coverage into ${executionResult.coverageFileUrl}`,
|
|
3241
|
+
);
|
|
3257
3242
|
}
|
|
3258
|
-
},
|
|
3259
|
-
}
|
|
3260
|
-
await operation.end();
|
|
3261
|
-
}
|
|
3262
|
-
};
|
|
3243
|
+
},
|
|
3244
|
+
});
|
|
3263
3245
|
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3246
|
+
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
3247
|
+
await readNodeV8CoverageDirectory({
|
|
3248
|
+
logger,
|
|
3249
|
+
signal,
|
|
3250
|
+
onV8Coverage: async (nodeV8Coverage) => {
|
|
3251
|
+
const nodeV8CoverageLight = await filterV8Coverage(nodeV8Coverage, {
|
|
3252
|
+
rootDirectoryUrl,
|
|
3253
|
+
coverageConfig,
|
|
3254
|
+
});
|
|
3255
|
+
v8Coverage = v8Coverage
|
|
3256
|
+
? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight)
|
|
3257
|
+
: nodeV8CoverageLight;
|
|
3258
|
+
},
|
|
3259
|
+
});
|
|
3267
3260
|
}
|
|
3268
3261
|
|
|
3269
|
-
//
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
if (coverageReport["source-map-cache"]) {
|
|
3276
|
-
Object.assign(sourceMapCache, coverageReport["source-map-cache"]);
|
|
3277
|
-
}
|
|
3278
|
-
};
|
|
3279
|
-
visit(firstV8Coverage);
|
|
3280
|
-
visit(secondV8Coverage);
|
|
3281
|
-
const v8Coverage = mergeProcessCovs([firstV8Coverage, secondV8Coverage]);
|
|
3282
|
-
v8Coverage["source-map-cache"] = sourceMapCache;
|
|
3262
|
+
// try to merge v8 with istanbul, if any
|
|
3263
|
+
let fileByFileCoverage;
|
|
3264
|
+
if (v8Coverage) {
|
|
3265
|
+
let v8FileByFileCoverage = await v8CoverageToIstanbul(v8Coverage, {
|
|
3266
|
+
signal,
|
|
3267
|
+
});
|
|
3283
3268
|
|
|
3284
|
-
|
|
3285
|
-
|
|
3269
|
+
v8FileByFileCoverage = normalizeFileByFileCoveragePaths(
|
|
3270
|
+
v8FileByFileCoverage,
|
|
3271
|
+
rootDirectoryUrl,
|
|
3272
|
+
);
|
|
3286
3273
|
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3274
|
+
if (fileByFileIstanbulCoverage) {
|
|
3275
|
+
fileByFileIstanbulCoverage = normalizeFileByFileCoveragePaths(
|
|
3276
|
+
fileByFileIstanbulCoverage,
|
|
3277
|
+
rootDirectoryUrl,
|
|
3278
|
+
);
|
|
3279
|
+
fileByFileCoverage = composeV8AndIstanbul(
|
|
3280
|
+
v8FileByFileCoverage,
|
|
3281
|
+
fileByFileIstanbulCoverage,
|
|
3282
|
+
{ coverageV8ConflictWarning },
|
|
3283
|
+
);
|
|
3284
|
+
} else {
|
|
3285
|
+
fileByFileCoverage = v8FileByFileCoverage;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
// get istanbul only
|
|
3289
|
+
else if (fileByFileIstanbulCoverage) {
|
|
3290
|
+
fileByFileCoverage = normalizeFileByFileCoveragePaths(
|
|
3291
|
+
fileByFileIstanbulCoverage,
|
|
3292
|
+
rootDirectoryUrl,
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
// no coverage found in execution (or zero file where executed)
|
|
3296
|
+
else {
|
|
3297
|
+
fileByFileCoverage = {};
|
|
3298
|
+
}
|
|
3302
3299
|
|
|
3303
|
-
|
|
3304
|
-
|
|
3300
|
+
// now add coverage for file not covered
|
|
3301
|
+
if (coverageIncludeMissing) {
|
|
3302
|
+
const missingFileByFileCoverage = await getMissingFileByFileCoverage({
|
|
3303
|
+
signal,
|
|
3304
|
+
rootDirectoryUrl,
|
|
3305
|
+
coverageConfig,
|
|
3306
|
+
fileByFileCoverage,
|
|
3307
|
+
});
|
|
3308
|
+
Object.assign(
|
|
3309
|
+
fileByFileCoverage,
|
|
3310
|
+
normalizeFileByFileCoveragePaths(
|
|
3311
|
+
missingFileByFileCoverage,
|
|
3312
|
+
rootDirectoryUrl,
|
|
3313
|
+
),
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
3305
3316
|
|
|
3306
|
-
|
|
3307
|
-
const { createFileCoverage } = importWithRequire("istanbul-lib-coverage");
|
|
3308
|
-
const istanbulFileCoverageObject = createFileCoverage(firstIstanbulCoverage);
|
|
3309
|
-
istanbulFileCoverageObject.merge(secondIstanbulCoverage);
|
|
3310
|
-
const istanbulCoverage = istanbulFileCoverageObject.toJSON();
|
|
3311
|
-
return istanbulCoverage;
|
|
3317
|
+
return fileByFileCoverage;
|
|
3312
3318
|
};
|
|
3313
3319
|
|
|
3314
|
-
const
|
|
3320
|
+
const getCoverageFromReport = async ({ signal, report, onMissing }) => {
|
|
3315
3321
|
const operation = Abort.startOperation();
|
|
3316
3322
|
operation.addAbortSignal(signal);
|
|
3317
3323
|
|
|
3318
3324
|
try {
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
let istanbulCoverageComposed = null;
|
|
3325
|
+
let v8Coverage;
|
|
3326
|
+
let fileByFileIstanbulCoverage;
|
|
3322
3327
|
|
|
3323
|
-
|
|
3328
|
+
// collect v8 and istanbul coverage from executions
|
|
3329
|
+
await Object.keys(report).reduce(async (previous, file) => {
|
|
3324
3330
|
operation.throwIfAborted();
|
|
3325
3331
|
await previous;
|
|
3326
3332
|
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
}
|
|
3333
|
-
// when v8 coverage comes from Node.js, the source can be read from sourcemapCache
|
|
3334
|
-
else if (sourcemapCache) {
|
|
3335
|
-
sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache);
|
|
3336
|
-
}
|
|
3337
|
-
const path = urlToFileSystemPath(fileV8Coverage.url);
|
|
3333
|
+
const executionResultForFile = report[file];
|
|
3334
|
+
await Object.keys(executionResultForFile).reduce(
|
|
3335
|
+
async (previous, executionName) => {
|
|
3336
|
+
operation.throwIfAborted();
|
|
3337
|
+
await previous;
|
|
3338
3338
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3339
|
+
const executionResultForFileOnRuntime =
|
|
3340
|
+
executionResultForFile[executionName];
|
|
3341
|
+
const { coverageFileUrl } = executionResultForFileOnRuntime;
|
|
3342
|
+
let executionCoverage;
|
|
3343
|
+
try {
|
|
3344
|
+
executionCoverage = JSON.parse(
|
|
3345
|
+
String(readFileSync(new URL(coverageFileUrl))),
|
|
3346
|
+
);
|
|
3347
|
+
} catch (e) {
|
|
3348
|
+
if (e.code === "ENOENT" || e.name === "SyntaxError") {
|
|
3349
|
+
onMissing({
|
|
3350
|
+
executionName,
|
|
3351
|
+
file,
|
|
3352
|
+
executionResult: executionResultForFileOnRuntime,
|
|
3353
|
+
});
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
throw e;
|
|
3357
|
+
}
|
|
3350
3358
|
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3359
|
+
if (isV8Coverage(executionCoverage)) {
|
|
3360
|
+
v8Coverage = v8Coverage
|
|
3361
|
+
? composeTwoV8Coverages(v8Coverage, executionCoverage)
|
|
3362
|
+
: executionCoverage;
|
|
3363
|
+
} else {
|
|
3364
|
+
fileByFileIstanbulCoverage = fileByFileIstanbulCoverage
|
|
3365
|
+
? composeTwoFileByFileIstanbulCoverages(
|
|
3366
|
+
fileByFileIstanbulCoverage,
|
|
3367
|
+
executionCoverage,
|
|
3368
|
+
)
|
|
3369
|
+
: executionCoverage;
|
|
3370
|
+
}
|
|
3371
|
+
},
|
|
3372
|
+
Promise.resolve(),
|
|
3373
|
+
);
|
|
3357
3374
|
}, Promise.resolve());
|
|
3358
3375
|
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
return istanbulCoverageComposed;
|
|
3376
|
+
return {
|
|
3377
|
+
v8Coverage,
|
|
3378
|
+
fileByFileIstanbulCoverage,
|
|
3379
|
+
};
|
|
3364
3380
|
} finally {
|
|
3365
3381
|
await operation.end();
|
|
3366
3382
|
}
|
|
3367
3383
|
};
|
|
3368
3384
|
|
|
3369
|
-
const
|
|
3370
|
-
const fileByFileMarked = {};
|
|
3371
|
-
Object.keys(fileByFileCoverage).forEach((key) => {
|
|
3372
|
-
const fileCoverage = fileByFileCoverage[key];
|
|
3373
|
-
fileByFileMarked[key] = {
|
|
3374
|
-
...fileCoverage,
|
|
3375
|
-
fromV8: true,
|
|
3376
|
-
};
|
|
3377
|
-
});
|
|
3378
|
-
return fileByFileMarked;
|
|
3379
|
-
};
|
|
3380
|
-
|
|
3381
|
-
const sourcesFromSourceMapCache = (url, sourceMapCache) => {
|
|
3382
|
-
const sourceMapAndLineLengths = sourceMapCache[url];
|
|
3383
|
-
if (!sourceMapAndLineLengths) {
|
|
3384
|
-
return {};
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
const { data, lineLengths } = sourceMapAndLineLengths;
|
|
3388
|
-
// See: https://github.com/nodejs/node/pull/34305
|
|
3389
|
-
if (!data) {
|
|
3390
|
-
return undefined;
|
|
3391
|
-
}
|
|
3385
|
+
const isV8Coverage = (coverage) => Boolean(coverage.result);
|
|
3392
3386
|
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3387
|
+
const generateCoverageJsonFile = async ({
|
|
3388
|
+
coverage,
|
|
3389
|
+
coverageJsonFileUrl,
|
|
3390
|
+
logger,
|
|
3391
|
+
}) => {
|
|
3392
|
+
const coverageAsText = JSON.stringify(coverage, null, " ");
|
|
3393
|
+
logger.info(
|
|
3394
|
+
`-> ${urlToFileSystemPath(coverageJsonFileUrl)} (${byteAsFileSize(
|
|
3395
|
+
Buffer.byteLength(coverageAsText),
|
|
3396
|
+
)})`,
|
|
3397
|
+
);
|
|
3398
|
+
await writeFile(coverageJsonFileUrl, coverageAsText);
|
|
3398
3399
|
};
|
|
3399
3400
|
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3401
|
+
const istanbulCoverageMapFromCoverage = (coverage) => {
|
|
3402
|
+
const { createCoverageMap } = importWithRequire("istanbul-lib-coverage");
|
|
3403
|
+
|
|
3404
|
+
const coverageAdjusted = {};
|
|
3405
|
+
Object.keys(coverage).forEach((key) => {
|
|
3406
|
+
coverageAdjusted[key.slice(2)] = {
|
|
3407
|
+
...coverage[key],
|
|
3408
|
+
path: key.slice(2),
|
|
3409
|
+
};
|
|
3404
3410
|
});
|
|
3405
|
-
|
|
3411
|
+
|
|
3412
|
+
const coverageMap = createCoverageMap(coverageAdjusted);
|
|
3413
|
+
return coverageMap;
|
|
3406
3414
|
};
|
|
3407
3415
|
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3416
|
+
const generateCoverageHtmlDirectory = async (
|
|
3417
|
+
coverage,
|
|
3418
|
+
{
|
|
3419
|
+
rootDirectoryUrl,
|
|
3420
|
+
coverageHtmlDirectoryRelativeUrl,
|
|
3421
|
+
coverageReportSkipEmpty,
|
|
3422
|
+
coverageReportSkipFull,
|
|
3423
|
+
},
|
|
3412
3424
|
) => {
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3415
|
-
const istanbulFiles = Object.keys(istanbulFileByFileCoverage);
|
|
3425
|
+
const libReport = importWithRequire("istanbul-lib-report");
|
|
3426
|
+
const reports = importWithRequire("istanbul-reports");
|
|
3416
3427
|
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
if (v8Coverage) {
|
|
3423
|
-
if (coverageV8ConflictWarning) {
|
|
3424
|
-
console.warn(
|
|
3425
|
-
createDetailedMessage(
|
|
3426
|
-
`Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`,
|
|
3427
|
-
{
|
|
3428
|
-
"details": `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
|
|
3429
|
-
"suggestion":
|
|
3430
|
-
"disable this warning with coverageV8ConflictWarning: false",
|
|
3431
|
-
"suggestion 2": `force coverage using istanbul with coverageMethodForBrowsers: "istanbul"`,
|
|
3432
|
-
},
|
|
3433
|
-
),
|
|
3434
|
-
);
|
|
3435
|
-
}
|
|
3436
|
-
fileByFileCoverage[key] = v8Coverage;
|
|
3437
|
-
} else {
|
|
3438
|
-
fileByFileCoverage[key] = istanbulFileByFileCoverage[key];
|
|
3439
|
-
}
|
|
3428
|
+
const context = libReport.createContext({
|
|
3429
|
+
dir: fileURLToPath(rootDirectoryUrl),
|
|
3430
|
+
coverageMap: istanbulCoverageMapFromCoverage(coverage),
|
|
3431
|
+
sourceFinder: (path) =>
|
|
3432
|
+
readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
|
|
3440
3433
|
});
|
|
3441
3434
|
|
|
3442
|
-
|
|
3435
|
+
const report = reports.create("html", {
|
|
3436
|
+
skipEmpty: coverageReportSkipEmpty,
|
|
3437
|
+
skipFull: coverageReportSkipFull,
|
|
3438
|
+
subdir: coverageHtmlDirectoryRelativeUrl,
|
|
3439
|
+
});
|
|
3440
|
+
report.execute(context);
|
|
3443
3441
|
};
|
|
3444
3442
|
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
|
|
3443
|
+
const generateCoverageTextLog = (
|
|
3444
|
+
coverage,
|
|
3445
|
+
{ coverageReportSkipEmpty, coverageReportSkipFull },
|
|
3448
3446
|
) => {
|
|
3449
|
-
const
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
? fileSystemPathToUrl(path)
|
|
3455
|
-
: new URL(path, rootDirectoryUrl).href;
|
|
3456
|
-
const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
|
|
3457
|
-
fileByFileNormalized[`./${relativeUrl}`] = {
|
|
3458
|
-
...fileCoverage,
|
|
3459
|
-
path: `./${relativeUrl}`,
|
|
3460
|
-
};
|
|
3447
|
+
const libReport = importWithRequire("istanbul-lib-report");
|
|
3448
|
+
const reports = importWithRequire("istanbul-reports");
|
|
3449
|
+
|
|
3450
|
+
const context = libReport.createContext({
|
|
3451
|
+
coverageMap: istanbulCoverageMapFromCoverage(coverage),
|
|
3461
3452
|
});
|
|
3462
|
-
|
|
3453
|
+
const report = reports.create("text", {
|
|
3454
|
+
skipEmpty: coverageReportSkipEmpty,
|
|
3455
|
+
skipFull: coverageReportSkipFull,
|
|
3456
|
+
});
|
|
3457
|
+
report.execute(context);
|
|
3463
3458
|
};
|
|
3464
3459
|
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3460
|
+
const pingServer = async (url) => {
|
|
3461
|
+
const server = createServer();
|
|
3462
|
+
const { hostname, port } = new URL(url);
|
|
3463
|
+
|
|
3464
|
+
try {
|
|
3465
|
+
await new Promise((resolve, reject) => {
|
|
3466
|
+
server.on("error", reject);
|
|
3467
|
+
server.on("listening", () => {
|
|
3468
|
+
resolve();
|
|
3469
|
+
});
|
|
3470
|
+
server.listen(port, hostname);
|
|
3471
|
+
});
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
if (error && error.code === "EADDRINUSE") {
|
|
3474
|
+
return true;
|
|
3475
|
+
}
|
|
3476
|
+
if (error && error.code === "EACCES") {
|
|
3477
|
+
return true;
|
|
3478
|
+
}
|
|
3479
|
+
throw error;
|
|
3480
|
+
}
|
|
3481
|
+
await new Promise((resolve, reject) => {
|
|
3482
|
+
server.on("error", reject);
|
|
3483
|
+
server.on("close", resolve);
|
|
3484
|
+
server.close();
|
|
3475
3485
|
});
|
|
3476
|
-
return
|
|
3486
|
+
return false;
|
|
3477
3487
|
};
|
|
3478
3488
|
|
|
3479
|
-
|
|
3489
|
+
const startServerUsingCommand = async (
|
|
3490
|
+
webServer,
|
|
3491
|
+
{ signal, allocatedMs, logger, teardown },
|
|
3492
|
+
) => {
|
|
3493
|
+
const spawnedProcess = spawn(webServer.command, [], {
|
|
3494
|
+
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
|
3495
|
+
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
|
3496
|
+
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
|
3497
|
+
detached: process.platform !== "win32",
|
|
3498
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3499
|
+
shell: true,
|
|
3500
|
+
cwd: webServer.cwd,
|
|
3501
|
+
});
|
|
3502
|
+
if (!spawnedProcess.pid) {
|
|
3503
|
+
await new Promise((resolve, reject) => {
|
|
3504
|
+
spawnedProcess.once("error", (error) => {
|
|
3505
|
+
reject(new Error(`Failed to launch: ${error}`));
|
|
3506
|
+
});
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3480
3509
|
|
|
3481
|
-
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3510
|
+
let errorReceived = false;
|
|
3511
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
3512
|
+
spawnedProcess.on("error", (e) => {
|
|
3513
|
+
errorReceived = true;
|
|
3514
|
+
reject(e);
|
|
3515
|
+
});
|
|
3516
|
+
});
|
|
3484
3517
|
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3518
|
+
// const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
|
3519
|
+
// stdout.on("line", () => {
|
|
3520
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
|
|
3521
|
+
// });
|
|
3522
|
+
// const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
|
3523
|
+
// stderr.on("line", (data) => {
|
|
3524
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
|
|
3525
|
+
// });
|
|
3526
|
+
let processClosed = false;
|
|
3527
|
+
const closedPromise = new Promise((resolve) => {
|
|
3528
|
+
spawnedProcess.once("exit", (exitCode, signal) => {
|
|
3529
|
+
logger.debug(
|
|
3530
|
+
`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
|
|
3531
|
+
);
|
|
3532
|
+
processClosed = true;
|
|
3533
|
+
resolve();
|
|
3534
|
+
});
|
|
3535
|
+
});
|
|
3536
|
+
const killProcess = async () => {
|
|
3537
|
+
logger.debug(`[pid=${spawnedProcess.pid}] <kill>`);
|
|
3538
|
+
if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
|
|
3539
|
+
logger.debug(
|
|
3540
|
+
`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
|
|
3541
|
+
);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
logger.debug(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
|
3545
|
+
// Force kill the browser.
|
|
3546
|
+
try {
|
|
3547
|
+
if (process.platform === "win32") {
|
|
3548
|
+
const taskkillProcess = spawnSync(
|
|
3549
|
+
`taskkill /pid ${spawnedProcess.pid} /T /F`,
|
|
3550
|
+
{ shell: true },
|
|
3551
|
+
);
|
|
3552
|
+
const [stdout, stderr] = [
|
|
3553
|
+
taskkillProcess.stdout.toString(),
|
|
3554
|
+
taskkillProcess.stderr.toString(),
|
|
3555
|
+
];
|
|
3556
|
+
if (stdout)
|
|
3557
|
+
logger.debug(
|
|
3558
|
+
`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`,
|
|
3509
3559
|
);
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
},
|
|
3523
|
-
},
|
|
3524
|
-
},
|
|
3560
|
+
if (stderr)
|
|
3561
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
|
|
3562
|
+
} else {
|
|
3563
|
+
process.kill(-spawnedProcess.pid, "SIGKILL");
|
|
3564
|
+
}
|
|
3565
|
+
} catch (e) {
|
|
3566
|
+
logger.info(
|
|
3567
|
+
`[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
|
|
3568
|
+
);
|
|
3569
|
+
// the process might have already stopped
|
|
3570
|
+
}
|
|
3571
|
+
await closedPromise;
|
|
3525
3572
|
};
|
|
3526
|
-
};
|
|
3527
|
-
|
|
3528
|
-
const relativeUrlToEmptyCoverage = async (
|
|
3529
|
-
relativeUrl,
|
|
3530
|
-
{ signal, rootDirectoryUrl },
|
|
3531
|
-
) => {
|
|
3532
|
-
const operation = Abort.startOperation();
|
|
3533
|
-
operation.addAbortSignal(signal);
|
|
3534
3573
|
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3574
|
+
const startOperation = Abort.startOperation();
|
|
3575
|
+
startOperation.addAbortSignal(signal);
|
|
3576
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
3577
|
+
startOperation.addAbortCallback(killProcess);
|
|
3578
|
+
teardown.addCallback(killProcess);
|
|
3538
3579
|
|
|
3539
|
-
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3580
|
+
const startedPromise = (async () => {
|
|
3581
|
+
const logScale = [100, 250, 500];
|
|
3582
|
+
// eslint-disable-next-line no-constant-condition
|
|
3583
|
+
while (true) {
|
|
3584
|
+
if (errorReceived) {
|
|
3585
|
+
break;
|
|
3586
|
+
}
|
|
3587
|
+
const connected = await pingServer(webServer.origin);
|
|
3588
|
+
if (connected) {
|
|
3589
|
+
break;
|
|
3590
|
+
}
|
|
3591
|
+
startOperation.throwIfAborted();
|
|
3592
|
+
const delay = logScale.shift() || 1000;
|
|
3593
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
3594
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
3549
3595
|
}
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
return coverage;
|
|
3596
|
+
})();
|
|
3597
|
+
|
|
3598
|
+
try {
|
|
3599
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
3555
3600
|
} catch (e) {
|
|
3556
|
-
if (e
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3601
|
+
if (Abort.isAbortError(e)) {
|
|
3602
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
3603
|
+
// aborted by timeout
|
|
3604
|
+
throw new Error(
|
|
3605
|
+
`"${webServer.command}" command did not start a server in less than ${allocatedMs}ms`,
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
if (signal.aborted) {
|
|
3609
|
+
// aborted from outside
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3560
3612
|
}
|
|
3561
3613
|
throw e;
|
|
3562
3614
|
} finally {
|
|
3563
|
-
await
|
|
3615
|
+
await startOperation.end();
|
|
3564
3616
|
}
|
|
3565
3617
|
};
|
|
3566
3618
|
|
|
3567
|
-
const
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
}) => {
|
|
3578
|
-
const relativeUrlsToCover = await listRelativeFileUrlToCover({
|
|
3579
|
-
signal,
|
|
3580
|
-
rootDirectoryUrl,
|
|
3581
|
-
coverageConfig,
|
|
3582
|
-
});
|
|
3583
|
-
const relativeUrlsMissing = relativeUrlsToCover.filter((relativeUrlToCover) =>
|
|
3584
|
-
Object.keys(fileByFileCoverage).every((key) => {
|
|
3585
|
-
return key !== `./${relativeUrlToCover}`;
|
|
3586
|
-
}),
|
|
3619
|
+
const startServerUsingModuleUrl = async (webServer, params) => {
|
|
3620
|
+
if (!existsSync(new URL(webServer.moduleUrl))) {
|
|
3621
|
+
throw new Error(`"${webServer.moduleUrl}" does not lead to a file`);
|
|
3622
|
+
}
|
|
3623
|
+
return startServerUsingCommand(
|
|
3624
|
+
{
|
|
3625
|
+
...webServer,
|
|
3626
|
+
command: `node ${fileURLToPath(webServer.moduleUrl)}`,
|
|
3627
|
+
},
|
|
3628
|
+
params,
|
|
3587
3629
|
);
|
|
3588
|
-
|
|
3589
|
-
const operation = Abort.startOperation();
|
|
3590
|
-
operation.addAbortSignal(signal);
|
|
3591
|
-
const missingFileByFileCoverage = {};
|
|
3592
|
-
await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
|
|
3593
|
-
operation.throwIfAborted();
|
|
3594
|
-
await previous;
|
|
3595
|
-
await operation.withSignal(async (signal) => {
|
|
3596
|
-
const emptyCoverage = await relativeUrlToEmptyCoverage(
|
|
3597
|
-
relativeUrlMissing,
|
|
3598
|
-
{
|
|
3599
|
-
signal,
|
|
3600
|
-
rootDirectoryUrl,
|
|
3601
|
-
},
|
|
3602
|
-
);
|
|
3603
|
-
missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage;
|
|
3604
|
-
});
|
|
3605
|
-
}, Promise.resolve());
|
|
3606
|
-
return missingFileByFileCoverage;
|
|
3607
3630
|
};
|
|
3608
3631
|
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3611
|
-
{
|
|
3612
|
-
signal,
|
|
3613
|
-
logger,
|
|
3614
|
-
rootDirectoryUrl,
|
|
3615
|
-
coverageConfig,
|
|
3616
|
-
coverageIncludeMissing,
|
|
3617
|
-
coverageMethodForNodeJs,
|
|
3618
|
-
coverageV8ConflictWarning,
|
|
3619
|
-
},
|
|
3632
|
+
const basicFetch = async (
|
|
3633
|
+
url,
|
|
3634
|
+
{ rejectUnauthorized = true, method = "GET", headers = {} } = {},
|
|
3620
3635
|
) => {
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
// - a test file without import
|
|
3629
|
-
// - a test file importing only file excluded from coverage
|
|
3630
|
-
// - a coverDescription badly configured so that we don't realize
|
|
3631
|
-
// a file should be covered
|
|
3632
|
-
|
|
3633
|
-
// 2. the file we wanted to executed timedout
|
|
3634
|
-
// - infinite loop
|
|
3635
|
-
// - too extensive operation
|
|
3636
|
-
// - a badly configured or too low allocatedMs for that execution.
|
|
3636
|
+
let requestModule;
|
|
3637
|
+
if (url.startsWith("http:")) {
|
|
3638
|
+
requestModule = await import("node:http");
|
|
3639
|
+
} else {
|
|
3640
|
+
requestModule = await import("node:https");
|
|
3641
|
+
}
|
|
3642
|
+
const { request } = requestModule;
|
|
3637
3643
|
|
|
3638
|
-
|
|
3644
|
+
const urlObject = new URL(url);
|
|
3639
3645
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
logger.warn(
|
|
3649
|
-
`"${executionName}" execution of ${file} did not properly write coverage into ${executionResult.coverageFileUrl}`,
|
|
3650
|
-
);
|
|
3651
|
-
}
|
|
3652
|
-
},
|
|
3653
|
-
});
|
|
3654
|
-
|
|
3655
|
-
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
3656
|
-
await readNodeV8CoverageDirectory({
|
|
3657
|
-
logger,
|
|
3658
|
-
signal,
|
|
3659
|
-
onV8Coverage: async (nodeV8Coverage) => {
|
|
3660
|
-
const nodeV8CoverageLight = await filterV8Coverage(nodeV8Coverage, {
|
|
3661
|
-
rootDirectoryUrl,
|
|
3662
|
-
coverageConfig,
|
|
3663
|
-
});
|
|
3664
|
-
v8Coverage = v8Coverage
|
|
3665
|
-
? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight)
|
|
3666
|
-
: nodeV8CoverageLight;
|
|
3667
|
-
},
|
|
3646
|
+
return new Promise((resolve, reject) => {
|
|
3647
|
+
const req = request({
|
|
3648
|
+
rejectUnauthorized,
|
|
3649
|
+
hostname: urlObject.hostname,
|
|
3650
|
+
port: urlObject.port,
|
|
3651
|
+
path: urlObject.pathname,
|
|
3652
|
+
method,
|
|
3653
|
+
headers,
|
|
3668
3654
|
});
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3655
|
+
req.on("response", (response) => {
|
|
3656
|
+
resolve({
|
|
3657
|
+
status: response.statusCode,
|
|
3658
|
+
headers: response.headers,
|
|
3659
|
+
json: () => {
|
|
3660
|
+
req.setTimeout(0);
|
|
3661
|
+
req.destroy();
|
|
3662
|
+
return new Promise((resolve) => {
|
|
3663
|
+
if (response.headers["content-type"] !== "application/json") {
|
|
3664
|
+
console.warn("not json");
|
|
3665
|
+
}
|
|
3666
|
+
let responseBody = "";
|
|
3667
|
+
response.setEncoding("utf8");
|
|
3668
|
+
response.on("data", (chunk) => {
|
|
3669
|
+
responseBody += chunk;
|
|
3670
|
+
});
|
|
3671
|
+
response.on("end", () => {
|
|
3672
|
+
resolve(JSON.parse(responseBody));
|
|
3673
|
+
});
|
|
3674
|
+
response.on("error", (e) => {
|
|
3675
|
+
reject(e);
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3678
|
+
},
|
|
3679
|
+
});
|
|
3676
3680
|
});
|
|
3681
|
+
req.on("error", reject);
|
|
3682
|
+
req.end();
|
|
3683
|
+
});
|
|
3684
|
+
};
|
|
3677
3685
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3686
|
+
const assertAndNormalizeWebServer = async (
|
|
3687
|
+
webServer,
|
|
3688
|
+
{ signal, logger, teardown },
|
|
3689
|
+
) => {
|
|
3690
|
+
if (!webServer) {
|
|
3691
|
+
throw new TypeError(
|
|
3692
|
+
`webServer is required when running tests on browser(s)`,
|
|
3681
3693
|
);
|
|
3682
|
-
|
|
3683
|
-
if (fileByFileIstanbulCoverage) {
|
|
3684
|
-
fileByFileIstanbulCoverage = normalizeFileByFileCoveragePaths(
|
|
3685
|
-
fileByFileIstanbulCoverage,
|
|
3686
|
-
rootDirectoryUrl,
|
|
3687
|
-
);
|
|
3688
|
-
fileByFileCoverage = composeV8AndIstanbul(
|
|
3689
|
-
v8FileByFileCoverage,
|
|
3690
|
-
fileByFileIstanbulCoverage,
|
|
3691
|
-
{ coverageV8ConflictWarning },
|
|
3692
|
-
);
|
|
3693
|
-
} else {
|
|
3694
|
-
fileByFileCoverage = v8FileByFileCoverage;
|
|
3695
|
-
}
|
|
3696
3694
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3695
|
+
const unexpectedParamNames = Object.keys(webServer).filter((key) => {
|
|
3696
|
+
return ![
|
|
3697
|
+
"origin",
|
|
3698
|
+
"moduleUrl",
|
|
3699
|
+
"command",
|
|
3700
|
+
"cwd",
|
|
3701
|
+
"rootDirectoryUrl",
|
|
3702
|
+
].includes(key);
|
|
3703
|
+
});
|
|
3704
|
+
if (unexpectedParamNames.length > 0) {
|
|
3705
|
+
throw new TypeError(
|
|
3706
|
+
`${unexpectedParamNames.join(",")}: there is no such param to webServer`,
|
|
3702
3707
|
);
|
|
3703
3708
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3709
|
+
if (typeof webServer.origin !== "string") {
|
|
3710
|
+
throw new TypeError(
|
|
3711
|
+
`webServer.origin must be a string, got ${webServer.origin}`,
|
|
3712
|
+
);
|
|
3707
3713
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3714
|
+
await ensureWebServerIsStarted(webServer, {
|
|
3715
|
+
signal,
|
|
3716
|
+
teardown,
|
|
3717
|
+
logger,
|
|
3718
|
+
});
|
|
3719
|
+
const { headers } = await basicFetch(webServer.origin, {
|
|
3720
|
+
method: "GET",
|
|
3721
|
+
rejectUnauthorized: false,
|
|
3722
|
+
headers: {
|
|
3723
|
+
"x-server-inspect": "1",
|
|
3724
|
+
},
|
|
3725
|
+
});
|
|
3726
|
+
if (String(headers["server"]).includes("jsenv_dev_server")) {
|
|
3727
|
+
webServer.isJsenvDevServer = true;
|
|
3728
|
+
const { json } = await basicFetch(`${webServer.origin}/__params__.json`, {
|
|
3729
|
+
rejectUnauthorized: false,
|
|
3716
3730
|
});
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3731
|
+
if (webServer.rootDirectoryUrl === undefined) {
|
|
3732
|
+
const jsenvDevServerParams = await json();
|
|
3733
|
+
webServer.rootDirectoryUrl = jsenvDevServerParams.sourceDirectoryUrl;
|
|
3734
|
+
} else {
|
|
3735
|
+
webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
3736
|
+
webServer.rootDirectoryUrl,
|
|
3737
|
+
"webServer.rootDirectoryUrl",
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
} else {
|
|
3741
|
+
webServer.rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
3742
|
+
webServer.rootDirectoryUrl,
|
|
3743
|
+
"webServer.rootDirectoryUrl",
|
|
3723
3744
|
);
|
|
3724
3745
|
}
|
|
3725
|
-
|
|
3726
|
-
return fileByFileCoverage;
|
|
3727
3746
|
};
|
|
3728
3747
|
|
|
3729
|
-
const
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
await
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
String(readFileSync(new URL(coverageFileUrl))),
|
|
3755
|
-
);
|
|
3756
|
-
} catch (e) {
|
|
3757
|
-
if (e.code === "ENOENT" || e.name === "SyntaxError") {
|
|
3758
|
-
onMissing({
|
|
3759
|
-
executionName,
|
|
3760
|
-
file,
|
|
3761
|
-
executionResult: executionResultForFileOnRuntime,
|
|
3762
|
-
});
|
|
3763
|
-
return;
|
|
3764
|
-
}
|
|
3765
|
-
throw e;
|
|
3766
|
-
}
|
|
3767
|
-
|
|
3768
|
-
if (isV8Coverage(executionCoverage)) {
|
|
3769
|
-
v8Coverage = v8Coverage
|
|
3770
|
-
? composeTwoV8Coverages(v8Coverage, executionCoverage)
|
|
3771
|
-
: executionCoverage;
|
|
3772
|
-
} else {
|
|
3773
|
-
fileByFileIstanbulCoverage = fileByFileIstanbulCoverage
|
|
3774
|
-
? composeTwoFileByFileIstanbulCoverages(
|
|
3775
|
-
fileByFileIstanbulCoverage,
|
|
3776
|
-
executionCoverage,
|
|
3777
|
-
)
|
|
3778
|
-
: executionCoverage;
|
|
3779
|
-
}
|
|
3780
|
-
},
|
|
3781
|
-
Promise.resolve(),
|
|
3782
|
-
);
|
|
3783
|
-
}, Promise.resolve());
|
|
3784
|
-
|
|
3785
|
-
return {
|
|
3786
|
-
v8Coverage,
|
|
3787
|
-
fileByFileIstanbulCoverage,
|
|
3788
|
-
};
|
|
3789
|
-
} finally {
|
|
3790
|
-
await operation.end();
|
|
3748
|
+
const ensureWebServerIsStarted = async (
|
|
3749
|
+
webServer,
|
|
3750
|
+
{ signal, teardown, logger, allocatedMs = 5_000 },
|
|
3751
|
+
) => {
|
|
3752
|
+
const aServerIsListening = await pingServer(webServer.origin);
|
|
3753
|
+
if (aServerIsListening) {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
if (webServer.moduleUrl) {
|
|
3757
|
+
await startServerUsingModuleUrl(webServer, {
|
|
3758
|
+
signal,
|
|
3759
|
+
allocatedMs,
|
|
3760
|
+
teardown,
|
|
3761
|
+
logger,
|
|
3762
|
+
});
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (webServer.command) {
|
|
3766
|
+
await startServerUsingCommand(webServer, {
|
|
3767
|
+
signal,
|
|
3768
|
+
allocatedMs,
|
|
3769
|
+
teardown,
|
|
3770
|
+
logger,
|
|
3771
|
+
});
|
|
3772
|
+
return;
|
|
3791
3773
|
}
|
|
3774
|
+
throw new TypeError(
|
|
3775
|
+
`webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
|
|
3776
|
+
);
|
|
3792
3777
|
};
|
|
3793
3778
|
|
|
3794
|
-
const
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
*
|
|
3799
|
-
* - Used internally by "executeTestPlan" part of the documented API
|
|
3800
|
-
* - Used internally by "execute" an advanced API not documented
|
|
3801
|
-
* - logs generated during file execution can be collected
|
|
3802
|
-
* - logs generated during file execution can be mirrored (re-logged to the console)
|
|
3803
|
-
* - File is given allocatedMs to complete
|
|
3804
|
-
* - Errors are collected
|
|
3805
|
-
* - File execution result is returned, it contains status/errors/namespace/consoleCalls
|
|
3806
|
-
*/
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
const run = async ({
|
|
3810
|
-
signal = new AbortController().signal,
|
|
3811
|
-
logger,
|
|
3812
|
-
allocatedMs,
|
|
3813
|
-
keepRunning = false,
|
|
3814
|
-
mirrorConsole = false,
|
|
3815
|
-
collectConsole = false,
|
|
3816
|
-
coverageEnabled = false,
|
|
3817
|
-
coverageTempDirectoryUrl,
|
|
3818
|
-
collectPerformance = false,
|
|
3819
|
-
runtime,
|
|
3820
|
-
runtimeParams,
|
|
3779
|
+
const executionStepsFromTestPlan = async ({
|
|
3780
|
+
signal,
|
|
3781
|
+
rootDirectoryUrl,
|
|
3782
|
+
testPlan,
|
|
3821
3783
|
}) => {
|
|
3822
|
-
const result = {
|
|
3823
|
-
status: "pending",
|
|
3824
|
-
errors: [],
|
|
3825
|
-
namespace: null,
|
|
3826
|
-
};
|
|
3827
|
-
const callbacks = [];
|
|
3828
|
-
|
|
3829
|
-
const onConsoleRef = { current: () => {} };
|
|
3830
|
-
const stopSignal = { notify: () => {} };
|
|
3831
|
-
const runtimeLabel = `${runtime.name}/${runtime.version}`;
|
|
3832
|
-
|
|
3833
|
-
const runOperation = Abort.startOperation();
|
|
3834
|
-
runOperation.addAbortSignal(signal);
|
|
3835
|
-
let timeoutAbortSource;
|
|
3836
|
-
if (
|
|
3837
|
-
// ideally we would rather log than the timeout is ignored
|
|
3838
|
-
// when keepRunning is true
|
|
3839
|
-
!keepRunning &&
|
|
3840
|
-
typeof allocatedMs === "number" &&
|
|
3841
|
-
allocatedMs !== Infinity
|
|
3842
|
-
) {
|
|
3843
|
-
timeoutAbortSource = runOperation.timeout(allocatedMs);
|
|
3844
|
-
}
|
|
3845
|
-
const consoleCalls = [];
|
|
3846
|
-
onConsoleRef.current = ({ type, text }) => {
|
|
3847
|
-
if (mirrorConsole) {
|
|
3848
|
-
if (type === "error") {
|
|
3849
|
-
process.stderr.write(text);
|
|
3850
|
-
} else {
|
|
3851
|
-
process.stdout.write(text);
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
if (collectConsole) {
|
|
3855
|
-
consoleCalls.push({ type, text });
|
|
3856
|
-
}
|
|
3857
|
-
};
|
|
3858
|
-
if (collectConsole) {
|
|
3859
|
-
result.consoleCalls = consoleCalls;
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
|
-
// we do not keep coverage in memory, it can grow very big
|
|
3863
|
-
// instead we store it on the filesystem
|
|
3864
|
-
// and they can be read later at "coverageFileUrl"
|
|
3865
|
-
let coverageFileUrl;
|
|
3866
|
-
if (coverageEnabled) {
|
|
3867
|
-
coverageFileUrl = new URL(
|
|
3868
|
-
`./${runtime.name}/${crypto.randomUUID()}.json`,
|
|
3869
|
-
coverageTempDirectoryUrl,
|
|
3870
|
-
).href;
|
|
3871
|
-
await ensureParentDirectories(coverageFileUrl);
|
|
3872
|
-
if (coverageEnabled) {
|
|
3873
|
-
result.coverageFileUrl = coverageFileUrl;
|
|
3874
|
-
// written within the child_process/worker_thread or during runtime.run()
|
|
3875
|
-
// for browsers
|
|
3876
|
-
// (because it takes time to serialize and transfer the coverage object)
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
|
-
const startMs = Date.now();
|
|
3881
|
-
callbacks.push(() => {
|
|
3882
|
-
result.duration = Date.now() - startMs;
|
|
3883
|
-
});
|
|
3884
|
-
|
|
3885
3784
|
try {
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
aborted: (cb) => {
|
|
3892
|
-
runOperation.signal.addEventListener("abort", cb);
|
|
3893
|
-
return () => {
|
|
3894
|
-
runOperation.signal.removeEventListener("abort", cb);
|
|
3895
|
-
};
|
|
3896
|
-
},
|
|
3897
|
-
runned: async (cb) => {
|
|
3898
|
-
try {
|
|
3899
|
-
const runResult = await runtime.run({
|
|
3900
|
-
signal: runOperation.signal,
|
|
3901
|
-
logger,
|
|
3902
|
-
...runtimeParams,
|
|
3903
|
-
collectConsole,
|
|
3904
|
-
collectPerformance,
|
|
3905
|
-
coverageFileUrl,
|
|
3906
|
-
keepRunning,
|
|
3907
|
-
stopSignal,
|
|
3908
|
-
onConsole: (log) => onConsoleRef.current(log),
|
|
3909
|
-
});
|
|
3910
|
-
cb(runResult);
|
|
3911
|
-
} catch (e) {
|
|
3912
|
-
cb({
|
|
3913
|
-
status: "failed",
|
|
3914
|
-
errors: [e],
|
|
3915
|
-
});
|
|
3916
|
-
}
|
|
3917
|
-
},
|
|
3918
|
-
},
|
|
3919
|
-
resolve,
|
|
3920
|
-
);
|
|
3785
|
+
const fileResultArray = await collectFiles({
|
|
3786
|
+
signal,
|
|
3787
|
+
directoryUrl: rootDirectoryUrl,
|
|
3788
|
+
associations: { testPlan },
|
|
3789
|
+
predicate: ({ testPlan }) => testPlan,
|
|
3921
3790
|
});
|
|
3922
|
-
const
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
result.performance = performance;
|
|
3932
|
-
}
|
|
3791
|
+
const executionSteps = [];
|
|
3792
|
+
fileResultArray.forEach(({ relativeUrl, meta }) => {
|
|
3793
|
+
const fileExecutionSteps = generateFileExecutionSteps({
|
|
3794
|
+
fileRelativeUrl: relativeUrl,
|
|
3795
|
+
filePlan: meta.testPlan,
|
|
3796
|
+
});
|
|
3797
|
+
executionSteps.push(...fileExecutionSteps);
|
|
3798
|
+
});
|
|
3799
|
+
return executionSteps;
|
|
3933
3800
|
} catch (e) {
|
|
3934
3801
|
if (Abort.isAbortError(e)) {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
result.status = "failed";
|
|
3942
|
-
result.errors.push(e);
|
|
3802
|
+
return {
|
|
3803
|
+
aborted: true,
|
|
3804
|
+
planSummary: {},
|
|
3805
|
+
planReport: {},
|
|
3806
|
+
planCoverage: null,
|
|
3807
|
+
};
|
|
3943
3808
|
}
|
|
3944
|
-
|
|
3945
|
-
await runOperation.end();
|
|
3809
|
+
throw e;
|
|
3946
3810
|
}
|
|
3947
|
-
|
|
3948
|
-
callbacks.forEach((callback) => {
|
|
3949
|
-
callback();
|
|
3950
|
-
});
|
|
3951
|
-
return result;
|
|
3952
3811
|
};
|
|
3953
3812
|
|
|
3954
|
-
const
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3813
|
+
const generateFileExecutionSteps = ({ fileRelativeUrl, filePlan }) => {
|
|
3814
|
+
const fileExecutionSteps = [];
|
|
3815
|
+
Object.keys(filePlan).forEach((executionName) => {
|
|
3816
|
+
const stepConfig = filePlan[executionName];
|
|
3817
|
+
if (stepConfig === null || stepConfig === undefined) {
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
if (typeof stepConfig !== "object") {
|
|
3821
|
+
throw new TypeError(
|
|
3822
|
+
createDetailedMessage(
|
|
3823
|
+
`found unexpected value in plan, they must be object`,
|
|
3824
|
+
{
|
|
3825
|
+
["file relative path"]: fileRelativeUrl,
|
|
3826
|
+
["execution name"]: executionName,
|
|
3827
|
+
["value"]: stepConfig,
|
|
3828
|
+
},
|
|
3829
|
+
),
|
|
3830
|
+
);
|
|
3831
|
+
}
|
|
3832
|
+
fileExecutionSteps.push({
|
|
3833
|
+
executionName,
|
|
3834
|
+
fileRelativeUrl,
|
|
3835
|
+
...stepConfig,
|
|
3836
|
+
});
|
|
3837
|
+
});
|
|
3838
|
+
return fileExecutionSteps;
|
|
3959
3839
|
};
|
|
3960
3840
|
|
|
3961
3841
|
const EXECUTION_COLORS = {
|
|
@@ -4452,424 +4332,28 @@ const formatConsoleSummary = (repartition) => {
|
|
|
4452
4332
|
return `console (${parts.join(" ")})`;
|
|
4453
4333
|
};
|
|
4454
4334
|
|
|
4455
|
-
const
|
|
4456
|
-
|
|
4457
|
-
{
|
|
4458
|
-
signal,
|
|
4459
|
-
teardown,
|
|
4460
|
-
logger,
|
|
4461
|
-
logRefresh,
|
|
4462
|
-
logRuntime,
|
|
4463
|
-
logEachDuration,
|
|
4464
|
-
logSummary,
|
|
4465
|
-
logTimeUsage,
|
|
4466
|
-
logMemoryHeapUsage,
|
|
4467
|
-
logFileRelativeUrl,
|
|
4468
|
-
logMergeForCompletedExecutions,
|
|
4469
|
-
logShortForCompletedExecutions,
|
|
4470
|
-
rootDirectoryUrl,
|
|
4471
|
-
webServer,
|
|
4472
|
-
|
|
4473
|
-
keepRunning,
|
|
4474
|
-
defaultMsAllocatedPerExecution,
|
|
4475
|
-
maxExecutionsInParallel,
|
|
4476
|
-
failFast,
|
|
4477
|
-
gcBetweenExecutions,
|
|
4478
|
-
cooldownBetweenExecutions,
|
|
4479
|
-
|
|
4480
|
-
coverageEnabled,
|
|
4481
|
-
coverageConfig,
|
|
4482
|
-
coverageIncludeMissing,
|
|
4483
|
-
coverageMethodForBrowsers,
|
|
4484
|
-
coverageMethodForNodeJs,
|
|
4485
|
-
coverageV8ConflictWarning,
|
|
4486
|
-
coverageTempDirectoryUrl,
|
|
4487
|
-
|
|
4488
|
-
beforeExecutionCallback = () => {},
|
|
4489
|
-
afterExecutionCallback = () => {},
|
|
4490
|
-
} = {},
|
|
4335
|
+
const githubAnnotationFromError = (
|
|
4336
|
+
error,
|
|
4337
|
+
{ rootDirectoryUrl, executionInfo },
|
|
4491
4338
|
) => {
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
}
|
|
4511
|
-
|
|
4512
|
-
if (coverageEnabled) {
|
|
4513
|
-
// when runned multiple times, we don't want to keep previous files in this directory
|
|
4514
|
-
await ensureEmptyDirectory(coverageTempDirectoryUrl);
|
|
4515
|
-
callbacks.push(async () => {
|
|
4516
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
4517
|
-
// don't try to do the coverage stuff
|
|
4518
|
-
return;
|
|
4519
|
-
}
|
|
4520
|
-
try {
|
|
4521
|
-
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
4522
|
-
takeCoverage();
|
|
4523
|
-
// conceptually we don't need coverage anymore so it would be
|
|
4524
|
-
// good to call v8.stopCoverage()
|
|
4525
|
-
// but it logs a strange message about "result is not an object"
|
|
4526
|
-
}
|
|
4527
|
-
const planCoverage = await reportToCoverage(report, {
|
|
4528
|
-
signal: multipleExecutionsOperation.signal,
|
|
4529
|
-
logger,
|
|
4530
|
-
rootDirectoryUrl,
|
|
4531
|
-
coverageConfig,
|
|
4532
|
-
coverageIncludeMissing,
|
|
4533
|
-
coverageMethodForNodeJs,
|
|
4534
|
-
coverageV8ConflictWarning,
|
|
4535
|
-
});
|
|
4536
|
-
executePlanReturnValue.planCoverage = planCoverage;
|
|
4537
|
-
} catch (e) {
|
|
4538
|
-
if (Abort.isAbortError(e)) {
|
|
4539
|
-
return;
|
|
4540
|
-
}
|
|
4541
|
-
throw e;
|
|
4542
|
-
}
|
|
4543
|
-
});
|
|
4544
|
-
}
|
|
4545
|
-
|
|
4546
|
-
const runtimeParams = {
|
|
4547
|
-
rootDirectoryUrl,
|
|
4548
|
-
webServer,
|
|
4549
|
-
|
|
4550
|
-
coverageEnabled,
|
|
4551
|
-
coverageConfig,
|
|
4552
|
-
coverageMethodForBrowsers,
|
|
4553
|
-
coverageMethodForNodeJs,
|
|
4554
|
-
isTestPlan: true,
|
|
4555
|
-
teardown,
|
|
4556
|
-
};
|
|
4557
|
-
|
|
4558
|
-
if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
|
|
4559
|
-
logMergeForCompletedExecutions = false;
|
|
4560
|
-
logger.debug(
|
|
4561
|
-
`Force logMergeForCompletedExecutions to false because process.stdout.isTTY is false`,
|
|
4562
|
-
);
|
|
4563
|
-
}
|
|
4564
|
-
const debugLogsEnabled = logger.levels.debug;
|
|
4565
|
-
const executionLogsEnabled = logger.levels.info;
|
|
4566
|
-
const executionSpinner =
|
|
4567
|
-
logRefresh &&
|
|
4568
|
-
!debugLogsEnabled &&
|
|
4569
|
-
executionLogsEnabled &&
|
|
4570
|
-
process.stdout.isTTY &&
|
|
4571
|
-
// if there is an error during execution npm will mess up the output
|
|
4572
|
-
// (happens when npm runs several command in a workspace)
|
|
4573
|
-
// so we enable spinner only when !process.exitCode (no error so far)
|
|
4574
|
-
process.exitCode !== 1;
|
|
4575
|
-
|
|
4576
|
-
const startMs = Date.now();
|
|
4577
|
-
let rawOutput = "";
|
|
4578
|
-
let executionLog = createLog({ newLine: "" });
|
|
4579
|
-
const counters = {
|
|
4580
|
-
total: executionSteps.length,
|
|
4581
|
-
aborted: 0,
|
|
4582
|
-
timedout: 0,
|
|
4583
|
-
failed: 0,
|
|
4584
|
-
completed: 0,
|
|
4585
|
-
done: 0,
|
|
4586
|
-
};
|
|
4587
|
-
await executeInParallel({
|
|
4588
|
-
multipleExecutionsOperation,
|
|
4589
|
-
maxExecutionsInParallel,
|
|
4590
|
-
cooldownBetweenExecutions,
|
|
4591
|
-
executionSteps,
|
|
4592
|
-
start: async (paramsFromStep) => {
|
|
4593
|
-
const executionIndex = executionSteps.indexOf(paramsFromStep);
|
|
4594
|
-
const { executionName, fileRelativeUrl, runtime } = paramsFromStep;
|
|
4595
|
-
const runtimeType = runtime.type;
|
|
4596
|
-
const runtimeName = runtime.name;
|
|
4597
|
-
const runtimeVersion = runtime.version;
|
|
4598
|
-
const executionParams = {
|
|
4599
|
-
measurePerformance: false,
|
|
4600
|
-
collectPerformance: false,
|
|
4601
|
-
collectConsole: true,
|
|
4602
|
-
allocatedMs: defaultMsAllocatedPerExecution,
|
|
4603
|
-
...paramsFromStep,
|
|
4604
|
-
runtimeParams: {
|
|
4605
|
-
fileRelativeUrl,
|
|
4606
|
-
...paramsFromStep.runtimeParams,
|
|
4607
|
-
},
|
|
4608
|
-
};
|
|
4609
|
-
const beforeExecutionInfo = {
|
|
4610
|
-
fileRelativeUrl,
|
|
4611
|
-
runtimeType,
|
|
4612
|
-
runtimeName,
|
|
4613
|
-
runtimeVersion,
|
|
4614
|
-
executionIndex,
|
|
4615
|
-
executionParams,
|
|
4616
|
-
startMs: Date.now(),
|
|
4617
|
-
executionResult: {
|
|
4618
|
-
status: "executing",
|
|
4619
|
-
},
|
|
4620
|
-
counters,
|
|
4621
|
-
timeEllapsed: Date.now() - startMs,
|
|
4622
|
-
memoryHeap: memoryUsage().heapUsed,
|
|
4623
|
-
};
|
|
4624
|
-
if (typeof executionParams.allocatedMs === "function") {
|
|
4625
|
-
executionParams.allocatedMs =
|
|
4626
|
-
executionParams.allocatedMs(beforeExecutionInfo);
|
|
4627
|
-
}
|
|
4628
|
-
let spinner;
|
|
4629
|
-
if (executionSpinner) {
|
|
4630
|
-
spinner = startSpinner({
|
|
4631
|
-
log: executionLog,
|
|
4632
|
-
render: () => {
|
|
4633
|
-
return createExecutionLog(beforeExecutionInfo, {
|
|
4634
|
-
logRuntime,
|
|
4635
|
-
logEachDuration,
|
|
4636
|
-
logTimeUsage,
|
|
4637
|
-
logMemoryHeapUsage,
|
|
4638
|
-
});
|
|
4639
|
-
},
|
|
4640
|
-
});
|
|
4641
|
-
}
|
|
4642
|
-
beforeExecutionCallback(beforeExecutionInfo);
|
|
4643
|
-
|
|
4644
|
-
const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
|
|
4645
|
-
let executionResult;
|
|
4646
|
-
if (existsSync(new URL(fileUrl))) {
|
|
4647
|
-
executionResult = await run({
|
|
4648
|
-
signal: multipleExecutionsOperation.signal,
|
|
4649
|
-
logger,
|
|
4650
|
-
allocatedMs: executionParams.allocatedMs,
|
|
4651
|
-
keepRunning,
|
|
4652
|
-
mirrorConsole: false, // file are executed in parallel, log would be a mess to read
|
|
4653
|
-
collectConsole: executionParams.collectConsole,
|
|
4654
|
-
coverageEnabled,
|
|
4655
|
-
coverageTempDirectoryUrl,
|
|
4656
|
-
runtime: executionParams.runtime,
|
|
4657
|
-
runtimeParams: {
|
|
4658
|
-
...runtimeParams,
|
|
4659
|
-
...executionParams.runtimeParams,
|
|
4660
|
-
},
|
|
4661
|
-
});
|
|
4662
|
-
} else {
|
|
4663
|
-
executionResult = {
|
|
4664
|
-
status: "failed",
|
|
4665
|
-
errors: [
|
|
4666
|
-
new Error(
|
|
4667
|
-
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
4668
|
-
),
|
|
4669
|
-
],
|
|
4670
|
-
};
|
|
4671
|
-
}
|
|
4672
|
-
counters.done++;
|
|
4673
|
-
const fileReport = report[fileRelativeUrl];
|
|
4674
|
-
if (fileReport) {
|
|
4675
|
-
fileReport[executionName] = executionResult;
|
|
4676
|
-
} else {
|
|
4677
|
-
report[fileRelativeUrl] = {
|
|
4678
|
-
[executionName]: executionResult,
|
|
4679
|
-
};
|
|
4680
|
-
}
|
|
4681
|
-
|
|
4682
|
-
const afterExecutionInfo = {
|
|
4683
|
-
...beforeExecutionInfo,
|
|
4684
|
-
runtimeVersion: runtime.version,
|
|
4685
|
-
endMs: Date.now(),
|
|
4686
|
-
executionResult,
|
|
4687
|
-
};
|
|
4688
|
-
|
|
4689
|
-
if (executionResult.status === "aborted") {
|
|
4690
|
-
counters.aborted++;
|
|
4691
|
-
} else if (executionResult.status === "timedout") {
|
|
4692
|
-
counters.timedout++;
|
|
4693
|
-
} else if (executionResult.status === "failed") {
|
|
4694
|
-
counters.failed++;
|
|
4695
|
-
} else if (executionResult.status === "completed") {
|
|
4696
|
-
counters.completed++;
|
|
4697
|
-
}
|
|
4698
|
-
if (gcBetweenExecutions) {
|
|
4699
|
-
global.gc();
|
|
4700
|
-
}
|
|
4701
|
-
if (executionLogsEnabled) {
|
|
4702
|
-
const log = createExecutionLog(afterExecutionInfo, {
|
|
4703
|
-
logShortForCompletedExecutions,
|
|
4704
|
-
logRuntime,
|
|
4705
|
-
logEachDuration,
|
|
4706
|
-
...(logTimeUsage
|
|
4707
|
-
? {
|
|
4708
|
-
timeEllapsed: Date.now() - startMs,
|
|
4709
|
-
}
|
|
4710
|
-
: {}),
|
|
4711
|
-
...(logMemoryHeapUsage
|
|
4712
|
-
? { memoryHeap: memoryUsage().heapUsed }
|
|
4713
|
-
: {}),
|
|
4714
|
-
});
|
|
4715
|
-
// replace spinner with this execution result
|
|
4716
|
-
if (spinner) spinner.stop();
|
|
4717
|
-
executionLog.write(log);
|
|
4718
|
-
rawOutput += stripAnsi(log);
|
|
4719
|
-
|
|
4720
|
-
const canOverwriteLog = canOverwriteLogGetter({
|
|
4721
|
-
logMergeForCompletedExecutions,
|
|
4722
|
-
executionResult,
|
|
4723
|
-
});
|
|
4724
|
-
if (canOverwriteLog) {
|
|
4725
|
-
// nothing to do, we reuse the current executionLog object
|
|
4726
|
-
} else {
|
|
4727
|
-
executionLog.destroy();
|
|
4728
|
-
executionLog = createLog({ newLine: "" });
|
|
4729
|
-
}
|
|
4730
|
-
}
|
|
4731
|
-
afterExecutionCallback(afterExecutionInfo);
|
|
4732
|
-
const isLastExecutionLog = executionIndex === executionSteps.length - 1;
|
|
4733
|
-
const cancelRemaining =
|
|
4734
|
-
failFast &&
|
|
4735
|
-
executionResult.status !== "completed" &&
|
|
4736
|
-
counters.done < counters.total;
|
|
4737
|
-
if (isLastExecutionLog && logger.levels.info) {
|
|
4738
|
-
executionLog.write("\n");
|
|
4739
|
-
}
|
|
4740
|
-
|
|
4741
|
-
if (cancelRemaining) {
|
|
4742
|
-
logger.info(`"failFast" enabled -> cancel remaining executions`);
|
|
4743
|
-
failFastAbortController.abort();
|
|
4744
|
-
}
|
|
4745
|
-
},
|
|
4746
|
-
});
|
|
4747
|
-
if (!keepRunning) {
|
|
4748
|
-
logger.debug("trigger test plan teardown");
|
|
4749
|
-
await teardown.trigger();
|
|
4750
|
-
}
|
|
4751
|
-
|
|
4752
|
-
counters.cancelled = counters.total - counters.done;
|
|
4753
|
-
const summary = {
|
|
4754
|
-
counters,
|
|
4755
|
-
// when execution is aborted, the remaining executions are "cancelled"
|
|
4756
|
-
duration: Date.now() - startMs,
|
|
4757
|
-
};
|
|
4758
|
-
if (logSummary) {
|
|
4759
|
-
const summaryLog = formatSummaryLog(summary);
|
|
4760
|
-
rawOutput += stripAnsi(summaryLog);
|
|
4761
|
-
logger.info(summaryLog);
|
|
4762
|
-
}
|
|
4763
|
-
if (summary.counters.total !== summary.counters.completed) {
|
|
4764
|
-
const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href;
|
|
4765
|
-
writeFileSync(logFileUrl, rawOutput);
|
|
4766
|
-
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`);
|
|
4767
|
-
}
|
|
4768
|
-
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted;
|
|
4769
|
-
executePlanReturnValue.planSummary = summary;
|
|
4770
|
-
executePlanReturnValue.planReport = report;
|
|
4771
|
-
for (const callback of callbacks) {
|
|
4772
|
-
await callback();
|
|
4773
|
-
}
|
|
4774
|
-
return executePlanReturnValue;
|
|
4775
|
-
} finally {
|
|
4776
|
-
await multipleExecutionsOperation.end();
|
|
4777
|
-
}
|
|
4778
|
-
};
|
|
4779
|
-
|
|
4780
|
-
const canOverwriteLogGetter = ({
|
|
4781
|
-
logMergeForCompletedExecutions,
|
|
4782
|
-
executionResult,
|
|
4783
|
-
}) => {
|
|
4784
|
-
if (!logMergeForCompletedExecutions) {
|
|
4785
|
-
return false;
|
|
4786
|
-
}
|
|
4787
|
-
if (executionResult.status === "aborted") {
|
|
4788
|
-
return true;
|
|
4789
|
-
}
|
|
4790
|
-
if (executionResult.status !== "completed") {
|
|
4791
|
-
return false;
|
|
4792
|
-
}
|
|
4793
|
-
const { consoleCalls = [] } = executionResult;
|
|
4794
|
-
if (consoleCalls.length > 0) {
|
|
4795
|
-
return false;
|
|
4796
|
-
}
|
|
4797
|
-
return true;
|
|
4798
|
-
};
|
|
4799
|
-
|
|
4800
|
-
const executeInParallel = async ({
|
|
4801
|
-
multipleExecutionsOperation,
|
|
4802
|
-
maxExecutionsInParallel,
|
|
4803
|
-
cooldownBetweenExecutions,
|
|
4804
|
-
executionSteps,
|
|
4805
|
-
start,
|
|
4806
|
-
}) => {
|
|
4807
|
-
const executionResults = [];
|
|
4808
|
-
let progressionIndex = 0;
|
|
4809
|
-
let remainingExecutionCount = executionSteps.length;
|
|
4810
|
-
|
|
4811
|
-
const nextChunk = async () => {
|
|
4812
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
4813
|
-
return;
|
|
4814
|
-
}
|
|
4815
|
-
const outputPromiseArray = [];
|
|
4816
|
-
while (
|
|
4817
|
-
remainingExecutionCount > 0 &&
|
|
4818
|
-
outputPromiseArray.length < maxExecutionsInParallel
|
|
4819
|
-
) {
|
|
4820
|
-
remainingExecutionCount--;
|
|
4821
|
-
const outputPromise = executeOne(progressionIndex);
|
|
4822
|
-
progressionIndex++;
|
|
4823
|
-
outputPromiseArray.push(outputPromise);
|
|
4824
|
-
}
|
|
4825
|
-
if (outputPromiseArray.length) {
|
|
4826
|
-
await Promise.all(outputPromiseArray);
|
|
4827
|
-
if (remainingExecutionCount > 0) {
|
|
4828
|
-
await nextChunk();
|
|
4829
|
-
}
|
|
4830
|
-
}
|
|
4831
|
-
};
|
|
4832
|
-
|
|
4833
|
-
const executeOne = async (index) => {
|
|
4834
|
-
const input = executionSteps[index];
|
|
4835
|
-
const output = await start(input);
|
|
4836
|
-
if (!multipleExecutionsOperation.signal.aborted) {
|
|
4837
|
-
executionResults[index] = output;
|
|
4838
|
-
}
|
|
4839
|
-
if (cooldownBetweenExecutions) {
|
|
4840
|
-
await new Promise((resolve) =>
|
|
4841
|
-
setTimeout(resolve, cooldownBetweenExecutions),
|
|
4842
|
-
);
|
|
4843
|
-
}
|
|
4844
|
-
};
|
|
4845
|
-
|
|
4846
|
-
await nextChunk();
|
|
4847
|
-
|
|
4848
|
-
return executionResults;
|
|
4849
|
-
};
|
|
4850
|
-
|
|
4851
|
-
const githubAnnotationFromError = (
|
|
4852
|
-
error,
|
|
4853
|
-
{ rootDirectoryUrl, executionInfo },
|
|
4854
|
-
) => {
|
|
4855
|
-
const annotation = {
|
|
4856
|
-
annotation_level: "failure",
|
|
4857
|
-
path: executionInfo.fileRelativeUrl,
|
|
4858
|
-
start_line: 1,
|
|
4859
|
-
end_line: 1,
|
|
4860
|
-
title: `Error while executing ${executionInfo.fileRelativeUrl} on ${executionInfo.runtimeName}@${executionInfo.runtimeVersion}`,
|
|
4861
|
-
};
|
|
4862
|
-
const exception = asException(error, { rootDirectoryUrl });
|
|
4863
|
-
if (exception.site && typeof exception.site.line === "number") {
|
|
4864
|
-
annotation.path = urlToRelativeUrl(exception.site.url, rootDirectoryUrl);
|
|
4865
|
-
annotation.start_line = exception.site.line;
|
|
4866
|
-
annotation.end_line = exception.site.line;
|
|
4867
|
-
annotation.start_column = exception.site.column;
|
|
4868
|
-
annotation.end_column = exception.site.column;
|
|
4869
|
-
}
|
|
4870
|
-
annotation.message = exception.stack;
|
|
4871
|
-
return annotation;
|
|
4872
|
-
};
|
|
4339
|
+
const annotation = {
|
|
4340
|
+
annotation_level: "failure",
|
|
4341
|
+
path: executionInfo.fileRelativeUrl,
|
|
4342
|
+
start_line: 1,
|
|
4343
|
+
end_line: 1,
|
|
4344
|
+
title: `Error while executing ${executionInfo.fileRelativeUrl} on ${executionInfo.runtimeName}@${executionInfo.runtimeVersion}`,
|
|
4345
|
+
};
|
|
4346
|
+
const exception = asException(error, { rootDirectoryUrl });
|
|
4347
|
+
if (exception.site && typeof exception.site.line === "number") {
|
|
4348
|
+
annotation.path = urlToRelativeUrl(exception.site.url, rootDirectoryUrl);
|
|
4349
|
+
annotation.start_line = exception.site.line;
|
|
4350
|
+
annotation.end_line = exception.site.line;
|
|
4351
|
+
annotation.start_column = exception.site.column;
|
|
4352
|
+
annotation.end_column = exception.site.column;
|
|
4353
|
+
}
|
|
4354
|
+
annotation.message = exception.stack;
|
|
4355
|
+
return annotation;
|
|
4356
|
+
};
|
|
4873
4357
|
|
|
4874
4358
|
const asException = (error, { rootDirectoryUrl }) => {
|
|
4875
4359
|
const exception = {
|
|
@@ -4979,6 +4463,171 @@ const replaceUrls = (source, replace) => {
|
|
|
4979
4463
|
});
|
|
4980
4464
|
};
|
|
4981
4465
|
|
|
4466
|
+
/*
|
|
4467
|
+
* Export a function capable to run a file on a runtime.
|
|
4468
|
+
*
|
|
4469
|
+
* - Used internally by "executeTestPlan" part of the documented API
|
|
4470
|
+
* - Used internally by "execute" an advanced API not documented
|
|
4471
|
+
* - logs generated during file execution can be collected
|
|
4472
|
+
* - logs generated during file execution can be mirrored (re-logged to the console)
|
|
4473
|
+
* - File is given allocatedMs to complete
|
|
4474
|
+
* - Errors are collected
|
|
4475
|
+
* - File execution result is returned, it contains status/errors/namespace/consoleCalls
|
|
4476
|
+
*/
|
|
4477
|
+
|
|
4478
|
+
|
|
4479
|
+
const run = async ({
|
|
4480
|
+
signal = new AbortController().signal,
|
|
4481
|
+
logger,
|
|
4482
|
+
allocatedMs,
|
|
4483
|
+
keepRunning = false,
|
|
4484
|
+
mirrorConsole = false,
|
|
4485
|
+
collectConsole = false,
|
|
4486
|
+
coverageEnabled = false,
|
|
4487
|
+
coverageTempDirectoryUrl,
|
|
4488
|
+
collectPerformance = false,
|
|
4489
|
+
runtime,
|
|
4490
|
+
runtimeParams,
|
|
4491
|
+
}) => {
|
|
4492
|
+
const result = {
|
|
4493
|
+
status: "pending",
|
|
4494
|
+
errors: [],
|
|
4495
|
+
namespace: null,
|
|
4496
|
+
};
|
|
4497
|
+
const callbacks = [];
|
|
4498
|
+
|
|
4499
|
+
const onConsoleRef = { current: () => {} };
|
|
4500
|
+
const stopSignal = { notify: () => {} };
|
|
4501
|
+
const runtimeLabel = `${runtime.name}/${runtime.version}`;
|
|
4502
|
+
|
|
4503
|
+
const runOperation = Abort.startOperation();
|
|
4504
|
+
runOperation.addAbortSignal(signal);
|
|
4505
|
+
let timeoutAbortSource;
|
|
4506
|
+
if (
|
|
4507
|
+
// ideally we would rather log than the timeout is ignored
|
|
4508
|
+
// when keepRunning is true
|
|
4509
|
+
!keepRunning &&
|
|
4510
|
+
typeof allocatedMs === "number" &&
|
|
4511
|
+
allocatedMs !== Infinity
|
|
4512
|
+
) {
|
|
4513
|
+
timeoutAbortSource = runOperation.timeout(allocatedMs);
|
|
4514
|
+
}
|
|
4515
|
+
const consoleCalls = [];
|
|
4516
|
+
onConsoleRef.current = ({ type, text }) => {
|
|
4517
|
+
if (mirrorConsole) {
|
|
4518
|
+
if (type === "error") {
|
|
4519
|
+
process.stderr.write(text);
|
|
4520
|
+
} else {
|
|
4521
|
+
process.stdout.write(text);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
if (collectConsole) {
|
|
4525
|
+
consoleCalls.push({ type, text });
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
if (collectConsole) {
|
|
4529
|
+
result.consoleCalls = consoleCalls;
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
// we do not keep coverage in memory, it can grow very big
|
|
4533
|
+
// instead we store it on the filesystem
|
|
4534
|
+
// and they can be read later at "coverageFileUrl"
|
|
4535
|
+
let coverageFileUrl;
|
|
4536
|
+
if (coverageEnabled) {
|
|
4537
|
+
coverageFileUrl = new URL(
|
|
4538
|
+
`./${runtime.name}/${crypto.randomUUID()}.json`,
|
|
4539
|
+
coverageTempDirectoryUrl,
|
|
4540
|
+
).href;
|
|
4541
|
+
await ensureParentDirectories(coverageFileUrl);
|
|
4542
|
+
if (coverageEnabled) {
|
|
4543
|
+
result.coverageFileUrl = coverageFileUrl;
|
|
4544
|
+
// written within the child_process/worker_thread or during runtime.run()
|
|
4545
|
+
// for browsers
|
|
4546
|
+
// (because it takes time to serialize and transfer the coverage object)
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
const startMs = Date.now();
|
|
4551
|
+
callbacks.push(() => {
|
|
4552
|
+
result.duration = Date.now() - startMs;
|
|
4553
|
+
});
|
|
4554
|
+
|
|
4555
|
+
try {
|
|
4556
|
+
logger.debug(`run() ${runtimeLabel}`);
|
|
4557
|
+
runOperation.throwIfAborted();
|
|
4558
|
+
const winnerPromise = new Promise((resolve) => {
|
|
4559
|
+
raceCallbacks(
|
|
4560
|
+
{
|
|
4561
|
+
aborted: (cb) => {
|
|
4562
|
+
runOperation.signal.addEventListener("abort", cb);
|
|
4563
|
+
return () => {
|
|
4564
|
+
runOperation.signal.removeEventListener("abort", cb);
|
|
4565
|
+
};
|
|
4566
|
+
},
|
|
4567
|
+
runned: async (cb) => {
|
|
4568
|
+
try {
|
|
4569
|
+
const runResult = await runtime.run({
|
|
4570
|
+
signal: runOperation.signal,
|
|
4571
|
+
logger,
|
|
4572
|
+
...runtimeParams,
|
|
4573
|
+
collectConsole,
|
|
4574
|
+
collectPerformance,
|
|
4575
|
+
coverageFileUrl,
|
|
4576
|
+
keepRunning,
|
|
4577
|
+
stopSignal,
|
|
4578
|
+
onConsole: (log) => onConsoleRef.current(log),
|
|
4579
|
+
});
|
|
4580
|
+
cb(runResult);
|
|
4581
|
+
} catch (e) {
|
|
4582
|
+
cb({
|
|
4583
|
+
status: "failed",
|
|
4584
|
+
errors: [e],
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
},
|
|
4588
|
+
},
|
|
4589
|
+
resolve,
|
|
4590
|
+
);
|
|
4591
|
+
});
|
|
4592
|
+
const winner = await winnerPromise;
|
|
4593
|
+
if (winner.name === "aborted") {
|
|
4594
|
+
runOperation.throwIfAborted();
|
|
4595
|
+
}
|
|
4596
|
+
const { status, namespace, errors, performance } = winner.data;
|
|
4597
|
+
result.status = status;
|
|
4598
|
+
result.errors.push(...errors);
|
|
4599
|
+
result.namespace = namespace;
|
|
4600
|
+
if (collectPerformance) {
|
|
4601
|
+
result.performance = performance;
|
|
4602
|
+
}
|
|
4603
|
+
} catch (e) {
|
|
4604
|
+
if (Abort.isAbortError(e)) {
|
|
4605
|
+
if (timeoutAbortSource && timeoutAbortSource.signal.aborted) {
|
|
4606
|
+
result.status = "timedout";
|
|
4607
|
+
} else {
|
|
4608
|
+
result.status = "aborted";
|
|
4609
|
+
}
|
|
4610
|
+
} else {
|
|
4611
|
+
result.status = "failed";
|
|
4612
|
+
result.errors.push(e);
|
|
4613
|
+
}
|
|
4614
|
+
} finally {
|
|
4615
|
+
await runOperation.end();
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
callbacks.forEach((callback) => {
|
|
4619
|
+
callback();
|
|
4620
|
+
});
|
|
4621
|
+
return result;
|
|
4622
|
+
};
|
|
4623
|
+
|
|
4624
|
+
const ensureGlobalGc = () => {
|
|
4625
|
+
if (!global.gc) {
|
|
4626
|
+
v8.setFlagsFromString("--expose_gc");
|
|
4627
|
+
global.gc = runInNewContext("gc");
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
|
|
4982
4631
|
/**
|
|
4983
4632
|
* Execute a list of files and log how it goes.
|
|
4984
4633
|
* @param {Object} testPlanParameters
|
|
@@ -5029,7 +4678,7 @@ const executeTestPlan = async ({
|
|
|
5029
4678
|
githubCheckEnabled = Boolean(process.env.GITHUB_WORKFLOW),
|
|
5030
4679
|
githubCheckLogLevel,
|
|
5031
4680
|
githubCheckName = "Jsenv tests",
|
|
5032
|
-
githubCheckTitle,
|
|
4681
|
+
githubCheckTitle = "Tests executions",
|
|
5033
4682
|
githubCheckToken,
|
|
5034
4683
|
githubCheckRepositoryOwner,
|
|
5035
4684
|
githubCheckRepositoryName,
|
|
@@ -5074,10 +4723,21 @@ const executeTestPlan = async ({
|
|
|
5074
4723
|
coverageReportJsonFileUrl,
|
|
5075
4724
|
coverageReportHtml = !process.env.CI,
|
|
5076
4725
|
coverageReportHtmlDirectoryUrl,
|
|
4726
|
+
|
|
4727
|
+
beforeExecutionCallback = () => {},
|
|
4728
|
+
afterExecutionCallback = () => {},
|
|
4729
|
+
afterAllExecutionCallback = () => {},
|
|
5077
4730
|
...rest
|
|
5078
4731
|
}) => {
|
|
5079
4732
|
const teardown = createTeardown();
|
|
5080
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
|
+
|
|
5081
4741
|
const operation = Abort.startOperation();
|
|
5082
4742
|
operation.addAbortSignal(signal);
|
|
5083
4743
|
if (handleSIGINT) {
|
|
@@ -5223,204 +4883,460 @@ To fix this warning:
|
|
|
5223
4883
|
// It would be strange, for a given file to be both covered and executed
|
|
5224
4884
|
throw new Error(
|
|
5225
4885
|
createDetailedMessage(
|
|
5226
|
-
`some file will be both covered and executed`,
|
|
4886
|
+
`some file will be both covered and executed`,
|
|
4887
|
+
{
|
|
4888
|
+
patterns: patternsMatchingCoverAndExecute,
|
|
4889
|
+
},
|
|
4890
|
+
),
|
|
4891
|
+
);
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
if (coverageTempDirectoryUrl === undefined) {
|
|
4896
|
+
coverageTempDirectoryUrl = new URL(
|
|
4897
|
+
"./.coverage/tmp/",
|
|
4898
|
+
rootDirectoryUrl,
|
|
4899
|
+
);
|
|
4900
|
+
} else {
|
|
4901
|
+
coverageTempDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
4902
|
+
coverageTempDirectoryUrl,
|
|
4903
|
+
"coverageTempDirectoryUrl",
|
|
4904
|
+
);
|
|
4905
|
+
}
|
|
4906
|
+
if (coverageReportJson) {
|
|
4907
|
+
if (coverageReportJsonFileUrl === undefined) {
|
|
4908
|
+
coverageReportJsonFileUrl = new URL(
|
|
4909
|
+
"./.coverage/coverage.json",
|
|
4910
|
+
rootDirectoryUrl,
|
|
4911
|
+
);
|
|
4912
|
+
} else {
|
|
4913
|
+
coverageReportJsonFileUrl = assertAndNormalizeFileUrl(
|
|
4914
|
+
coverageReportJsonFileUrl,
|
|
4915
|
+
"coverageReportJsonFileUrl",
|
|
4916
|
+
);
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
if (coverageReportHtml) {
|
|
4920
|
+
if (coverageReportHtmlDirectoryUrl === undefined) {
|
|
4921
|
+
coverageReportHtmlDirectoryUrl = new URL(
|
|
4922
|
+
"./.coverage/",
|
|
4923
|
+
rootDirectoryUrl,
|
|
4924
|
+
);
|
|
4925
|
+
} else {
|
|
4926
|
+
coverageReportHtmlDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
4927
|
+
coverageReportHtmlDirectoryUrl,
|
|
4928
|
+
"coverageReportHtmlDirectoryUrl",
|
|
4929
|
+
);
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
logger.debug(
|
|
4936
|
+
createDetailedMessage(`Prepare executing plan`, {
|
|
4937
|
+
runtimes: JSON.stringify(runtimes, null, " "),
|
|
4938
|
+
}),
|
|
4939
|
+
);
|
|
4940
|
+
|
|
4941
|
+
// param normalization
|
|
4942
|
+
{
|
|
4943
|
+
if (coverageEnabled) {
|
|
4944
|
+
if (Object.keys(coverageConfig).length === 0) {
|
|
4945
|
+
logger.warn(
|
|
4946
|
+
`coverageConfig is an empty object. Nothing will be instrumented for coverage so your coverage will be empty`,
|
|
4947
|
+
);
|
|
4948
|
+
}
|
|
4949
|
+
if (
|
|
4950
|
+
someNodeRuntime &&
|
|
4951
|
+
coverageEnabled &&
|
|
4952
|
+
coverageMethodForNodeJs === "NODE_V8_COVERAGE"
|
|
4953
|
+
) {
|
|
4954
|
+
if (process.env.NODE_V8_COVERAGE) {
|
|
4955
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
4956
|
+
await ensureEmptyDirectory(process.env.NODE_V8_COVERAGE);
|
|
4957
|
+
} else {
|
|
4958
|
+
coverageMethodForNodeJs = "Profiler";
|
|
4959
|
+
logger.warn(
|
|
4960
|
+
createDetailedMessage(
|
|
4961
|
+
`process.env.NODE_V8_COVERAGE is required to generate coverage for Node.js subprocesses`,
|
|
5227
4962
|
{
|
|
5228
|
-
|
|
4963
|
+
"suggestion": `set process.env.NODE_V8_COVERAGE`,
|
|
4964
|
+
"suggestion 2": `use coverageMethodForNodeJs: "Profiler". But it means coverage for child_process and worker_thread cannot be collected`,
|
|
5229
4965
|
},
|
|
5230
4966
|
),
|
|
5231
4967
|
);
|
|
5232
4968
|
}
|
|
5233
4969
|
}
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
5234
4972
|
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
4973
|
+
testPlan = {
|
|
4974
|
+
"file:///**/node_modules/": null,
|
|
4975
|
+
"**/*./": null,
|
|
4976
|
+
...testPlan,
|
|
4977
|
+
"**/.jsenv/": null,
|
|
4978
|
+
};
|
|
4979
|
+
logger.debug(`Generate executions`);
|
|
4980
|
+
let executionSteps = await executionStepsFromTestPlan({
|
|
4981
|
+
signal,
|
|
4982
|
+
testPlan,
|
|
4983
|
+
rootDirectoryUrl,
|
|
4984
|
+
});
|
|
4985
|
+
logger.debug(`${executionSteps.length} executions planned`);
|
|
4986
|
+
if (githubCheckEnabled) {
|
|
4987
|
+
const githubCheckRun = await startGithubCheckRun({
|
|
4988
|
+
logLevel: githubCheckLogLevel,
|
|
4989
|
+
githubToken: githubCheckToken,
|
|
4990
|
+
repositoryOwner: githubCheckRepositoryOwner,
|
|
4991
|
+
repositoryName: githubCheckRepositoryName,
|
|
4992
|
+
commitSha: githubCheckCommitSha,
|
|
4993
|
+
checkName: githubCheckName,
|
|
4994
|
+
checkTitle: githubCheckTitle,
|
|
4995
|
+
checkSummary: `${executionSteps.length} files will be executed`,
|
|
4996
|
+
});
|
|
4997
|
+
const annotations = [];
|
|
4998
|
+
afterExecutionCallbackSet.add((afterExecutionInfo) => {
|
|
4999
|
+
const { executionResult } = afterExecutionInfo;
|
|
5000
|
+
const { errors = [] } = executionResult;
|
|
5001
|
+
for (const error of errors) {
|
|
5002
|
+
const annotation = githubAnnotationFromError(error, {
|
|
5238
5003
|
rootDirectoryUrl,
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
coverageTempDirectoryUrl,
|
|
5243
|
-
"coverageTempDirectoryUrl",
|
|
5244
|
-
);
|
|
5004
|
+
executionInfo: afterExecutionInfo,
|
|
5005
|
+
});
|
|
5006
|
+
annotations.push(annotation);
|
|
5245
5007
|
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5008
|
+
});
|
|
5009
|
+
afterAllExecutionCallbackSet.add(async (returnValue) => {
|
|
5010
|
+
const { summary } = returnValue;
|
|
5011
|
+
const title = "Jsenv test results";
|
|
5012
|
+
const summaryText = stripAnsi(formatSummary(summary));
|
|
5013
|
+
if (summary.counters.total !== summary.counters.completed) {
|
|
5014
|
+
await githubCheckRun.fail({
|
|
5015
|
+
title,
|
|
5016
|
+
summary: summaryText,
|
|
5017
|
+
annotations,
|
|
5018
|
+
});
|
|
5019
|
+
return;
|
|
5020
|
+
}
|
|
5021
|
+
await githubCheckRun.pass({
|
|
5022
|
+
title,
|
|
5023
|
+
summary: summaryText,
|
|
5024
|
+
annotations,
|
|
5025
|
+
});
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
executionSteps = executionSteps.filter(
|
|
5030
|
+
(executionStep) => !executionStep.runtime?.disabled,
|
|
5031
|
+
);
|
|
5032
|
+
|
|
5033
|
+
const returnValue = {
|
|
5034
|
+
aborted: false,
|
|
5035
|
+
summary: null,
|
|
5036
|
+
report: null,
|
|
5037
|
+
coverage: null,
|
|
5038
|
+
};
|
|
5039
|
+
const report = {};
|
|
5040
|
+
const callbacks = [];
|
|
5041
|
+
|
|
5042
|
+
const multipleExecutionsOperation = Abort.startOperation();
|
|
5043
|
+
multipleExecutionsOperation.addAbortSignal(signal);
|
|
5044
|
+
const failFastAbortController = new AbortController();
|
|
5045
|
+
if (failFast) {
|
|
5046
|
+
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
try {
|
|
5050
|
+
if (gcBetweenExecutions) {
|
|
5051
|
+
ensureGlobalGc();
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
if (coverageEnabled) {
|
|
5055
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
5056
|
+
await ensureEmptyDirectory(coverageTempDirectoryUrl);
|
|
5057
|
+
callbacks.push(async () => {
|
|
5058
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
5059
|
+
// don't try to do the coverage stuff
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
try {
|
|
5063
|
+
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
5064
|
+
takeCoverage();
|
|
5065
|
+
// conceptually we don't need coverage anymore so it would be
|
|
5066
|
+
// good to call v8.stopCoverage()
|
|
5067
|
+
// but it logs a strange message about "result is not an object"
|
|
5068
|
+
}
|
|
5069
|
+
const coverage = await reportToCoverage(report, {
|
|
5070
|
+
signal: multipleExecutionsOperation.signal,
|
|
5071
|
+
logger,
|
|
5250
5072
|
rootDirectoryUrl,
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5073
|
+
coverageConfig,
|
|
5074
|
+
coverageIncludeMissing,
|
|
5075
|
+
coverageMethodForNodeJs,
|
|
5076
|
+
coverageV8ConflictWarning,
|
|
5077
|
+
});
|
|
5078
|
+
returnValue.coverage = coverage;
|
|
5079
|
+
} catch (e) {
|
|
5080
|
+
if (Abort.isAbortError(e)) {
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
throw e;
|
|
5084
|
+
}
|
|
5085
|
+
});
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
const runtimeParams = {
|
|
5089
|
+
rootDirectoryUrl,
|
|
5090
|
+
webServer,
|
|
5091
|
+
|
|
5092
|
+
coverageEnabled,
|
|
5093
|
+
coverageConfig,
|
|
5094
|
+
coverageMethodForBrowsers,
|
|
5095
|
+
coverageMethodForNodeJs,
|
|
5096
|
+
isTestPlan: true,
|
|
5097
|
+
teardown,
|
|
5098
|
+
};
|
|
5099
|
+
|
|
5100
|
+
if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
|
|
5101
|
+
logMergeForCompletedExecutions = false;
|
|
5102
|
+
logger.debug(
|
|
5103
|
+
`Force logMergeForCompletedExecutions to false because process.stdout.isTTY is false`,
|
|
5104
|
+
);
|
|
5105
|
+
}
|
|
5106
|
+
const debugLogsEnabled = logger.levels.debug;
|
|
5107
|
+
const executionLogsEnabled = logger.levels.info;
|
|
5108
|
+
const executionSpinner =
|
|
5109
|
+
logRefresh &&
|
|
5110
|
+
maxExecutionsInParallel === 1 &&
|
|
5111
|
+
!debugLogsEnabled &&
|
|
5112
|
+
executionLogsEnabled &&
|
|
5113
|
+
process.stdout.isTTY &&
|
|
5114
|
+
// if there is an error during execution npm will mess up the output
|
|
5115
|
+
// (happens when npm runs several command in a workspace)
|
|
5116
|
+
// so we enable spinner only when !process.exitCode (no error so far)
|
|
5117
|
+
process.exitCode !== 1;
|
|
5118
|
+
|
|
5119
|
+
const startMs = Date.now();
|
|
5120
|
+
let rawOutput = "";
|
|
5121
|
+
let executionLog = createLog({ newLine: "" });
|
|
5122
|
+
const counters = {
|
|
5123
|
+
total: executionSteps.length,
|
|
5124
|
+
aborted: 0,
|
|
5125
|
+
timedout: 0,
|
|
5126
|
+
failed: 0,
|
|
5127
|
+
completed: 0,
|
|
5128
|
+
done: 0,
|
|
5129
|
+
};
|
|
5130
|
+
|
|
5131
|
+
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
5132
|
+
|
|
5133
|
+
await executeInParallel({
|
|
5134
|
+
multipleExecutionsOperation,
|
|
5135
|
+
maxExecutionsInParallel,
|
|
5136
|
+
cooldownBetweenExecutions,
|
|
5137
|
+
executionSteps,
|
|
5138
|
+
start: async (paramsFromStep) => {
|
|
5139
|
+
const executionIndex = executionSteps.indexOf(paramsFromStep);
|
|
5140
|
+
const { executionName, fileRelativeUrl, runtime } = paramsFromStep;
|
|
5141
|
+
const runtimeType = runtime.type;
|
|
5142
|
+
const runtimeName = runtime.name;
|
|
5143
|
+
const runtimeVersion = runtime.version;
|
|
5144
|
+
const executionParams = {
|
|
5145
|
+
measurePerformance: false,
|
|
5146
|
+
collectPerformance: false,
|
|
5147
|
+
collectConsole: true,
|
|
5148
|
+
allocatedMs: defaultMsAllocatedPerExecution,
|
|
5149
|
+
...paramsFromStep,
|
|
5150
|
+
runtimeParams: {
|
|
5151
|
+
fileRelativeUrl,
|
|
5152
|
+
...paramsFromStep.runtimeParams,
|
|
5153
|
+
},
|
|
5154
|
+
};
|
|
5155
|
+
const beforeExecutionInfo = {
|
|
5156
|
+
fileRelativeUrl,
|
|
5157
|
+
runtimeType,
|
|
5158
|
+
runtimeName,
|
|
5159
|
+
runtimeVersion,
|
|
5160
|
+
executionIndex,
|
|
5161
|
+
executionParams,
|
|
5162
|
+
startMs: Date.now(),
|
|
5163
|
+
executionResult: {
|
|
5164
|
+
status: "executing",
|
|
5165
|
+
},
|
|
5166
|
+
counters,
|
|
5167
|
+
timeEllapsed: Date.now() - startMs,
|
|
5168
|
+
memoryHeap: memoryUsage().heapUsed,
|
|
5169
|
+
};
|
|
5170
|
+
if (typeof executionParams.allocatedMs === "function") {
|
|
5171
|
+
executionParams.allocatedMs =
|
|
5172
|
+
executionParams.allocatedMs(beforeExecutionInfo);
|
|
5173
|
+
}
|
|
5174
|
+
let spinner;
|
|
5175
|
+
if (executionSpinner) {
|
|
5176
|
+
spinner = startSpinner({
|
|
5177
|
+
log: executionLog,
|
|
5178
|
+
render: () => {
|
|
5179
|
+
return createExecutionLog(beforeExecutionInfo, {
|
|
5180
|
+
logRuntime,
|
|
5181
|
+
logEachDuration,
|
|
5182
|
+
logTimeUsage,
|
|
5183
|
+
logMemoryHeapUsage,
|
|
5184
|
+
});
|
|
5185
|
+
},
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
for (const beforeExecutionCallback of beforeExecutionCallbackSet) {
|
|
5189
|
+
beforeExecutionCallback(beforeExecutionInfo);
|
|
5190
|
+
}
|
|
5191
|
+
const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
|
|
5192
|
+
let executionResult;
|
|
5193
|
+
if (existsSync(new URL(fileUrl))) {
|
|
5194
|
+
executionResult = await run({
|
|
5195
|
+
signal: multipleExecutionsOperation.signal,
|
|
5196
|
+
logger,
|
|
5197
|
+
allocatedMs: executionParams.allocatedMs,
|
|
5198
|
+
keepRunning,
|
|
5199
|
+
mirrorConsole: false, // might be executed in parallel: log would be a mess to read
|
|
5200
|
+
collectConsole: executionParams.collectConsole,
|
|
5201
|
+
coverageEnabled,
|
|
5202
|
+
coverageTempDirectoryUrl,
|
|
5203
|
+
runtime: executionParams.runtime,
|
|
5204
|
+
runtimeParams: {
|
|
5205
|
+
...runtimeParams,
|
|
5206
|
+
...executionParams.runtimeParams,
|
|
5207
|
+
},
|
|
5208
|
+
});
|
|
5209
|
+
} else {
|
|
5210
|
+
executionResult = {
|
|
5211
|
+
status: "failed",
|
|
5212
|
+
errors: [
|
|
5213
|
+
new Error(
|
|
5214
|
+
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
5215
|
+
),
|
|
5216
|
+
],
|
|
5217
|
+
};
|
|
5257
5218
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
coverageReportHtmlDirectoryUrl = new URL(
|
|
5262
|
-
"./.coverage/",
|
|
5263
|
-
rootDirectoryUrl,
|
|
5264
|
-
);
|
|
5219
|
+
const fileReport = report[fileRelativeUrl];
|
|
5220
|
+
if (fileReport) {
|
|
5221
|
+
fileReport[executionName] = executionResult;
|
|
5265
5222
|
} else {
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
);
|
|
5223
|
+
report[fileRelativeUrl] = {
|
|
5224
|
+
[executionName]: executionResult,
|
|
5225
|
+
};
|
|
5270
5226
|
}
|
|
5271
|
-
}
|
|
5272
|
-
}
|
|
5273
|
-
}
|
|
5274
5227
|
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5228
|
+
const afterExecutionInfo = {
|
|
5229
|
+
...beforeExecutionInfo,
|
|
5230
|
+
runtimeVersion: runtime.version,
|
|
5231
|
+
endMs: Date.now(),
|
|
5232
|
+
executionResult,
|
|
5233
|
+
};
|
|
5280
5234
|
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
if (coverageEnabled) {
|
|
5284
|
-
if (Object.keys(coverageConfig).length === 0) {
|
|
5285
|
-
logger.warn(
|
|
5286
|
-
`coverageConfig is an empty object. Nothing will be instrumented for coverage so your coverage will be empty`,
|
|
5287
|
-
);
|
|
5288
|
-
}
|
|
5289
|
-
if (
|
|
5290
|
-
someNodeRuntime &&
|
|
5291
|
-
coverageEnabled &&
|
|
5292
|
-
coverageMethodForNodeJs === "NODE_V8_COVERAGE"
|
|
5293
|
-
) {
|
|
5294
|
-
if (process.env.NODE_V8_COVERAGE) {
|
|
5295
|
-
// when runned multiple times, we don't want to keep previous files in this directory
|
|
5296
|
-
await ensureEmptyDirectory(process.env.NODE_V8_COVERAGE);
|
|
5297
|
-
} else {
|
|
5298
|
-
coverageMethodForNodeJs = "Profiler";
|
|
5299
|
-
logger.warn(
|
|
5300
|
-
createDetailedMessage(
|
|
5301
|
-
`process.env.NODE_V8_COVERAGE is required to generate coverage for Node.js subprocesses`,
|
|
5302
|
-
{
|
|
5303
|
-
"suggestion": `set process.env.NODE_V8_COVERAGE`,
|
|
5304
|
-
"suggestion 2": `use coverageMethodForNodeJs: "Profiler". But it means coverage for child_process and worker_thread cannot be collected`,
|
|
5305
|
-
},
|
|
5306
|
-
),
|
|
5307
|
-
);
|
|
5235
|
+
if (gcBetweenExecutions) {
|
|
5236
|
+
global.gc();
|
|
5308
5237
|
}
|
|
5309
|
-
}
|
|
5310
|
-
}
|
|
5311
|
-
}
|
|
5312
5238
|
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
let beforeExecutionCallback;
|
|
5327
|
-
let afterExecutionCallback;
|
|
5328
|
-
let afterAllExecutionCallback = () => {};
|
|
5329
|
-
if (githubCheckEnabled) {
|
|
5330
|
-
const githubCheckRun = await startGithubCheckRun({
|
|
5331
|
-
logLevel: githubCheckLogLevel,
|
|
5332
|
-
githubToken: githubCheckToken,
|
|
5333
|
-
repositoryOwner: githubCheckRepositoryOwner,
|
|
5334
|
-
repositoryName: githubCheckRepositoryName,
|
|
5335
|
-
commitSha: githubCheckCommitSha,
|
|
5336
|
-
checkName: githubCheckName,
|
|
5337
|
-
checkTitle: `Tests executions`,
|
|
5338
|
-
checkSummary: `${executionSteps.length} files will be executed`,
|
|
5339
|
-
});
|
|
5340
|
-
const annotations = [];
|
|
5341
|
-
afterExecutionCallback = (afterExecutionInfo) => {
|
|
5342
|
-
const { executionResult } = afterExecutionInfo;
|
|
5343
|
-
const { errors = [] } = executionResult;
|
|
5344
|
-
for (const error of errors) {
|
|
5345
|
-
const annotation = githubAnnotationFromError(error, {
|
|
5346
|
-
rootDirectoryUrl,
|
|
5347
|
-
executionInfo: afterExecutionInfo,
|
|
5348
|
-
});
|
|
5349
|
-
annotations.push(annotation);
|
|
5350
|
-
}
|
|
5351
|
-
};
|
|
5352
|
-
afterAllExecutionCallback = async ({ testPlanSummary }) => {
|
|
5353
|
-
const title = "Jsenv test results";
|
|
5354
|
-
const summary = stripAnsi(formatSummary(testPlanSummary));
|
|
5355
|
-
if (
|
|
5356
|
-
testPlanSummary.counters.total !== testPlanSummary.counters.completed
|
|
5357
|
-
) {
|
|
5358
|
-
await githubCheckRun.fail({
|
|
5359
|
-
title,
|
|
5360
|
-
summary,
|
|
5361
|
-
annotations,
|
|
5362
|
-
});
|
|
5363
|
-
return;
|
|
5364
|
-
}
|
|
5365
|
-
await githubCheckRun.pass({
|
|
5366
|
-
title,
|
|
5367
|
-
summary,
|
|
5368
|
-
annotations,
|
|
5369
|
-
});
|
|
5370
|
-
};
|
|
5371
|
-
}
|
|
5239
|
+
const timeEllapsed = Date.now() - startMs;
|
|
5240
|
+
const memoryHeap = memoryUsage().heapUsed;
|
|
5241
|
+
callWhenPreviousExecutionAreDone(executionIndex, () => {
|
|
5242
|
+
counters.done++;
|
|
5243
|
+
if (executionResult.status === "aborted") {
|
|
5244
|
+
counters.aborted++;
|
|
5245
|
+
} else if (executionResult.status === "timedout") {
|
|
5246
|
+
counters.timedout++;
|
|
5247
|
+
} else if (executionResult.status === "failed") {
|
|
5248
|
+
counters.failed++;
|
|
5249
|
+
} else if (executionResult.status === "completed") {
|
|
5250
|
+
counters.completed++;
|
|
5251
|
+
}
|
|
5372
5252
|
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
logRuntime,
|
|
5380
|
-
logEachDuration,
|
|
5381
|
-
logTimeUsage,
|
|
5382
|
-
logMemoryHeapUsage,
|
|
5383
|
-
logFileRelativeUrl,
|
|
5384
|
-
logShortForCompletedExecutions,
|
|
5385
|
-
logMergeForCompletedExecutions,
|
|
5386
|
-
rootDirectoryUrl,
|
|
5387
|
-
webServer,
|
|
5253
|
+
if (executionLogsEnabled) {
|
|
5254
|
+
// replace spinner with this execution result
|
|
5255
|
+
if (spinner) {
|
|
5256
|
+
spinner.stop();
|
|
5257
|
+
spinner = null;
|
|
5258
|
+
}
|
|
5388
5259
|
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
githubCheckEnabled,
|
|
5397
|
-
githubCheckName,
|
|
5398
|
-
githubCheckTitle,
|
|
5399
|
-
githubCheckToken,
|
|
5400
|
-
githubCheckRepositoryOwner,
|
|
5401
|
-
githubCheckRepositoryName,
|
|
5402
|
-
githubCheckCommitSha,
|
|
5403
|
-
|
|
5404
|
-
coverageEnabled,
|
|
5405
|
-
coverageConfig,
|
|
5406
|
-
coverageIncludeMissing,
|
|
5407
|
-
coverageMethodForBrowsers,
|
|
5408
|
-
coverageMethodForNodeJs,
|
|
5409
|
-
coverageV8ConflictWarning,
|
|
5410
|
-
coverageTempDirectoryUrl,
|
|
5260
|
+
const log = createExecutionLog(afterExecutionInfo, {
|
|
5261
|
+
logShortForCompletedExecutions,
|
|
5262
|
+
logRuntime,
|
|
5263
|
+
logEachDuration,
|
|
5264
|
+
...(logTimeUsage ? { timeEllapsed } : {}),
|
|
5265
|
+
...(logMemoryHeapUsage ? { memoryHeap } : {}),
|
|
5266
|
+
});
|
|
5411
5267
|
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5268
|
+
executionLog.write(log);
|
|
5269
|
+
rawOutput += stripAnsi(log);
|
|
5270
|
+
const canOverwriteLog = canOverwriteLogGetter({
|
|
5271
|
+
logMergeForCompletedExecutions,
|
|
5272
|
+
executionResult,
|
|
5273
|
+
});
|
|
5274
|
+
if (canOverwriteLog) {
|
|
5275
|
+
// nothing to do, we reuse the current executionLog object
|
|
5276
|
+
} else {
|
|
5277
|
+
executionLog.destroy();
|
|
5278
|
+
executionLog = createLog({ newLine: "" });
|
|
5279
|
+
}
|
|
5280
|
+
const isLastExecutionLog =
|
|
5281
|
+
executionIndex === executionSteps.length - 1;
|
|
5282
|
+
if (isLastExecutionLog && logger.levels.info) {
|
|
5283
|
+
executionLog.write("\n");
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
for (const afterExecutionCallback of afterExecutionCallbackSet) {
|
|
5288
|
+
afterExecutionCallback(afterExecutionInfo);
|
|
5289
|
+
}
|
|
5290
|
+
const cancelRemaining =
|
|
5291
|
+
failFast &&
|
|
5292
|
+
executionResult.status !== "completed" &&
|
|
5293
|
+
counters.done < counters.total;
|
|
5294
|
+
if (cancelRemaining) {
|
|
5295
|
+
logger.info(`"failFast" enabled -> cancel remaining executions`);
|
|
5296
|
+
failFastAbortController.abort();
|
|
5297
|
+
}
|
|
5298
|
+
},
|
|
5299
|
+
});
|
|
5300
|
+
if (!keepRunning) {
|
|
5301
|
+
logger.debug("trigger test plan teardown");
|
|
5302
|
+
await teardown.trigger();
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
counters.cancelled = counters.total - counters.done;
|
|
5306
|
+
const summary = {
|
|
5307
|
+
counters,
|
|
5308
|
+
// when execution is aborted, the remaining executions are "cancelled"
|
|
5309
|
+
duration: Date.now() - startMs,
|
|
5310
|
+
};
|
|
5311
|
+
if (logSummary) {
|
|
5312
|
+
const summaryLog = formatSummaryLog(summary);
|
|
5313
|
+
rawOutput += stripAnsi(summaryLog);
|
|
5314
|
+
logger.info(summaryLog);
|
|
5315
|
+
}
|
|
5316
|
+
if (summary.counters.total !== summary.counters.completed) {
|
|
5317
|
+
const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href;
|
|
5318
|
+
writeFileSync(logFileUrl, rawOutput);
|
|
5319
|
+
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`);
|
|
5320
|
+
}
|
|
5321
|
+
returnValue.aborted = multipleExecutionsOperation.signal.aborted;
|
|
5322
|
+
returnValue.summary = summary;
|
|
5323
|
+
returnValue.report = report;
|
|
5324
|
+
for (const callback of callbacks) {
|
|
5325
|
+
await callback();
|
|
5326
|
+
}
|
|
5327
|
+
} finally {
|
|
5328
|
+
await multipleExecutionsOperation.end();
|
|
5329
|
+
}
|
|
5415
5330
|
|
|
5416
5331
|
const hasFailed =
|
|
5417
|
-
|
|
5332
|
+
returnValue.summary.counters.total !==
|
|
5333
|
+
returnValue.summary.counters.completed;
|
|
5418
5334
|
if (updateProcessExitCode && hasFailed) {
|
|
5419
5335
|
process.exitCode = 1;
|
|
5420
5336
|
}
|
|
5421
|
-
const
|
|
5337
|
+
const coverage = returnValue.coverage;
|
|
5422
5338
|
// planCoverage can be null when execution is aborted
|
|
5423
|
-
if (
|
|
5339
|
+
if (coverage) {
|
|
5424
5340
|
const promises = [];
|
|
5425
5341
|
// keep this one first because it does ensureEmptyDirectory
|
|
5426
5342
|
// and in case coverage json file gets written in the same directory
|
|
@@ -5432,7 +5348,7 @@ To fix this warning:
|
|
|
5432
5348
|
`-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`,
|
|
5433
5349
|
);
|
|
5434
5350
|
promises.push(
|
|
5435
|
-
generateCoverageHtmlDirectory(
|
|
5351
|
+
generateCoverageHtmlDirectory(coverage, {
|
|
5436
5352
|
rootDirectoryUrl,
|
|
5437
5353
|
coverageHtmlDirectoryRelativeUrl: urlToRelativeUrl(
|
|
5438
5354
|
coverageReportHtmlDirectoryUrl,
|
|
@@ -5446,7 +5362,7 @@ To fix this warning:
|
|
|
5446
5362
|
if (coverageEnabled && coverageReportJson) {
|
|
5447
5363
|
promises.push(
|
|
5448
5364
|
generateCoverageJsonFile({
|
|
5449
|
-
coverage
|
|
5365
|
+
coverage,
|
|
5450
5366
|
coverageJsonFileUrl: coverageReportJsonFileUrl,
|
|
5451
5367
|
logger,
|
|
5452
5368
|
}),
|
|
@@ -5454,7 +5370,7 @@ To fix this warning:
|
|
|
5454
5370
|
}
|
|
5455
5371
|
if (coverageEnabled && coverageReportTextLog) {
|
|
5456
5372
|
promises.push(
|
|
5457
|
-
generateCoverageTextLog(
|
|
5373
|
+
generateCoverageTextLog(coverage, {
|
|
5458
5374
|
coverageReportSkipEmpty,
|
|
5459
5375
|
coverageReportSkipFull,
|
|
5460
5376
|
}),
|
|
@@ -5463,16 +5379,85 @@ To fix this warning:
|
|
|
5463
5379
|
await Promise.all(promises);
|
|
5464
5380
|
}
|
|
5465
5381
|
|
|
5466
|
-
const
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
testPlanReport: result.planReport,
|
|
5470
|
-
testPlanCoverage: planCoverage,
|
|
5471
|
-
};
|
|
5472
|
-
await afterAllExecutionCallback(returnValue);
|
|
5382
|
+
for (const afterAllExecutionCallback of afterAllExecutionCallbackSet) {
|
|
5383
|
+
await afterAllExecutionCallback(returnValue);
|
|
5384
|
+
}
|
|
5473
5385
|
return returnValue;
|
|
5474
5386
|
};
|
|
5475
5387
|
|
|
5388
|
+
const canOverwriteLogGetter = ({
|
|
5389
|
+
logMergeForCompletedExecutions,
|
|
5390
|
+
executionResult,
|
|
5391
|
+
}) => {
|
|
5392
|
+
if (!logMergeForCompletedExecutions) {
|
|
5393
|
+
return false;
|
|
5394
|
+
}
|
|
5395
|
+
if (executionResult.status === "aborted") {
|
|
5396
|
+
return true;
|
|
5397
|
+
}
|
|
5398
|
+
if (executionResult.status !== "completed") {
|
|
5399
|
+
return false;
|
|
5400
|
+
}
|
|
5401
|
+
const { consoleCalls = [] } = executionResult;
|
|
5402
|
+
if (consoleCalls.length > 0) {
|
|
5403
|
+
return false;
|
|
5404
|
+
}
|
|
5405
|
+
return true;
|
|
5406
|
+
};
|
|
5407
|
+
|
|
5408
|
+
const executeInParallel = async ({
|
|
5409
|
+
multipleExecutionsOperation,
|
|
5410
|
+
maxExecutionsInParallel,
|
|
5411
|
+
cooldownBetweenExecutions,
|
|
5412
|
+
executionSteps,
|
|
5413
|
+
start,
|
|
5414
|
+
}) => {
|
|
5415
|
+
const executionResults = [];
|
|
5416
|
+
let progressionIndex = 0;
|
|
5417
|
+
let remainingExecutionCount = executionSteps.length;
|
|
5418
|
+
|
|
5419
|
+
const nextChunk = async () => {
|
|
5420
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
5421
|
+
return;
|
|
5422
|
+
}
|
|
5423
|
+
const outputPromiseArray = [];
|
|
5424
|
+
let previousExecPromise = Promise.resolve();
|
|
5425
|
+
while (
|
|
5426
|
+
remainingExecutionCount > 0 &&
|
|
5427
|
+
outputPromiseArray.length < maxExecutionsInParallel
|
|
5428
|
+
) {
|
|
5429
|
+
remainingExecutionCount--;
|
|
5430
|
+
const outputPromise = executeOne(progressionIndex, previousExecPromise);
|
|
5431
|
+
previousExecPromise = outputPromise;
|
|
5432
|
+
progressionIndex++;
|
|
5433
|
+
outputPromiseArray.push(outputPromise);
|
|
5434
|
+
}
|
|
5435
|
+
if (outputPromiseArray.length) {
|
|
5436
|
+
await Promise.all(outputPromiseArray);
|
|
5437
|
+
if (remainingExecutionCount > 0) {
|
|
5438
|
+
await nextChunk();
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
};
|
|
5442
|
+
|
|
5443
|
+
const executeOne = async (index, previousExecPromise) => {
|
|
5444
|
+
const input = executionSteps[index];
|
|
5445
|
+
const output = await start(input, previousExecPromise);
|
|
5446
|
+
if (!multipleExecutionsOperation.signal.aborted) {
|
|
5447
|
+
executionResults[index] = output;
|
|
5448
|
+
}
|
|
5449
|
+
if (cooldownBetweenExecutions) {
|
|
5450
|
+
await new Promise((resolve) =>
|
|
5451
|
+
setTimeout(resolve, cooldownBetweenExecutions),
|
|
5452
|
+
);
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
|
|
5456
|
+
await nextChunk();
|
|
5457
|
+
|
|
5458
|
+
return executionResults;
|
|
5459
|
+
};
|
|
5460
|
+
|
|
5476
5461
|
const memoize = (compute) => {
|
|
5477
5462
|
let memoized = false;
|
|
5478
5463
|
let memoizedValue;
|