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