@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.
Files changed (56) hide show
  1. package/dist/controllable_child_process.mjs +129 -0
  2. package/dist/controllable_worker_thread.mjs +91 -0
  3. package/dist/importmap_node_loader.mjs +551 -0
  4. package/dist/js/execute_using_dynamic_import.js +850 -0
  5. package/dist/js/v8_coverage.js +508 -0
  6. package/dist/jsenv_test.js +5993 -0
  7. package/dist/no_experimental_warnings.cjs +10 -0
  8. package/package.json +56 -0
  9. package/src/coverage/babel_plugin_instrument.js +48 -0
  10. package/src/coverage/coverage_reporter_html_directory.js +32 -0
  11. package/src/coverage/coverage_reporter_json_file.js +17 -0
  12. package/src/coverage/coverage_reporter_text_log.js +19 -0
  13. package/src/coverage/empty_coverage_factory.js +52 -0
  14. package/src/coverage/file_by_file_coverage.js +25 -0
  15. package/src/coverage/istanbul_coverage_composition.js +28 -0
  16. package/src/coverage/istanbul_coverage_map_from_coverage.js +16 -0
  17. package/src/coverage/list_files_not_covered.js +15 -0
  18. package/src/coverage/missing_coverage.js +41 -0
  19. package/src/coverage/report_to_coverage.js +198 -0
  20. package/src/coverage/v8_and_istanbul.js +37 -0
  21. package/src/coverage/v8_coverage.js +26 -0
  22. package/src/coverage/v8_coverage_composition.js +24 -0
  23. package/src/coverage/v8_coverage_node_directory.js +85 -0
  24. package/src/coverage/v8_coverage_to_istanbul.js +99 -0
  25. package/src/execution/execute.js +111 -0
  26. package/src/execution/execute_steps.js +423 -0
  27. package/src/execution/execute_test_plan.js +372 -0
  28. package/src/execution/execution_colors.js +10 -0
  29. package/src/execution/execution_steps.js +65 -0
  30. package/src/execution/gc.js +9 -0
  31. package/src/execution/logs_file_execution.js +427 -0
  32. package/src/execution/logs_file_execution.test.mjs +41 -0
  33. package/src/execution/run.js +160 -0
  34. package/src/execution/web_server_param.js +73 -0
  35. package/src/main.js +10 -0
  36. package/src/runtime_browsers/chromium.js +24 -0
  37. package/src/runtime_browsers/firefox.js +23 -0
  38. package/src/runtime_browsers/middleware_istanbul.js +65 -0
  39. package/src/runtime_browsers/middleware_js_supervisor.js +98 -0
  40. package/src/runtime_browsers/using_playwright.js +573 -0
  41. package/src/runtime_browsers/webkit.js +40 -0
  42. package/src/runtime_node/child_exec_options.js +166 -0
  43. package/src/runtime_node/controllable_child_process.mjs +135 -0
  44. package/src/runtime_node/controllable_worker_thread.mjs +103 -0
  45. package/src/runtime_node/exec_options.js +57 -0
  46. package/src/runtime_node/execute_using_dynamic_import.js +53 -0
  47. package/src/runtime_node/exit_codes.js +9 -0
  48. package/src/runtime_node/importmap_node_loader.mjs +51 -0
  49. package/src/runtime_node/importmap_node_loader_file_url.js +4 -0
  50. package/src/runtime_node/kill_process_tree.js +76 -0
  51. package/src/runtime_node/no_experimental_warnings.cjs +13 -0
  52. package/src/runtime_node/no_experimental_warnings_file_url.js +4 -0
  53. package/src/runtime_node/node_child_process.js +374 -0
  54. package/src/runtime_node/node_execution_performance.js +67 -0
  55. package/src/runtime_node/node_worker_thread.js +301 -0
  56. package/src/runtime_node/profiler_v8_coverage.js +56 -0
