@jsenv/test 2.1.3 → 3.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 (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,39 +1,29 @@
1
1
  import { parentPort } from "node:worker_threads";
2
+ import { memoryUsage } from "node:process";
3
+ import { setFlagsFromString } from "node:v8";
4
+ import { runInNewContext } from "node:vm";
2
5
 
3
6
  import { createException } from "../execution/exception.js";
4
7
  import { executeUsingDynamicImport } from "./execute_using_dynamic_import.js";
5
8
 
6
- const ACTIONS_AVAILABLE = {
7
- "execute-using-dynamic-import": executeUsingDynamicImport,
8
- };
9
- const ACTION_REQUEST_EVENT_NAME = "action";
10
- const ACTION_RESPONSE_EVENT_NAME = "action-result";
11
- const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
12
- const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
9
+ setFlagsFromString("--expose_gc");
10
+ global.gc = runInNewContext("gc");
13
11
 
14
- const sendActionFailed = (error) => {
15
- const exception = createException(error);
16
- sendToParent(
17
- ACTION_RESPONSE_EVENT_NAME,
18
- JSON.stringify({
19
- status: ACTION_RESPONSE_STATUS_FAILED,
20
- value: exception,
21
- }),
22
- );
23
- };
12
+ let memoryHeapUsedAtStart;
13
+ if (process.env.MEASURE_MEMORY_AT_START) {
14
+ global.gc();
15
+ memoryHeapUsedAtStart = memoryUsage().heapUsed;
16
+ }
24
17
 
25
- const sendActionCompleted = (value) => {
26
- sendToParent(
27
- ACTION_RESPONSE_EVENT_NAME,
28
- // here we use JSON.stringify because we should not
29
- // have non enumerable value (unlike there is on Error objects)
30
- // otherwise uneval is quite slow to turn a giant object
31
- // into a string (and value can be giant when using coverage)
32
- JSON.stringify({
33
- status: ACTION_RESPONSE_STATUS_COMPLETED,
34
- value,
35
- }),
36
- );
18
+ const ACTIONS_AVAILABLE = {
19
+ "execute-using-dynamic-import": (params) => {
20
+ return executeUsingDynamicImport(params);
21
+ },
22
+ "measure-memory-usage": () => {
23
+ // we compare usage - usageAtstart to prevent
24
+ // node or os specificities to have too much influences on the measures
25
+ return memoryUsage().heapUsed - memoryHeapUsedAtStart;
26
+ },
37
27
  };
38
28
 
39
29
  const sendToParent = (type, data) => {
@@ -42,16 +32,14 @@ const sendToParent = (type, data) => {
42
32
  // It means node process may stay alive longer than expected
43
33
  // the time to send the data to the parent.
44
34
  parentPort.postMessage({
45
- jsenv: true,
46
- type,
35
+ __jsenv__: type,
47
36
  data,
48
37
  });
49
38
  };
50
39
 
51
- const onceParentMessage = (type, callback) => {
40
+ const onActionRequestedByParent = (callback) => {
52
41
  const listener = (message) => {
53
- if (message && message.jsenv && message.type === type) {
54
- removeListener(); // commenting this line keep this worker alive
42
+ if (message && message.__jsenv__ === "action") {
55
43
  callback(message.data);
56
44
  }
57
45
  };
@@ -62,31 +50,60 @@ const onceParentMessage = (type, callback) => {
62
50
  return removeListener;
63
51
  };
64
52
 
65
- const removeActionRequestListener = onceParentMessage(
66
- ACTION_REQUEST_EVENT_NAME,
67
- async ({ actionType, actionParams }) => {
68
- const action = ACTIONS_AVAILABLE[actionType];
53
+ const removeActionRequestListener = onActionRequestedByParent(
54
+ async ({ id, type, params = {} }) => {
55
+ const sendActionInternalError = (id, error) => {
56
+ const exception = createException(error);
57
+ sendToParent(
58
+ "action-result",
59
+ JSON.stringify({
60
+ id,
61
+ status: "error",
62
+ value: exception,
63
+ }),
64
+ );
65
+ };
66
+ const sendActionCompleted = (id, value) => {
67
+ sendToParent(
68
+ "action-result",
69
+ // here we use JSON.stringify because we should not
70
+ // have non enumerable value (unlike there is on Error objects)
71
+ // otherwise uneval is quite slow to turn a giant object
72
+ // into a string (and value can be giant when using coverage)
73
+ JSON.stringify({
74
+ id,
75
+ status: "completed",
76
+ value,
77
+ }),
78
+ );
79
+ };
80
+
81
+ const action = ACTIONS_AVAILABLE[type];
69
82
  if (!action) {
70
- sendActionFailed(new Error(`unknown action ${actionType}`));
83
+ sendActionInternalError(id, new Error(`unknown action ${type}`));
71
84
  return;
72
85
  }
73
86
 
74
- let value;
75
- let failed = false;
87
+ let gotInternalError = false;
88
+ const onUncaughtException = (err) => {
89
+ gotInternalError = true;
90
+ sendActionInternalError(id, err);
91
+ process.exit(1);
92
+ };
93
+ process.on("uncaughtException", onUncaughtException);
94
+
76
95
  try {
77
- value = await action(actionParams);
96
+ const value = await action(params);
97
+ if (!gotInternalError) {
98
+ sendActionCompleted(id, value);
99
+ }
78
100
  } catch (e) {
79
- failed = true;
80
- value = e;
81
- }
82
-
83
- if (failed) {
84
- sendActionFailed(value);
85
- } else {
86
- sendActionCompleted(value);
87
- }
88
- if (actionParams.exitAfterAction) {
89
- removeActionRequestListener();
101
+ sendActionInternalError(id, e);
102
+ } finally {
103
+ process.removeListener("uncaughtException", onUncaughtException);
104
+ if (params.exitAfterAction) {
105
+ removeActionRequestListener();
106
+ }
90
107
  }
91
108
  },
92
109
  );
@@ -1,32 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import { readFileSync } from "node:fs";
3
-
4
- import { importWithRequire } from "../helpers/import_with_require.js";
5
- import { istanbulCoverageMapFromCoverage } from "./istanbul_coverage_map_from_coverage.js";
6
-
7
- export const generateCoverageHtmlDirectory = async (
8
- coverage,
9
- {
10
- rootDirectoryUrl,
11
- coverageHtmlDirectoryRelativeUrl,
12
- coverageReportSkipEmpty,
13
- coverageReportSkipFull,
14
- },
15
- ) => {
16
- const libReport = importWithRequire("istanbul-lib-report");
17
- const reports = importWithRequire("istanbul-reports");
18
-
19
- const context = libReport.createContext({
20
- dir: fileURLToPath(rootDirectoryUrl),
21
- coverageMap: istanbulCoverageMapFromCoverage(coverage),
22
- sourceFinder: (path) =>
23
- readFileSync(new URL(path, rootDirectoryUrl), "utf8"),
24
- });
25
-
26
- const report = reports.create("html", {
27
- skipEmpty: coverageReportSkipEmpty,
28
- skipFull: coverageReportSkipFull,
29
- subdir: coverageHtmlDirectoryRelativeUrl,
30
- });
31
- report.execute(context);
32
- };
@@ -1,17 +0,0 @@
1
- import { writeFile } from "@jsenv/filesystem";
2
- import { urlToFileSystemPath } from "@jsenv/urls";
3
- import { byteAsFileSize } from "@jsenv/log";
4
-
5
- export const generateCoverageJsonFile = async ({
6
- coverage,
7
- coverageJsonFileUrl,
8
- logger,
9
- }) => {
10
- const coverageAsText = JSON.stringify(coverage, null, " ");
11
- logger.info(
12
- `-> ${urlToFileSystemPath(coverageJsonFileUrl)} (${byteAsFileSize(
13
- Buffer.byteLength(coverageAsText),
14
- )})`,
15
- );
16
- await writeFile(coverageJsonFileUrl, coverageAsText);
17
- };
@@ -1,198 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { Abort } from "@jsenv/abort";
3
-
4
- import { filterV8Coverage } from "./v8_coverage.js";
5
- import { readNodeV8CoverageDirectory } from "./v8_coverage_node_directory.js";
6
- import { composeTwoV8Coverages } from "./v8_coverage_composition.js";
7
- import { composeTwoFileByFileIstanbulCoverages } from "./istanbul_coverage_composition.js";
8
- import { v8CoverageToIstanbul } from "./v8_coverage_to_istanbul.js";
9
- import { composeV8AndIstanbul } from "./v8_and_istanbul.js";
10
- import { normalizeFileByFileCoveragePaths } from "./file_by_file_coverage.js";
11
- import { getMissingFileByFileCoverage } from "./missing_coverage.js";
12
-
13
- export const reportToCoverage = async (
14
- report,
15
- {
16
- signal,
17
- logger,
18
- rootDirectoryUrl,
19
- coverageConfig,
20
- coverageIncludeMissing,
21
- coverageMethodForNodeJs,
22
- coverageV8ConflictWarning,
23
- },
24
- ) => {
25
- // collect v8 and istanbul coverage from executions
26
- let { v8Coverage, fileByFileIstanbulCoverage } = await getCoverageFromReport({
27
- signal,
28
- report,
29
- onMissing: ({ file, executionResult, executionName }) => {
30
- // several reasons not to have coverage here:
31
- // 1. the file we executed did not import an instrumented file.
32
- // - a test file without import
33
- // - a test file importing only file excluded from coverage
34
- // - a coverDescription badly configured so that we don't realize
35
- // a file should be covered
36
-
37
- // 2. the file we wanted to executed timedout
38
- // - infinite loop
39
- // - too extensive operation
40
- // - a badly configured or too low allocatedMs for that execution.
41
-
42
- // 3. the file we wanted to execute contains syntax-error
43
-
44
- // in any scenario we are fine because
45
- // coverDescription will generate empty coverage for files
46
- // that were suppose to be coverage but were not.
47
- if (
48
- executionResult.status === "completed" &&
49
- executionResult.type === "node" &&
50
- coverageMethodForNodeJs !== "NODE_V8_COVERAGE"
51
- ) {
52
- logger.warn(
53
- `"${executionName}" execution of ${file} did not properly write coverage into ${executionResult.coverageFileUrl}`,
54
- );
55
- }
56
- },
57
- });
58
-
59
- if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
60
- await readNodeV8CoverageDirectory({
61
- logger,
62
- signal,
63
- onV8Coverage: async (nodeV8Coverage) => {
64
- const nodeV8CoverageLight = await filterV8Coverage(nodeV8Coverage, {
65
- rootDirectoryUrl,
66
- coverageConfig,
67
- });
68
- v8Coverage = v8Coverage
69
- ? composeTwoV8Coverages(v8Coverage, nodeV8CoverageLight)
70
- : nodeV8CoverageLight;
71
- },
72
- });
73
- }
74
-
75
- // try to merge v8 with istanbul, if any
76
- let fileByFileCoverage;
77
- if (v8Coverage) {
78
- let v8FileByFileCoverage = await v8CoverageToIstanbul(v8Coverage, {
79
- signal,
80
- });
81
-
82
- v8FileByFileCoverage = normalizeFileByFileCoveragePaths(
83
- v8FileByFileCoverage,
84
- rootDirectoryUrl,
85
- );
86
-
87
- if (fileByFileIstanbulCoverage) {
88
- fileByFileIstanbulCoverage = normalizeFileByFileCoveragePaths(
89
- fileByFileIstanbulCoverage,
90
- rootDirectoryUrl,
91
- );
92
- fileByFileCoverage = composeV8AndIstanbul(
93
- v8FileByFileCoverage,
94
- fileByFileIstanbulCoverage,
95
- { coverageV8ConflictWarning },
96
- );
97
- } else {
98
- fileByFileCoverage = v8FileByFileCoverage;
99
- }
100
- }
101
- // get istanbul only
102
- else if (fileByFileIstanbulCoverage) {
103
- fileByFileCoverage = normalizeFileByFileCoveragePaths(
104
- fileByFileIstanbulCoverage,
105
- rootDirectoryUrl,
106
- );
107
- }
108
- // no coverage found in execution (or zero file where executed)
109
- else {
110
- fileByFileCoverage = {};
111
- }
112
-
113
- // now add coverage for file not covered
114
- if (coverageIncludeMissing) {
115
- const missingFileByFileCoverage = await getMissingFileByFileCoverage({
116
- signal,
117
- rootDirectoryUrl,
118
- coverageConfig,
119
- fileByFileCoverage,
120
- });
121
- Object.assign(
122
- fileByFileCoverage,
123
- normalizeFileByFileCoveragePaths(
124
- missingFileByFileCoverage,
125
- rootDirectoryUrl,
126
- ),
127
- );
128
- }
129
-
130
- return fileByFileCoverage;
131
- };
132
-
133
- const getCoverageFromReport = async ({ signal, report, onMissing }) => {
134
- const operation = Abort.startOperation();
135
- operation.addAbortSignal(signal);
136
-
137
- try {
138
- let v8Coverage;
139
- let fileByFileIstanbulCoverage;
140
-
141
- // collect v8 and istanbul coverage from executions
142
- await Object.keys(report).reduce(async (previous, file) => {
143
- operation.throwIfAborted();
144
- await previous;
145
-
146
- const executionResultForFile = report[file];
147
- await Object.keys(executionResultForFile).reduce(
148
- async (previous, executionName) => {
149
- operation.throwIfAborted();
150
- await previous;
151
-
152
- const executionResultForFileOnRuntime =
153
- executionResultForFile[executionName];
154
- const { coverageFileUrl } = executionResultForFileOnRuntime;
155
- let executionCoverage;
156
- try {
157
- executionCoverage = JSON.parse(
158
- String(readFileSync(new URL(coverageFileUrl))),
159
- );
160
- } catch (e) {
161
- if (e.code === "ENOENT" || e.name === "SyntaxError") {
162
- onMissing({
163
- executionName,
164
- file,
165
- executionResult: executionResultForFileOnRuntime,
166
- });
167
- return;
168
- }
169
- throw e;
170
- }
171
-
172
- if (isV8Coverage(executionCoverage)) {
173
- v8Coverage = v8Coverage
174
- ? composeTwoV8Coverages(v8Coverage, executionCoverage)
175
- : executionCoverage;
176
- } else {
177
- fileByFileIstanbulCoverage = fileByFileIstanbulCoverage
178
- ? composeTwoFileByFileIstanbulCoverages(
179
- fileByFileIstanbulCoverage,
180
- executionCoverage,
181
- )
182
- : executionCoverage;
183
- }
184
- },
185
- Promise.resolve(),
186
- );
187
- }, Promise.resolve());
188
-
189
- return {
190
- v8Coverage,
191
- fileByFileIstanbulCoverage,
192
- };
193
- } finally {
194
- await operation.end();
195
- }
196
- };
197
-
198
- const isV8Coverage = (coverage) => Boolean(coverage.result);
@@ -1,10 +0,0 @@
1
- import { ANSI } from "@jsenv/log";
2
-
3
- export const EXECUTION_COLORS = {
4
- executing: ANSI.BLUE,
5
- aborted: ANSI.MAGENTA,
6
- timedout: ANSI.MAGENTA,
7
- failed: ANSI.RED,
8
- completed: ANSI.GREEN,
9
- cancelled: ANSI.GREY,
10
- };
@@ -1,65 +0,0 @@
1
- import { Abort } from "@jsenv/abort";
2
- import { collectFiles } from "@jsenv/filesystem";
3
- import { createDetailedMessage } from "@jsenv/log";
4
-
5
- export const executionStepsFromTestPlan = async ({
6
- signal,
7
- rootDirectoryUrl,
8
- testPlan,
9
- }) => {
10
- try {
11
- const fileResultArray = await collectFiles({
12
- signal,
13
- directoryUrl: rootDirectoryUrl,
14
- associations: { testPlan },
15
- predicate: ({ testPlan }) => testPlan,
16
- });
17
- const executionSteps = [];
18
- fileResultArray.forEach(({ relativeUrl, meta }) => {
19
- const fileExecutionSteps = generateFileExecutionSteps({
20
- fileRelativeUrl: relativeUrl,
21
- filePlan: meta.testPlan,
22
- });
23
- executionSteps.push(...fileExecutionSteps);
24
- });
25
- return executionSteps;
26
- } catch (e) {
27
- if (Abort.isAbortError(e)) {
28
- return {
29
- aborted: true,
30
- planSummary: {},
31
- planReport: {},
32
- planCoverage: null,
33
- };
34
- }
35
- throw e;
36
- }
37
- };
38
-
39
- export const generateFileExecutionSteps = ({ fileRelativeUrl, filePlan }) => {
40
- const fileExecutionSteps = [];
41
- Object.keys(filePlan).forEach((executionName) => {
42
- const stepConfig = filePlan[executionName];
43
- if (stepConfig === null || stepConfig === undefined) {
44
- return;
45
- }
46
- if (typeof stepConfig !== "object") {
47
- throw new TypeError(
48
- createDetailedMessage(
49
- `found unexpected value in plan, they must be object`,
50
- {
51
- ["file relative path"]: fileRelativeUrl,
52
- ["execution name"]: executionName,
53
- ["value"]: stepConfig,
54
- },
55
- ),
56
- );
57
- }
58
- fileExecutionSteps.push({
59
- executionName,
60
- fileRelativeUrl,
61
- ...stepConfig,
62
- });
63
- });
64
- return fileExecutionSteps;
65
- };