@jsenv/test 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllable_child_process.mjs +129 -0
- package/dist/controllable_worker_thread.mjs +91 -0
- package/dist/importmap_node_loader.mjs +551 -0
- package/dist/js/execute_using_dynamic_import.js +850 -0
- package/dist/js/v8_coverage.js +508 -0
- package/dist/jsenv_test.js +5993 -0
- package/dist/no_experimental_warnings.cjs +10 -0
- package/package.json +56 -0
- package/src/coverage/babel_plugin_instrument.js +48 -0
- package/src/coverage/coverage_reporter_html_directory.js +32 -0
- package/src/coverage/coverage_reporter_json_file.js +17 -0
- package/src/coverage/coverage_reporter_text_log.js +19 -0
- package/src/coverage/empty_coverage_factory.js +52 -0
- package/src/coverage/file_by_file_coverage.js +25 -0
- package/src/coverage/istanbul_coverage_composition.js +28 -0
- package/src/coverage/istanbul_coverage_map_from_coverage.js +16 -0
- package/src/coverage/list_files_not_covered.js +15 -0
- package/src/coverage/missing_coverage.js +41 -0
- package/src/coverage/report_to_coverage.js +198 -0
- package/src/coverage/v8_and_istanbul.js +37 -0
- package/src/coverage/v8_coverage.js +26 -0
- package/src/coverage/v8_coverage_composition.js +24 -0
- package/src/coverage/v8_coverage_node_directory.js +85 -0
- package/src/coverage/v8_coverage_to_istanbul.js +99 -0
- package/src/execution/execute.js +111 -0
- package/src/execution/execute_steps.js +423 -0
- package/src/execution/execute_test_plan.js +372 -0
- package/src/execution/execution_colors.js +10 -0
- package/src/execution/execution_steps.js +65 -0
- package/src/execution/gc.js +9 -0
- package/src/execution/logs_file_execution.js +427 -0
- package/src/execution/logs_file_execution.test.mjs +41 -0
- package/src/execution/run.js +160 -0
- package/src/execution/web_server_param.js +73 -0
- package/src/main.js +10 -0
- package/src/runtime_browsers/chromium.js +24 -0
- package/src/runtime_browsers/firefox.js +23 -0
- package/src/runtime_browsers/middleware_istanbul.js +65 -0
- package/src/runtime_browsers/middleware_js_supervisor.js +98 -0
- package/src/runtime_browsers/using_playwright.js +573 -0
- package/src/runtime_browsers/webkit.js +40 -0
- package/src/runtime_node/child_exec_options.js +166 -0
- package/src/runtime_node/controllable_child_process.mjs +135 -0
- package/src/runtime_node/controllable_worker_thread.mjs +103 -0
- package/src/runtime_node/exec_options.js +57 -0
- package/src/runtime_node/execute_using_dynamic_import.js +53 -0
- package/src/runtime_node/exit_codes.js +9 -0
- package/src/runtime_node/importmap_node_loader.mjs +51 -0
- package/src/runtime_node/importmap_node_loader_file_url.js +4 -0
- package/src/runtime_node/kill_process_tree.js +76 -0
- package/src/runtime_node/no_experimental_warnings.cjs +13 -0
- package/src/runtime_node/no_experimental_warnings_file_url.js +4 -0
- package/src/runtime_node/node_child_process.js +374 -0
- package/src/runtime_node/node_execution_performance.js +67 -0
- package/src/runtime_node/node_worker_thread.js +301 -0
- package/src/runtime_node/profiler_v8_coverage.js +56 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs"
|
|
2
|
+
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
|
|
3
|
+
import { createDetailedMessage } from "@jsenv/log"
|
|
4
|
+
import { Abort } from "@jsenv/abort"
|
|
5
|
+
|
|
6
|
+
export const readNodeV8CoverageDirectory = async ({
|
|
7
|
+
logger,
|
|
8
|
+
signal,
|
|
9
|
+
onV8Coverage,
|
|
10
|
+
maxMsWaitingForNodeToWriteCoverageFile = 2000,
|
|
11
|
+
}) => {
|
|
12
|
+
const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE
|
|
13
|
+
const operation = Abort.startOperation()
|
|
14
|
+
operation.addAbortSignal(signal)
|
|
15
|
+
|
|
16
|
+
let timeSpentTrying = 0
|
|
17
|
+
const tryReadDirectory = async () => {
|
|
18
|
+
const dirContent = readdirSync(NODE_V8_COVERAGE)
|
|
19
|
+
if (dirContent.length > 0) {
|
|
20
|
+
return dirContent
|
|
21
|
+
}
|
|
22
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
24
|
+
timeSpentTrying += 200
|
|
25
|
+
logger.debug("retry to read coverage directory")
|
|
26
|
+
return tryReadDirectory()
|
|
27
|
+
}
|
|
28
|
+
logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`)
|
|
29
|
+
return dirContent
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
operation.throwIfAborted()
|
|
34
|
+
const dirContent = await tryReadDirectory()
|
|
35
|
+
|
|
36
|
+
const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
37
|
+
NODE_V8_COVERAGE,
|
|
38
|
+
"NODE_V8_COVERAGE",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await dirContent.reduce(async (previous, dirEntry) => {
|
|
42
|
+
operation.throwIfAborted()
|
|
43
|
+
await previous
|
|
44
|
+
|
|
45
|
+
const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl)
|
|
46
|
+
const tryReadJsonFile = async () => {
|
|
47
|
+
const fileContent = String(readFileSync(dirEntryUrl))
|
|
48
|
+
if (fileContent === "") {
|
|
49
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
51
|
+
timeSpentTrying += 200
|
|
52
|
+
return tryReadJsonFile()
|
|
53
|
+
}
|
|
54
|
+
console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const fileAsJson = JSON.parse(fileContent)
|
|
60
|
+
return fileAsJson
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
64
|
+
timeSpentTrying += 200
|
|
65
|
+
return tryReadJsonFile()
|
|
66
|
+
}
|
|
67
|
+
console.warn(
|
|
68
|
+
createDetailedMessage(`Error while reading coverage file`, {
|
|
69
|
+
"error stack": e.stack,
|
|
70
|
+
"file": dirEntryUrl,
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fileContent = await tryReadJsonFile()
|
|
78
|
+
if (fileContent) {
|
|
79
|
+
await onV8Coverage(fileContent)
|
|
80
|
+
}
|
|
81
|
+
}, Promise.resolve())
|
|
82
|
+
} finally {
|
|
83
|
+
await operation.end()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { urlToFileSystemPath } from "@jsenv/urls"
|
|
2
|
+
import { Abort } from "@jsenv/abort"
|
|
3
|
+
import { requireFromJsenv } from "@jsenv/core/src/helpers/require_from_jsenv.js"
|
|
4
|
+
|
|
5
|
+
import { composeTwoFileByFileIstanbulCoverages } from "./istanbul_coverage_composition.js"
|
|
6
|
+
|
|
7
|
+
export const v8CoverageToIstanbul = async (v8Coverage, { signal }) => {
|
|
8
|
+
const operation = Abort.startOperation()
|
|
9
|
+
operation.addAbortSignal(signal)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const v8ToIstanbul = requireFromJsenv("v8-to-istanbul")
|
|
13
|
+
const sourcemapCache = v8Coverage["source-map-cache"]
|
|
14
|
+
let istanbulCoverageComposed = null
|
|
15
|
+
|
|
16
|
+
await v8Coverage.result.reduce(async (previous, fileV8Coverage) => {
|
|
17
|
+
operation.throwIfAborted()
|
|
18
|
+
await previous
|
|
19
|
+
|
|
20
|
+
const { source } = fileV8Coverage
|
|
21
|
+
let sources
|
|
22
|
+
// when v8 coverage comes from playwright (chromium) v8Coverage.source is set
|
|
23
|
+
if (typeof source === "string") {
|
|
24
|
+
sources = { source }
|
|
25
|
+
}
|
|
26
|
+
// when v8 coverage comes from Node.js, the source can be read from sourcemapCache
|
|
27
|
+
else if (sourcemapCache) {
|
|
28
|
+
sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache)
|
|
29
|
+
}
|
|
30
|
+
const path = urlToFileSystemPath(fileV8Coverage.url)
|
|
31
|
+
|
|
32
|
+
const converter = v8ToIstanbul(
|
|
33
|
+
path,
|
|
34
|
+
// wrapperLength is undefined we don't need it
|
|
35
|
+
// https://github.com/istanbuljs/v8-to-istanbul/blob/2b54bc97c5edf8a37b39a171ec29134ba9bfd532/lib/v8-to-istanbul.js#L27
|
|
36
|
+
undefined,
|
|
37
|
+
sources,
|
|
38
|
+
)
|
|
39
|
+
await converter.load()
|
|
40
|
+
|
|
41
|
+
converter.applyCoverage(fileV8Coverage.functions)
|
|
42
|
+
const istanbulCoverage = converter.toIstanbul()
|
|
43
|
+
|
|
44
|
+
istanbulCoverageComposed = istanbulCoverageComposed
|
|
45
|
+
? composeTwoFileByFileIstanbulCoverages(
|
|
46
|
+
istanbulCoverageComposed,
|
|
47
|
+
istanbulCoverage,
|
|
48
|
+
)
|
|
49
|
+
: istanbulCoverage
|
|
50
|
+
}, Promise.resolve())
|
|
51
|
+
|
|
52
|
+
if (!istanbulCoverageComposed) {
|
|
53
|
+
return {}
|
|
54
|
+
}
|
|
55
|
+
istanbulCoverageComposed = markAsConvertedFromV8(istanbulCoverageComposed)
|
|
56
|
+
return istanbulCoverageComposed
|
|
57
|
+
} finally {
|
|
58
|
+
await operation.end()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const markAsConvertedFromV8 = (fileByFileCoverage) => {
|
|
63
|
+
const fileByFileMarked = {}
|
|
64
|
+
Object.keys(fileByFileCoverage).forEach((key) => {
|
|
65
|
+
const fileCoverage = fileByFileCoverage[key]
|
|
66
|
+
fileByFileMarked[key] = {
|
|
67
|
+
...fileCoverage,
|
|
68
|
+
fromV8: true,
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
return fileByFileMarked
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sourcesFromSourceMapCache = (url, sourceMapCache) => {
|
|
75
|
+
const sourceMapAndLineLengths = sourceMapCache[url]
|
|
76
|
+
if (!sourceMapAndLineLengths) {
|
|
77
|
+
return {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { data, lineLengths } = sourceMapAndLineLengths
|
|
81
|
+
// See: https://github.com/nodejs/node/pull/34305
|
|
82
|
+
if (!data) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sources = {
|
|
87
|
+
sourcemap: data,
|
|
88
|
+
...(lineLengths ? { source: sourcesFromLineLengths(lineLengths) } : {}),
|
|
89
|
+
}
|
|
90
|
+
return sources
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sourcesFromLineLengths = (lineLengths) => {
|
|
94
|
+
let source = ""
|
|
95
|
+
lineLengths.forEach((length) => {
|
|
96
|
+
source += `${"".padEnd(length, ".")}\n`
|
|
97
|
+
})
|
|
98
|
+
return source
|
|
99
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Export a function capable to execute a file on a runtime (browser or node) and return how it goes.
|
|
3
|
+
*
|
|
4
|
+
* - can be useful to execute a file in a browser/node.js programmatically
|
|
5
|
+
* - not documented
|
|
6
|
+
* - the most importants parts:
|
|
7
|
+
* - fileRelativeUrl: the file to execute inside rootDirectoryUrl
|
|
8
|
+
* - runtime: an object with a "run" method.
|
|
9
|
+
* The run method will start a browser/node process and execute file in it
|
|
10
|
+
* - Most of the logic lives in "./run.js" used by executeTestPlan to run tests
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort"
|
|
14
|
+
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
|
|
15
|
+
import { createLogger } from "@jsenv/log"
|
|
16
|
+
|
|
17
|
+
import { assertAndNormalizeWebServer } from "./web_server_param.js"
|
|
18
|
+
import { run } from "./run.js"
|
|
19
|
+
|
|
20
|
+
export const execute = async ({
|
|
21
|
+
signal = new AbortController().signal,
|
|
22
|
+
handleSIGINT = true,
|
|
23
|
+
logLevel,
|
|
24
|
+
rootDirectoryUrl,
|
|
25
|
+
webServer,
|
|
26
|
+
importMap,
|
|
27
|
+
|
|
28
|
+
fileRelativeUrl,
|
|
29
|
+
allocatedMs,
|
|
30
|
+
mirrorConsole = true,
|
|
31
|
+
keepRunning = false,
|
|
32
|
+
|
|
33
|
+
collectConsole,
|
|
34
|
+
collectCoverage,
|
|
35
|
+
coverageTempDirectoryUrl,
|
|
36
|
+
collectPerformance = false,
|
|
37
|
+
runtime,
|
|
38
|
+
runtimeParams,
|
|
39
|
+
|
|
40
|
+
ignoreError = false,
|
|
41
|
+
}) => {
|
|
42
|
+
const logger = createLogger({ logLevel })
|
|
43
|
+
rootDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
44
|
+
rootDirectoryUrl,
|
|
45
|
+
"rootDirectoryUrl",
|
|
46
|
+
)
|
|
47
|
+
const executeOperation = Abort.startOperation()
|
|
48
|
+
executeOperation.addAbortSignal(signal)
|
|
49
|
+
if (handleSIGINT) {
|
|
50
|
+
executeOperation.addAbortSource((abort) => {
|
|
51
|
+
return raceProcessTeardownEvents(
|
|
52
|
+
{
|
|
53
|
+
SIGINT: true,
|
|
54
|
+
},
|
|
55
|
+
abort,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (runtime.type === "browser") {
|
|
61
|
+
await assertAndNormalizeWebServer(webServer)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let resultTransformer = (result) => result
|
|
65
|
+
runtimeParams = {
|
|
66
|
+
rootDirectoryUrl,
|
|
67
|
+
webServer,
|
|
68
|
+
fileRelativeUrl,
|
|
69
|
+
importMap,
|
|
70
|
+
...runtimeParams,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let result = await run({
|
|
74
|
+
signal: executeOperation.signal,
|
|
75
|
+
logger,
|
|
76
|
+
allocatedMs,
|
|
77
|
+
keepRunning,
|
|
78
|
+
mirrorConsole,
|
|
79
|
+
collectConsole,
|
|
80
|
+
collectCoverage,
|
|
81
|
+
coverageTempDirectoryUrl,
|
|
82
|
+
collectPerformance,
|
|
83
|
+
runtime,
|
|
84
|
+
runtimeParams,
|
|
85
|
+
})
|
|
86
|
+
result = resultTransformer(result)
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (result.status === "failed") {
|
|
90
|
+
if (ignoreError) {
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
/*
|
|
94
|
+
Warning: when node launched with --unhandled-rejections=strict, despites
|
|
95
|
+
this promise being rejected by throw result.error node will completely ignore it.
|
|
96
|
+
|
|
97
|
+
The error can be logged by doing
|
|
98
|
+
```js
|
|
99
|
+
process.setUncaughtExceptionCaptureCallback((error) => {
|
|
100
|
+
console.error(error.stack)
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
But it feels like a hack.
|
|
104
|
+
*/
|
|
105
|
+
throw result.errors[result.errors.length - 1]
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
108
|
+
} finally {
|
|
109
|
+
await executeOperation.end()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { memoryUsage } from "node:process"
|
|
3
|
+
import { takeCoverage } from "node:v8"
|
|
4
|
+
import stripAnsi from "strip-ansi"
|
|
5
|
+
import { urlToFileSystemPath } from "@jsenv/urls"
|
|
6
|
+
import { createLog, startSpinner } from "@jsenv/log"
|
|
7
|
+
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort"
|
|
8
|
+
import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem"
|
|
9
|
+
|
|
10
|
+
import { reportToCoverage } from "../coverage/report_to_coverage.js"
|
|
11
|
+
import { run } from "./run.js"
|
|
12
|
+
import { ensureGlobalGc } from "./gc.js"
|
|
13
|
+
import { createExecutionLog, createSummaryLog } from "./logs_file_execution.js"
|
|
14
|
+
|
|
15
|
+
export const executeSteps = async (
|
|
16
|
+
executionSteps,
|
|
17
|
+
{
|
|
18
|
+
signal,
|
|
19
|
+
handleSIGINT,
|
|
20
|
+
logger,
|
|
21
|
+
logRefresh,
|
|
22
|
+
logRuntime,
|
|
23
|
+
logEachDuration,
|
|
24
|
+
logSummary,
|
|
25
|
+
logTimeUsage,
|
|
26
|
+
logMemoryHeapUsage,
|
|
27
|
+
logFileRelativeUrl,
|
|
28
|
+
completedExecutionLogMerging,
|
|
29
|
+
completedExecutionLogAbbreviation,
|
|
30
|
+
rootDirectoryUrl,
|
|
31
|
+
webServer,
|
|
32
|
+
|
|
33
|
+
keepRunning,
|
|
34
|
+
defaultMsAllocatedPerExecution,
|
|
35
|
+
maxExecutionsInParallel,
|
|
36
|
+
failFast,
|
|
37
|
+
gcBetweenExecutions,
|
|
38
|
+
cooldownBetweenExecutions,
|
|
39
|
+
|
|
40
|
+
coverageEnabled,
|
|
41
|
+
coverageConfig,
|
|
42
|
+
coverageIncludeMissing,
|
|
43
|
+
coverageMethodForBrowsers,
|
|
44
|
+
coverageMethodForNodeJs,
|
|
45
|
+
coverageV8ConflictWarning,
|
|
46
|
+
coverageTempDirectoryUrl,
|
|
47
|
+
|
|
48
|
+
beforeExecutionCallback = () => {},
|
|
49
|
+
afterExecutionCallback = () => {},
|
|
50
|
+
} = {},
|
|
51
|
+
) => {
|
|
52
|
+
const executePlanReturnValue = {}
|
|
53
|
+
const report = {}
|
|
54
|
+
const callbacks = []
|
|
55
|
+
const stopAfterAllSignal = { notify: () => {} }
|
|
56
|
+
|
|
57
|
+
const multipleExecutionsOperation = Abort.startOperation()
|
|
58
|
+
multipleExecutionsOperation.addAbortSignal(signal)
|
|
59
|
+
if (handleSIGINT) {
|
|
60
|
+
multipleExecutionsOperation.addAbortSource((abort) => {
|
|
61
|
+
return raceProcessTeardownEvents(
|
|
62
|
+
{
|
|
63
|
+
SIGINT: true,
|
|
64
|
+
},
|
|
65
|
+
() => {
|
|
66
|
+
logger.debug(`SIGINT abort`)
|
|
67
|
+
abort()
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
const failFastAbortController = new AbortController()
|
|
73
|
+
if (failFast) {
|
|
74
|
+
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (gcBetweenExecutions) {
|
|
79
|
+
ensureGlobalGc()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (coverageEnabled) {
|
|
83
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
84
|
+
await ensureEmptyDirectory(coverageTempDirectoryUrl)
|
|
85
|
+
callbacks.push(async () => {
|
|
86
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
87
|
+
// don't try to do the coverage stuff
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
92
|
+
takeCoverage()
|
|
93
|
+
// conceptually we don't need coverage anymore so it would be
|
|
94
|
+
// good to call v8.stopCoverage()
|
|
95
|
+
// but it logs a strange message about "result is not an object"
|
|
96
|
+
}
|
|
97
|
+
const planCoverage = await reportToCoverage(report, {
|
|
98
|
+
signal: multipleExecutionsOperation.signal,
|
|
99
|
+
logger,
|
|
100
|
+
rootDirectoryUrl,
|
|
101
|
+
coverageConfig,
|
|
102
|
+
coverageIncludeMissing,
|
|
103
|
+
coverageMethodForBrowsers,
|
|
104
|
+
coverageV8ConflictWarning,
|
|
105
|
+
})
|
|
106
|
+
executePlanReturnValue.planCoverage = planCoverage
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (Abort.isAbortError(e)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
throw e
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const runtimeParams = {
|
|
117
|
+
rootDirectoryUrl,
|
|
118
|
+
webServer,
|
|
119
|
+
|
|
120
|
+
coverageEnabled,
|
|
121
|
+
coverageConfig,
|
|
122
|
+
coverageMethodForBrowsers,
|
|
123
|
+
coverageMethodForNodeJs,
|
|
124
|
+
stopAfterAllSignal,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (completedExecutionLogMerging && !process.stdout.isTTY) {
|
|
128
|
+
completedExecutionLogMerging = false
|
|
129
|
+
logger.debug(
|
|
130
|
+
`Force completedExecutionLogMerging to false because process.stdout.isTTY is false`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const debugLogsEnabled = logger.levels.debug
|
|
134
|
+
const executionLogsEnabled = logger.levels.info
|
|
135
|
+
const executionSpinner =
|
|
136
|
+
logRefresh &&
|
|
137
|
+
!debugLogsEnabled &&
|
|
138
|
+
executionLogsEnabled &&
|
|
139
|
+
process.stdout.isTTY &&
|
|
140
|
+
// if there is an error during execution npm will mess up the output
|
|
141
|
+
// (happens when npm runs several command in a workspace)
|
|
142
|
+
// so we enable spinner only when !process.exitCode (no error so far)
|
|
143
|
+
process.exitCode !== 1
|
|
144
|
+
|
|
145
|
+
const startMs = Date.now()
|
|
146
|
+
let rawOutput = ""
|
|
147
|
+
let executionLog = createLog({ newLine: "" })
|
|
148
|
+
const counters = {
|
|
149
|
+
total: executionSteps.length,
|
|
150
|
+
aborted: 0,
|
|
151
|
+
timedout: 0,
|
|
152
|
+
failed: 0,
|
|
153
|
+
completed: 0,
|
|
154
|
+
done: 0,
|
|
155
|
+
}
|
|
156
|
+
await executeInParallel({
|
|
157
|
+
multipleExecutionsOperation,
|
|
158
|
+
maxExecutionsInParallel,
|
|
159
|
+
cooldownBetweenExecutions,
|
|
160
|
+
executionSteps,
|
|
161
|
+
start: async (paramsFromStep) => {
|
|
162
|
+
const executionIndex = executionSteps.indexOf(paramsFromStep)
|
|
163
|
+
const { executionName, fileRelativeUrl, runtime } = paramsFromStep
|
|
164
|
+
const runtimeType = runtime.type
|
|
165
|
+
const runtimeName = runtime.name
|
|
166
|
+
const runtimeVersion = runtime.version
|
|
167
|
+
const executionParams = {
|
|
168
|
+
measurePerformance: false,
|
|
169
|
+
collectPerformance: false,
|
|
170
|
+
collectConsole: true,
|
|
171
|
+
allocatedMs: defaultMsAllocatedPerExecution,
|
|
172
|
+
...paramsFromStep,
|
|
173
|
+
runtimeParams: {
|
|
174
|
+
fileRelativeUrl,
|
|
175
|
+
...paramsFromStep.runtimeParams,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
const beforeExecutionInfo = {
|
|
179
|
+
fileRelativeUrl,
|
|
180
|
+
runtimeType,
|
|
181
|
+
runtimeName,
|
|
182
|
+
runtimeVersion,
|
|
183
|
+
executionIndex,
|
|
184
|
+
executionParams,
|
|
185
|
+
startMs: Date.now(),
|
|
186
|
+
executionResult: {
|
|
187
|
+
status: "executing",
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
let spinner
|
|
191
|
+
if (executionSpinner) {
|
|
192
|
+
spinner = startSpinner({
|
|
193
|
+
log: executionLog,
|
|
194
|
+
render: () => {
|
|
195
|
+
return createExecutionLog(beforeExecutionInfo, {
|
|
196
|
+
counters,
|
|
197
|
+
logRuntime,
|
|
198
|
+
logEachDuration,
|
|
199
|
+
...(logTimeUsage
|
|
200
|
+
? {
|
|
201
|
+
timeEllapsed: Date.now() - startMs,
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
204
|
+
...(logMemoryHeapUsage
|
|
205
|
+
? { memoryHeap: memoryUsage().heapUsed }
|
|
206
|
+
: {}),
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
beforeExecutionCallback(beforeExecutionInfo)
|
|
212
|
+
|
|
213
|
+
const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`
|
|
214
|
+
let executionResult
|
|
215
|
+
if (existsSync(new URL(fileUrl))) {
|
|
216
|
+
executionResult = await run({
|
|
217
|
+
signal: multipleExecutionsOperation.signal,
|
|
218
|
+
logger,
|
|
219
|
+
allocatedMs:
|
|
220
|
+
typeof executionParams.allocatedMs === "function"
|
|
221
|
+
? executionParams.allocatedMs(beforeExecutionInfo)
|
|
222
|
+
: executionParams.allocatedMs,
|
|
223
|
+
keepRunning,
|
|
224
|
+
mirrorConsole: false, // file are executed in parallel, log would be a mess to read
|
|
225
|
+
collectConsole: executionParams.collectConsole,
|
|
226
|
+
coverageEnabled,
|
|
227
|
+
coverageTempDirectoryUrl,
|
|
228
|
+
runtime: executionParams.runtime,
|
|
229
|
+
runtimeParams: {
|
|
230
|
+
...runtimeParams,
|
|
231
|
+
...executionParams.runtimeParams,
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
} else {
|
|
235
|
+
executionResult = {
|
|
236
|
+
status: "failed",
|
|
237
|
+
errors: [
|
|
238
|
+
new Error(
|
|
239
|
+
`No file at ${fileRelativeUrl} for execution "${executionName}"`,
|
|
240
|
+
),
|
|
241
|
+
],
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
counters.done++
|
|
245
|
+
const fileReport = report[fileRelativeUrl]
|
|
246
|
+
if (fileReport) {
|
|
247
|
+
fileReport[executionName] = executionResult
|
|
248
|
+
} else {
|
|
249
|
+
report[fileRelativeUrl] = {
|
|
250
|
+
[executionName]: executionResult,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const afterExecutionInfo = {
|
|
255
|
+
...beforeExecutionInfo,
|
|
256
|
+
runtimeVersion: runtime.version,
|
|
257
|
+
endMs: Date.now(),
|
|
258
|
+
executionResult,
|
|
259
|
+
}
|
|
260
|
+
afterExecutionCallback(afterExecutionInfo)
|
|
261
|
+
|
|
262
|
+
if (executionResult.status === "aborted") {
|
|
263
|
+
counters.aborted++
|
|
264
|
+
} else if (executionResult.status === "timedout") {
|
|
265
|
+
counters.timedout++
|
|
266
|
+
} else if (executionResult.status === "failed") {
|
|
267
|
+
counters.failed++
|
|
268
|
+
} else if (executionResult.status === "completed") {
|
|
269
|
+
counters.completed++
|
|
270
|
+
}
|
|
271
|
+
if (gcBetweenExecutions) {
|
|
272
|
+
global.gc()
|
|
273
|
+
}
|
|
274
|
+
if (executionLogsEnabled) {
|
|
275
|
+
const log = createExecutionLog(afterExecutionInfo, {
|
|
276
|
+
completedExecutionLogAbbreviation,
|
|
277
|
+
counters,
|
|
278
|
+
logRuntime,
|
|
279
|
+
logEachDuration,
|
|
280
|
+
...(logTimeUsage
|
|
281
|
+
? {
|
|
282
|
+
timeEllapsed: Date.now() - startMs,
|
|
283
|
+
}
|
|
284
|
+
: {}),
|
|
285
|
+
...(logMemoryHeapUsage
|
|
286
|
+
? { memoryHeap: memoryUsage().heapUsed }
|
|
287
|
+
: {}),
|
|
288
|
+
})
|
|
289
|
+
// replace spinner with this execution result
|
|
290
|
+
if (spinner) spinner.stop()
|
|
291
|
+
executionLog.write(log)
|
|
292
|
+
rawOutput += stripAnsi(log)
|
|
293
|
+
|
|
294
|
+
const canOverwriteLog = canOverwriteLogGetter({
|
|
295
|
+
completedExecutionLogMerging,
|
|
296
|
+
executionResult,
|
|
297
|
+
})
|
|
298
|
+
if (canOverwriteLog) {
|
|
299
|
+
// nothing to do, we reuse the current executionLog object
|
|
300
|
+
} else {
|
|
301
|
+
executionLog.destroy()
|
|
302
|
+
executionLog = createLog({ newLine: "" })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const isLastExecutionLog = executionIndex === executionSteps.length - 1
|
|
306
|
+
const cancelRemaining =
|
|
307
|
+
failFast &&
|
|
308
|
+
executionResult.status !== "completed" &&
|
|
309
|
+
counters.done < counters.total
|
|
310
|
+
if (isLastExecutionLog && logger.levels.info) {
|
|
311
|
+
executionLog.write("\n")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (cancelRemaining) {
|
|
315
|
+
logger.info(`"failFast" enabled -> cancel remaining executions`)
|
|
316
|
+
failFastAbortController.abort()
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
if (!keepRunning) {
|
|
321
|
+
logger.debug("stopAfterAllSignal.notify()")
|
|
322
|
+
await stopAfterAllSignal.notify()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
counters.cancelled = counters.total - counters.done
|
|
326
|
+
const summary = {
|
|
327
|
+
counters,
|
|
328
|
+
// when execution is aborted, the remaining executions are "cancelled"
|
|
329
|
+
duration: Date.now() - startMs,
|
|
330
|
+
}
|
|
331
|
+
if (logSummary) {
|
|
332
|
+
const summaryLog = createSummaryLog(summary)
|
|
333
|
+
rawOutput += stripAnsi(summaryLog)
|
|
334
|
+
logger.info(summaryLog)
|
|
335
|
+
}
|
|
336
|
+
if (summary.counters.total !== summary.counters.completed) {
|
|
337
|
+
const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href
|
|
338
|
+
writeFileSync(logFileUrl, rawOutput)
|
|
339
|
+
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`)
|
|
340
|
+
}
|
|
341
|
+
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted
|
|
342
|
+
executePlanReturnValue.planSummary = summary
|
|
343
|
+
executePlanReturnValue.planReport = report
|
|
344
|
+
await callbacks.reduce(async (previous, callback) => {
|
|
345
|
+
await previous
|
|
346
|
+
await callback()
|
|
347
|
+
}, Promise.resolve())
|
|
348
|
+
return executePlanReturnValue
|
|
349
|
+
} finally {
|
|
350
|
+
await multipleExecutionsOperation.end()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const canOverwriteLogGetter = ({
|
|
355
|
+
completedExecutionLogMerging,
|
|
356
|
+
executionResult,
|
|
357
|
+
}) => {
|
|
358
|
+
if (!completedExecutionLogMerging) {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
if (executionResult.status === "aborted") {
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
if (executionResult.status !== "completed") {
|
|
365
|
+
return false
|
|
366
|
+
}
|
|
367
|
+
const { consoleCalls = [] } = executionResult
|
|
368
|
+
if (consoleCalls.length > 0) {
|
|
369
|
+
return false
|
|
370
|
+
}
|
|
371
|
+
return true
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const executeInParallel = async ({
|
|
375
|
+
multipleExecutionsOperation,
|
|
376
|
+
maxExecutionsInParallel,
|
|
377
|
+
cooldownBetweenExecutions,
|
|
378
|
+
executionSteps,
|
|
379
|
+
start,
|
|
380
|
+
}) => {
|
|
381
|
+
const executionResults = []
|
|
382
|
+
let progressionIndex = 0
|
|
383
|
+
let remainingExecutionCount = executionSteps.length
|
|
384
|
+
|
|
385
|
+
const nextChunk = async () => {
|
|
386
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
const outputPromiseArray = []
|
|
390
|
+
while (
|
|
391
|
+
remainingExecutionCount > 0 &&
|
|
392
|
+
outputPromiseArray.length < maxExecutionsInParallel
|
|
393
|
+
) {
|
|
394
|
+
remainingExecutionCount--
|
|
395
|
+
const outputPromise = executeOne(progressionIndex)
|
|
396
|
+
progressionIndex++
|
|
397
|
+
outputPromiseArray.push(outputPromise)
|
|
398
|
+
}
|
|
399
|
+
if (outputPromiseArray.length) {
|
|
400
|
+
await Promise.all(outputPromiseArray)
|
|
401
|
+
if (remainingExecutionCount > 0) {
|
|
402
|
+
await nextChunk()
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const executeOne = async (index) => {
|
|
408
|
+
const input = executionSteps[index]
|
|
409
|
+
const output = await start(input)
|
|
410
|
+
if (!multipleExecutionsOperation.signal.aborted) {
|
|
411
|
+
executionResults[index] = output
|
|
412
|
+
}
|
|
413
|
+
if (cooldownBetweenExecutions) {
|
|
414
|
+
await new Promise((resolve) =>
|
|
415
|
+
setTimeout(resolve, cooldownBetweenExecutions),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
await nextChunk()
|
|
421
|
+
|
|
422
|
+
return executionResults
|
|
423
|
+
}
|