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