Line data Source code
1 : // Copyright (c) 2015, 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 : import 'dart:async';
6 :
7 : import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
8 : import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
9 : import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
10 :
11 : import '../../util/pretty_print.dart';
12 : import '../engine.dart';
13 : import '../load_exception.dart';
14 : import '../load_suite.dart';
15 : import '../reporter.dart';
16 :
17 : /// A reporter that prints each test on its own line.
18 : ///
19 : /// This is currently used in place of [CompactReporter] by `lib/test.dart`,
20 : /// which can't transitively import `dart:io` but still needs access to a runner
21 : /// so that test files can be run directly. This means that until issue 6943 is
22 : /// fixed, this must not import `dart:io`.
23 : class ExpandedReporter implements Reporter {
24 : /// Whether the reporter should emit terminal color escapes.
25 : final bool _color;
26 :
27 : /// The terminal escape for green text, or the empty string if this is Windows
28 : /// or not outputting to a terminal.
29 : final String _green;
30 :
31 : /// The terminal escape for red text, or the empty string if this is Windows
32 : /// or not outputting to a terminal.
33 : final String _red;
34 :
35 : /// The terminal escape for yellow text, or the empty string if this is
36 : /// Windows or not outputting to a terminal.
37 : final String _yellow;
38 :
39 : /// The terminal escape for gray text, or the empty string if this is
40 : /// Windows or not outputting to a terminal.
41 : final String _gray;
42 :
43 : /// The terminal escape for bold text, or the empty string if this is
44 : /// Windows or not outputting to a terminal.
45 : final String _bold;
46 :
47 : /// The terminal escape for removing test coloring, or the empty string if
48 : /// this is Windows or not outputting to a terminal.
49 : final String _noColor;
50 :
51 : /// The engine used to run the tests.
52 : final Engine _engine;
53 :
54 : /// Whether the path to each test's suite should be printed.
55 : final bool _printPath;
56 :
57 : /// Whether the platform each test is running on should be printed.
58 : final bool _printPlatform;
59 :
60 : /// A stopwatch that tracks the duration of the full run.
61 : final _stopwatch = Stopwatch();
62 :
63 : /// The size of `_engine.passed` last time a progress notification was
64 : /// printed.
65 : int _lastProgressPassed = 0;
66 :
67 : /// The size of `_engine.skipped` last time a progress notification was
68 : /// printed.
69 : int _lastProgressSkipped = 0;
70 :
71 : /// The size of `_engine.failed` last time a progress notification was
72 : /// printed.
73 : int _lastProgressFailed = 0;
74 :
75 : /// The message printed for the last progress notification.
76 : String _lastProgressMessage = '';
77 :
78 : /// The suffix added to the last progress notification.
79 : String? _lastProgressSuffix;
80 :
81 : /// Whether the reporter is paused.
82 : var _paused = false;
83 :
84 : // Whether a notice should be logged about enabling stack trace chaining at
85 : // the end of all tests running.
86 : var _shouldPrintStackTraceChainingNotice = false;
87 :
88 : /// The set of all subscriptions to various streams.
89 : final _subscriptions = <StreamSubscription>{};
90 :
91 : final StringSink _sink;
92 :
93 : /// Watches the tests run by [engine] and prints their results to the
94 : /// terminal.
95 : ///
96 : /// If [color] is `true`, this will use terminal colors; if it's `false`, it
97 : /// won't. If [printPath] is `true`, this will print the path name as part of
98 : /// the test description. Likewise, if [printPlatform] is `true`, this will
99 : /// print the platform as part of the test description.
100 0 : static ExpandedReporter watch(Engine engine, StringSink sink,
101 : {required bool color,
102 : required bool printPath,
103 : required bool printPlatform}) =>
104 0 : ExpandedReporter._(engine, sink,
105 : color: color, printPath: printPath, printPlatform: printPlatform);
106 :
107 0 : ExpandedReporter._(this._engine, this._sink,
108 : {required bool color,
109 : required bool printPath,
110 : required bool printPlatform})
111 : : _printPath = printPath,
112 : _printPlatform = printPlatform,
113 : _color = color,
114 : _green = color ? '\u001b[32m' : '',
115 : _red = color ? '\u001b[31m' : '',
116 : _yellow = color ? '\u001b[33m' : '',
117 : _gray = color ? '\u001b[1;30m' : '',
118 : _bold = color ? '\u001b[1m' : '',
119 : _noColor = color ? '\u001b[0m' : '' {
120 0 : _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
121 :
122 : // Convert the future to a stream so that the subscription can be paused or
123 : // canceled.
124 0 : _subscriptions.add(_engine.success.asStream().listen(_onDone));
125 : }
126 :
127 0 : @override
128 : void pause() {
129 0 : if (_paused) return;
130 0 : _paused = true;
131 :
132 0 : _stopwatch.stop();
133 :
134 0 : for (var subscription in _subscriptions) {
135 0 : subscription.pause();
136 : }
137 : }
138 :
139 0 : @override
140 : void resume() {
141 0 : if (!_paused) return;
142 :
143 0 : _stopwatch.start();
144 :
145 0 : for (var subscription in _subscriptions) {
146 0 : subscription.resume();
147 : }
148 : }
149 :
150 0 : void _cancel() {
151 0 : for (var subscription in _subscriptions) {
152 0 : subscription.cancel();
153 : }
154 0 : _subscriptions.clear();
155 : }
156 :
157 : /// A callback called when the engine begins running [liveTest].
158 0 : void _onTestStarted(LiveTest liveTest) {
159 0 : if (liveTest.suite is! LoadSuite) {
160 0 : if (!_stopwatch.isRunning) _stopwatch.start();
161 :
162 : // If this is the first non-load test to start, print a progress line so
163 : // the user knows what's running.
164 0 : if (_engine.active.length == 1) _progressLine(_description(liveTest));
165 :
166 : // The engine surfaces load tests when there are no other tests running,
167 : // but because the expanded reporter's output is always visible, we don't
168 : // emit information about them unless they fail.
169 0 : _subscriptions.add(liveTest.onStateChange
170 0 : .listen((state) => _onStateChange(liveTest, state)));
171 0 : } else if (_engine.active.isEmpty &&
172 0 : _engine.activeSuiteLoads.length == 1 &&
173 0 : _engine.activeSuiteLoads.first == liveTest &&
174 0 : liveTest.test.name.startsWith('compiling ')) {
175 : // Print a progress line for load tests that come from compiling JS, since
176 : // that takes a long time.
177 0 : _progressLine(_description(liveTest));
178 : }
179 :
180 0 : _subscriptions.add(liveTest.onError
181 0 : .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
182 :
183 0 : _subscriptions.add(liveTest.onMessage.listen((message) {
184 0 : _progressLine(_description(liveTest));
185 0 : var text = message.text;
186 0 : if (message.type == MessageType.skip) text = ' $_yellow$text$_noColor';
187 0 : _sink.writeln(text);
188 : }));
189 : }
190 :
191 : /// A callback called when [liveTest]'s state becomes [state].
192 0 : void _onStateChange(LiveTest liveTest, State state) {
193 0 : if (state.status != Status.complete) return;
194 :
195 : // If any tests are running, display the name of the oldest active
196 : // test.
197 0 : if (_engine.active.isNotEmpty) {
198 0 : _progressLine(_description(_engine.active.first));
199 : }
200 : }
201 :
202 : /// A callback called when [liveTest] throws [error].
203 0 : void _onError(LiveTest liveTest, error, StackTrace stackTrace) {
204 0 : if (!liveTest.test.metadata.chainStackTraces &&
205 0 : !liveTest.suite.isLoadSuite) {
206 0 : _shouldPrintStackTraceChainingNotice = true;
207 : }
208 :
209 0 : if (liveTest.state.status != Status.complete) return;
210 :
211 0 : _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor');
212 :
213 0 : if (error is! LoadException) {
214 0 : _sink
215 0 : ..writeln(indent('$error'))
216 0 : ..writeln(indent('$stackTrace'));
217 : return;
218 : }
219 :
220 : // TODO - what type is this?
221 0 : _sink.writeln(indent(error.toString(color: _color)));
222 :
223 : // Only print stack traces for load errors that come from the user's code.
224 0 : if (error.innerError is! FormatException && error.innerError is! String) {
225 0 : _sink.writeln(indent('$stackTrace'));
226 : }
227 : }
228 :
229 : /// A callback called when the engine is finished running tests.
230 : ///
231 : /// [success] will be `true` if all tests passed, `false` if some tests
232 : /// failed, and `null` if the engine was closed prematurely.
233 0 : void _onDone(bool? success) {
234 0 : _cancel();
235 : // A null success value indicates that the engine was closed before the
236 : // tests finished running, probably because of a signal from the user, in
237 : // which case we shouldn't print summary information.
238 : if (success == null) return;
239 :
240 0 : if (_engine.liveTests.isEmpty) {
241 0 : _sink.writeln('No tests ran.');
242 : } else if (!success) {
243 0 : for (var liveTest in _engine.active) {
244 0 : _progressLine(_description(liveTest),
245 0 : suffix: ' - did not complete $_bold$_red[E]$_noColor');
246 : }
247 0 : _progressLine('Some tests failed.', color: _red);
248 0 : } else if (_engine.passed.isEmpty) {
249 0 : _progressLine('All tests skipped.');
250 : } else {
251 0 : _progressLine('All tests passed!');
252 : }
253 :
254 0 : if (_shouldPrintStackTraceChainingNotice) {
255 0 : _sink
256 0 : ..writeln('')
257 0 : ..writeln('Consider enabling the flag chain-stack-traces to '
258 : 'receive more detailed exceptions.\n'
259 : "For example, 'dart test --chain-stack-traces'.");
260 : }
261 : }
262 :
263 : /// Prints a line representing the current state of the tests.
264 : ///
265 : /// [message] goes after the progress report. If [color] is passed, it's used
266 : /// as the color for [message]. If [suffix] is passed, it's added to the end
267 : /// of [message].
268 0 : void _progressLine(String message, {String? color, String? suffix}) {
269 : // Print nothing if nothing has changed since the last progress line.
270 0 : if (_engine.passed.length == _lastProgressPassed &&
271 0 : _engine.skipped.length == _lastProgressSkipped &&
272 0 : _engine.failed.length == _lastProgressFailed &&
273 0 : message == _lastProgressMessage &&
274 : // Don't re-print just because a suffix was removed.
275 0 : (suffix == null || suffix == _lastProgressSuffix)) {
276 : return;
277 : }
278 :
279 0 : _lastProgressPassed = _engine.passed.length;
280 0 : _lastProgressSkipped = _engine.skipped.length;
281 0 : _lastProgressFailed = _engine.failed.length;
282 0 : _lastProgressMessage = message;
283 0 : _lastProgressSuffix = suffix;
284 :
285 0 : if (suffix != null) message += suffix;
286 : color ??= '';
287 0 : var duration = _stopwatch.elapsed;
288 0 : var buffer = StringBuffer();
289 :
290 : // \r moves back to the beginning of the current line.
291 0 : buffer.write('${_timeString(duration)} ');
292 0 : buffer.write(_green);
293 0 : buffer.write('+');
294 0 : buffer.write(_engine.passed.length);
295 0 : buffer.write(_noColor);
296 :
297 0 : if (_engine.skipped.isNotEmpty) {
298 0 : buffer.write(_yellow);
299 0 : buffer.write(' ~');
300 0 : buffer.write(_engine.skipped.length);
301 0 : buffer.write(_noColor);
302 : }
303 :
304 0 : if (_engine.failed.isNotEmpty) {
305 0 : buffer.write(_red);
306 0 : buffer.write(' -');
307 0 : buffer.write(_engine.failed.length);
308 0 : buffer.write(_noColor);
309 : }
310 :
311 0 : buffer.write(': ');
312 0 : buffer.write(color);
313 0 : buffer.write(message);
314 0 : buffer.write(_noColor);
315 :
316 0 : _sink.writeln(buffer.toString());
317 : }
318 :
319 : /// Returns a representation of [duration] as `MM:SS`.
320 0 : String _timeString(Duration duration) {
321 0 : return "${duration.inMinutes.toString().padLeft(2, '0')}:"
322 0 : "${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
323 : }
324 :
325 : /// Returns a description of [liveTest].
326 : ///
327 : /// This differs from the test's own description in that it may also include
328 : /// the suite's name.
329 0 : String _description(LiveTest liveTest) {
330 0 : var name = liveTest.test.name;
331 :
332 0 : if (_printPath &&
333 0 : liveTest.suite is! LoadSuite &&
334 0 : liveTest.suite.path != null) {
335 0 : name = '${liveTest.suite.path}: $name';
336 : }
337 :
338 0 : if (_printPlatform) {
339 0 : name = '[${liveTest.suite.platform.runtime.name}] $name';
340 : }
341 :
342 0 : if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor';
343 :
344 : return name;
345 : }
346 : }
|