@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
@@ -0,0 +1,187 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { inspectFileContent } from "@jsenv/inspect";
3
+ import { urlToFileSystemPath } from "@jsenv/urls";
4
+ import { ANSI } from "@jsenv/log";
5
+
6
+ export const formatErrorForTerminal = (
7
+ error,
8
+ { rootDirectoryUrl, mainFileRelativeUrl, mockFluctuatingValues, tryColors },
9
+ ) => {
10
+ if (!error.stack) {
11
+ return error.message;
12
+ }
13
+
14
+ let ansiSupported = ANSI.supported;
15
+ if (!tryColors) {
16
+ ANSI.supported = false;
17
+ }
18
+
19
+ let text = "";
20
+ write_code_frame: {
21
+ if (
22
+ error.site &&
23
+ error.site.url &&
24
+ error.site.url.startsWith("file:") &&
25
+ typeof error.site.line === "number"
26
+ ) {
27
+ const content = readFileSync(new URL(error.site.url), "utf8");
28
+ text += inspectFileContent({
29
+ content,
30
+ line: error.site.line,
31
+ column: error.site.column,
32
+ linesAbove: 2,
33
+ linesBelow: 0,
34
+ lineMaxWidth: process.stdout.columns,
35
+ format: (string, type) => {
36
+ return {
37
+ line_number_aside: () => ANSI.color(string, ANSI.GREY),
38
+ char: () => string,
39
+ marker_overflow_left: () => ANSI.color(string, ANSI.GREY),
40
+ marker_overflow_right: () => ANSI.color(string, ANSI.GREY),
41
+ marker_line: () => ANSI.color(string, ANSI.RED),
42
+ marker_column: () => ANSI.color(string, ANSI.RED),
43
+ }[type]();
44
+ },
45
+ });
46
+ text += `\n`;
47
+ }
48
+ }
49
+ text += `${error.name}: ${error.message}`;
50
+ write_stack: {
51
+ const stringifyUrlSite = ({ url, line, column, urlIsMain }) => {
52
+ let urlAsPath = String(url).startsWith("file:")
53
+ ? urlToFileSystemPath(url)
54
+ : url;
55
+ if (mockFluctuatingValues) {
56
+ const rootDirectoryPath = urlToFileSystemPath(rootDirectoryUrl);
57
+ urlAsPath = urlAsPath.replace(rootDirectoryPath, "[mock]");
58
+ if (process.platform === "win32") {
59
+ urlAsPath = urlAsPath.replace(/\\/g, "/");
60
+ }
61
+ }
62
+ if (urlIsMain) {
63
+ urlAsPath = ANSI.effect(urlAsPath, ANSI.BOLD);
64
+ }
65
+ if (typeof line === "number" && typeof column === "number") {
66
+ return `${urlAsPath}:${line}:${column}`;
67
+ }
68
+ if (typeof line === "number") {
69
+ return `${urlAsPath}:${line}`;
70
+ }
71
+ return urlAsPath;
72
+ };
73
+
74
+ let stackTrace = "";
75
+ const stackFrames = error.stackFrames;
76
+ if (stackFrames && stackFrames.length) {
77
+ let atLeastOneNonNative = false;
78
+ let lastStackFrameForMain;
79
+ const mainFileUrl = new URL(mainFileRelativeUrl, rootDirectoryUrl).href;
80
+ for (const stackFrame of stackFrames) {
81
+ if (stackFrame.url === mainFileUrl) {
82
+ lastStackFrameForMain = stackFrame;
83
+ }
84
+ if (!stackFrame.native) {
85
+ atLeastOneNonNative = true;
86
+ }
87
+ }
88
+
89
+ for (const stackFrame of stackFrames) {
90
+ if (atLeastOneNonNative && stackFrame.native) {
91
+ continue;
92
+ }
93
+ let stackFrameString = stackFrame.raw;
94
+ stackFrameString = replaceUrls(
95
+ stackFrameString,
96
+ ({ url, line, column }) => {
97
+ const replacement = stringifyUrlSite({
98
+ url,
99
+ urlIsMain: stackFrame === lastStackFrameForMain,
100
+ line,
101
+ column,
102
+ });
103
+ return replacement;
104
+ },
105
+ );
106
+ if (stackTrace) stackTrace += "\n";
107
+ stackTrace += stackFrameString;
108
+ }
109
+ } else if (error.stackTrace) {
110
+ stackTrace = replaceUrls(error.stackTrace, ({ url, line, column }) => {
111
+ const replacement = stringifyUrlSite({
112
+ url,
113
+ line,
114
+ column,
115
+ });
116
+ return replacement;
117
+ });
118
+ } else if (error.site) {
119
+ stackTrace += ` ${stringifyUrlSite({
120
+ url: error.site.url,
121
+ line: error.site.line,
122
+ column: error.site.column,
123
+ })}`;
124
+ }
125
+
126
+ if (stackTrace) {
127
+ text += `\n${stackTrace}`;
128
+ }
129
+ }
130
+
131
+ if (!tryColors) {
132
+ ANSI.supported = ansiSupported;
133
+ }
134
+
135
+ return text;
136
+ };
137
+
138
+ // `Error: yo
139
+ // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
140
+ // at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
141
+ // at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
142
+ // at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
143
+ // debugger
144
+ // })
145
+ const replaceUrls = (source, replace) => {
146
+ return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
147
+ let replacement = "";
148
+ const lastChar = match[match.length - 1];
149
+
150
+ // hotfix because our url regex sucks a bit
151
+ const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
152
+ if (endsWithSeparationChar) {
153
+ match = match.slice(0, -1);
154
+ }
155
+
156
+ const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/;
157
+ const lineAndColumMatch = match.match(lineAndColumnPattern);
158
+ if (lineAndColumMatch) {
159
+ const lineAndColumnString = lineAndColumMatch[0];
160
+ const lineString = lineAndColumMatch[1];
161
+ const columnString = lineAndColumMatch[2];
162
+ replacement = replace({
163
+ url: match.slice(0, -lineAndColumnString.length),
164
+ line: lineString ? parseInt(lineString) : null,
165
+ column: columnString ? parseInt(columnString) : null,
166
+ });
167
+ } else {
168
+ const linePattern = /:([0-9]+)$/;
169
+ const lineMatch = match.match(linePattern);
170
+ if (lineMatch) {
171
+ const lineString = lineMatch[0];
172
+ replacement = replace({
173
+ url: match.slice(0, -lineString.length),
174
+ line: lineString ? parseInt(lineString) : null,
175
+ });
176
+ } else {
177
+ replacement = replace({
178
+ url: match,
179
+ });
180
+ }
181
+ }
182
+ if (endsWithSeparationChar) {
183
+ return `${replacement}${lastChar}`;
184
+ }
185
+ return replacement;
186
+ });
187
+ };
@@ -2,14 +2,14 @@ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
2
2
 
