1 | 'use strict';
|
2 | const os = require('os');
|
3 | const path = require('path');
|
4 |
|
5 | const plur = require('plur');
|
6 | const stripAnsi = require('strip-ansi');
|
7 | const supertap = require('supertap');
|
8 | const indentString = require('indent-string');
|
9 |
|
10 | const beautifyStack = require('./beautify-stack');
|
11 | const prefixTitle = require('./prefix-title');
|
12 |
|
13 | function dumpError(error) {
|
14 | const object = {...error.object};
|
15 | if (error.name) {
|
16 | object.name = error.name;
|
17 | }
|
18 |
|
19 | if (error.message) {
|
20 | object.message = error.message;
|
21 | }
|
22 |
|
23 | if (error.avaAssertionError) {
|
24 | if (error.assertion) {
|
25 | object.assertion = error.assertion;
|
26 | }
|
27 |
|
28 | if (error.operator) {
|
29 | object.operator = error.operator;
|
30 | }
|
31 |
|
32 | if (error.values.length > 0) {
|
33 | object.values = error.values.reduce((acc, value) => {
|
34 | acc[value.label] = stripAnsi(value.formatted);
|
35 | return acc;
|
36 | }, {});
|
37 | }
|
38 | }
|
39 |
|
40 | if (error.nonErrorObject) {
|
41 | object.message = 'Non-error object';
|
42 | object.formatted = stripAnsi(error.formatted);
|
43 | }
|
44 |
|
45 | if (error.stack) {
|
46 | object.at = error.shouldBeautifyStack ? beautifyStack(error.stack).join('\n') : error.stack;
|
47 | }
|
48 |
|
49 | return object;
|
50 | }
|
51 |
|
52 | class TapReporter {
|
53 | constructor(options) {
|
54 | this.i = 0;
|
55 |
|
56 | this.stdStream = options.stdStream;
|
57 | this.reportStream = options.reportStream;
|
58 |
|
59 | this.crashCount = 0;
|
60 | this.filesWithMissingAvaImports = new Set();
|
61 | this.prefixTitle = (testFile, title) => title;
|
62 | this.relativeFile = file => path.relative(options.projectDir, file);
|
63 | this.stats = null;
|
64 | }
|
65 |
|
66 | startRun(plan) {
|
67 | if (plan.files.length > 1) {
|
68 | this.prefixTitle = (testFile, title) => prefixTitle(plan.filePathPrefix, testFile, title);
|
69 | }
|
70 |
|
71 | plan.status.on('stateChange', evt => this.consumeStateChange(evt));
|
72 |
|
73 | this.reportStream.write(supertap.start() + os.EOL);
|
74 | }
|
75 |
|
76 | endRun() {
|
77 | if (this.stats) {
|
78 | this.reportStream.write(supertap.finish({
|
79 | crashed: this.crashCount,
|
80 | failed: this.stats.failedTests + this.stats.remainingTests,
|
81 | passed: this.stats.passedTests + this.stats.passedKnownFailingTests,
|
82 | skipped: this.stats.skippedTests,
|
83 | todo: this.stats.todoTests
|
84 | }) + os.EOL);
|
85 |
|
86 | if (this.stats.parallelRuns) {
|
87 | const {currentFileCount, currentIndex, totalRuns} = this.stats.parallelRuns;
|
88 | this.reportStream.write(`# Ran ${currentFileCount} test ${plur('file', currentFileCount)} out of ${this.stats.files} for job ${currentIndex + 1} of ${totalRuns}` + os.EOL + os.EOL);
|
89 | }
|
90 | } else {
|
91 | this.reportStream.write(supertap.finish({
|
92 | crashed: this.crashCount,
|
93 | failed: 0,
|
94 | passed: 0,
|
95 | skipped: 0,
|
96 | todo: 0
|
97 | }) + os.EOL);
|
98 | }
|
99 | }
|
100 |
|
101 | writeTest(evt, flags) {
|
102 | this.reportStream.write(supertap.test(this.prefixTitle(evt.testFile, evt.title), {
|
103 | comment: evt.logs,
|
104 | error: evt.err ? dumpError(evt.err) : null,
|
105 | index: ++this.i,
|
106 | passed: flags.passed,
|
107 | skip: flags.skip,
|
108 | todo: flags.todo
|
109 | }) + os.EOL);
|
110 | }
|
111 |
|
112 | writeCrash(evt, title) {
|
113 | this.crashCount++;
|
114 | this.reportStream.write(supertap.test(title || evt.err.summary || evt.type, {
|
115 | comment: evt.logs,
|
116 | error: evt.err ? dumpError(evt.err) : null,
|
117 | index: ++this.i,
|
118 | passed: false,
|
119 | skip: false,
|
120 | todo: false
|
121 | }) + os.EOL);
|
122 | }
|
123 |
|
124 | writeComment(evt, {title = this.prefixTitle(evt.testFile, evt.title)}) {
|
125 | this.reportStream.write(`# ${stripAnsi(title)}${os.EOL}`);
|
126 | if (evt.logs) {
|
127 | for (const log of evt.logs) {
|
128 | const logLines = indentString(log, 4).replace(/^ {4}/, ' # ');
|
129 | this.reportStream.write(`${logLines}${os.EOL}`);
|
130 | }
|
131 | }
|
132 | }
|
133 |
|
134 | consumeStateChange(evt) {
|
135 | const fileStats = this.stats && evt.testFile ? this.stats.byFile.get(evt.testFile) : null;
|
136 |
|
137 | switch (evt.type) {
|
138 | case 'declared-test':
|
139 |
|
140 | break;
|
141 | case 'hook-failed':
|
142 | this.writeTest(evt, {passed: false, todo: false, skip: false});
|
143 | break;
|
144 | case 'hook-finished':
|
145 | this.writeComment(evt, {});
|
146 | break;
|
147 | case 'internal-error':
|
148 | this.writeCrash(evt);
|
149 | break;
|
150 | case 'missing-ava-import':
|
151 | this.filesWithMissingAvaImports.add(evt.testFile);
|
152 | this.writeCrash(evt, `No tests found in ${this.relativeFile(evt.testFile)}, make sure to import "ava" at the top of your test file`);
|
153 | break;
|
154 | case 'selected-test':
|
155 | if (evt.skip) {
|
156 | this.writeTest(evt, {passed: true, todo: false, skip: true});
|
157 | } else if (evt.todo) {
|
158 | this.writeTest(evt, {passed: false, todo: true, skip: false});
|
159 | }
|
160 |
|
161 | break;
|
162 | case 'snapshot-error':
|
163 | this.writeComment(evt, {title: 'Could not update snapshots'});
|
164 | break;
|
165 | case 'stats':
|
166 | this.stats = evt.stats;
|
167 | break;
|
168 | case 'test-failed':
|
169 | this.writeTest(evt, {passed: false, todo: false, skip: false});
|
170 | break;
|
171 | case 'test-passed':
|
172 | this.writeTest(evt, {passed: true, todo: false, skip: false});
|
173 | break;
|
174 | case 'timeout':
|
175 | this.writeCrash(evt, `Exited because no new tests completed within the last ${evt.period}ms of inactivity`);
|
176 | break;
|
177 | case 'uncaught-exception':
|
178 | this.writeCrash(evt);
|
179 | break;
|
180 | case 'unhandled-rejection':
|
181 | this.writeCrash(evt);
|
182 | break;
|
183 | case 'worker-failed':
|
184 | if (!this.filesWithMissingAvaImports.has(evt.testFile)) {
|
185 | if (evt.nonZeroExitCode) {
|
186 | this.writeCrash(evt, `${this.relativeFile(evt.testFile)} exited with a non-zero exit code: ${evt.nonZeroExitCode}`);
|
187 | } else {
|
188 | this.writeCrash(evt, `${this.relativeFile(evt.testFile)} exited due to ${evt.signal}`);
|
189 | }
|
190 | }
|
191 |
|
192 | break;
|
193 | case 'worker-finished':
|
194 | if (!evt.forcedExit && !this.filesWithMissingAvaImports.has(evt.testFile)) {
|
195 | if (fileStats.declaredTests === 0) {
|
196 | this.writeCrash(evt, `No tests found in ${this.relativeFile(evt.testFile)}`);
|
197 | } else if (!this.failFastEnabled && fileStats.remainingTests > 0) {
|
198 | this.writeComment(evt, {title: `${fileStats.remainingTests} ${plur('test', fileStats.remainingTests)} remaining in ${this.relativeFile(evt.testFile)}`});
|
199 | }
|
200 | }
|
201 |
|
202 | break;
|
203 | case 'worker-stderr':
|
204 | case 'worker-stdout':
|
205 | this.stdStream.write(evt.chunk);
|
206 | break;
|
207 | default:
|
208 | break;
|
209 | }
|
210 | }
|
211 | }
|
212 | module.exports = TapReporter;
|