@jsenv/test 2.1.3 → 3.0.1

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 (46) hide show
  1. package/README.md +2 -1
  2. package/dist/js/exception.js +908 -0
  3. package/dist/js/execute_using_dynamic_import.js +76 -167
  4. package/dist/js/v8_coverage.js +4 -482
  5. package/dist/jsenv_test.js +4608 -2917
  6. package/dist/node_child_process_controlled.mjs +95 -76
  7. package/dist/node_worker_thread_controlled.mjs +72 -54
  8. package/package.json +23 -14
  9. package/src/coverage/generate_coverage.js +186 -0
  10. package/src/coverage/list_files_not_covered.js +2 -2
  11. package/src/coverage/missing_coverage.js +2 -2
  12. package/src/coverage/report_coverage_as_html.js +45 -0
  13. package/src/coverage/report_coverage_as_json.js +20 -0
  14. package/src/coverage/{coverage_reporter_text_log.js → report_coverage_in_console.js} +13 -7
  15. package/src/coverage/v8_and_istanbul.js +4 -4
  16. package/src/coverage/v8_coverage.js +2 -2
  17. package/src/execution/exception.js +151 -68
  18. package/src/execution/execute.js +24 -11
  19. package/src/execution/execute_test_plan.js +834 -750
  20. package/src/execution/format_error_for_terminal.js +187 -0
  21. package/src/execution/github_annotation_from_error.js +3 -3
  22. package/src/execution/junit_xml_file/report_as_junit_xml.js +149 -0
  23. package/src/execution/junit_xml_file/xml_generator.js +141 -0
  24. package/src/execution/reporters/reporter_list.js +687 -0
  25. package/src/execution/run.js +92 -30
  26. package/src/execution/web_server/start_using_command.js +2 -2
  27. package/src/execution/web_server_param.js +5 -5
  28. package/src/helpers/cpu_usage.js +142 -0
  29. package/src/helpers/cpu_usage.test_manual.js +7 -0
  30. package/src/main.js +8 -0
  31. package/src/runtime_browsers/chromium.js +1 -3
  32. package/src/runtime_browsers/firefox.js +0 -3
  33. package/src/runtime_browsers/middleware_istanbul.js +2 -2
  34. package/src/runtime_browsers/using_playwright.js +296 -220
  35. package/src/runtime_browsers/webkit.js +0 -3
  36. package/src/runtime_node/execute_using_dynamic_import.js +75 -28
  37. package/src/runtime_node/node_child_process.js +250 -139
  38. package/src/runtime_node/node_child_process_controlled.mjs +94 -75
  39. package/src/runtime_node/node_worker_thread.js +206 -120
  40. package/src/runtime_node/node_worker_thread_controlled.mjs +70 -53
  41. package/src/coverage/coverage_reporter_html_directory.js +0 -32
  42. package/src/coverage/coverage_reporter_json_file.js +0 -17
  43. package/src/coverage/report_to_coverage.js +0 -198
  44. package/src/execution/execution_colors.js +0 -10
  45. package/src/execution/execution_steps.js +0 -65
  46. package/src/execution/logs_file_execution.js +0 -495
@@ -1,53 +1,52 @@
1
+ import { memoryUsage } from "node:process";
2
+
1
3
  import { createException } from "../execution/exception.js";
2
4
  import { executeUsingDynamicImport } from "./execute_using_dynamic_import.js";
3
5
 