3
3
  export const githubAnnotationFromError = (
4
4
  error,
5
- { rootDirectoryUrl, executionInfo },
5
+ { rootDirectoryUrl, execution },
6
6
  ) => {
7
7
  const annotation = {
8
8
  annotation_level: "failure",
9
- path: executionInfo.fileRelativeUrl,
9
+ path: execution.fileRelativeUrl,
10
10
  start_line: 1,
11
11
  end_line: 1,
12
- title: `Error while executing ${executionInfo.fileRelativeUrl} on ${executionInfo.runtimeName}@${executionInfo.runtimeVersion}`,
12
+ title: `Error while executing ${execution.fileRelativeUrl} on ${execution.runtimeName}@${execution.runtimeVersion}`,
13
13
  };
14
14
  const exception = asException(error, { rootDirectoryUrl });
15
15
  if (exception.site && typeof exception.site.line === "number") {
@@ -0,0 +1,149 @@
1
+ // run prettier on it
2
+
3
+ // https://github.com/testmoapp/junitxml
4
+ // https://llg.cubic.org/docs/junit/
5
+
6
+ import { assertAndNormalizeFileUrl, writeFileSync } from "@jsenv/filesystem";
7
+
8
+ import { formatErrorForTerminal } from "../format_error_for_terminal.js";
9
+ import { createXmlGenerator } from "./xml_generator.js";
10
+
11
+ export const reportAsJunitXml = async (
12
+ testPlanResult,
13
+ fileUrl,
14
+ { mockFluctuatingValues } = {},
15
+ ) => {
16
+ fileUrl = assertAndNormalizeFileUrl(fileUrl);
17
+
18
+ const createRootNode = createXmlGenerator({
19
+ rootNodeName: "testsuite",
20
+ canReceiveChildNames: ["testsuite", "testcase", "properties"],
21
+ canReceiveContentNames: ["failure", "error", "system-out", "system-err"],
22
+ });
23
+ const testSuite = createRootNode({
24
+ time: mockFluctuatingValues ? "[mock]" : testPlanResult.timings.end,
25
+ timestamp: mockFluctuatingValues
26
+ ? "[mock]"
27
+ : new Date(testPlanResult.timings.origin).toISOString().slice(0, -5), // Date and time of when the test run was executed
28
+ tests: testPlanResult.counters.planified,
29
+ skipped:
30
+ testPlanResult.counters.aborted + testPlanResult.counters.cancelled,
31
+ failures: testPlanResult.counters.timedout + testPlanResult.counters.failed, // Total number of failed tests
32
+ });
33
+
34
+ for (const fileRelativeUrl of Object.keys(testPlanResult.results)) {
35
+ const fileResults = testPlanResult.results[fileRelativeUrl];
36
+ for (const group of Object.keys(fileResults)) {
37
+ const executionResult = fileResults[group];
38
+ const testCase = testSuite.createNode("testcase", {
39
+ file: fileRelativeUrl,
40
+ name: group,
41
+ time: mockFluctuatingValues ? "[mock]" : executionResult.timings.end, // duration of this execution
42
+ timestamp: mockFluctuatingValues
43
+ ? "[mock]"
44
+ : new Date(executionResult.timings.origin).toISOString().slice(0, -5), // Date and time of when the test run was executed
45
+ tests: 1, // Total number of tests for this execution
46
+ });
47
+ testSuite.appendChild(testCase);
48
+
49
+ if (executionResult.status === "aborted") {
50
+ testCase.attributes.skipped = 1;
51
+ const skipped = testCase.createNode("skipped", {
52
+ message: "Execution was aborted",
53
+ });
54
+ testSuite.appendChild(skipped);
55
+ }
56
+ if (executionResult.status === "timedout") {
57
+ testCase.attributes.failures = 1;
58
+ const failure = testCase.createNode("failure", {
59
+ message: `Execution timeout after ${executionResult.params.allocatedMs}ms"`,
60
+ type: "timeout",
61
+ });
62
+ testSuite.appendChild(failure);
63
+ }
64
+ if (executionResult.status === "failed") {
65
+ const [error] = executionResult.errors;
66
+ if (
67
+ error &&
68
+ typeof error.name === "string" &&
69
+ error.name.includes("AssertionError")
70
+ ) {
71
+ testCase.attributes.failures = 1;
72
+ const failure = testCase.createNode("failure", {
73
+ type: error.name,
74
+ });
75
+ testSuite.appendChild(failure);
76
+ failure.setContent(
77
+ formatErrorForTerminal(error, {
78
+ rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
79
+ mainFileRelativeUrl: fileRelativeUrl,
80
+ mockFluctuatingValues,
81
+ tryColors: false,
82
+ }),
83
+ );
84
+ } else {
85
+ testCase.attributes.errors = 1;
86
+ const errorNode = testCase.createNode("error", {
87
+ type: error.name,
88
+ });
89
+ testSuite.appendChild(errorNode);
90
+ errorNode.setContent(
91
+ formatErrorForTerminal(error, {
92
+ rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
93
+ mainFileRelativeUrl: fileRelativeUrl,
94
+ mockFluctuatingValues,
95
+ tryColors: false,
96
+ }),
97
+ );
98
+ }
99
+ }
100
+
101
+ const groupInfo = testPlanResult.groups[group];
102
+ const properties = {
103
+ runtimeName: groupInfo.runtimeName,
104
+ runtimeVersion: mockFluctuatingValues
105
+ ? "[mock]"
106
+ : groupInfo.runtimeVersion,
107
+ };
108
+ const propertiesNode = testCase.createNode("properties");
109
+ testCase.appendChild(propertiesNode);
110
+ for (const propertyName of Object.keys(properties)) {
111
+ const propertyNode = propertiesNode.createNode("property", {
112
+ name: propertyName,
113
+ value: properties[propertyName],
114
+ });
115
+ propertiesNode.appendChild(propertyNode);
116
+ }
117
+
118
+ const { consoleCalls } = executionResult;
119
+ let stdout = "";
120
+ let stderr = "";
121
+ for (const consoleCall of consoleCalls) {
122
+ if (groupInfo.runtimeType === "node") {
123
+ if (consoleCall.type === "error") {
124
+ stdout += consoleCall.text;
125
+ } else {
126
+ stderr += consoleCall.text;
127
+ }
128
+ } else {
129
+ stdout += consoleCall.text;
130
+ }
131
+ }
132
+ if (stdout.length) {
133
+ const systemOut = testSuite.createNode("system-out");
134
+ testCase.appendChild(systemOut);
135
+ systemOut.setContent(stdout);
136
+ }
137
+ if (stderr.length) {
138
+ const systemErr = testSuite.createNode("system-err");
139
+ testCase.appendChild(systemErr);
140
+ systemErr.setContent(stderr);
141
+ }
142
+ }
143
+ }
144
+
145
+ let junitXmlFileContent = `<?xml version="1.0" encoding="UTF-8"?>
146
+ ${testSuite.renderAsString()}`;
147
+
148
+ writeFileSync(new URL(fileUrl), junitXmlFileContent);
149
+ };
@@ -0,0 +1,141 @@
1
+ import he from "he";
2
+
3
+ export const createXmlGenerator = ({
4
+ rootNodeName,
5
+ canSelfCloseNames = [],
6
+ canReceiveChildNames = [],
7
+ canReceiveContentNames = [],
8
+ }) => {
9
+ const createNode = (name, attributes = {}) => {
10
+ const canSelfClose = canSelfCloseNames.includes(name);
11
+ const canReceiveChild = canReceiveChildNames.includes(name);
12
+ const canReceiveContent = canReceiveContentNames.includes(name);
13
+
14
+ const children = [];
15
+
16
+ const node = {
17
+ name,
18
+ content: "",
19
+ children,
20
+ attributes,
21
+ canSelfClose,
22
+ createNode,
23
+ appendChild: (childNode) => {
24
+ if (!canReceiveChild) {
25
+ throw new Error(`cannot appendChild into ${name}`);
26
+ }
27
+ children.push(childNode);
28
+ return childNode;
29
+ },
30
+ setContent: (value) => {
31
+ if (!canReceiveContent) {
32
+ throw new Error(`cannot setContent on ${name}`);
33
+ }
34
+ node.content = value;
35
+ },
36
+ renderAsString: () => {
37
+ const renderNode = (node, { depth }) => {
38
+ let nodeString = "";
39
+ nodeString += `<${node.name}`;
40
+
41
+ write_attributes: {
42
+ const attributeNames = Object.keys(node.attributes);
43
+ if (attributeNames.length) {
44
+ let attributesSingleLine = "";
45
+ let attributesMultiLine = "";
46
+
47
+ for (const attributeName of attributeNames) {
48
+ let attributeValue = node.attributes[attributeName];
49
+ if (typeof attributeValue === "number") {
50
+ attributeValue = round(attributeValue);
51
+ }
52
+ if (attributeName === "viewBox") {
53
+ attributeValue = attributeValue
54
+ .split(",")
55
+ .map((v) => round(parseFloat(v.trim())))
56
+ .join(", ");
57
+ }
58
+ attributesSingleLine += ` ${attributeName}="${attributeValue}"`;
59
+ attributesMultiLine += `\n `;
60
+ attributesMultiLine += " ".repeat(depth);
61
+ attributesMultiLine += `${attributeName}="${attributeValue}"`;
62
+ }
63
+ attributesMultiLine += "\n";
64
+ attributesMultiLine += " ".repeat(depth);
65
+
66
+ if (attributesSingleLine.length < 100) {
67
+ nodeString += attributesSingleLine;
68
+ } else {
69
+ nodeString += attributesMultiLine;
70
+ }
71
+ }
72
+ }
73
+
74
+ let innerHTML = "";
75
+ if (node.content) {
76
+ const contentEncoded = he.encode(node.content, { decimal: false });
77
+ innerHTML += contentEncoded;
78
+ }
79
+ write_children: {
80
+ if (node.children.length > 0) {
81
+ for (const child of node.children) {
82
+ innerHTML += "\n ";
83
+ innerHTML += " ".repeat(depth);
84
+ innerHTML += renderNode(child, {
85
+ depth: depth + 1,
86
+ });
87
+ }
88
+ innerHTML += "\n";
89
+ innerHTML += " ".repeat(depth);
90
+ }
91
+ }
92
+ if (innerHTML === "") {
93
+ if (node.canSelfClose) {
94
+ nodeString += `/>`;
95
+ } else {
96
+ nodeString += `></${node.name}>`;
97
+ }
98
+ } else {
99
+ nodeString += `>`;
100
+ nodeString += innerHTML;
101
+ nodeString += `</${node.name}>`;
102
+ }
103
+ return nodeString;
104
+ };
105
+
106
+ return renderNode(node, {
107
+ depth: 0,
108
+ });
109
+ },
110
+ };
111
+
112
+ return node;
113
+ };
114
+
115
+ return (rootNodeAttributes) => createNode(rootNodeName, rootNodeAttributes);
116
+ };
117
+
118
+ export const createSvgRootNode = createXmlGenerator({
119
+ rootNodeName: "svg",
120
+ canSelfCloseNames: ["path", "rect", "circle"],
121
+ canReceiveChildNames: ["svg", "foreignObject", "g"],
122
+ canReceiveContentNames: ["text", "style"],
123
+ });
124
+
125
+ // not used for now and needs to configure canReceiveChildNames and so on...
126
+ export const createXmlRootNode = createXmlGenerator({
127
+ rootNodeName: "xml",
128
+ });
129
+
130
+ // not used for now and needs to configure canReceiveChildNames and so on...
131
+ export const createHtmlRootNode = createXmlGenerator({
132
+ rootNodeName: "html",
133
+ });
134
+
135
+ // Round: Make number values smaller in output
136
+ // Eg: 14.23734 becomes 14.24
137
+ // Credit @Chris Martin: https://stackoverflow.com/a/43012696/2816869
138
+ const round = (x) => {
139
+ const rounded = Number(`${Math.round(`${x}e2`)}e-2`);
140
+ return rounded;
141
+ };