@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
@@ -0,0 +1,45 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { readFileSync } from "node:fs";
3
+ import { urlToRelativeUrl } from "@jsenv/urls";
4
+ import { ensureEmptyDirectorySync } from "@jsenv/filesystem";
5
+
6
+ import { importWithRequire } from "../helpers/import_with_require.js";
7
+ import { istanbulCoverageMapFromCoverage } from "./istanbul_coverage_map_from_coverage.js";
8
+
9
+ export const reportCoverageAsHtml = (
10
+ testPlanResult,
11
+ directoryUrl,
12
+ { skipEmpty, skipFull } = {},
13
+ ) => {
14
+ if (testPlanResult.aborted) {
15
+ return;
16
+ }
17
+ const testPlanCoverage = testPlanResult.coverage;
18
+ if (!testPlanCoverage) {
19
+ // TODO
20
+ }
21
+
22
+ const { rootDirectoryUrl } = testPlanResult;
23
+ ensureEmptyDirectorySync(directoryUrl);
24
+ const coverageHtmlDirectoryRelativeUrl = urlToRelativeUrl(
25
+ directoryUrl,
26
+ rootDirectoryUrl,
27
+ );
28
+
29
+ const libReport = importWithRequire("istanbul-lib-report");
30
+ const reports = importWithRequire("istanbul-reports");
31
+ const context = libReport.createContext({
32
+ dir: fileURLToPath(rootDirectoryUrl),
33
+ coverageMap: istanbulCoverageMapFromCoverage(testPlanCoverage),
34
+ sourceFinder: (path) =>
35
+ readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
36
+ });
37
+ const report = reports.create("html", {
38
+ skipEmpty,
39
+ skipFull,
40
+ subdir: coverageHtmlDirectoryRelativeUrl,
41
+ });
42
+ report.execute(context);
43
+ // const htmlCoverageDirectoryIndexFileUrl = `${directoryUrl}index.html`;
44
+ // console.log(`-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`);
45
+ };
@@ -0,0 +1,20 @@
1
+ import { urlToFileSystemPath } from "@jsenv/urls";
2
+ import { writeFileSync } from "@jsenv/filesystem";
3
+ import { inspectFileSize } from "@jsenv/inspect";
4
+
5
+ export const reportCoverageAsJson = (testPlanResult, fileUrl) => {
6
+ if (testPlanResult.aborted) {
7
+ return;
8
+ }
9
+ const testPlanCoverage = testPlanResult.coverage;
10
+ if (!testPlanCoverage) {
11
+ // TODO
12
+ }
13
+ const coverageAsText = JSON.stringify(testPlanCoverage, null, " ");
14
+ writeFileSync(fileUrl, coverageAsText);
15
+ console.log(
16
+ `-> ${urlToFileSystemPath(fileUrl)} (${inspectFileSize(
17
+ Buffer.byteLength(coverageAsText),
18
+ )})`,
19
+ );
20
+ };
@@ -1,19 +1,25 @@
1
1
  import { importWithRequire } from "../helpers/import_with_require.js";
2
2
  import { istanbulCoverageMapFromCoverage } from "./istanbul_coverage_map_from_coverage.js";
3
3
 
