@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,301 @@
1
+ // https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/fork.js#L23
2
+ // https://nodejs.org/api/worker_threads.html
3
+ // https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/worker/base.js
4
+ import { Worker } from "node:worker_threads"
5
+ import { fileURLToPath } from "node:url"
6
+ import {
7
+ Abort,
8
+ createCallbackListNotifiedOnce,
9
+ raceCallbacks,
10
+ } from "@jsenv/abort"
11
+ import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
12
+
13
+ import { createChildExecOptions } from "./child_exec_options.js"
14
+ import { ExecOptions } from "./exec_options.js"
15
+ import { EXIT_CODES } from "./exit_codes.js"
16
+ import { IMPORTMAP_NODE_LOADER_FILE_URL } from "./importmap_node_loader_file_url.js"
17
+ import { NO_EXPERIMENTAL_WARNING_FILE_URL } from "./no_experimental_warnings_file_url.js"
18
+
19
+ const CONTROLLABLE_WORKER_THREAD_URL = new URL(
20
+ "./controllable_worker_thread.mjs?entry_point",
21
+ import.meta.url,
22
+ ).href
23
+
24
+ export const nodeWorkerThread = ({
25
+ importMap,
26
+ env,
27
+ debugPort,
28
+ debugMode,
29
+ debugModeInheritBreak,
30
+ inheritProcessEnv = true,
31
+ commandLineOptions = [],
32
+ } = {}) => {
33
+ if (env !== undefined && typeof env !== "object") {
34
+ throw new TypeError(`env must be an object, got ${env}`)
35
+ }
36
+ env = {
37
+ ...env,
38
+ JSENV: true,
39
+ }
40
+
41
+ return {
42
+ type: "node",
43
+ name: "node_worker_thread",
44
+ version: process.version.slice(1),
45
+ run: async ({
46
+ signal = new AbortController().signal,
47
+ // logger,
48
+ rootDirectoryUrl,
49
+ fileRelativeUrl,
50
+
51
+ keepRunning,
52
+ stopSignal,
53
+ onConsole,
54
+
55
+ collectConsole = false,
56
+ collectPerformance,
57
+ coverageEnabled = false,
58
+ coverageConfig,
59
+ coverageMethodForNodeJs,
60
+ coverageFileUrl,
61
+ }) => {
62
+ if (coverageMethodForNodeJs !== "NODE_V8_COVERAGE") {
63
+ env.NODE_V8_COVERAGE = ""
64
+ }
65
+ if (importMap) {
66
+ env.IMPORT_MAP = JSON.stringify(importMap)
67
+ env.IMPORT_MAP_BASE_URL = rootDirectoryUrl
68
+ commandLineOptions.push(
69
+ `--experimental-loader=${IMPORTMAP_NODE_LOADER_FILE_URL}`,
70
+ )
71
+ commandLineOptions.push(
72
+ `--require=${fileURLToPath(NO_EXPERIMENTAL_WARNING_FILE_URL)}`,
73
+ )
74
+ }
75
+
76
+ const workerThreadExecOptions = await createChildExecOptions({
77
+ signal,
78
+ debugPort,
79
+ debugMode,
80
+ debugModeInheritBreak,
81
+ })
82
+ const execArgvForWorkerThread = ExecOptions.toExecArgv({
83
+ ...workerThreadExecOptions,
84
+ ...ExecOptions.fromExecArgv(commandLineOptions),
85
+ })
86
+ const envForWorkerThread = {
87
+ ...(inheritProcessEnv ? process.env : {}),
88
+ ...env,
89
+ }
90
+
91
+ const cleanupCallbackList = createCallbackListNotifiedOnce()
92
+ const cleanup = async (reason) => {
93
+ await cleanupCallbackList.notify({ reason })
94
+ }
95
+ const actionOperation = Abort.startOperation()
96
+ actionOperation.addAbortSignal(signal)
97
+ // https://nodejs.org/api/worker_threads.html#new-workerfilename-options
98
+ const workerThread = new Worker(
99
+ fileURLToPath(CONTROLLABLE_WORKER_THREAD_URL),
100
+ {
101
+ env: envForWorkerThread,
102
+ execArgv: execArgvForWorkerThread,
103
+ // workerData: { options },
104
+ stdin: true,
105
+ stdout: true,
106
+ stderr: true,
107
+ },
108
+ )
109
+ const removeOutputListener = installWorkerThreadOutputListener(
110
+ workerThread,
111
+ ({ type, text }) => {
112
+ onConsole({ type, text })
113
+ },
114
+ )
115
+ const workerThreadReadyPromise = new Promise((resolve) => {
116
+ onceWorkerThreadMessage(workerThread, "ready", resolve)
117
+ })
118
+
119
+ const stop = memoize(async () => {
120
+ // read all stdout before terminating
121
+ // (no need for stderr because it's sync)
122
+ if (collectConsole) {
123
+ while (workerThread.stdout.read() !== null) {}
124
+ await new Promise((resolve) => {
125
+ setTimeout(resolve, 50)
126
+ })
127
+ }
128
+ await workerThread.terminate()
129
+ })
130
+
131
+ const winnerPromise = new Promise((resolve) => {
132
+ raceCallbacks(
133
+ {
134
+ aborted: (cb) => {
135
+ return actionOperation.addAbortCallback(cb)
136
+ },
137
+ error: (cb) => {
138
+ return onceWorkerThreadEvent(workerThread, "error", cb)
139
+ },
140
+ exit: (cb) => {
141
+ return onceWorkerThreadEvent(
142
+ workerThread,
143
+ "exit",
144
+ (code, signal) => {
145
+ cb({ code, signal })
146
+ },
147
+ )
148
+ },
149
+ response: (cb) => {
150
+ return onceWorkerThreadMessage(workerThread, "action-result", cb)
151
+ },
152
+ },
153
+ resolve,
154
+ )
155
+ })
156
+
157
+ const result = {
158
+ status: "executing",
159
+ errors: [],
160
+ namespace: null,
161
+ }
162
+
163
+ const writeResult = async () => {
164
+ actionOperation.throwIfAborted()
165
+ await workerThreadReadyPromise
166
+ actionOperation.throwIfAborted()
167
+ await sendToWorkerThread(workerThread, {
168
+ type: "action",
169
+ data: {
170
+ actionType: "execute-using-dynamic-import",
171
+ actionParams: {
172
+ rootDirectoryUrl,
173
+ fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
174
+ collectPerformance,
175
+ coverageEnabled,
176
+ coverageConfig,
177
+ coverageMethodForNodeJs,
178
+ coverageFileUrl,
179
+ exitAfterAction: true,
180
+ },
181
+ },
182
+ })
183
+ const winner = await winnerPromise
184
+ if (winner.name === "aborted") {
185
+ result.status = "aborted"
186
+ return
187
+ }
188
+ if (winner.name === "error") {
189
+ const error = winner.data
190
+ removeOutputListener()
191
+ result.status = "failed"
192
+ result.errors.push(error)
193
+ return
194
+ }
195
+ if (winner.name === "exit") {
196
+ const { code } = winner.data
197
+ await cleanup("process exit")
198
+ if (code === 12) {
199
+ result.status = "failed"
200
+ result.errors.push(
201
+ new Error(
202
+ `node process exited with 12 (the forked child process wanted to use a non-available port for debug)`,
203
+ ),
204
+ )
205
+ return
206
+ }
207
+ if (
208
+ code === null ||
209
+ code === 0 ||
210
+ code === EXIT_CODES.SIGINT ||
211
+ code === EXIT_CODES.SIGTERM ||
212
+ code === EXIT_CODES.SIGABORT
213
+ ) {
214
+ result.status = "failed"
215
+ result.errors.push(
216
+ new Error(`node worker thread exited during execution`),
217
+ )
218
+ return
219
+ }
220
+ // process.exit(1) in child process or process.exitCode = 1 + process.exit()
221
+ // means there was an error even if we don't know exactly what.
222
+ result.status = "failed"
223
+ result.errors.push(
224
+ new Error(
225
+ `node worker thread exited with code ${code} during execution`,
226
+ ),
227
+ )
228
+ }
229
+ const { status, value } = winner.data
230
+ if (status === "action-failed") {
231
+ result.status = "failed"
232
+ result.errors.push(value)
233
+ return
234
+ }
235
+ const { namespace, performance, coverage } = value
236
+ result.status = "completed"
237
+ result.namespace = namespace
238
+ result.performance = performance
239
+ result.coverage = coverage
240
+ }
241
+
242
+ try {
243
+ await writeResult()
244
+ } catch (e) {
245
+ result.status = "failed"
246
+ result.errors.push(e)
247
+ }
248
+
249
+ if (keepRunning) {
250
+ stopSignal.notify = stop
251
+ } else {
252
+ await stop()
253
+ }
254
+ await actionOperation.end()
255
+ return result
256
+ },
257
+ }
258
+ }
259
+
260
+ const installWorkerThreadOutputListener = (workerThread, callback) => {
261
+ // beware that we may receive ansi output here, should not be a problem but keep that in mind
262
+ const stdoutDataCallback = (chunk) => {
263
+ const text = String(chunk)
264
+ callback({ type: "log", text })
265
+ }
266
+ workerThread.stdout.on("data", stdoutDataCallback)
267
+ const stdErrorDataCallback = (chunk) => {
268
+ const text = String(chunk)
269
+ callback({ type: "error", text })
270
+ }
271
+ workerThread.stderr.on("data", stdErrorDataCallback)
272
+ return () => {
273
+ workerThread.stdout.removeListener("data", stdoutDataCallback)
274
+ workerThread.stderr.removeListener("data", stdErrorDataCallback)
275
+ }
276
+ }
277
+
278
+ const sendToWorkerThread = (worker, { type, data }) => {
279
+ worker.postMessage({ jsenv: true, type, data })
280
+ }
281
+
282
+ const onceWorkerThreadMessage = (workerThread, type, callback) => {
283
+ const onmessage = (message) => {
284
+ if (message && message.jsenv && message.type === type) {
285
+ workerThread.removeListener("message", onmessage)
286
+ // eslint-disable-next-line no-eval
287
+ callback(message.data ? eval(`(${message.data})`) : undefined)
288
+ }
289
+ }
290
+ workerThread.on("message", onmessage)
291
+ return () => {
292
+ workerThread.removeListener("message", onmessage)
293
+ }
294
+ }
295
+
296
+ const onceWorkerThreadEvent = (worker, type, callback) => {
297
+ worker.once(type, callback)
298
+ return () => {
299
+ worker.removeListener(type, callback)
300
+ }
301
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Calling Profiler.startPreciseCoverage DO NOT propagate to
3
+ * subprocesses (new Worker or child_process.fork())
4
+ * So the best solution remains NODE_V8_COVERAGE
5
+ * This profiler strategy remains useful when:
6
+ * - As fallback when NODE_V8_COVERAGE is not configured
7
+ * - If explicitely enabled with coverageMethodForNodeJs: 'Profiler'
8
+ * - Used by jsenv during automated tests about coverage
9
+ * - Anyone prefering this approach over NODE_V8_COVERAGE and assuming
10
+ * it will not fork subprocess or don't care if coverage is missed for this code
11
+ * - https://v8.dev/blog/javascript-code-coverage#for-embedders
12
+ * - https://github.com/nodejs/node/issues/28283
13
+ * - https://vanilla.aslushnikov.com/?Profiler.startPreciseCoverage
14
+ */
15
+
16
+ import { Session } from "node:inspector"
17
+
18
+ export const startJsCoverage = async ({
19
+ callCount = true,
20
+ detailed = true,
21
+ } = {}) => {
22
+ const session = new Session()
23
+ session.connect()
24
+ const postSession = (action, options) => {
25
+ const promise = new Promise((resolve, reject) => {
26
+ session.post(action, options, (error, data) => {
27
+ if (error) {
28
+ reject(error)
29
+ } else {
30
+ resolve(data)
31
+ }
32
+ })
33
+ })
34
+ return promise
35
+ }
36
+
37
+ await postSession("Profiler.enable")
38
+ await postSession("Profiler.startPreciseCoverage", { callCount, detailed })
39
+
40
+ const takeJsCoverage = async () => {
41
+ const coverage = await postSession("Profiler.takePreciseCoverage")
42
+ return coverage
43
+ }
44
+
45
+ const stopJsCoverage = async () => {
46
+ const coverage = await takeJsCoverage()
47
+ await postSession("Profiler.stopPreciseCoverage")
48
+ await postSession("Profiler.disable")
49
+ return coverage
50
+ }
51
+
52
+ return {
53
+ takeJsCoverage,
54
+ stopJsCoverage,
55
+ }
56
+ }