Skip to content

Commit 7724aab

Browse files
authored
Add failure only reporter (#2190)
Closes #829 Copies the expanded reporter and test and removes output unrelated to failed tests.
1 parent 525f77b commit 7724aab

File tree

5 files changed

+563
-1
lines changed

5 files changed

+563
-1
lines changed

pkgs/test/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dependencies:
3535

3636
# Use an exact version until the test_api and test_core package are stable.
3737
test_api: 0.7.0
38-
test_core: 0.6.0
38+
test_core: 0.6.1
3939

4040
typed_data: ^1.3.0
4141
web_socket_channel: ^2.0.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@TestOn('vm')
6+
library;
7+
8+
import 'dart:async';
9+
10+
import 'package:test/test.dart';
11+
import 'package:test_descriptor/test_descriptor.dart' as d;
12+
13+
import '../io.dart';
14+
15+
void main() {
16+
setUpAll(precompileTestExecutable);
17+
18+
test('reports when no tests are run', () async {
19+
await d.file('test.dart', 'void main() {}').create();
20+
21+
var test = await runTest(['test.dart'], reporter: 'failures-only');
22+
expect(test.stdout, emitsThrough(contains('No tests ran.')));
23+
await test.shouldExit(79);
24+
});
25+
26+
test('runs several successful tests and reports only at the end', () {
27+
return _expectReport('''
28+
test('success 1', () {});
29+
test('success 2', () {});
30+
test('success 3', () {});''', '''
31+
+3: All tests passed!''');
32+
});
33+
34+
test('runs several failing tests and reports when each fails', () {
35+
return _expectReport('''
36+
test('failure 1', () => throw TestFailure('oh no'));
37+
test('failure 2', () => throw TestFailure('oh no'));
38+
test('failure 3', () => throw TestFailure('oh no'));''', '''
39+
+0 -1: failure 1 [E]
40+
oh no
41+
test.dart 6:33 main.<fn>
42+
43+
+0 -2: failure 2 [E]
44+
oh no
45+
test.dart 7:33 main.<fn>
46+
47+
+0 -3: failure 3 [E]
48+
oh no
49+
test.dart 8:33 main.<fn>
50+
51+
+0 -3: Some tests failed.''');
52+
});
53+
54+
test('includes the full stack trace with --verbose-trace', () async {
55+
await d.file('test.dart', '''
56+
import 'dart:async';
57+
58+
import 'package:test/test.dart';
59+
60+
void main() {
61+
test("failure", () => throw "oh no");
62+
}
63+
''').create();
64+
65+
var test = await runTest(['--verbose-trace', 'test.dart'],
66+
reporter: 'failures-only');
67+
expect(test.stdout, emitsThrough(contains('dart:async')));
68+
await test.shouldExit(1);
69+
});
70+
71+
test('reports only failing tests amid successful tests', () {
72+
return _expectReport('''
73+
test('failure 1', () => throw TestFailure('oh no'));
74+
test('success 1', () {});
75+
test('failure 2', () => throw TestFailure('oh no'));
76+
test('success 2', () {});''', '''
77+
+0 -1: failure 1 [E]
78+
oh no
79+
test.dart 6:33 main.<fn>
80+
81+
+1 -2: failure 2 [E]
82+
oh no
83+
test.dart 8:33 main.<fn>
84+
85+
+2 -2: Some tests failed.''');
86+
});
87+
88+
group('print:', () {
89+
test('handles multiple prints', () {
90+
return _expectReport('''
91+
test('test', () {
92+
print("one");
93+
print("two");
94+
print("three");
95+
print("four");
96+
});''', '''
97+
+0: test
98+
one
99+
two
100+
three
101+
four
102+
+1: All tests passed!''');
103+
});
104+
105+
test('handles a print after the test completes', () {
106+
return _expectReport('''
107+
// This completer ensures that the test isolate isn't killed until all
108+
// prints have happened.
109+
var testDone = Completer();
110+
var waitStarted = Completer();
111+
test('test', () async {
112+
waitStarted.future.then((_) {
113+
Future(() => print("one"));
114+
Future(() => print("two"));
115+
Future(() => print("three"));
116+
Future(() => print("four"));
117+
Future(testDone.complete);
118+
});
119+
});
120+
121+
test('wait', () {
122+
waitStarted.complete();
123+
return testDone.future;
124+
});''', '''
125+
+1: test
126+
one
127+
two
128+
three
129+
four
130+
+2: All tests passed!''');
131+
});
132+
133+
test('interleaves prints and errors', () {
134+
return _expectReport('''
135+
// This completer ensures that the test isolate isn't killed until all
136+
// prints have happened.
137+
var completer = Completer();
138+
test('test', () {
139+
scheduleMicrotask(() {
140+
print("three");
141+
print("four");
142+
throw "second error";
143+
});
144+
145+
scheduleMicrotask(() {
146+
print("five");
147+
print("six");
148+
completer.complete();
149+
});
150+
151+
print("one");
152+
print("two");
153+
throw "first error";
154+
});
155+
156+
test('wait', () => completer.future);''', '''
157+
+0: test
158+
one
159+
two
160+
+0 -1: test [E]
161+
first error
162+
test.dart 24:11 main.<fn>
163+
164+
three
165+
four
166+
second error
167+
test.dart 13:13 main.<fn>.<fn>
168+
===== asynchronous gap ===========================
169+
dart:async scheduleMicrotask
170+
test.dart 10:11 main.<fn>
171+
172+
five
173+
six
174+
+1 -1: Some tests failed.''');
175+
});
176+
});
177+
178+
group('skip:', () {
179+
test('does not emit for skips', () {
180+
return _expectReport('''
181+
test('skip 1', () {}, skip: true);
182+
test('skip 2', () {}, skip: true);
183+
test('skip 3', () {}, skip: true);''', '''
184+
+0 ~3: All tests skipped.''');
185+
});
186+
187+
test('runs skipped tests along with successful and failing tests', () {
188+
return _expectReport('''
189+
test('failure 1', () => throw TestFailure('oh no'));
190+
test('skip 1', () {}, skip: true);
191+
test('success 1', () {});
192+
test('failure 2', () => throw TestFailure('oh no'));
193+
test('skip 2', () {}, skip: true);
194+
test('success 2', () {});''', '''
195+
+0 -1: failure 1 [E]
196+
oh no
197+
test.dart 6:35 main.<fn>
198+
199+
+1 ~1 -2: failure 2 [E]
200+
oh no
201+
test.dart 9:35 main.<fn>
202+
203+
+2 ~2 -2: Some tests failed.''');
204+
});
205+
});
206+
207+
test('Directs users to enable stack trace chaining if disabled', () async {
208+
await _expectReport(
209+
'''test('failure 1', () => throw TestFailure('oh no'));''', '''
210+
+0 -1: failure 1 [E]
211+
oh no
212+
test.dart 6:25 main.<fn>
213+
214+
+0 -1: Some tests failed.
215+
216+
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
217+
For example, 'dart test --chain-stack-traces'.''',
218+
chainStackTraces: false);
219+
});
220+
}
221+
222+
Future<void> _expectReport(String tests, String expected,
223+
{List<String> args = const [], bool chainStackTraces = true}) async {
224+
await d.file('test.dart', '''
225+
import 'dart:async';
226+
227+
import 'package:test/test.dart';
228+
229+
void main() {
230+
$tests
231+
}
232+
''').create();
233+
234+
var test = await runTest([
235+
'test.dart',
236+
if (chainStackTraces) '--chain-stack-traces',
237+
...args,
238+
], reporter: 'failures-only');
239+
await test.shouldExit();
240+
241+
var stdoutLines = await test.stdoutStream().toList();
242+
243+
// Remove excess trailing whitespace.
244+
var actual = stdoutLines.map((line) {
245+
if (line.startsWith(' ') || line.isEmpty) return line.trimRight();
246+
return line.trim();
247+
}).join('\n');
248+
249+
// Un-indent the expected string.
250+
var indentation = expected.indexOf(RegExp('[^ ]'));
251+
expected = expected.split('\n').map((line) {
252+
if (line.isEmpty) return line;
253+
return line.substring(indentation);
254+
}).join('\n');
255+
256+
expect(actual, equals(expected));
257+
}

pkgs/test/test/runner/runner_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Output:
108108
109109
[compact] A single line, updated continuously.
110110
[expanded] (default) A separate line for each update.
111+
[failures-only] A separate line for failing tests with no output for passing tests
111112
[github] A custom reporter for GitHub Actions (the default reporter when running on GitHub Actions).
112113
[json] A machine-readable format (see https://dart.dev/go/test-docs/json_reporter.md).
113114
[silent] A reporter with no output. May be useful when only the exit code is meaningful.

pkgs/test_core/lib/src/runner/configuration/reporters.dart

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../engine.dart';
1111
import '../reporter.dart';
1212
import '../reporter/compact.dart';
1313
import '../reporter/expanded.dart';
14+
import '../reporter/failures_only.dart';
1415
import '../reporter/github.dart';
1516
import '../reporter/json.dart';
1617

@@ -46,6 +47,14 @@ final _allReporters = <String, ReporterDetails>{
4647
Directory(config.testSelections.keys.single).existsSync(),
4748
printPlatform: config.suiteDefaults.runtimes.length > 1 ||
4849
config.suiteDefaults.compilerSelections != null)),
50+
'failures-only': ReporterDetails(
51+
'A separate line for failing tests with no output for passing tests',
52+
(config, engine, sink) => FailuresOnlyReporter.watch(engine, sink,
53+
color: config.color,
54+
printPath: config.testSelections.length > 1 ||
55+
Directory(config.testSelections.keys.single).existsSync(),
56+
printPlatform: config.suiteDefaults.runtimes.length > 1 ||
57+
config.suiteDefaults.compilerSelections != null)),
4958
'github': ReporterDetails(
5059
'A custom reporter for GitHub Actions '
5160
'(the default reporter when running on GitHub Actions).',

0 commit comments

Comments
 (0)