@jsenv/core 27.1.0 → 27.2.2
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 +580 -806
- package/package.json +10 -9
- 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/chromium.js +1 -1
- package/src/execute/runtimes/browsers/firefox.js +1 -1
- package/src/execute/runtimes/browsers/from_playwright.js +13 -8
- package/src/execute/runtimes/browsers/webkit.js +1 -1
- 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
|
@@ -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,
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import v8 from "node:v8";
|
|
2
|
-
import { u as uneval } from "./uneval.js";
|
|
3
|
-
import { performance, PerformanceObserver } from "node:perf_hooks";
|
|
4
|
-
|
|
5
|
-
const startObservingPerformances = () => {
|
|
6
|
-
const measureEntries = []; // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html
|
|
7
|
-
|
|
8
|
-
const perfObserver = new PerformanceObserver(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceobserverentrylist
|
|
9
|
-
list) => {
|
|
10
|
-
const perfMeasureEntries = list.getEntriesByType("measure");
|
|
11
|
-
measureEntries.push(...perfMeasureEntries);
|
|
12
|
-
});
|
|
13
|
-
perfObserver.observe({
|
|
14
|
-
entryTypes: ["measure"]
|
|
15
|
-
});
|
|
16
|
-
return async () => {
|
|
17
|
-
// wait for node to call the performance observer
|
|
18
|
-
await new Promise(resolve => {
|
|
19
|
-
setTimeout(resolve);
|
|
20
|
-
});
|
|
21
|
-
performance.clearMarks();
|
|
22
|
-
perfObserver.disconnect();
|
|
23
|
-
return { ...readNodePerformance(),
|
|
24
|
-
measures: measuresFromMeasureEntries(measureEntries)
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const readNodePerformance = () => {
|
|
30
|
-
const nodePerformance = {
|
|
31
|
-
nodeTiming: asPlainObject(performance.nodeTiming),
|
|
32
|
-
timeOrigin: performance.timeOrigin,
|
|
33
|
-
eventLoopUtilization: performance.eventLoopUtilization()
|
|
34
|
-
};
|
|
35
|
-
return nodePerformance;
|
|
36
|
-
}; // remove getters that cannot be stringified
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const asPlainObject = objectWithGetters => {
|
|
40
|
-
const objectWithoutGetters = {};
|
|
41
|
-
Object.keys(objectWithGetters).forEach(key => {
|
|
42
|
-
objectWithoutGetters[key] = objectWithGetters[key];
|
|
43
|
-
});
|
|
44
|
-
return objectWithoutGetters;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const measuresFromMeasureEntries = measureEntries => {
|
|
48
|
-
const measures = {}; // Sort to ensure measures order is predictable
|
|
49
|
-
// It seems to be already predictable on Node 16+ but
|
|
50
|
-
// it's not the case on Node 14.
|
|
51
|
-
|
|
52
|
-
measureEntries.sort((a, b) => {
|
|
53
|
-
return a.startTime - b.startTime;
|
|
54
|
-
});
|
|
55
|
-
measureEntries.forEach(( // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceentry
|
|
56
|
-
perfMeasureEntry) => {
|
|
57
|
-
measures[perfMeasureEntry.name] = perfMeasureEntry.duration;
|
|
58
|
-
});
|
|
59
|
-
return measures;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const ACTIONS_AVAILABLE = {
|
|
63
|
-
"execute-using-dynamic-import": async ({
|
|
64
|
-
fileUrl,
|
|
65
|
-
collectPerformance
|
|
66
|
-
}) => {
|
|
67
|
-
const getNamespace = async () => {
|
|
68
|
-
const namespace = await import(fileUrl);
|
|
69
|
-
const namespaceResolved = {};
|
|
70
|
-
await Promise.all([...Object.keys(namespace).map(async key => {
|
|
71
|
-
const value = await namespace[key];
|
|
72
|
-
namespaceResolved[key] = value;
|
|
73
|
-
})]);
|
|
74
|
-
return namespaceResolved;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (collectPerformance) {
|
|
78
|
-
const getPerformance = startObservingPerformances();
|
|
79
|
-
const namespace = await getNamespace();
|
|
80
|
-
const performance = await getPerformance();
|
|
81
|
-
return {
|
|
82
|
-
namespace,
|
|
83
|
-
performance
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const namespace = await getNamespace();
|
|
88
|
-
return {
|
|
89
|
-
namespace
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
"execute-using-require": async ({
|
|
93
|
-
fileUrl
|
|
94
|
-
}) => {
|
|
95
|
-
const {
|
|
96
|
-
createRequire
|
|
97
|
-
} = await import("module");
|
|
98
|
-
const {
|
|
99
|
-
fileURLToPath
|
|
100
|
-
} = await import("url");
|
|
101
|
-
const filePath = fileURLToPath(fileUrl);
|
|
102
|
-
|
|
103
|
-
const require = createRequire(fileUrl); // eslint-disable-next-line import/no-dynamic-require
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const namespace = require(filePath);
|
|
107
|
-
|
|
108
|
-
const namespaceResolved = {};
|
|
109
|
-
await Promise.all([...Object.keys(namespace).map(async key => {
|
|
110
|
-
const value = await namespace[key];
|
|
111
|
-
namespaceResolved[key] = value;
|
|
112
|
-
})]);
|
|
113
|
-
return namespaceResolved;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const ACTION_REQUEST_EVENT_NAME = "action";
|
|
117
|
-
const ACTION_RESPONSE_EVENT_NAME = "action-result";
|
|
118
|
-
const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
|
|
119
|
-
const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
|
|
120
|
-
|
|
121
|
-
const sendActionFailed = error => {
|
|
122
|
-
if (error.hasOwnProperty("toString")) {
|
|
123
|
-
delete error.toString;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
sendToParent(ACTION_RESPONSE_EVENT_NAME, // process.send algorithm does not send non enumerable values
|
|
127
|
-
// so use @jsenv/uneval
|
|
128
|
-
uneval({
|
|
129
|
-
status: ACTION_RESPONSE_STATUS_FAILED,
|
|
130
|
-
value: error
|
|
131
|
-
}, {
|
|
132
|
-
ignoreSymbols: true
|
|
133
|
-
}));
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const sendActionCompleted = value => {
|
|
137
|
-
sendToParent(ACTION_RESPONSE_EVENT_NAME, // here we use JSON.stringify because we should not
|
|
138
|
-
// have non enumerable value (unlike there is on Error objects)
|
|
139
|
-
// otherwise uneval is quite slow to turn a giant object
|
|
140
|
-
// into a string (and value can be giant when using coverage)
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
status: ACTION_RESPONSE_STATUS_COMPLETED,
|
|
143
|
-
value
|
|
144
|
-
}));
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const sendToParent = (type, data) => {
|
|
148
|
-
// https://nodejs.org/api/process.html#process_process_connected
|
|
149
|
-
// not connected anymore, cannot communicate with parent
|
|
150
|
-
if (!process.connected) {
|
|
151
|
-
return;
|
|
152
|
-
} // this can keep process alive longer than expected
|
|
153
|
-
// when source is a long string.
|
|
154
|
-
// It means node process may stay alive longer than expected
|
|
155
|
-
// the time to send the data to the parent.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
process.send({
|
|
159
|
-
type,
|
|
160
|
-
data
|
|
161
|
-
});
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const onceProcessMessage = (type, callback) => {
|
|
165
|
-
const listener = event => {
|
|
166
|
-
if (event.type === type) {
|
|
167
|
-
// commenting line below keep this process alive
|
|
168
|
-
removeListener(); // eslint-disable-next-line no-eval
|
|
169
|
-
|
|
170
|
-
callback(eval(`(${event.data})`));
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const removeListener = () => {
|
|
175
|
-
process.removeListener("message", listener);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
process.on("message", listener);
|
|
179
|
-
return removeListener;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const removeActionRequestListener = onceProcessMessage(ACTION_REQUEST_EVENT_NAME, async ({
|
|
183
|
-
actionType,
|
|
184
|
-
actionParams
|
|
185
|
-
}) => {
|
|
186
|
-
const action = ACTIONS_AVAILABLE[actionType];
|
|
187
|
-
|
|
188
|
-
if (!action) {
|
|
189
|
-
sendActionFailed(new Error(`unknown action ${actionType}`));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let value;
|
|
194
|
-
let failed = false;
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
value = await action(actionParams);
|
|
198
|
-
} catch (e) {
|
|
199
|
-
failed = true;
|
|
200
|
-
value = e;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (process.env.NODE_V8_COVERAGE) {
|
|
204
|
-
v8.takeCoverage(); // if (actionParams.stopCoverageAfterExecution) {
|
|
205
|
-
// v8.stopCoverage()
|
|
206
|
-
// }
|
|
207
|
-
} // setTimeout(() => {}, 100)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (failed) {
|
|
211
|
-
sendActionFailed(value);
|
|
212
|
-
} else {
|
|
213
|
-
sendActionCompleted(value);
|
|
214
|
-
} // removeActionRequestListener()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (actionParams.exitAfterAction) {
|
|
218
|
-
removeActionRequestListener(); // for some reason this fixes v8 coverage directory sometimes empty on Ubuntu
|
|
219
|
-
// process.exit()
|
|
220
|
-
}
|
|
221
|
-
}); // remove listener to process.on('message')
|
|
222
|
-
// which is sufficient to let child process die
|
|
223
|
-
// assuming nothing else keeps it alive
|
|
224
|
-
// process.once("SIGTERM", removeActionRequestListener)
|
|
225
|
-
// process.once("SIGINT", removeActionRequestListener)
|
|
226
|
-
|
|
227
|
-
setTimeout(() => sendToParent("ready"));
|