4
- export const generateCoverageTextLog = (
5
- coverage,
6
- { coverageReportSkipEmpty, coverageReportSkipFull },
4
+ export const reportCoverageInConsole = (
5
+ testPlanResult,
6
+ { skipEmpty, skipFull } = {},
7
7
  ) => {
8
+ if (testPlanResult.aborted) {
9
+ return;
10
+ }
11
+ const testPlanCoverage = testPlanResult.coverage;
12
+ if (!testPlanCoverage) {
13
+ // TODO
14
+ }
8
15
  const libReport = importWithRequire("istanbul-lib-report");
9
16
  const reports = importWithRequire("istanbul-reports");
10
-
11
17
  const context = libReport.createContext({
12
- coverageMap: istanbulCoverageMapFromCoverage(coverage),
18
+ coverageMap: istanbulCoverageMapFromCoverage(testPlanCoverage),
13
19
  });
14
20
  const report = reports.create("text", {
15
- skipEmpty: coverageReportSkipEmpty,
16
- skipFull: coverageReportSkipFull,
21
+ skipEmpty,
22
+ skipFull,
17
23
  });
18
24
  report.execute(context);
19
25
  };
@@ -3,7 +3,7 @@ import { createDetailedMessage } from "@jsenv/log";
3
3
  export const composeV8AndIstanbul = (
4
4
  v8FileByFileCoverage,
5
5
  istanbulFileByFileCoverage,
6
- { coverageV8ConflictWarning },
6
+ { v8ConflictWarning },
7
7
  ) => {
8
8
  const fileByFileCoverage = {};
9
9
  const v8Files = Object.keys(v8FileByFileCoverage);
@@ -15,15 +15,15 @@ export const composeV8AndIstanbul = (
15
15
  istanbulFiles.forEach((key) => {
16
16
  const v8Coverage = v8FileByFileCoverage[key];
17
17
  if (v8Coverage) {
18
- if (coverageV8ConflictWarning) {
18
+ if (v8ConflictWarning) {
19
19
  console.warn(
20
20
  createDetailedMessage(
21
21
  `Coverage conflict on "${key}", found two coverage that cannot be merged together: v8 and istanbul. The istanbul coverage will be ignored.`,
22
22
  {
23
23
  "details": `This happens when a file is executed on a runtime using v8 coverage (node or chromium) and on runtime using istanbul coverage (firefox or webkit)`,
24
24
  "suggestion":
25
- "disable this warning with coverageV8ConflictWarning: false",
26
- "suggestion 2": `force coverage using istanbul with coverageMethodForBrowsers: "istanbul"`,
25
+ "disable this warning with coverage.v8ConflictWarning: false",
26
+ "suggestion 2": `force coverage using istanbul with coverage.methodForBrowsers: "istanbul"`,
27
27
  },
28
28
  ),
29
29
  );
@@ -2,10 +2,10 @@ import { URL_META } from "@jsenv/url-meta";
2
2
 
3
3
  export const filterV8Coverage = async (
4
4
  v8Coverage,
5
- { rootDirectoryUrl, coverageConfig },
5
+ { rootDirectoryUrl, coverageInclude },
6
6
  ) => {
7
7
  const associations = URL_META.resolveAssociations(
8
- { cover: coverageConfig },
8
+ { cover: coverageInclude },
9
9
  rootDirectoryUrl,
10
10
  );
11
11
  const urlShouldBeCovered = (url) => {
@@ -17,7 +17,15 @@
17
17
  * The browser part can be found in "supervisor.js"
18
18
  */
19
19
 
20
- export const createException = (reason) => {
20
+ // https://github.com/marvinhagemeister/errorstacks/tree/main
21
+ // https://cdn.jsdelivr.net/npm/errorstacks@latest/dist/esm/index.mjs
22
+ import { parseStackTrace } from "errorstacks";
23
+ import { URL_META } from "@jsenv/url-meta";
24
+
25
+ const isDev = process.execArgv.includes("--conditions=development");
26
+ const jsenvCoreDirectoryUrl = new URL("../../../../../", import.meta.url);
27
+
28
+ export const createException = (reason, { rootDirectoryUrl } = {}) => {
21
29
  const exception = {
22
30
  isException: true,
23
31
  isError: false,
@@ -25,6 +33,7 @@ export const createException = (reason) => {
25
33
  message: "",
26
34
  stack: "",
27
35
  stackTrace: "",
36
+ stackFrames: undefined,
28
37
  site: null,
29
38
  };
30
39
 
@@ -32,88 +41,162 @@ export const createException = (reason) => {
32
41
  exception.message = "undefined";
33
42
  return exception;
34
43
  }
35
- if (reason === undefined) {
36
- exception.message = "undefined";
44
+ if (reason === null) {
45
+ exception.message = "null";
37
46
  return exception;
38
47
  }
39
48
  if (typeof reason === "string") {
40
49
  exception.message = reason;
41
50
  return exception;
42
51
  }
43
- if (reason instanceof Error) {
44
- exception.isError = true;
45
- exception.name = reason.name;
46
- exception.message = reason.message;
47
- const stackInfo = getStackInfo(reason);
48
- if (stackInfo) {
49
- const { stackTrace, stackObject } = stackInfo;
50
- exception.stackTrace = stackTrace;
51
- const [firstCallSite] = stackObject;
52
- writePropertiesFromCallSite(exception, firstCallSite);
53
- }
54
- exception.stack = reason.stack;
55
- return exception;
56
- }
57
- if (typeof reason === "object") {
58
- exception.code = reason.code;
59
- exception.message = reason.message;
60
- exception.stack = reason.stack;
52
+ if (typeof reason !== "object") {
53
+ exception.message = JSON.stringify(reason);
61
54
  return exception;
62
55
  }
63
- exception.message = JSON.stringify(reason);
64
- return exception;
65
- };
56
+ if (reason.stackFrames === undefined && "stack" in reason) {
57
+ let stackFrames;
66
58
 
67
- const getStackInfo = (reason) => {
68
- if (reason instanceof Error) {
69
59
  const { prepareStackTrace } = Error;
70
- let stackTrace;
71
- let stackObject;
72
- Error.prepareStackTrace = (e, secondArg) => {
60
+ Error.prepareStackTrace = (e, callSites) => {
73
61
  Error.prepareStackTrace = prepareStackTrace;
74
- stackObject = secondArg;
75
- stackTrace = secondArg.map((callSite) => ` at ${callSite}`).join("\n");
76
- const name = e.name || "Error";
77
- const message = e.message || "";
78
- let stack = `${name}: ${message}`;
79
- if (stackTrace) {
80
- stack += `\n${stackTrace}`;
62
+
63
+ stackFrames = [];
64
+ for (const callSite of callSites) {
65
+ const stackFrame = {
66
+ raw: ` at ${String(callSite)}`,
67
+ url: callSite.getFileName() || callSite.getScriptNameOrSourceURL(),
68
+ line: callSite.getLineNumber(),
69
+ column: callSite.getColumnNumber(),
70
+ functionName: callSite.getFunctionName(),
71
+ isNative: callSite.isNative(),
72
+ isEval: callSite.isEval(),
73
+ isConstructor: callSite.isConstructor(),
74
+ isAsync: callSite.isAsync(),
75
+ evalSite: null,
76
+ };
77
+ if (stackFrame.isEval) {
78
+ const evalOrigin = stackFrame.getEvalOrigin();
79
+ if (evalOrigin) {
80
+ stackFrame.evalSite = getPropertiesFromEvalOrigin(evalOrigin);
81
+ }
82
+ }
83
+ stackFrames.push(stackFrame);
81
84
  }
82
- return stack;
85
+ return "";
83
86
  };
84
- // eslint-disable-next-line no-unused-expressions
85
- reason.stack;
86
- if (stackTrace === undefined) {
87
- return null;
87
+ exception.stack = reason.stack;
88
+ if (stackFrames === undefined) {
89
+ // Error.prepareStackTrace not trigerred
90
+ // - reason is not an error
91
+ // - reason.stack already get
92
+ Error.prepareStackTrace = prepareStackTrace;
93
+
94
+ const calls = parseStackTrace(reason.stack);
95
+ stackFrames = [];
96
+ for (const call of calls) {
97
+ if (call.fileName === "") {
98
+ continue;
99
+ }
100
+ stackFrames.push({
101
+ raw: call.raw,
102
+ functionName: call.name,
103
+ url: call.fileName,
104
+ line: call.line,
105
+ column: call.column,
106
+ native: call.type === "native",
107
+ });
108
+ }
109
+ }
110
+
111
+ const stackFramesNonNative = [];
112
+ for (const stackFrame of stackFrames) {
113
+ if (stackFrame.url.startsWith("node:")) {
114
+ stackFrame.native = "node";
115
+ continue;
116
+ }
117
+ if (stackFrame.url.startsWith("file:")) {
118
+ if (rootDirectoryUrl && stackFrame.url.startsWith(rootDirectoryUrl)) {
119
+ stackFramesNonNative.push(stackFrame);
120
+ continue;
121
+ }
122
+
123
+ if (isDev) {
124
+ // while developing jsenv itself we want to exclude any
125
+ // - src/*
126
+ // - packages/**/src/
127
+ // for the users of jsenv it's easier, we want to exclude
128
+ // - **/node_modules/@jsenv/**
129
+ if (
130
+ URL_META.matches(stackFrame.url, {
131
+ [`${jsenvCoreDirectoryUrl}src/`]: true,
132
+ [`${jsenvCoreDirectoryUrl}packages/**/src/`]: true,
133
+ })
134
+ ) {
135
+ stackFrame.native = "jsenv";
136
+ continue;
137
+ }
138
+ } else if (
139
+ URL_META.matches(stackFrame.url, {
140
+ "file:///**/node_modules/@jsenv/core/": true,
141
+ })
142
+ ) {
143
+ stackFrame.native = "jsenv";
144
+ continue;
145
+ }
146
+ }
147
+ stackFramesNonNative.push(stackFrame);
148
+ }
149
+ if (stackFramesNonNative.length) {
150
+ stackFrames = stackFramesNonNative;
151
+ }
152
+
153
+ reason.stackFrames = stackFrames;
154
+
155
+ let stackTrace = "";
156
+ for (const stackFrame of stackFrames) {
157
+ if (stackTrace) stackTrace += "\n";
158
+ stackTrace += stackFrame.raw;
159
+ }
160
+ reason.stackTrace = stackTrace;
161
+ let stack = "";
162
+ const name = reason.name || "Error";
163
+ const message = reason.message || "";
164
+ stack += `${name}: ${message}`;
165
+ if (stackTrace) {
166
+ stack += `\n${stackTrace}`;
167
+ }
168
+ reason.stack = stack;
169
+
170
+ const [firstCallFrame] = stackFrames;
171
+ if (firstCallFrame && (!reason.site || !reason.site.isInline)) {
172
+ reason.site = firstCallFrame.url
173
+ ? {
174
+ url: firstCallFrame.url,
175
+ line: firstCallFrame.line,
176
+ column: firstCallFrame.column,
177
+ }
178
+ : firstCallFrame.evalSite;
88
179
  }
89
- return {
90
- stackTrace,
91
- stackObject,
92
- };
93
180
  }
94
- return null;
95
- };
96
- const writePropertiesFromCallSite = (exception, callSite) => {
97
- const source = callSite.getFileName() || callSite.getScriptNameOrSourceURL();
98
- if (source) {
99
- const line = callSite.getLineNumber();
100
- const column = callSite.getColumnNumber() - 1;
101
- exception.site = {
102
- url: source,
103
- line,
104
- column,
105
- };
106
- return;
181
+ // getOwnPropertyNames to catch non enumerable properties on reason
182
+ // (happens mostly when reason is instanceof Error)
183
+ // like .stack, .message
184
+ // some properties are even on the prototype like .name
185
+ for (const ownPropertyName of Object.getOwnPropertyNames(reason)) {
186
+ exception[ownPropertyName] = reason[ownPropertyName];
107
187
  }
108
- // Code called using eval() needs special handling
109
- if (callSite.isEval()) {
110
- const origin = callSite.getEvalOrigin();
111
- if (origin) {
112
- writePropertiesFromEvalOrigin(exception, origin);
113
- }
188
+ const isError = reason instanceof Error;
189
+ exception.isError = isError;
190
+ if (isError) {
191
+ // getOwnPropertyNames is not enough to copy .name and .message
192
+ // on error instances
193
+ exception.name = reason.name;
194
+ exception.message = reason.message;
114
195
  }
196
+ return exception;
115
197
  };
116
- const writePropertiesFromEvalOrigin = (exception, origin) => {
198
+
199
+ const getPropertiesFromEvalOrigin = (origin) => {
117
200
  // Most eval() calls are in this format
118
201
  const topLevelEvalMatch = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(
119
202
  origin,
@@ -122,16 +205,16 @@ const writePropertiesFromEvalOrigin = (exception, origin) => {
122
205
  const source = topLevelEvalMatch[2];
123
206
  const line = Number(topLevelEvalMatch[3]);
124
207
  const column = topLevelEvalMatch[4] - 1;
125
- exception.site = {
208
+ return {
126
209
  url: source,
127
210
  line,
128
211
  column,
129
212
  };
130
- return;
131
213
  }
132
214
  // Parse nested eval() calls using recursion
133
215
  const nestedEvalMatch = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
134
216
  if (nestedEvalMatch) {
135
- writePropertiesFromEvalOrigin(exception, nestedEvalMatch[2]);
217
+ return getPropertiesFromEvalOrigin(nestedEvalMatch[2]);
136
218
  }
219
+ return null;
137
220
  };
@@ -14,13 +14,14 @@ import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
14
14
  import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
15
15
  import { createLogger } from "@jsenv/log";
16
16
 
17
- import { createTeardown } from "../helpers/teardown.js";
18
17
  import { assertAndNormalizeWebServer } from "./web_server_param.js";
19
18
  import { run } from "./run.js";
20
19
 
21
20
  export const execute = async ({
22
21
  signal = new AbortController().signal,
23
22
  handleSIGINT = true,
23
+ handleSIGUP = true,
24
+ handleSIGTERM = true,
24
25
  logLevel,
25
26
  rootDirectoryUrl,
26
27
  webServer,
@@ -31,10 +32,12 @@ export const execute = async ({
31
32
  mirrorConsole = true,
32
33
  keepRunning = false,
33
34
 
34
- collectConsole,
35
- collectCoverage,
36
- coverageTempDirectoryUrl,
35
+ collectConsole = false,
36
+ measureMemoryUsage = false,
37
+ onMeasureMemoryAvailable,
37
38
  collectPerformance = false,
39
+ collectCoverage = false,
40
+ coverageTempDirectoryUrl,
38
41
  runtime,
39
42
  runtimeParams,
40
43
 
@@ -45,14 +48,16 @@ export const execute = async ({
45
48
  rootDirectoryUrl,
46
49
  "rootDirectoryUrl",
47
50
  );
48
- const teardown = createTeardown();
51
+ const teardownCallbackSet = new Set();
49
52
  const executeOperation = Abort.startOperation();
50
53
  executeOperation.addAbortSignal(signal);
51
- if (handleSIGINT) {
54
+ if (handleSIGINT || handleSIGUP || handleSIGTERM) {
52
55
  executeOperation.addAbortSource((abort) => {
53
56
  return raceProcessTeardownEvents(
54
57
  {
55
- SIGINT: true,
58
+ SIGINT: handleSIGINT,
59
+ SIGHUP: handleSIGUP,
60
+ SIGTERM: handleSIGTERM,
56
61
  },
57
62
  abort,
58
63
  );
@@ -60,7 +65,11 @@ export const execute = async ({
60
65
  }
61
66
 
62
67
  if (runtime.type === "browser") {
63
- await assertAndNormalizeWebServer(webServer, { signal, teardown, logger });
68
+ await assertAndNormalizeWebServer(webServer, {
69
+ signal,
70
+ teardownCallbackSet,
71
+ logger,
72
+ });
64
73
  }
65
74
 
66
75
  let resultTransformer = (result) => result;
@@ -69,7 +78,7 @@ export const execute = async ({
69
78
  webServer,
70
79
  fileRelativeUrl,
71
80
  importMap,
72
- teardown,
81
+ teardownCallbackSet,
73
82
  ...runtimeParams,
74
83
  };
75
84
 
@@ -80,9 +89,11 @@ export const execute = async ({
80
89
  keepRunning,
81
90
  mirrorConsole,
82
91
  collectConsole,
92
+ measureMemoryUsage,
93
+ onMeasureMemoryAvailable,
94
+ collectPerformance,
83
95
  collectCoverage,
84
96
  coverageTempDirectoryUrl,
85
- collectPerformance,
86
97
  runtime,
87
98
  runtimeParams,
88
99
  });
@@ -109,7 +120,9 @@ export const execute = async ({
109
120
  }
110
121
  return result;
111
122
  } finally {
112
- await teardown.trigger();
123
+ for (const teardownCallback of teardownCallbackSet) {
124
+ await teardownCallback();
125
+ }
113
126
  await executeOperation.end();
114
127
  }
115
128
  };