@@ -0,0 +1,374 @@
1
+ import { fork } from "node:child_process"
2
+ import { fileURLToPath } from "node:url"
3
+ import {
4
+ Abort,
5
+ raceCallbacks,
6
+ createCallbackListNotifiedOnce,
7
+ } from "@jsenv/abort"
8
+ import { createDetailedMessage } from "@jsenv/log"
9
+ import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
10
+
11
+ import { createChildExecOptions } from "./child_exec_options.js"
12
+ import { ExecOptions } from "./exec_options.js"
13
+ import { killProcessTree } from "./kill_process_tree.js"
14
+ import { EXIT_CODES } from "./exit_codes.js"
15
+ import { IMPORTMAP_NODE_LOADER_FILE_URL } from "./importmap_node_loader_file_url.js"
16
+ import { NO_EXPERIMENTAL_WARNING_FILE_URL } from "./no_experimental_warnings_file_url.js"
17
+
18
+ const CONTROLLABLE_CHILD_PROCESS_URL = new URL(
19
+ "./controllable_child_process.mjs?entry_point",
20
+ import.meta.url,
21
+ ).href
22
+
23
+ export const nodeChildProcess = ({
24
+ logProcessCommand = false,
25
+ importMap,
26
+ gracefulStopAllocatedMs = 4000,
27
+ env,
28
+ debugPort,
29
+ debugMode,
30
+ debugModeInheritBreak,
31
+ inheritProcessEnv = true,
32
+ commandLineOptions = [],
33
+ stdin = "pipe",
34
+ stdout = "pipe",
35
+ stderr = "pipe",
36
+ } = {}) => {
37
+ if (env !== undefined && typeof env !== "object") {
38
+ throw new TypeError(`env must be an object, got ${env}`)
39
+ }
40
+ env = {
41
+ ...env,
42
+ JSENV: true,
43
+ }
44
+
45
+ return {
46
+ type: "node",
47
+ name: "node_child_process",
48
+ version: process.version.slice(1),
49
+ run: async ({
50
+ signal = new AbortController().signal,
51
+ logger,
52
+
53
+ rootDirectoryUrl,
54
+ fileRelativeUrl,
55
+
56
+ keepRunning,
57
+ stopSignal,
58
+ onConsole,
59
+
60
+ coverageEnabled = false,
61
+ coverageConfig,
62
+ coverageMethodForNodeJs,
63
+ coverageFileUrl,
64
+ collectPerformance,
65
+ }) => {
66
+ if (coverageMethodForNodeJs !== "NODE_V8_COVERAGE") {
67
+ env.NODE_V8_COVERAGE = ""
68
+ }
69
+ commandLineOptions = [
70
+ "--experimental-import-meta-resolve",
71
+ ...commandLineOptions,
72
+ ]
73
+
74
+ if (importMap) {
75
+ env.IMPORT_MAP = JSON.stringify(importMap)
76
+ env.IMPORT_MAP_BASE_URL = rootDirectoryUrl
77
+ commandLineOptions.push(
78
+ `--experimental-loader=${IMPORTMAP_NODE_LOADER_FILE_URL}`,
79
+ )
80
+ commandLineOptions.push(
81
+ `--require=${fileURLToPath(NO_EXPERIMENTAL_WARNING_FILE_URL)}`,
82
+ )
83
+ }
84
+
85
+ const cleanupCallbackList = createCallbackListNotifiedOnce()
86
+ const cleanup = async (reason) => {
87
+ await cleanupCallbackList.notify({ reason })
88
+ }
89
+
90
+ const childExecOptions = await createChildExecOptions({
91
+ signal,
92
+ debugPort,
93
+ debugMode,
94
+ debugModeInheritBreak,
95
+ })
96
+ const execArgv = ExecOptions.toExecArgv({
97
+ ...childExecOptions,
98
+ ...ExecOptions.fromExecArgv(commandLineOptions),
99
+ })
100
+ const envForChildProcess = {
101
+ ...(inheritProcessEnv ? process.env : {}),
102
+ ...env,
103
+ }
104
+ logger[logProcessCommand ? "info" : "debug"](
105
+ `${process.argv[0]} ${execArgv.join(" ")} ${fileURLToPath(
106
+ CONTROLLABLE_CHILD_PROCESS_URL,
107
+ )}`,
108
+ )
109
+ const childProcess = fork(fileURLToPath(CONTROLLABLE_CHILD_PROCESS_URL), {
110
+ execArgv,
111
+ // silent: true
112
+ stdio: ["pipe", "pipe", "pipe", "ipc"],
113
+ env: envForChildProcess,
114
+ })
115
+ logger.debug(
116
+ createDetailedMessage(
117
+ `child process forked (pid ${childProcess.pid})`,
118
+ {
119
+ "custom env": JSON.stringify(env, null, " "),
120
+ },
121
+ ),
122
+ )
123
+ // if we pass stream, pipe them https://github.com/sindresorhus/execa/issues/81
124
+ if (typeof stdin === "object") {
125
+ stdin.pipe(childProcess.stdin)
126
+ }
127
+ if (typeof stdout === "object") {
128
+ childProcess.stdout.pipe(stdout)
129
+ }
130
+ if (typeof stderr === "object") {
131
+ childProcess.stderr.pipe(stderr)
132
+ }
133
+ const childProcessReadyPromise = new Promise((resolve) => {
134
+ onceChildProcessMessage(childProcess, "ready", resolve)
135
+ })
136
+ const removeOutputListener = installChildProcessOutputListener(
137
+ childProcess,
138
+ ({ type, text }) => {
139
+ onConsole({ type, text })
140
+ },
141
+ )
142
+ const stop = memoize(async ({ gracefulStopAllocatedMs } = {}) => {
143
+ // all libraries are facing problem on windows when trying
144
+ // to kill a process spawning other processes.
145
+ // "killProcessTree" is theorically correct but sometimes keep process handing forever.
146
+ // Inside GitHub workflow the whole Virtual machine gets unresponsive and ends up being killed
147
+ // There is no satisfying solution to this problem so we stick to the basic
148
+ // childProcess.kill()
149
+ if (process.platform === "win32") {
150
+ childProcess.kill()
151
+ return
152
+ }
153
+ if (gracefulStopAllocatedMs) {
154
+ try {
155
+ await killProcessTree(childProcess.pid, {
156
+ signal: GRACEFUL_STOP_SIGNAL,
157
+ timeout: gracefulStopAllocatedMs,
158
+ })
159
+ return
160
+ } catch (e) {
161
+ if (e.code === "TIMEOUT") {
162
+ logger.debug(
163
+ `kill with SIGTERM because gracefulStop still pending after ${gracefulStopAllocatedMs}ms`,
164
+ )
165
+ await killProcessTree(childProcess.pid, {
166
+ signal: GRACEFUL_STOP_FAILED_SIGNAL,
167
+ })
168
+ return
169
+ }
170
+ throw e
171
+ }
172
+ }
173
+ await killProcessTree(childProcess.pid, { signal: STOP_SIGNAL })
174
+ return
175
+ })
176
+
177
+ const actionOperation = Abort.startOperation()
178
+ actionOperation.addAbortSignal(signal)
179
+ const winnerPromise = new Promise((resolve) => {
180
+ raceCallbacks(
181
+ {
182
+ aborted: (cb) => {
183
+ return actionOperation.addAbortCallback(cb)
184
+ },
185
+ // https://nodejs.org/api/child_process.html#child_process_event_disconnect
186
+ // disconnect: (cb) => {
187
+ // return onceProcessEvent(childProcess, "disconnect", cb)
188
+ // },
189
+ // https://nodejs.org/api/child_process.html#child_process_event_error
190
+ error: (cb) => {
191
+ return onceChildProcessEvent(childProcess, "error", cb)
192
+ },
193
+ exit: (cb) => {
194
+ return onceChildProcessEvent(
195
+ childProcess,
196
+ "exit",
197
+ (code, signal) => {
198
+ cb({ code, signal })
199
+ },
200
+ )
201
+ },
202
+ response: (cb) => {
203
+ return onceChildProcessMessage(childProcess, "action-result", cb)
204
+ },
205
+ },
206
+ resolve,
207
+ )
208
+ })
209
+ const result = {
210
+ status: "executing",
211
+ errors: [],
212
+ namespace: null,
213
+ }
214
+
215
+ const writeResult = async () => {
216
+ actionOperation.throwIfAborted()
217
+ await childProcessReadyPromise
218
+ actionOperation.throwIfAborted()
219
+ await sendToChildProcess(childProcess, {
220
+ type: "action",
221
+ data: {
222
+ actionType: "execute-using-dynamic-import",
223
+ actionParams: {
224
+ rootDirectoryUrl,
225
+ fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
226
+ collectPerformance,
227
+ coverageEnabled,
228
+ coverageConfig,
229
+ coverageMethodForNodeJs,
230
+ coverageFileUrl,
231
+ exitAfterAction: true,
232
+ },
233
+ },
234
+ })
235
+ const winner = await winnerPromise
236
+ if (winner.name === "aborted") {
237
+ result.status = "aborted"
238
+ return
239
+ }
240
+ if (winner.name === "error") {
241
+ const error = winner.data
242
+ removeOutputListener()
243
+ result.status = "failed"
244
+ result.errors.push(error)
245
+ return
246
+ }
247
+ if (winner.name === "exit") {
248
+ const { code } = winner.data
249
+ await cleanup("process exit")
250
+ if (code === 12) {
251
+ result.status = "failed"
252
+ result.errors.push(
253
+ new Error(
254
+ `node process exited with 12 (the forked child process wanted to use a non-available port for debug)`,
255
+ ),
256
+ )
257
+ return
258
+ }
259
+ if (
260
+ code === null ||
261
+ code === 0 ||
262
+ code === EXIT_CODES.SIGINT ||
263
+ code === EXIT_CODES.SIGTERM ||
264
+ code === EXIT_CODES.SIGABORT
265
+ ) {
266
+ result.status = "failed"
267
+ result.errors.push(
268
+ new Error(`node process exited during execution`),
269
+ )
270
+ return
271
+ }
272
+ // process.exit(1) in child process or process.exitCode = 1 + process.exit()
273
+ // means there was an error even if we don't know exactly what.
274
+ result.status = "failed"
275
+ result.errors.push(
276
+ new Error(`node process exited with code ${code} during execution`),
277
+ )
278
+ return
279
+ }
280
+ const { status, value } = winner.data
281
+ if (status === "action-failed") {
282
+ result.status = "failed"
283
+ result.errors.push(value)
284
+ return
285
+ }
286
+ const { namespace, performance, coverage } = value
287
+ result.status = "completed"
288
+ result.namespace = namespace
289
+ result.performance = performance
290
+ result.coverage = coverage
291
+ }
292
+
293
+ try {
294
+ await writeResult()
295
+ } catch (e) {
296
+ result.status = "failed"
297
+ result.errors.push(e)
298
+ }
299
+ if (keepRunning) {
300
+ stopSignal.notify = stop
301
+ } else {
302
+ await stop({
303
+ gracefulStopAllocatedMs,
304
+ })
305
+ }
306
+ await actionOperation.end()
307
+ return result
308
+ },
309
+ }
310
+ }
311
+
312
+ // http://man7.org/linux/man-pages/man7/signal.7.html
313
+ // https:// github.com/nodejs/node/blob/1d9511127c419ec116b3ddf5fc7a59e8f0f1c1e4/lib/internal/child_process.js#L472
314
+ const GRACEFUL_STOP_SIGNAL = "SIGTERM"
315
+ const STOP_SIGNAL = "SIGKILL"
316
+ // it would be more correct if GRACEFUL_STOP_FAILED_SIGNAL was SIGHUP instead of SIGKILL.
317
+ // but I'm not sure and it changes nothing so just use SIGKILL
318
+ const GRACEFUL_STOP_FAILED_SIGNAL = "SIGKILL"
319
+
320
+ const sendToChildProcess = async (childProcess, { type, data }) => {
321
+ return new Promise((resolve, reject) => {
322
+ childProcess.send(
323
+ {
324
+ jsenv: true,
325
+ type,
326
+ data,
327
+ },
328
+ (error) => {
329
+ if (error) {
330
+ reject(error)
331
+ } else {
332
+ resolve()
333
+ }
334
+ },
335
+ )
336
+ })
337
+ }
338
+
339
+ const installChildProcessOutputListener = (childProcess, callback) => {
340
+ // beware that we may receive ansi output here, should not be a problem but keep that in mind
341
+ const stdoutDataCallback = (chunk) => {
342
+ callback({ type: "log", text: String(chunk) })
343
+ }
344
+ childProcess.stdout.on("data", stdoutDataCallback)
345
+ const stdErrorDataCallback = (chunk) => {
346
+ callback({ type: "error", text: String(chunk) })
347
+ }
348
+ childProcess.stderr.on("data", stdErrorDataCallback)
349
+ return () => {
350
+ childProcess.stdout.removeListener("data", stdoutDataCallback)
351
+ childProcess.stderr.removeListener("data", stdoutDataCallback)
352
+ }
353
+ }
354
+
355
+ const onceChildProcessMessage = (childProcess, type, callback) => {
356
+ const onmessage = (message) => {
357
+ if (message && message.jsenv && message.type === type) {
358
+ childProcess.removeListener("message", onmessage)
359
+ // eslint-disable-next-line no-eval
360
+ callback(message.data ? eval(`(${message.data})`) : "")
361
+ }
362
+ }
363
+ childProcess.on("message", onmessage)
364
+ return () => {
365
+ childProcess.removeListener("message", onmessage)
366
+ }
367
+ }
368
+
369
+ const onceChildProcessEvent = (childProcess, type, callback) => {
370
+ childProcess.once(type, callback)
371
+ return () => {
372
+ childProcess.removeListener(type, callback)
373
+ }
374
+ }
@@ -0,0 +1,67 @@
1
+ import { PerformanceObserver, performance } from "node:perf_hooks"
2
+
3
+ export const startObservingPerformances = () => {
4
+ const measureEntries = []
5
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html
6
+ const perfObserver = new PerformanceObserver(
7
+ (
8
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceobserverentrylist
9
+ list,
10
+ ) => {
11
+ const perfMeasureEntries = list.getEntriesByType("measure")
12
+ measureEntries.push(...perfMeasureEntries)
13
+ },
14
+ )
15
+ perfObserver.observe({
16
+ entryTypes: ["measure"],
17
+ })
18
+ return async () => {
19
+ // wait for node to call the performance observer
20
+ await new Promise((resolve) => {
21
+ setTimeout(resolve)
22
+ })
23
+ performance.clearMarks()
24
+ perfObserver.disconnect()
25
+ return {
26
+ ...readNodePerformance(),
27
+ measures: measuresFromMeasureEntries(measureEntries),
28
+ }
29
+ }
30
+ }
31
+
32
+ const readNodePerformance = () => {
33
+ const nodePerformance = {
34
+ nodeTiming: asPlainObject(performance.nodeTiming),
35
+ timeOrigin: performance.timeOrigin,
36
+ eventLoopUtilization: performance.eventLoopUtilization(),
37
+ }
38
+ return nodePerformance
39
+ }
40
+
41
+ // remove getters that cannot be stringified
42
+ const asPlainObject = (objectWithGetters) => {
43
+ const objectWithoutGetters = {}
44
+ Object.keys(objectWithGetters).forEach((key) => {
45
+ objectWithoutGetters[key] = objectWithGetters[key]
46
+ })
47
+ return objectWithoutGetters
48
+ }
49
+
50
+ const measuresFromMeasureEntries = (measureEntries) => {
51
+ const measures = {}
52
+ // Sort to ensure measures order is predictable
53
+ // It seems to be already predictable on Node 16+ but
54
+ // it's not the case on Node 14.
55
+ measureEntries.sort((a, b) => {
56
+ return a.startTime - b.startTime
57
+ })
58
+ measureEntries.forEach(
59
+ (
60
+ // https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#perf_hooks_class_performanceentry
61
+ perfMeasureEntry,
62
+ ) => {
63
+ measures[perfMeasureEntry.name] = perfMeasureEntry.duration
64
+ },
65
+ )
66
+ return measures
67
+ }