@jsenv/core 27.1.0 → 27.2.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 +139 -0
- package/dist/controllable_worker_thread.mjs +103 -0
- package/dist/js/execute_using_dynamic_import.js +169 -0
- package/dist/js/v8_coverage.js +539 -0
- package/dist/main.js +575 -804
- package/package.json +8 -7
- package/src/build/build.js +9 -12
- package/src/build/build_urls_generator.js +1 -1
- package/src/build/inject_global_version_mappings.js +3 -2
- package/src/build/inject_service_worker_urls.js +1 -2
- package/src/execute/run.js +50 -68
- package/src/execute/runtimes/browsers/from_playwright.js +13 -8
- package/src/execute/runtimes/node/{controllable_file.mjs → controllable_child_process.mjs} +18 -50
- package/src/execute/runtimes/node/controllable_worker_thread.mjs +103 -0
- package/src/execute/runtimes/node/execute_using_dynamic_import.js +49 -0
- package/src/execute/runtimes/node/exit_codes.js +9 -0
- package/src/execute/runtimes/node/{node_process.js → node_child_process.js} +56 -50
- package/src/execute/runtimes/node/node_worker_thread.js +268 -25
- package/src/execute/runtimes/node/profiler_v8_coverage.js +56 -0
- package/src/main.js +3 -1
- package/src/omega/kitchen.js +19 -6
- package/src/omega/server/file_service.js +2 -2
- package/src/omega/url_graph/url_graph_load.js +0 -1
- package/src/omega/url_graph.js +1 -0
- package/src/plugins/bundling/js_module/bundle_js_module.js +2 -5
- package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +18 -15
- package/src/plugins/url_resolution/jsenv_plugin_url_resolution.js +2 -1
- package/src/test/coverage/report_to_coverage.js +16 -19
- package/src/test/coverage/v8_coverage.js +26 -0
- package/src/test/coverage/{v8_coverage_from_directory.js → v8_coverage_node_directory.js} +22 -26
- package/src/test/execute_plan.js +92 -91
- package/src/test/execute_test_plan.js +15 -13
- package/dist/js/controllable_file.mjs +0 -227
|
@@ -50,10 +50,10 @@ export const createFileService = ({
|
|
|
50
50
|
reference = urlGraph.inferReference(request.ressource, parentUrl)
|
|
51
51
|
}
|
|
52
52
|
if (!reference) {
|
|
53
|
-
const entryPoint = kitchen.
|
|
53
|
+
const entryPoint = kitchen.injectReference({
|
|
54
54
|
trace: parentUrl || rootDirectoryUrl,
|
|
55
55
|
parentUrl: parentUrl || rootDirectoryUrl,
|
|
56
|
-
type: "
|
|
56
|
+
type: "http_request",
|
|
57
57
|
specifier: request.ressource,
|
|
58
58
|
})
|
|
59
59
|
reference = entryPoint[0]
|
package/src/omega/url_graph.js
CHANGED
|
@@ -94,7 +94,7 @@ const rollupPluginJsenv = ({
|
|
|
94
94
|
let previousNonEntryPointModuleId
|
|
95
95
|
jsModuleUrlInfos.forEach((jsModuleUrlInfo) => {
|
|
96
96
|
const id = jsModuleUrlInfo.url
|
|
97
|
-
if (jsModuleUrlInfo.
|
|
97
|
+
if (jsModuleUrlInfo.isEntryPoint) {
|
|
98
98
|
emitChunk({
|
|
99
99
|
id,
|
|
100
100
|
})
|
|
@@ -355,10 +355,7 @@ const willBeInsideJsDirectory = ({
|
|
|
355
355
|
// generated by rollup
|
|
356
356
|
return true
|
|
357
357
|
}
|
|
358
|
-
if (
|
|
359
|
-
!jsModuleUrlInfo.data.isEntryPoint &&
|
|
360
|
-
!jsModuleUrlInfo.data.isWebWorkerEntryPoint
|
|
361
|
-
) {
|
|
358
|
+
if (!jsModuleUrlInfo.isEntryPoint) {
|
|
362
359
|
// not an entry point, jsenv will put it inside js/ directory
|
|
363
360
|
return true
|
|
364
361
|
}
|
|
@@ -101,24 +101,24 @@ const jsenvPluginAsJsClassicConversion = ({
|
|
|
101
101
|
if (!originalUrlInfo) {
|
|
102
102
|
return null
|
|
103
103
|
}
|
|
104
|
-
const isJsEntryPoint =
|
|
105
|
-
// in general html files are entry points
|
|
106
|
-
// but during build js can be sepcified as an entry point
|
|
107
|
-
// (meaning there is no html file where we can inject systemjs)
|
|
108
|
-
// in that case we need to inject systemjs in the js file
|
|
109
|
-
originalUrlInfo.data.isEntryPoint ||
|
|
110
|
-
// In thoose case we need to inject systemjs the worker js file
|
|
111
|
-
originalUrlInfo.data.isWebWorkerEntryPoint
|
|
112
|
-
// if it's an entry point without dependency (it does not use import)
|
|
113
|
-
// then we can use UMD, otherwise we have to use systemjs
|
|
114
|
-
// because it is imported by systemjs
|
|
115
104
|
const jsClassicFormat =
|
|
116
|
-
|
|
105
|
+
// in general html file are entry points, but js can be entry point when:
|
|
106
|
+
// - passed in entryPoints to build
|
|
107
|
+
// - is used by web worker
|
|
108
|
+
// - the reference contains ?entry_point
|
|
109
|
+
// When js is entry point there can be no HTML to inject systemjs
|
|
110
|
+
// and systemjs must be injected into the js file
|
|
111
|
+
originalUrlInfo.isEntryPoint &&
|
|
112
|
+
// if it's an entry point without dependency (it does not use import)
|
|
113
|
+
// then we can use UMD, otherwise we have to use systemjs
|
|
114
|
+
// because it is imported by systemjs
|
|
115
|
+
!originalUrlInfo.data.usesImport
|
|
116
|
+
? "umd"
|
|
117
|
+
: "system"
|
|
117
118
|
const { content, sourcemap } = await convertJsModuleToJsClassic({
|
|
118
119
|
systemJsInjection,
|
|
119
120
|
systemJsClientFileUrl,
|
|
120
121
|
urlInfo: originalUrlInfo,
|
|
121
|
-
isJsEntryPoint,
|
|
122
122
|
jsClassicFormat,
|
|
123
123
|
})
|
|
124
124
|
urlInfo.data.jsClassicFormat = jsClassicFormat
|
|
@@ -160,7 +160,6 @@ const convertJsModuleToJsClassic = async ({
|
|
|
160
160
|
systemJsInjection,
|
|
161
161
|
systemJsClientFileUrl,
|
|
162
162
|
urlInfo,
|
|
163
|
-
isJsEntryPoint,
|
|
164
163
|
jsClassicFormat,
|
|
165
164
|
}) => {
|
|
166
165
|
const { code, map } = await applyBabelPlugins({
|
|
@@ -193,7 +192,11 @@ const convertJsModuleToJsClassic = async ({
|
|
|
193
192
|
})
|
|
194
193
|
let sourcemap = urlInfo.sourcemap
|
|
195
194
|
sourcemap = await composeTwoSourcemaps(sourcemap, map)
|
|
196
|
-
if (
|
|
195
|
+
if (
|
|
196
|
+
systemJsInjection &&
|
|
197
|
+
jsClassicFormat === "system" &&
|
|
198
|
+
urlInfo.isEntryPoint
|
|
199
|
+
) {
|
|
197
200
|
const magicSource = createMagicSource(code)
|
|
198
201
|
const systemjsCode = readFileSync(systemJsClientFileUrl, { as: "string" })
|
|
199
202
|
magicSource.prepend(`${systemjsCode}\n\n`)
|
|
@@ -9,7 +9,8 @@ export const jsenvPluginUrlResolution = () => {
|
|
|
9
9
|
name: "jsenv:url_resolution",
|
|
10
10
|
appliesDuring: "*",
|
|
11
11
|
resolveUrl: {
|
|
12
|
-
"
|
|
12
|
+
"http_request": urlResolver, // during dev
|
|
13
|
+
"entry_point": urlResolver, // during build
|
|
13
14
|
"link_href": urlResolver,
|
|
14
15
|
"script_src": urlResolver,
|
|
15
16
|
"a_href": urlResolver,
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
2
|
import { Abort } from "@jsenv/abort"
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
filterV8Coverage,
|
|
7
|
-
} from "./v8_coverage_from_directory.js"
|
|
4
|
+
import { filterV8Coverage } from "./v8_coverage.js"
|
|
5
|
+
import { readNodeV8CoverageDirectory } from "./v8_coverage_node_directory.js"
|
|
8
6
|
import { composeTwoV8Coverages } from "./v8_coverage_composition.js"
|
|
9
7
|
import { composeTwoFileByFileIstanbulCoverages } from "./istanbul_coverage_composition.js"
|
|
10
8
|
import { v8CoverageToIstanbul } from "./v8_coverage_to_istanbul.js"
|
|
@@ -20,8 +18,7 @@ export const reportToCoverage = async (
|
|
|
20
18
|
rootDirectoryUrl,
|
|
21
19
|
coverageConfig,
|
|
22
20
|
coverageIncludeMissing,
|
|
23
|
-
|
|
24
|
-
coverageForceIstanbul,
|
|
21
|
+
coverageMethodForNodeJs,
|
|
25
22
|
coverageV8ConflictWarning,
|
|
26
23
|
},
|
|
27
24
|
) => {
|
|
@@ -49,24 +46,24 @@ export const reportToCoverage = async (
|
|
|
49
46
|
// that were suppose to be coverage but were not.
|
|
50
47
|
if (
|
|
51
48
|
executionResult.status === "completed" &&
|
|
52
|
-
executionResult.
|
|
53
|
-
|
|
49
|
+
executionResult.type === "node" &&
|
|
50
|
+
coverageMethodForNodeJs !== "NODE_V8_COVERAGE"
|
|
54
51
|
) {
|
|
55
52
|
logger.warn(
|
|
56
|
-
`No
|
|
53
|
+
`No "coverageFileUrl" from execution named "${executionName}" of ${file}`,
|
|
57
54
|
)
|
|
58
55
|
}
|
|
59
56
|
},
|
|
60
57
|
})
|
|
61
58
|
|
|
62
|
-
if (
|
|
63
|
-
await
|
|
59
|
+
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
60
|
+
await readNodeV8CoverageDirectory({
|
|
64
61
|
logger,
|
|
65
62
|
signal,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
onV8Coverage: async (nodeV8Coverage) => {
|
|
64
|
+
const nodeV8CoverageLight = await filterV8Coverage(nodeV8Coverage, {
|
|
65
|
+
rootDirectoryUrl,
|
|
66
|
+
coverageConfig,
|
|
70
67
|
})
|
|
71
68
|
v8Coverage = v8Coverage
|
|
72
69
|
? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight)
|
|
@@ -164,9 +161,9 @@ const getCoverageFromReport = async ({ signal, report, onMissing }) => {
|
|
|
164
161
|
return
|
|
165
162
|
}
|
|
166
163
|
|
|
167
|
-
const executionCoverage =
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
const executionCoverage = JSON.parse(
|
|
165
|
+
String(readFileSync(new URL(coverageFileUrl))),
|
|
166
|
+
)
|
|
170
167
|
if (isV8Coverage(executionCoverage)) {
|
|
171
168
|
v8Coverage = v8Coverage
|
|
172
169
|
? composeTwoV8Coverages(v8Coverage, executionCoverage)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { URL_META } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const filterV8Coverage = async (
|
|
4
|
+
v8Coverage,
|
|
5
|
+
{ rootDirectoryUrl, coverageConfig },
|
|
6
|
+
) => {
|
|
7
|
+
const associations = URL_META.resolveAssociations(
|
|
8
|
+
{ cover: coverageConfig },
|
|
9
|
+
rootDirectoryUrl,
|
|
10
|
+
)
|
|
11
|
+
const urlShouldBeCovered = (url) => {
|
|
12
|
+
const { cover } = URL_META.applyAssociations({
|
|
13
|
+
url: new URL(url, rootDirectoryUrl).href,
|
|
14
|
+
associations,
|
|
15
|
+
})
|
|
16
|
+
return cover
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const v8CoverageFiltered = {
|
|
20
|
+
...v8Coverage,
|
|
21
|
+
result: v8Coverage.result.filter((fileReport) =>
|
|
22
|
+
urlShouldBeCovered(fileReport.url),
|
|
23
|
+
),
|
|
24
|
+
}
|
|
25
|
+
return v8CoverageFiltered
|
|
26
|
+
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
readDirectory,
|
|
4
|
-
readFile,
|
|
5
|
-
} from "@jsenv/filesystem"
|
|
6
|
-
import { resolveUrl } from "@jsenv/urls"
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs"
|
|
2
|
+
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
|
|
7
3
|
import { createDetailedMessage } from "@jsenv/log"
|
|
8
4
|
import { Abort } from "@jsenv/abort"
|
|
9
5
|
|
|
10
|
-
export const
|
|
6
|
+
export const readNodeV8CoverageDirectory = async ({
|
|
11
7
|
logger,
|
|
12
8
|
signal,
|
|
13
|
-
NODE_V8_COVERAGE,
|
|
14
9
|
onV8Coverage,
|
|
15
10
|
maxMsWaitingForNodeToWriteCoverageFile = 2000,
|
|
16
11
|
}) => {
|
|
12
|
+
const NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE
|
|
17
13
|
const operation = Abort.startOperation()
|
|
18
14
|
operation.addAbortSignal(signal)
|
|
19
15
|
|
|
16
|
+
let timeSpentTrying = 0
|
|
20
17
|
const tryReadDirectory = async () => {
|
|
21
|
-
const dirContent =
|
|
18
|
+
const dirContent = readdirSync(NODE_V8_COVERAGE)
|
|
22
19
|
if (dirContent.length > 0) {
|
|
23
20
|
return dirContent
|
|
24
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
|
+
}
|
|
25
28
|
logger.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`)
|
|
26
29
|
return dirContent
|
|
27
30
|
}
|
|
@@ -32,17 +35,19 @@ export const visitNodeV8Directory = async ({
|
|
|
32
35
|
|
|
33
36
|
const coverageDirectoryUrl =
|
|
34
37
|
assertAndNormalizeDirectoryUrl(NODE_V8_COVERAGE)
|
|
38
|
+
|
|
35
39
|
await dirContent.reduce(async (previous, dirEntry) => {
|
|
36
40
|
operation.throwIfAborted()
|
|
37
41
|
await previous
|
|
38
42
|
|
|
39
|
-
const dirEntryUrl =
|
|
40
|
-
const tryReadJsonFile = async (
|
|
41
|
-
const fileContent =
|
|
43
|
+
const dirEntryUrl = new URL(dirEntry, coverageDirectoryUrl)
|
|
44
|
+
const tryReadJsonFile = async () => {
|
|
45
|
+
const fileContent = String(readFileSync(dirEntryUrl))
|
|
42
46
|
if (fileContent === "") {
|
|
43
|
-
if (timeSpentTrying <
|
|
47
|
+
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
44
48
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
45
|
-
|
|
49
|
+
timeSpentTrying += 200
|
|
50
|
+
return tryReadJsonFile()
|
|
46
51
|
}
|
|
47
52
|
console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`)
|
|
48
53
|
return null
|
|
@@ -54,7 +59,8 @@ export const visitNodeV8Directory = async ({
|
|
|
54
59
|
} catch (e) {
|
|
55
60
|
if (timeSpentTrying < maxMsWaitingForNodeToWriteCoverageFile) {
|
|
56
61
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
57
|
-
|
|
62
|
+
timeSpentTrying += 200
|
|
63
|
+
return tryReadJsonFile()
|
|
58
64
|
}
|
|
59
65
|
console.warn(
|
|
60
66
|
createDetailedMessage(`Error while reading coverage file`, {
|
|
@@ -68,20 +74,10 @@ export const visitNodeV8Directory = async ({
|
|
|
68
74
|
|
|
69
75
|
const fileContent = await tryReadJsonFile()
|
|
70
76
|
if (fileContent) {
|
|
71
|
-
onV8Coverage(fileContent)
|
|
77
|
+
await onV8Coverage(fileContent)
|
|
72
78
|
}
|
|
73
79
|
}, Promise.resolve())
|
|
74
80
|
} finally {
|
|
75
81
|
await operation.end()
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
|
-
|
|
79
|
-
export const filterV8Coverage = (v8Coverage, { urlShouldBeCovered }) => {
|
|
80
|
-
const v8CoverageFiltered = {
|
|
81
|
-
...v8Coverage,
|
|
82
|
-
result: v8Coverage.result.filter((fileReport) =>
|
|
83
|
-
urlShouldBeCovered(fileReport.url),
|
|
84
|
-
),
|
|
85
|
-
}
|
|
86
|
-
return v8CoverageFiltered
|
|
87
|
-
}
|
package/src/test/execute_plan.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { memoryUsage } from "node:process"
|
|
3
|
+
import { takeCoverage } from "node:v8"
|
|
3
4
|
import wrapAnsi from "wrap-ansi"
|
|
4
5
|
import stripAnsi from "strip-ansi"
|
|
5
|
-
import cuid from "cuid"
|
|
6
6
|
|
|
7
|
-
import { URL_META } from "@jsenv/url-meta"
|
|
8
7
|
import { urlToFileSystemPath } from "@jsenv/urls"
|
|
9
8
|
import {
|
|
10
9
|
createDetailedMessage,
|
|
@@ -13,11 +12,7 @@ import {
|
|
|
13
12
|
startSpinner,
|
|
14
13
|
} from "@jsenv/log"
|
|
15
14
|
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort"
|
|
16
|
-
import {
|
|
17
|
-
writeDirectory,
|
|
18
|
-
ensureEmptyDirectory,
|
|
19
|
-
writeFileSync,
|
|
20
|
-
} from "@jsenv/filesystem"
|
|
15
|
+
import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem"
|
|
21
16
|
|
|
22
17
|
import { babelPluginInstrument } from "./coverage/babel_plugin_instrument.js"
|
|
23
18
|
import { reportToCoverage } from "./coverage/report_to_coverage.js"
|
|
@@ -54,10 +49,11 @@ export const executePlan = async (
|
|
|
54
49
|
gcBetweenExecutions,
|
|
55
50
|
cooldownBetweenExecutions,
|
|
56
51
|
|
|
57
|
-
|
|
52
|
+
coverageEnabled,
|
|
58
53
|
coverageConfig,
|
|
59
54
|
coverageIncludeMissing,
|
|
60
|
-
|
|
55
|
+
coverageMethodForBrowsers,
|
|
56
|
+
coverageMethodForNodeJs,
|
|
61
57
|
coverageV8ConflictWarning,
|
|
62
58
|
coverageTempDirectoryRelativeUrl,
|
|
63
59
|
|
|
@@ -79,9 +75,13 @@ export const executePlan = async (
|
|
|
79
75
|
afterExecutionCallback = () => {},
|
|
80
76
|
} = {},
|
|
81
77
|
) => {
|
|
78
|
+
const executePlanReturnValue = {}
|
|
79
|
+
const report = {}
|
|
80
|
+
const callbacks = []
|
|
82
81
|
const stopAfterAllSignal = { notify: () => {} }
|
|
83
82
|
|
|
84
83
|
let someNeedsServer = false
|
|
84
|
+
let someNodeRuntime = false
|
|
85
85
|
const runtimes = {}
|
|
86
86
|
Object.keys(plan).forEach((filePattern) => {
|
|
87
87
|
const filePlan = plan[filePattern]
|
|
@@ -93,6 +93,9 @@ export const executePlan = async (
|
|
|
93
93
|
if (runtime.needsServer) {
|
|
94
94
|
someNeedsServer = true
|
|
95
95
|
}
|
|
96
|
+
if (runtime.type === "node") {
|
|
97
|
+
someNodeRuntime = true
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
100
|
})
|
|
98
101
|
})
|
|
@@ -122,10 +125,76 @@ export const executePlan = async (
|
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
try {
|
|
128
|
+
const coverageTempDirectoryUrl = new URL(
|
|
129
|
+
coverageTempDirectoryRelativeUrl,
|
|
130
|
+
rootDirectoryUrl,
|
|
131
|
+
).href
|
|
132
|
+
if (
|
|
133
|
+
someNodeRuntime &&
|
|
134
|
+
coverageEnabled &&
|
|
135
|
+
coverageMethodForNodeJs === "NODE_V8_COVERAGE"
|
|
136
|
+
) {
|
|
137
|
+
if (process.env.NODE_V8_COVERAGE) {
|
|
138
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
139
|
+
await ensureEmptyDirectory(process.env.NODE_V8_COVERAGE)
|
|
140
|
+
} else {
|
|
141
|
+
coverageMethodForNodeJs = "Profiler"
|
|
142
|
+
logger.warn(
|
|
143
|
+
createDetailedMessage(
|
|
144
|
+
`process.env.NODE_V8_COVERAGE is required to generate coverage for Node.js subprocesses`,
|
|
145
|
+
{
|
|
146
|
+
"suggestion": `Preprend NODE_V8_COVERAGE=.coverage/node to the command executing this process`,
|
|
147
|
+
"suggestion 2": `use coverageMethodForNodeJs: "Profiler". But it means coverage for child_process and worker_thread cannot be collected`,
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (gcBetweenExecutions) {
|
|
155
|
+
ensureGlobalGc()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (coverageEnabled) {
|
|
159
|
+
// when runned multiple times, we don't want to keep previous files in this directory
|
|
160
|
+
await ensureEmptyDirectory(coverageTempDirectoryUrl)
|
|
161
|
+
callbacks.push(async () => {
|
|
162
|
+
if (multipleExecutionsOperation.signal.aborted) {
|
|
163
|
+
// don't try to do the coverage stuff
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
|
|
168
|
+
takeCoverage()
|
|
169
|
+
// conceptually we don't need coverage anymore so it would be
|
|
170
|
+
// good to call v8.stopCoverage()
|
|
171
|
+
// but it logs a strange message about "result is not an object"
|
|
172
|
+
}
|
|
173
|
+
const planCoverage = await reportToCoverage(report, {
|
|
174
|
+
signal: multipleExecutionsOperation.signal,
|
|
175
|
+
logger,
|
|
176
|
+
rootDirectoryUrl,
|
|
177
|
+
coverageConfig,
|
|
178
|
+
coverageIncludeMissing,
|
|
179
|
+
coverageMethodForBrowsers,
|
|
180
|
+
coverageV8ConflictWarning,
|
|
181
|
+
})
|
|
182
|
+
executePlanReturnValue.planCoverage = planCoverage
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (Abort.isAbortError(e)) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
throw e
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
125
192
|
let runtimeParams = {
|
|
126
193
|
rootDirectoryUrl,
|
|
127
|
-
|
|
128
|
-
|
|
194
|
+
coverageEnabled,
|
|
195
|
+
coverageConfig,
|
|
196
|
+
coverageMethodForBrowsers,
|
|
197
|
+
coverageMethodForNodeJs,
|
|
129
198
|
stopAfterAllSignal,
|
|
130
199
|
}
|
|
131
200
|
if (someNeedsServer) {
|
|
@@ -154,7 +223,7 @@ export const executePlan = async (
|
|
|
154
223
|
...transpilation,
|
|
155
224
|
getCustomBabelPlugins: ({ clientRuntimeCompat }) => {
|
|
156
225
|
if (
|
|
157
|
-
|
|
226
|
+
coverageEnabled &&
|
|
158
227
|
Object.keys(clientRuntimeCompat)[0] !== "chrome"
|
|
159
228
|
) {
|
|
160
229
|
return {
|
|
@@ -221,76 +290,8 @@ export const executePlan = async (
|
|
|
221
290
|
process.exitCode !== 1
|
|
222
291
|
|
|
223
292
|
const startMs = Date.now()
|
|
224
|
-
const report = {}
|
|
225
293
|
let rawOutput = ""
|
|
226
294
|
|
|
227
|
-
let transformReturnValue = (value) => value
|
|
228
|
-
if (gcBetweenExecutions) {
|
|
229
|
-
ensureGlobalGc()
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const coverageTempDirectoryUrl = new URL(
|
|
233
|
-
coverageTempDirectoryRelativeUrl,
|
|
234
|
-
rootDirectoryUrl,
|
|
235
|
-
).href
|
|
236
|
-
|
|
237
|
-
if (coverage) {
|
|
238
|
-
const associations = URL_META.resolveAssociations(
|
|
239
|
-
{ cover: coverageConfig },
|
|
240
|
-
rootDirectoryUrl,
|
|
241
|
-
)
|
|
242
|
-
const urlShouldBeCovered = (url) => {
|
|
243
|
-
const { cover } = URL_META.applyAssociations({
|
|
244
|
-
url: new URL(url, rootDirectoryUrl).href,
|
|
245
|
-
associations,
|
|
246
|
-
})
|
|
247
|
-
return cover
|
|
248
|
-
}
|
|
249
|
-
runtimeParams.urlShouldBeCovered = urlShouldBeCovered
|
|
250
|
-
|
|
251
|
-
// in case runned multiple times, we don't want to keep writing lot of files in this directory
|
|
252
|
-
if (!process.env.NODE_V8_COVERAGE) {
|
|
253
|
-
await ensureEmptyDirectory(coverageTempDirectoryUrl)
|
|
254
|
-
}
|
|
255
|
-
if (runtimes.node) {
|
|
256
|
-
// v8 coverage is written in a directoy and auto propagate to subprocesses
|
|
257
|
-
// through process.env.NODE_V8_COVERAGE.
|
|
258
|
-
if (!coverageForceIstanbul && !process.env.NODE_V8_COVERAGE) {
|
|
259
|
-
const v8CoverageDirectory = new URL(
|
|
260
|
-
`./node_v8/${cuid()}`,
|
|
261
|
-
coverageTempDirectoryUrl,
|
|
262
|
-
).href
|
|
263
|
-
await writeDirectory(v8CoverageDirectory, { allowUseless: true })
|
|
264
|
-
process.env.NODE_V8_COVERAGE =
|
|
265
|
-
urlToFileSystemPath(v8CoverageDirectory)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
transformReturnValue = async (value) => {
|
|
269
|
-
if (multipleExecutionsOperation.signal.aborted) {
|
|
270
|
-
// don't try to do the coverage stuff
|
|
271
|
-
return value
|
|
272
|
-
}
|
|
273
|
-
try {
|
|
274
|
-
value.coverage = await reportToCoverage(value.report, {
|
|
275
|
-
signal: multipleExecutionsOperation.signal,
|
|
276
|
-
logger,
|
|
277
|
-
rootDirectoryUrl,
|
|
278
|
-
coverageConfig,
|
|
279
|
-
coverageIncludeMissing,
|
|
280
|
-
coverageForceIstanbul,
|
|
281
|
-
urlShouldBeCovered,
|
|
282
|
-
coverageV8ConflictWarning,
|
|
283
|
-
})
|
|
284
|
-
} catch (e) {
|
|
285
|
-
if (Abort.isAbortError(e)) {
|
|
286
|
-
return value
|
|
287
|
-
}
|
|
288
|
-
throw e
|
|
289
|
-
}
|
|
290
|
-
return value
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
295
|
logger.info("")
|
|
295
296
|
let executionLog = createLog({ newLine: "" })
|
|
296
297
|
const counters = {
|
|
@@ -309,6 +310,7 @@ export const executePlan = async (
|
|
|
309
310
|
start: async (paramsFromStep) => {
|
|
310
311
|
const executionIndex = executionSteps.indexOf(paramsFromStep)
|
|
311
312
|
const { executionName, fileRelativeUrl, runtime } = paramsFromStep
|
|
313
|
+
const runtimeType = runtime.type
|
|
312
314
|
const runtimeName = runtime.name
|
|
313
315
|
const runtimeVersion = runtime.version
|
|
314
316
|
const executionParams = {
|
|
@@ -324,6 +326,7 @@ export const executePlan = async (
|
|
|
324
326
|
}
|
|
325
327
|
const beforeExecutionInfo = {
|
|
326
328
|
fileRelativeUrl,
|
|
329
|
+
runtimeType,
|
|
327
330
|
runtimeName,
|
|
328
331
|
runtimeVersion,
|
|
329
332
|
executionIndex,
|
|
@@ -366,7 +369,7 @@ export const executePlan = async (
|
|
|
366
369
|
keepRunning,
|
|
367
370
|
mirrorConsole: false, // file are executed in parallel, log would be a mess to read
|
|
368
371
|
collectConsole: executionParams.collectConsole,
|
|
369
|
-
|
|
372
|
+
coverageEnabled,
|
|
370
373
|
coverageTempDirectoryUrl,
|
|
371
374
|
runtime: executionParams.runtime,
|
|
372
375
|
runtimeParams: {
|
|
@@ -483,16 +486,14 @@ export const executePlan = async (
|
|
|
483
486
|
writeFileSync(logFileUrl, rawOutput)
|
|
484
487
|
logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`)
|
|
485
488
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
planCoverage: result.coverage,
|
|
495
|
-
}
|
|
489
|
+
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted
|
|
490
|
+
executePlanReturnValue.planSummary = summary
|
|
491
|
+
executePlanReturnValue.planReport = report
|
|
492
|
+
await callbacks.reduce(async (previous, callback) => {
|
|
493
|
+
await previous
|
|
494
|
+
await callback()
|
|
495
|
+
}, Promise.resolve())
|
|
496
|
+
return executePlanReturnValue
|
|
496
497
|
} finally {
|
|
497
498
|
await multipleExecutionsOperation.end()
|
|
498
499
|
}
|
|
@@ -28,7 +28,7 @@ import { executePlan } from "./execute_plan.js"
|
|
|
28
28
|
* @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
|
|
29
29
|
* @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
|
|
30
30
|
* @param {boolean} [testPlanParameters.logMemoryHeapUsage=false] Add memory heap usage during logs
|
|
31
|
-
* @param {boolean} [testPlanParameters.
|
|
31
|
+
* @param {boolean} [testPlanParameters.coverageEnabled=false] Controls if coverage is collected during files executions
|
|
32
32
|
* @param {boolean} [testPlanParameters.coverageV8ConflictWarning=true] Warn when coverage from 2 executions cannot be merged
|
|
33
33
|
* @return {Object} An object containing the result of all file executions
|
|
34
34
|
*/
|
|
@@ -60,23 +60,24 @@ export const executeTestPlan = async ({
|
|
|
60
60
|
cooldownBetweenExecutions = 0,
|
|
61
61
|
gcBetweenExecutions = logMemoryHeapUsage,
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
coverageEnabled = process.argv.includes("--cover") ||
|
|
64
64
|
process.argv.includes("--coverage"),
|
|
65
|
-
coverageTempDirectoryRelativeUrl = "./.coverage/tmp/",
|
|
66
65
|
coverageConfig = {
|
|
67
66
|
"./src/": true,
|
|
68
67
|
},
|
|
69
68
|
coverageIncludeMissing = true,
|
|
70
69
|
coverageAndExecutionAllowed = false,
|
|
71
|
-
|
|
70
|
+
coverageMethodForNodeJs = "NODE_V8_COVERAGE", // "Profiler" also accepted
|
|
71
|
+
coverageMethodForBrowsers = "playwright_api", // "istanbul" also accepted
|
|
72
72
|
coverageV8ConflictWarning = true,
|
|
73
|
-
|
|
74
|
-
coverageReportJsonFile = process.env.CI ? null : "./.coverage/coverage.json",
|
|
75
|
-
coverageReportHtmlDirectory = process.env.CI ? "./.coverage/" : null,
|
|
73
|
+
coverageTempDirectoryRelativeUrl = "./.coverage/tmp/",
|
|
76
74
|
// skip empty means empty files won't appear in the coverage reports (json and html)
|
|
77
75
|
coverageReportSkipEmpty = false,
|
|
78
76
|
// skip full means file with 100% coverage won't appear in coverage reports (json and html)
|
|
79
77
|
coverageReportSkipFull = false,
|
|
78
|
+
coverageReportTextLog = true,
|
|
79
|
+
coverageReportJsonFile = process.env.CI ? null : "./.coverage/coverage.json",
|
|
80
|
+
coverageReportHtmlDirectory = process.env.CI ? "./.coverage/" : null,
|
|
80
81
|
|
|
81
82
|
sourcemaps = "inline",
|
|
82
83
|
plugins = [],
|
|
@@ -95,7 +96,7 @@ export const executeTestPlan = async ({
|
|
|
95
96
|
if (typeof testPlan !== "object") {
|
|
96
97
|
throw new Error(`testPlan must be an object, got ${testPlan}`)
|
|
97
98
|
}
|
|
98
|
-
if (
|
|
99
|
+
if (coverageEnabled) {
|
|
99
100
|
if (typeof coverageConfig !== "object") {
|
|
100
101
|
throw new TypeError(
|
|
101
102
|
`coverageConfig must be an object, got ${coverageConfig}`,
|
|
@@ -156,10 +157,11 @@ export const executeTestPlan = async ({
|
|
|
156
157
|
cooldownBetweenExecutions,
|
|
157
158
|
gcBetweenExecutions,
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
coverageEnabled,
|
|
160
161
|
coverageConfig,
|
|
161
162
|
coverageIncludeMissing,
|
|
162
|
-
|
|
163
|
+
coverageMethodForBrowsers,
|
|
164
|
+
coverageMethodForNodeJs,
|
|
163
165
|
coverageV8ConflictWarning,
|
|
164
166
|
coverageTempDirectoryRelativeUrl,
|
|
165
167
|
|
|
@@ -189,7 +191,7 @@ export const executeTestPlan = async ({
|
|
|
189
191
|
// keep this one first because it does ensureEmptyDirectory
|
|
190
192
|
// and in case coverage json file gets written in the same directory
|
|
191
193
|
// it must be done before
|
|
192
|
-
if (
|
|
194
|
+
if (coverageEnabled && coverageReportHtmlDirectory) {
|
|
193
195
|
const coverageHtmlDirectoryUrl = resolveDirectoryUrl(
|
|
194
196
|
coverageReportHtmlDirectory,
|
|
195
197
|
rootDirectoryUrl,
|
|
@@ -216,7 +218,7 @@ export const executeTestPlan = async ({
|
|
|
216
218
|
}),
|
|
217
219
|
)
|
|
218
220
|
}
|
|
219
|
-
if (
|
|
221
|
+
if (coverageEnabled && coverageReportJsonFile) {
|
|
220
222
|
const coverageJsonFileUrl = new URL(
|
|
221
223
|
coverageReportJsonFile,
|
|
222
224
|
rootDirectoryUrl,
|
|
@@ -229,7 +231,7 @@ export const executeTestPlan = async ({
|
|
|
229
231
|
}),
|
|
230
232
|
)
|
|
231
233
|
}
|
|
232
|
-
if (
|
|
234
|
+
if (coverageEnabled && coverageReportTextLog) {
|
|
233
235
|
promises.push(
|
|
234
236
|
generateCoverageTextLog(result.planCoverage, {
|
|
235
237
|
coverageReportSkipEmpty,
|