6
+ let memoryHeapUsedAtStart;
7
+ if (process.env.MEASURE_MEMORY_AT_START) {
8
+ global.gc();
9
+ memoryHeapUsedAtStart = memoryUsage().heapUsed;
10
+ }
11
+
4
12
  const ACTIONS_AVAILABLE = {
5
- "execute-using-dynamic-import": executeUsingDynamicImport,
13
+ "execute-using-dynamic-import": (params) => {
14
+ return executeUsingDynamicImport(params);
15
+ },
6
16
  "execute-using-require": async ({ fileUrl }) => {
7
- const { createRequire } = await import("node:module");
8
- const { fileURLToPath } = await import("node:url");
9
- const filePath = fileURLToPath(fileUrl);
10
- const require = createRequire(fileUrl);
11
- // eslint-disable-next-line import/no-dynamic-require
12
- const namespace = require(filePath);
13
- const namespaceResolved = {};
14
- await Promise.all(
15
- Object.keys(namespace).map(async (key) => {
16
- const value = await namespace[key];
17
- namespaceResolved[key] = value;
18
- }),
19
- );
20
- return namespaceResolved;
17
+ const result = {
18
+ timings: {
19
+ start: null,
20
+ end: null,
21
+ },
22
+ namespace: null,
23
+ };
24
+ try {
25
+ const { createRequire } = await import("node:module");
26
+ const { fileURLToPath } = await import("node:url");
27
+ const filePath = fileURLToPath(fileUrl);
28
+ const require = createRequire(fileUrl);
29
+ result.timings.start = Date.now();
30
+ // eslint-disable-next-line import/no-dynamic-require
31
+ const namespace = require(filePath);
32
+ const namespaceResolved = {};
33
+ await Promise.all(
34
+ Object.keys(namespace).map(async (key) => {
35
+ const value = await namespace[key];
36
+ namespaceResolved[key] = value;
37
+ }),
38
+ );
39
+ result.namespace = namespaceResolved;
40
+ } finally {
41
+ result.timings.end = Date.now();
42
+ return result;
43
+ }
44
+ },
45
+ "measure-memory-usage": () => {
46
+ // we compare usage - usageAtstart to prevent
47
+ // node or os specificities to have too much influences on the measures
48
+ return memoryUsage().heapUsed - memoryHeapUsedAtStart;
21
49
  },
22
- };
23
- const ACTION_REQUEST_EVENT_NAME = "action";
24
- const ACTION_RESPONSE_EVENT_NAME = "action-result";
25
- const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
26
- const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
27
-
28
- const sendActionFailed = (error) => {
29
- const exception = createException(error);
30
- sendToParent(
31
- ACTION_RESPONSE_EVENT_NAME,
32
- JSON.stringify({
33
- status: ACTION_RESPONSE_STATUS_FAILED,
34
- value: exception,
35
- }),
36
- );
37
- };
38
-
39
- const sendActionCompleted = (value) => {
40
- sendToParent(
41
- ACTION_RESPONSE_EVENT_NAME,
42
- // here we use JSON.stringify because we should not
43
- // have non enumerable value (unlike there is on Error objects)
44
- // otherwise uneval is quite slow to turn a giant object
45
- // into a string (and value can be giant when using coverage)
46
- JSON.stringify({
47
- status: ACTION_RESPONSE_STATUS_COMPLETED,
48
- value,
49
- }),
50
- );
51
50
  };
52
51
 
