Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(406)

Side by Side Diff: lib/src/console_reporter.dart

Issue 913123006: Add a ConsoleReporter for printing human-friendly test progress reports. (Closed) Base URL: git@github.com:dart-lang/unittest@master
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | lib/src/io.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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 library unittest.console_reporter;
6
7 import 'dart:async';
8 import 'dart:io';
9
10 import 'engine.dart';
11 import 'io.dart';
12 import 'live_test.dart';
13 import 'state.dart';
14 import 'suite.dart';
15 import 'utils.dart';
16
17 /// The terminal escape for green text, or the empty string if this is Windows
18 /// or not outputting to a terminal.
19 final _green = getSpecial('\u001b[32m');
20
21 /// The terminal escape for red text, or the empty string if this is Windows or
22 /// not outputting to a terminal.
23 final _red = getSpecial('\u001b[31m');
24
25 /// The terminal escape for removing test coloring, or the empty string if this
26 /// is Windows or not outputting to a terminal.
27 final _noColor = getSpecial('\u001b[0m');
28
29 /// The maximum console line length.
30 ///
31 /// Lines longer than this will be cropped.
32 const _lineLength = 80;
33
34 /// A reporter that prints test results to the console in a single
35 /// continuously-updating line.
36 class ConsoleReporter {
37 /// The engine used to run the tests.
38 final Engine _engine;
39
40 /// Whether multiple test suites are being run.
41 final bool _multipleSuites;
42
43 /// A stopwatch that tracks the duration of the full run.
44 final _stopwatch = new Stopwatch();
45
46 /// The set of tests that have completed and been marked as passing.
47 final _passed = new Set<LiveTest>();
48
49 /// The set of tests that have completed and been marked as failing or error.
50 final _failed = new Set<LiveTest>();
51
52 /// Creates a [ConsoleReporter] that will run all tests in [suites].
53 ConsoleReporter(Iterable<Suite> suites)
54 : _multipleSuites = suites.length > 1,
kevmoo 2015/02/11 22:37:39 I hate length checks against Iterable -> causes a
nweiz 2015/02/11 23:34:43 Not really, since Engine.liveTests flattens across
55 _engine = new Engine(suites) {
56
57 _engine.onTestStarted.listen((liveTest) {
58 _progressLine(_description(liveTest));
59 liveTest.onStateChange.listen((state) {
60 if (state.status != Status.complete) return;
61 if (state.result == Result.success) {
62 _passed.add(liveTest);
63 } else {
64 _passed.remove(liveTest);
65 _failed.add(liveTest);
66 }
67 _progressLine(_description(liveTest));
68 });
69
70 liveTest.onError.listen((error) {
71 if (liveTest.state.status != Status.complete) return;
72
73 // TODO(nweiz): don't re-print the progress line if a test has multiple
74 // errors in a row.
75 _progressLine(_description(liveTest));
76 print('');
77 print(indent("${error.error}\n${error.stackTrace}"));
78 });
79 });
80 }
81
82 /// Runs all tests in all provided suites.
83 ///
84 /// This returns `true` if all tests succeed, and `false` otherwise. It will
85 /// only return once all tests have finished running.
86 Future<bool> run() {
87 if (_stopwatch.isRunning) {
88 throw new StateError("ConsoleReporter.run() may not be called more than "
89 "once.");
90 }
91
92 _stopwatch.start();
93 return _engine.run().then((success) {
94 if (_engine.liveTests.isEmpty) {
95 print("\nNo tests ran.");
96 } else if (success) {
97 _progressLine("All tests passed!");
98 print('');
99 } else {
100 _progressLine('Some tests failed.', color: _red);
101 print('');
102 }
103
104 return success;
105 });
106 }
107
108 /// Prints a line representing the current state of the tests.
109 ///
110 /// [message] goes after the progress report, and may be truncated to fit the
111 /// entire line within [_lineLength]. If [color] is passed, it's used as the
112 /// color for [message].
113 void _progressLine(String message, {String color}) {
114 if (color == null) color = '';
115 var duration = _stopwatch.elapsed;
116 var buffer = new StringBuffer();
117
118 // \r moves back to the beginning of the current line.
119 buffer.write('\r${_timeString(duration)} ');
120 buffer.write(_green);
121 buffer.write('+');
122 buffer.write(_passed.length);
123 buffer.write(_noColor);
124
125 if (_failed.isNotEmpty) {
126 buffer.write(_red);
127 buffer.write(' -');
128 buffer.write(_failed.length);
129 buffer.write(_noColor);
130 }
131
132 buffer.write(': ');
133 buffer.write(color);
134
135 // Ensure the line fits within [_lineLength]. [buffer] includes the color
136 // escape sequences too. Because these sequences are not visible characters,
137 // we make sure they are not counted towards the limit.
138 var nonVisible = 1 + _green.length + _noColor.length + color.length +
139 (_failed.isEmpty ? 0 : _red.length + _noColor.length);
140 var length = buffer.length - nonVisible;
141 buffer.write(_truncate(message, _lineLength - length));
142 buffer.write(_noColor);
143
144 // Pad the rest of the line so that it looks erased.
145 length = buffer.length - nonVisible - _noColor.length;
146 buffer.write(' ' * (_lineLength - length));
147 stdout.write(buffer.toString());
148 }
149
150 /// Returns a representation of [duration] as `MM:SS`.
151 String _timeString(Duration duration) {
152 return "${duration.inMinutes.toString().padLeft(2, '0')}:"
153 "${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
154 }
155
156 /// Truncates [text] to fit within [maxLength].
157 ///
158 /// This will try to truncate along word boundaries and preserve words both at
159 /// the beginning and the end of [text].
160 String _truncate(String text, int maxLength) {
161 // Return the full message if it fits.
162 if (text.length <= maxLength) return text;
163
164 // If we can fit the first and last three words, do so.
165 var words = text.split(' ');
166 if (words.length > 1) {
167 var i = words.length;
168 var length = words.first.length + 4;
169 do {
170 i--;
171 length += 1 + words[i].length;
172 } while (length <= maxLength && i > 0);
173 if (length > maxLength || i == 0) i++;
174 if (i < words.length - 4) {
175 // Require at least 3 words at the end.
176 var buffer = new StringBuffer();
177 buffer.write(words.first);
178 buffer.write(' ...');
179 for ( ; i < words.length; i++) {
180 buffer.write(' ');
181 buffer.write(words[i]);
182 }
183 return buffer.toString();
184 }
185 }
186
187 // Otherwise truncate to return the trailing text, but attempt to start at
188 // the beginning of a word.
189 var result = text.substring(text.length - maxLength + 4);
190 var firstSpace = result.indexOf(' ');
191 if (firstSpace > 0) {
192 result = result.substring(firstSpace);
193 }
194 return '...$result';
195 }
196
197 /// Returns a description of [liveTest].
198 ///
199 /// This differs from the test's own description in that it may also include
200 /// the suite's name.
201 String _description(LiveTest liveTest) {
202 if (_multipleSuites) return "${liveTest.suite.name}: ${liveTest.test.name}";
203 return liveTest.test.name;
204 }
205 }
OLDNEW
« no previous file with comments | « no previous file | lib/src/io.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698