@reporters/gh 1.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.
- package/README.md +41 -0
- package/index.js +260 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@reporters/gh)  [](https://codecov.io/gh/MoLow/reporters)
|
|
2
|
+
|
|
3
|
+
# Github Actions Reporter
|
|
4
|
+
A Github actions reporter for `node:test`
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install --save-dev @reporters/gh
|
|
10
|
+
```
|
|
11
|
+
or
|
|
12
|
+
```bash
|
|
13
|
+
yarn add --dev @reporters/gh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
# .github/workflows/test.yml
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: node --test --test-reporter=@reporters/gh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Result
|
|
25
|
+
|
|
26
|
+
when test failed, annotations will be added inside the github UI, with corresponding errors and diagnostics.
|
|
27
|
+
see [example run](https://github.com/MoLow/reporters/actions/runs/5607828636):
|
|
28
|
+
|
|
29
|
+
#### Inline annotations
|
|
30
|
+
|
|
31
|
+
<img width="810" alt="Inline Annotation" src="https://user-images.githubusercontent.com/8221854/254798653-0c06278e-696b-42eb-8275-364b7eb3133b.png">
|
|
32
|
+
|
|
33
|
+
additionally, Annotations and summary will be added to the summary of the test run.
|
|
34
|
+
|
|
35
|
+
#### Annotations
|
|
36
|
+
|
|
37
|
+
<img width="810" alt="Annotation" src="https://user-images.githubusercontent.com/8221854/254798495-38c2a8ea-c9e0-4e87-a13e-677826b72192.png">
|
|
38
|
+
|
|
39
|
+
#### Summary
|
|
40
|
+
<img width="815" alt="Summary" src="https://github.com/MoLow/reporters/assets/8221854/8934f5bb-3342-430c-9ae0-3c608a40c9f0">
|
|
41
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-underscore-dangle */
|
|
4
|
+
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const { styleText, inspect } = require('node:util');
|
|
7
|
+
const { Transform } = require('node:stream');
|
|
8
|
+
const { relative } = require('node:path');
|
|
9
|
+
// eslint-disable-next-line import/no-unresolved
|
|
10
|
+
const { spec: Spec } = require('node:test/reporters');
|
|
11
|
+
const { emitSummary, handleEvent, isTopLevelDiagnostic } = require('@reporters/github');
|
|
12
|
+
|
|
13
|
+
const reporterColorMap = {
|
|
14
|
+
'test:fail': 'red',
|
|
15
|
+
'test:pass': 'green',
|
|
16
|
+
'test:diagnostic': 'blue',
|
|
17
|
+
warn: 'yellow',
|
|
18
|
+
error: 'red',
|
|
19
|
+
info: 'blue',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const reporterUnicodeSymbolMap = {
|
|
23
|
+
'test:fail': '\u2716 ',
|
|
24
|
+
'test:pass': '\u2714 ',
|
|
25
|
+
'test:diagnostic': '\u2139 ',
|
|
26
|
+
'test:coverage': '\u2139 ',
|
|
27
|
+
'arrow:right': '\u25B6 ',
|
|
28
|
+
'hyphen:minus': '\uFE63 ',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const indentMemo = new Map();
|
|
32
|
+
function indent(nesting) {
|
|
33
|
+
let value = indentMemo.get(nesting);
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
value = ' '.repeat(nesting);
|
|
36
|
+
indentMemo.set(nesting, value);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function inspectWithNoCustomRetry(obj, options) {
|
|
42
|
+
try {
|
|
43
|
+
return inspect(obj, options);
|
|
44
|
+
} catch {
|
|
45
|
+
/* c8 ignore next 2 */
|
|
46
|
+
return inspect(obj, { ...options, customInspect: false });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inspectOptions = {
|
|
51
|
+
__proto__: null,
|
|
52
|
+
breakLength: Infinity,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function formatError(error, indentation) {
|
|
56
|
+
if (!error) return '';
|
|
57
|
+
const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error;
|
|
58
|
+
const message = inspectWithNoCustomRetry(err, inspectOptions).split(/\r?\n/).join(`\n${indentation} `);
|
|
59
|
+
return `\n${indentation} ${message}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const formatDuration = (m) => {
|
|
63
|
+
let ms = m;
|
|
64
|
+
if (ms < 0) ms = -ms;
|
|
65
|
+
const time = {
|
|
66
|
+
day: Math.floor(ms / 86400000),
|
|
67
|
+
hour: Math.floor(ms / 3600000) % 24,
|
|
68
|
+
minute: Math.floor(ms / 60000) % 60,
|
|
69
|
+
second: Math.floor(ms / 1000) % 60,
|
|
70
|
+
millisecond: Math.floor(ms) % 1000,
|
|
71
|
+
};
|
|
72
|
+
if (ms < 1 && ms > 0) {
|
|
73
|
+
return `${ms.toFixed(3)} milliseconds`;
|
|
74
|
+
}
|
|
75
|
+
if (time.day !== 0 || time.hour !== 0 || time.minute !== 0 || time.second !== 0) {
|
|
76
|
+
/* c8 ignore next 2 */
|
|
77
|
+
time.millisecond = 0;
|
|
78
|
+
}
|
|
79
|
+
return Object.entries(time)
|
|
80
|
+
.filter((val) => val[1] !== 0)
|
|
81
|
+
.map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
|
|
82
|
+
.join(', ');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
class SpecReporter extends Transform {
|
|
86
|
+
#isGitHubActions = Boolean(process.env.GITHUB_ACTIONS);
|
|
87
|
+
|
|
88
|
+
#specReporter = new Spec();
|
|
89
|
+
|
|
90
|
+
#stack = [];
|
|
91
|
+
|
|
92
|
+
#reported = [];
|
|
93
|
+
|
|
94
|
+
#failedTests = [];
|
|
95
|
+
|
|
96
|
+
#cwd = process.cwd();
|
|
97
|
+
|
|
98
|
+
#reportedGroup = false;
|
|
99
|
+
|
|
100
|
+
constructor() {
|
|
101
|
+
super({ __proto__: null, writableObjectMode: true });
|
|
102
|
+
if (this.#isGitHubActions) {
|
|
103
|
+
inspectOptions.colors = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#formatTestReport(type, data, prefix = '', indentation = '', hasChildren = false, showErrorDetails = true) {
|
|
108
|
+
let color = reporterColorMap[type] ?? 'white';
|
|
109
|
+
let symbol = reporterUnicodeSymbolMap[type] ?? ' ';
|
|
110
|
+
const { skip, todo } = data;
|
|
111
|
+
const durationMs = data.details?.duration_ms ? styleText(['gray', 'italic'], ` (${formatDuration(data.details.duration_ms)})`, { validateStream: !this.#isGitHubActions }) : '';
|
|
112
|
+
let title = `${data.name}${durationMs}`;
|
|
113
|
+
|
|
114
|
+
if (skip !== undefined) {
|
|
115
|
+
title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`;
|
|
116
|
+
} else if (todo !== undefined) {
|
|
117
|
+
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const error = showErrorDetails ? formatError(data.details?.error, indentation) : '';
|
|
121
|
+
let err = error;
|
|
122
|
+
if (hasChildren) {
|
|
123
|
+
err = !error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (skip !== undefined) {
|
|
127
|
+
color = 'gray';
|
|
128
|
+
symbol = reporterUnicodeSymbolMap['hyphen:minus'];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let ghGroup = '';
|
|
132
|
+
let p = prefix;
|
|
133
|
+
if (this.#isGitHubActions) {
|
|
134
|
+
ghGroup = '::group::';
|
|
135
|
+
if (this.#reportedGroup) {
|
|
136
|
+
p = `::endgroup::\n${prefix}`;
|
|
137
|
+
}
|
|
138
|
+
this.#reportedGroup = true;
|
|
139
|
+
}
|
|
140
|
+
return `${p}${ghGroup}${indentation}${styleText(color, `${symbol}${title}`, { validateStream: !this.#isGitHubActions })}${err}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#formatFailedTestResults() {
|
|
144
|
+
if (this.#failedTests.length === 0) {
|
|
145
|
+
/* c8 ignore next 2 */
|
|
146
|
+
return this.#reportedGroup ? '::endgroup::\n' : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const results = [
|
|
150
|
+
`\n${styleText('red', `${reporterUnicodeSymbolMap['test:fail']}failing tests:`, { validateStream: !this.#isGitHubActions })}\n`,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (this.#reportedGroup) {
|
|
154
|
+
results.unshift('::endgroup::\n');
|
|
155
|
+
this.#reportedGroup = false; // Reset the group state for the next run
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < this.#failedTests.length; i += 1) {
|
|
159
|
+
const test = this.#failedTests[i];
|
|
160
|
+
const formattedErr = this.#formatTestReport('test:fail', test);
|
|
161
|
+
|
|
162
|
+
if (test.file) {
|
|
163
|
+
const relPath = relative(this.#cwd, test.file);
|
|
164
|
+
const location = `test at ${relPath}:${test.line}:${test.column}`;
|
|
165
|
+
results.push(location);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
results.push(formattedErr);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.#reportedGroup) {
|
|
172
|
+
results.push('::endgroup::\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.#failedTests = []; // Clean up the failed tests
|
|
176
|
+
return results.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#handleTestReportEvent(type, data) {
|
|
180
|
+
const subtest = this.#stack.shift(); // This is the matching `test:start` event
|
|
181
|
+
if (subtest) {
|
|
182
|
+
assert(subtest.type === 'test:start');
|
|
183
|
+
assert(subtest.data.nesting === data.nesting);
|
|
184
|
+
assert(subtest.data.name === data.name);
|
|
185
|
+
}
|
|
186
|
+
let prefix = '';
|
|
187
|
+
while (this.#stack.length) {
|
|
188
|
+
// Report all the parent `test:start` events
|
|
189
|
+
const parent = this.#stack.pop();
|
|
190
|
+
assert(parent.type === 'test:start');
|
|
191
|
+
const msg = parent.data;
|
|
192
|
+
this.#reported.unshift(msg);
|
|
193
|
+
if (this.#isGitHubActions) {
|
|
194
|
+
prefix += `${reporterUnicodeSymbolMap['arrow:right']}${indent(msg.nesting)}${msg.name}\n`;
|
|
195
|
+
} else {
|
|
196
|
+
prefix += `${indent(msg.nesting)}${reporterUnicodeSymbolMap['arrow:right']}${msg.name}\n`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
let hasChildren = false;
|
|
200
|
+
if (this.#reported[0] && this.#reported[0].nesting === data.nesting
|
|
201
|
+
&& this.#reported[0].name === data.name) {
|
|
202
|
+
this.#reported.shift();
|
|
203
|
+
hasChildren = true;
|
|
204
|
+
}
|
|
205
|
+
const indentation = indent(data.nesting);
|
|
206
|
+
return `${this.#formatTestReport(type, data, prefix, indentation, hasChildren, false)}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#handleEvent({ type, data }) {
|
|
210
|
+
if (this.#isGitHubActions) {
|
|
211
|
+
handleEvent({ type, data });
|
|
212
|
+
}
|
|
213
|
+
switch (type) {
|
|
214
|
+
case 'test:fail':
|
|
215
|
+
if (data.details?.error?.failureType !== 'subtestsFailed') {
|
|
216
|
+
this.#failedTests.push(data);
|
|
217
|
+
}
|
|
218
|
+
return this.#handleTestReportEvent(type, data);
|
|
219
|
+
case 'test:pass':
|
|
220
|
+
return this.#handleTestReportEvent(type, data);
|
|
221
|
+
case 'test:start':
|
|
222
|
+
this.#stack.unshift({ __proto__: null, data, type });
|
|
223
|
+
break;
|
|
224
|
+
case 'test:diagnostic': {
|
|
225
|
+
if (isTopLevelDiagnostic(data)) {
|
|
226
|
+
return '';
|
|
227
|
+
}
|
|
228
|
+
const diagnosticColor = reporterColorMap[data.level] || reporterColorMap.info;
|
|
229
|
+
return `${indent(data.nesting)}${styleText(diagnosticColor, `${reporterUnicodeSymbolMap[type]}${data.message}`, { validateStream: !this.#isGitHubActions })}\n`;
|
|
230
|
+
}
|
|
231
|
+
case 'test:summary':
|
|
232
|
+
// We report only the root test summary
|
|
233
|
+
if (data.file === undefined) {
|
|
234
|
+
/* c8 ignore next 2 */
|
|
235
|
+
return this.#formatFailedTestResults();
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
}
|
|
240
|
+
return ''; // No output for other event types
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_transform({ type, data }, encoding, callback) {
|
|
244
|
+
if (type === 'test:coverage' || type === 'test:stderr' || type === 'test:stdout') {
|
|
245
|
+
/* c8 ignore next 3 */
|
|
246
|
+
this.#specReporter._transform({ type, data }, encoding, callback);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
callback(null, this.#handleEvent({ __proto__: null, type, data }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_flush(callback) {
|
|
253
|
+
callback(null, this.#formatFailedTestResults());
|
|
254
|
+
if (this.#isGitHubActions) {
|
|
255
|
+
emitSummary();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = new SpecReporter();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reporters/gh",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A github actions reporter for `node:test`",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"github actions",
|
|
8
|
+
"node:test",
|
|
9
|
+
"test",
|
|
10
|
+
"reporter",
|
|
11
|
+
"reporters"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@actions/core": "^1.10.0",
|
|
15
|
+
"@reporters/github": "*"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"./index.js"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test-reporter=./index.js --test-reporter-destination=stdout --test"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/MoLow/reporters/issues"
|
|
25
|
+
},
|
|
26
|
+
"main": "index.js",
|
|
27
|
+
"homepage": "https://github.com/MoLow/reporters/tree/main/packages/github",
|
|
28
|
+
"repository": "https://github.com/MoLow/reporters.git",
|
|
29
|
+
"author": "Moshe Atlow",
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|