UNPKG

6.06 kBJavaScriptView Raw
1'use strict';
2const os = require('os');
3const path = require('path');
4
5const plur = require('plur');
6const stripAnsi = require('strip-ansi');
7const supertap = require('supertap');
8const indentString = require('indent-string');
9
10const beautifyStack = require('./beautify-stack');
11const prefixTitle = require('./prefix-title');
12
13function 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) => { // eslint-disable-line unicorn/no-reduce
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
52class 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) { // eslint-disable-line complexity
135 const fileStats = this.stats && evt.testFile ? this.stats.byFile.get(evt.testFile) : null;
136
137 switch (evt.type) {
138 case 'declared-test':
139 // Ignore
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}
212module.exports = TapReporter;