53
52
  const sendToParent = (type, data) => {
@@ -61,16 +60,13 @@ const sendToParent = (type, data) => {
61
60
  // It means node process may stay alive longer than expected
62
61
  // the time to send the data to the parent.
63
62
  process.send({
64
- jsenv: true,
65
- type,
63
+ __jsenv__: type,
66
64
  data,
67
65
  });
68
66
  };
69
-
70
- const onceParentMessage = (type, callback) => {
67
+ const onActionRequestedByParent = (callback) => {
71
68
  const listener = (message) => {
72
- if (message && message.jsenv && message.type === type) {
73
- removeListener(); // commenting this line keep this process alive
69
+ if (message && message.__jsenv__ === "action") {
74
70
  callback(message.data);
75
71
  }
76
72
  };
@@ -81,37 +77,60 @@ const onceParentMessage = (type, callback) => {
81
77
  return removeListener;
82
78
  };
83
79
 
84
- const removeActionRequestListener = onceParentMessage(
85
- ACTION_REQUEST_EVENT_NAME,
86
- async ({ actionType, actionParams }) => {
87
- const action = ACTIONS_AVAILABLE[actionType];
80
+ const removeActionRequestListener = onActionRequestedByParent(
81
+ async ({ id, type, params = {} }) => {
82
+ const sendActionInternalError = (id, error) => {
83
+ const exception = createException(error);
84
+ sendToParent(
85
+ "action-result",
86
+ JSON.stringify({
87
+ id,
88
+ status: "error",
89
+ value: exception,
90
+ }),
91
+ );
92
+ };
93
+ const sendActionCompleted = (id, value) => {
94
+ sendToParent(
95
+ "action-result",
96
+ // here we use JSON.stringify because we should not
97
+ // have non enumerable value (unlike there is on Error objects)
98
+ // otherwise uneval is quite slow to turn a giant object
99
+ // into a string (and value can be giant when using coverage)
100
+ JSON.stringify({
101
+ id,
102
+ status: "completed",
103
+ value,
104
+ }),
105
+ );
106
+ };
107
+
108
+ const action = ACTIONS_AVAILABLE[type];
88
109
  if (!action) {
89
- sendActionFailed(new Error(`unknown action ${actionType}`));
110
+ sendActionInternalError(id, new Error(`unknown action ${type}`));
90
111
  return;
91
112
  }
92
113
 
93
- let value;
94
- let failed = false;
114
+ let gotInternalError = false;
115
+ const onUncaughtException = (err) => {
116
+ gotInternalError = true;
117
+ sendActionInternalError(id, err);
118
+ process.exit(1);
119
+ };
120
+ process.on("uncaughtException", onUncaughtException);
121
+
95
122
  try {
96
- value = await action(actionParams);
123
+ const value = await action(params);
124
+ if (!gotInternalError) {
125
+ sendActionCompleted(id, value);
126
+ }
97
127
  } catch (e) {
98
- failed = true;
99
- value = e;
100
- }
101
-
102
- // setTimeout(() => {}, 100)
103
-
104
- if (failed) {
105
- sendActionFailed(value);
106
- } else {
107
- sendActionCompleted(value);
108
- }
109
-
110
- // removeActionRequestListener()
111
- if (actionParams.exitAfterAction) {
112
- removeActionRequestListener();
113
- // for some reason this fixes v8 coverage directory sometimes empty on Ubuntu
114
- // process.exit()
128
+ sendActionInternalError(id, e);
129
+ } finally {
130
+ process.removeListener("uncaughtException", onUncaughtException);
131
+ if (params.exitAfterAction) {
132
+ removeActionRequestListener();
133
+ }
115
134
  }
116
135
  },
117
136
  );
@@ -3,11 +3,7 @@
3
3
  // https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/worker/base.js
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { fileURLToPath } from "node:url";
6
- import {
7
- Abort,
8
- createCallbackListNotifiedOnce,
9
- raceCallbacks,
10
- } from "@jsenv/abort";
6
+ import { Abort, raceCallbacks } from "@jsenv/abort";
11
7
  import { memoize } from "@jsenv/utils/src/memoize/memoize.js";
12
8
 
13
9
  import { createChildExecOptions } from "./child_exec_options.js";
@@ -51,17 +47,24 @@ export const nodeWorkerThread = ({
51
47
  keepRunning,
52
48
  stopSignal,
53
49
  onConsole,
50
+ onRuntimeStarted,
51
+ onRuntimeStopped,
54
52
 
53
+ measureMemoryUsage,
54
+ onMeasureMemoryAvailable,
55
55
  collectConsole = false,
56
56
  collectPerformance,
57
57
  coverageEnabled = false,
58
- coverageConfig,
58
+ coverageInclude,
59
59
  coverageMethodForNodeJs,
60
60
  coverageFileUrl,
61
61
  }) => {
62
62
  if (coverageMethodForNodeJs !== "NODE_V8_COVERAGE") {
63
63
  env.NODE_V8_COVERAGE = "";
64
64
  }
65
+ if (onMeasureMemoryAvailable) {
66
+ env.MEASURE_MEMORY_AT_START = "1";
67
+ }
65
68
  if (importMap) {
66
69
  env.IMPORT_MAP = JSON.stringify(importMap);
67
70
  env.IMPORT_MAP_BASE_URL = rootDirectoryUrl;
@@ -86,10 +89,16 @@ export const nodeWorkerThread = ({
86
89
  ...env,
87
90
  };
88
91
 
89
- const cleanupCallbackList = createCallbackListNotifiedOnce();
92
+ const cleanupCallbackSet = new Set();
90
93
  const cleanup = async (reason) => {
91
- await cleanupCallbackList.notify({ reason });
94
+ const promises = [];
95
+ for (const cleanupCallback of cleanupCallbackSet) {
96
+ promises.push(cleanupCallback({ reason }));
97
+ }
98
+ cleanupCallbackSet.clear();
99
+ await Promise.all(promises);
92
100
  };
101
+
93
102
  const actionOperation = Abort.startOperation();
94
103
  actionOperation.addAbortSignal(signal);
95
104
  // https://nodejs.org/api/worker_threads.html#new-workerfilename-options
@@ -111,13 +120,26 @@ export const nodeWorkerThread = ({
111
120
  },
112
121
  );
113
122
  const workerThreadReadyPromise = new Promise((resolve) => {
114
- onceWorkerThreadMessage(workerThread, "ready", resolve);
123
+ const removeReadyListener = onWorkerThreadMessage(
124
+ workerThread,
125
+ "ready",
126
+ () => {
127
+ removeReadyListener();
128
+ onRuntimeStarted();
129
+ resolve();
130
+ },
131
+ );
115
132
  });
133
+ cleanupCallbackSet.add(
134
+ onceWorkerThreadEvent(workerThread, "exit", () => {
135
+ onRuntimeStopped();
136
+ }),
137
+ );
116
138
 
117
139
  const stop = memoize(async () => {
118
140
  // read all stdout before terminating
119
141
  // (no need for stderr because it's sync)
120
- if (collectConsole) {
142
+ if (collectConsole || onConsole) {
121
143
  while (workerThread.stdout.read() !== null) {}
122
144
  await new Promise((resolve) => {
123
145
  setTimeout(resolve, 50);
@@ -126,134 +148,175 @@ export const nodeWorkerThread = ({
126
148
  await workerThread.terminate();
127
149
  });
128
150
 
129
- const winnerPromise = new Promise((resolve) => {
130
- raceCallbacks(
131
- {
132
- aborted: (cb) => {
133
- return actionOperation.addAbortCallback(cb);
134
- },
135
- error: (cb) => {
136
- return onceWorkerThreadEvent(workerThread, "error", cb);
137
- },
138
- exit: (cb) => {
139
- return onceWorkerThreadEvent(
140
- workerThread,
141
- "exit",
142
- (code, signal) => {
143
- cb({ code, signal });
144
- },
145
- );
146
- },
147
- response: (cb) => {
148
- return onceWorkerThreadMessage(workerThread, "action-result", cb);
149
- },
150
- },
151
- resolve,
152
- );
153
- });
154
-
155
151
  const result = {
156
152
  status: "executing",
157
153
  errors: [],
158
154
  namespace: null,
155
+ timings: {},
156
+ memoryUsage: null,
157
+ performance: null,
159
158
  };
160
159
 
161
- const writeResult = async () => {
160
+ try {
161
+ let executionInternalErrorCallback;
162
+ let executionCompletedCallback;
163
+ const winnerPromise = new Promise((resolve) => {
164
+ raceCallbacks(
165
+ {
166
+ aborted: (cb) => {
167
+ return actionOperation.addAbortCallback(cb);
168
+ },
169
+ error: (cb) => {
170
+ return onceWorkerThreadEvent(workerThread, "error", cb);
171
+ },
172
+ exit: (cb) => {
173
+ return onceWorkerThreadEvent(
174
+ workerThread,
175
+ "exit",
176
+ (code, signal) => {
177
+ cb({ code, signal });
178
+ },
179
+ );
180
+ },
181
+ execution_internal_error: (cb) => {
182
+ executionInternalErrorCallback = cb;
183
+ },
184
+ execution_completed: (cb) => {
185
+ executionCompletedCallback = cb;
186
+ },
187
+ },
188
+ resolve,
189
+ );
190
+ });
191
+ const raceHandlers = {
192
+ aborted: () => {
193
+ result.status = "aborted";
194
+ },
195
+ error: (error) => {
196
+ removeOutputListener();
197
+ result.status = "failed";
198
+ result.errors.push(error);
199
+ },
200
+ exit: ({ code }) => {
201
+ onRuntimeStopped();
202
+ if (code === 12) {
203
+ result.status = "failed";
204
+ result.errors.push(
205
+ new Error(
206
+ `node process exited with 12 (the forked child process wanted to use a non-available port for debug)`,
207
+ ),
208
+ );
209
+ return;
210
+ }
211
+ if (code === null || code === 0) {
212
+ result.status = "completed";
213
+ result.namespace = {};
214
+ return;
215
+ }
216
+ if (
217
+ code === EXIT_CODES.SIGINT ||
218
+ code === EXIT_CODES.SIGTERM ||
219
+ code === EXIT_CODES.SIGABORT
220
+ ) {
221
+ result.status = "failed";
222
+ result.errors.push(
223
+ new Error(`node worker thread exited during execution`),
224
+ );
225
+ return;
226
+ }
227
+ // process.exit(1) in child process or process.exitCode = 1 + process.exit()
228
+ // means there was an error even if we don't know exactly what.
229
+ result.status = "failed";
230
+ result.errors.push(
231
+ new Error(
232
+ `node worker thread exited with code ${code} during execution`,
233
+ ),
234
+ );
235
+ },
236
+ execution_internal_error: (error) => {
237
+ result.status = "failed";
238
+ result.errors.push(error);
239
+ },
240
+ execution_completed: ({
241
+ status,
242
+ errors,
243
+ namespace,
244
+ timings,
245
+ memoryUsage,
246
+ performance,
247
+ coverage,
248
+ }) => {
249
+ result.status = status;
250
+ result.errors = errors;
251
+ result.namespace = namespace;
252
+ result.timings = timings;
253
+ result.memoryUsage = memoryUsage;
254
+ result.performance = performance;
255
+ result.coverage = coverage;
256
+ },
257
+ };
258
+
162
259
  actionOperation.throwIfAborted();
163
260
  await workerThreadReadyPromise;
164
261
  actionOperation.throwIfAborted();
165
- await sendToWorkerThread(workerThread, {
166
- type: "action",
167
- data: {
168
- actionType: "execute-using-dynamic-import",
169
- actionParams: {
262
+ if (onMeasureMemoryAvailable) {
263
+ onMeasureMemoryAvailable(async () => {
264
+ let _resolve;
265
+ const memoryUsagePromise = new Promise((resolve) => {
266
+ _resolve = resolve;
267
+ });
268
+ await requestActionOnWorkerThread(
269
+ workerThread,
270
+ {
271
+ type: "measure-memory-usage",
272
+ },
273
+ ({ value }) => {
274
+ _resolve(value);
275
+ },
276
+ );
277
+ return memoryUsagePromise;
278
+ });
279
+ }
280
+ await requestActionOnWorkerThread(
281
+ workerThread,
282
+ {
283
+ type: "execute-using-dynamic-import",
284
+ params: {
170
285
  rootDirectoryUrl,
171
286
  fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
287
+ measureMemoryUsage,
172
288
  collectPerformance,
173
289
  coverageEnabled,
174
- coverageConfig,
290
+ coverageInclude,
175
291
  coverageMethodForNodeJs,
176
292
  coverageFileUrl,
177
293
  exitAfterAction: true,
178
294
  },
179
295
  },
180
- });
296
+ ({ status, value }) => {
297
+ if (status === "error") {
298
+ executionInternalErrorCallback(value);
299
+ } else {
300
+ executionCompletedCallback(value);
301
+ }
302
+ },
303
+ );
181
304
  const winner = await winnerPromise;
182
- if (winner.name === "aborted") {
183
- result.status = "aborted";
184
- return;
185
- }
186
- if (winner.name === "error") {
187
- const error = winner.data;
188
- removeOutputListener();
189
- result.status = "failed";
190
- result.errors.push(error);
191
- return;
192
- }
193
- if (winner.name === "exit") {
194
- const { code } = winner.data;
195
- await cleanup("process exit");
196
- if (code === 12) {
197
- result.status = "failed";
198
- result.errors.push(
199
- new Error(
200
- `node process exited with 12 (the forked child process wanted to use a non-available port for debug)`,
201
- ),
202
- );
203
- return;
204
- }
205
- if (code === null || code === 0) {
206
- result.status = "completed";
207
- result.namespace = {};
208
- return;
209
- }
210
- if (
211
- code === EXIT_CODES.SIGINT ||
212
- code === EXIT_CODES.SIGTERM ||
213
- code === EXIT_CODES.SIGABORT
214
- ) {
215
- result.status = "failed";
216
- result.errors.push(
217
- new Error(`node worker thread exited during execution`),
218
- );
219
- return;
220
- }
221
- // process.exit(1) in child process or process.exitCode = 1 + process.exit()
222
- // means there was an error even if we don't know exactly what.
223
- result.status = "failed";
224
- result.errors.push(
225
- new Error(
226
- `node worker thread exited with code ${code} during execution`,
227
- ),
228
- );
229
- }
230
- const { status, value } = winner.data;
231
- if (status === "action-failed") {
232
- result.status = "failed";
233
- result.errors.push(value);
234
- return;
235
- }
236
- const { namespace, performance, coverage } = value;
237
- result.status = "completed";
238
- result.namespace = namespace;
239
- result.performance = performance;
240
- result.coverage = coverage;
241
- };
242
-
243
- try {
244
- await writeResult();
305
+ raceHandlers[winner.name](winner.data);
245
306
  } catch (e) {
246
307
  result.status = "failed";
247
308
  result.errors.push(e);
309
+ } finally {
310
+ if (keepRunning) {
311
+ stopSignal.notify = stop;
312
+ } else {
313
+ await stop();
314
+ onRuntimeStopped();
315
+ }
316
+ await actionOperation.end();
317
+ await cleanup();
318
+ return result;
248
319
  }
249
-
250
- if (keepRunning) {
251
- stopSignal.notify = stop;
252
- } else {
253
- await stop();
254
- }
255
- await actionOperation.end();
256
- return result;
257
320
  },
258
321
  };
259
322
  };
@@ -276,14 +339,37 @@ const installWorkerThreadOutputListener = (workerThread, callback) => {
276
339
  };
277
340
  };
278
341
 
279
- const sendToWorkerThread = (worker, { type, data }) => {
280
- worker.postMessage({ jsenv: true, type, data });
342
+ let previousId = 0;
343
+ const requestActionOnWorkerThread = (
344
+ workerThread,
345
+ { type, params },
346
+ onResponse,
347
+ ) => {
348
+ const actionId = previousId + 1;
349
+ previousId = actionId;
350
+ const removeResultListener = onWorkerThreadMessage(
351
+ workerThread,
352
+ "action-result",
353
+ ({ id, ...payload }) => {
354
+ if (id === actionId) {
355
+ removeResultListener();
356
+ onResponse(payload);
357
+ }
358
+ },
359
+ );
360
+ workerThread.postMessage({
361
+ __jsenv__: "action",
362
+ data: {
363
+ id: actionId,
364
+ type,
365
+ params,
366
+ },
367
+ });
281
368
  };
282
369
 
283
- const onceWorkerThreadMessage = (workerThread, type, callback) => {
370
+ const onWorkerThreadMessage = (workerThread, type, callback) => {
284
371
  const onmessage = (message) => {
285
- if (message && message.jsenv && message.type === type) {
286
- workerThread.removeListener("message", onmessage);
372
+ if (message && message.__jsenv__ === type) {
287
373
  callback(message.data ? JSON.parse(message.data) : undefined);
288
374
  }
289
375
  };