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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | lib/src/io.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/src/console_reporter.dart
diff --git a/lib/src/console_reporter.dart b/lib/src/console_reporter.dart
new file mode 100644
index 0000000000000000000000000000000000000000..3faa1b47579b3d98049918a4d91e48a5efef5707
--- /dev/null
+++ b/lib/src/console_reporter.dart
@@ -0,0 +1,205 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library unittest.console_reporter;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'engine.dart';
+import 'io.dart';
+import 'live_test.dart';
+import 'state.dart';
+import 'suite.dart';
+import 'utils.dart';
+
+/// The terminal escape for green text, or the empty string if this is Windows
+/// or not outputting to a terminal.
+final _green = getSpecial('\u001b[32m');
+
+/// The terminal escape for red text, or the empty string if this is Windows or
+/// not outputting to a terminal.
+final _red = getSpecial('\u001b[31m');
+
+/// The terminal escape for removing test coloring, or the empty string if this
+/// is Windows or not outputting to a terminal.
+final _noColor = getSpecial('\u001b[0m');
+
+/// The maximum console line length.
+///
+/// Lines longer than this will be cropped.
+const _lineLength = 80;
+
+/// A reporter that prints test results to the console in a single
+/// continuously-updating line.
+class ConsoleReporter {
+ /// The engine used to run the tests.
+ final Engine _engine;
+
+ /// Whether multiple test suites are being run.
+ final bool _multipleSuites;
+
+ /// A stopwatch that tracks the duration of the full run.
+ final _stopwatch = new Stopwatch();
+
+ /// The set of tests that have completed and been marked as passing.
+ final _passed = new Set<LiveTest>();
+
+ /// The set of tests that have completed and been marked as failing or error.
+ final _failed = new Set<LiveTest>();
+
+ /// Creates a [ConsoleReporter] that will run all tests in [suites].
+ ConsoleReporter(Iterable<Suite> suites)
+ : _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
+ _engine = new Engine(suites) {
+
+ _engine.onTestStarted.listen((liveTest) {
+ _progressLine(_description(liveTest));
+ liveTest.onStateChange.listen((state) {
+ if (state.status != Status.complete) return;
+ if (state.result == Result.success) {
+ _passed.add(liveTest);
+ } else {
+ _passed.remove(liveTest);
+ _failed.add(liveTest);
+ }
+ _progressLine(_description(liveTest));
+ });
+
+ liveTest.onError.listen((error) {
+ if (liveTest.state.status != Status.complete) return;
+
+ // TODO(nweiz): don't re-print the progress line if a test has multiple
+ // errors in a row.
+ _progressLine(_description(liveTest));
+ print('');
+ print(indent("${error.error}\n${error.stackTrace}"));
+ });
+ });
+ }
+
+ /// Runs all tests in all provided suites.
+ ///
+ /// This returns `true` if all tests succeed, and `false` otherwise. It will
+ /// only return once all tests have finished running.
+ Future<bool> run() {
+ if (_stopwatch.isRunning) {
+ throw new StateError("ConsoleReporter.run() may not be called more than "
+ "once.");
+ }
+
+ _stopwatch.start();
+ return _engine.run().then((success) {
+ if (_engine.liveTests.isEmpty) {
+ print("\nNo tests ran.");
+ } else if (success) {
+ _progressLine("All tests passed!");
+ print('');
+ } else {
+ _progressLine('Some tests failed.', color: _red);
+ print('');
+ }
+
+ return success;
+ });
+ }
+
+ /// Prints a line representing the current state of the tests.
+ ///
+ /// [message] goes after the progress report, and may be truncated to fit the
+ /// entire line within [_lineLength]. If [color] is passed, it's used as the
+ /// color for [message].
+ void _progressLine(String message, {String color}) {
+ if (color == null) color = '';
+ var duration = _stopwatch.elapsed;
+ var buffer = new StringBuffer();
+
+ // \r moves back to the beginning of the current line.
+ buffer.write('\r${_timeString(duration)} ');
+ buffer.write(_green);
+ buffer.write('+');
+ buffer.write(_passed.length);
+ buffer.write(_noColor);
+
+ if (_failed.isNotEmpty) {
+ buffer.write(_red);
+ buffer.write(' -');
+ buffer.write(_failed.length);
+ buffer.write(_noColor);
+ }
+
+ buffer.write(': ');
+ buffer.write(color);
+
+ // Ensure the line fits within [_lineLength]. [buffer] includes the color
+ // escape sequences too. Because these sequences are not visible characters,
+ // we make sure they are not counted towards the limit.
+ var nonVisible = 1 + _green.length + _noColor.length + color.length +
+ (_failed.isEmpty ? 0 : _red.length + _noColor.length);
+ var length = buffer.length - nonVisible;
+ buffer.write(_truncate(message, _lineLength - length));
+ buffer.write(_noColor);
+
+ // Pad the rest of the line so that it looks erased.
+ length = buffer.length - nonVisible - _noColor.length;
+ buffer.write(' ' * (_lineLength - length));
+ stdout.write(buffer.toString());
+ }
+
+ /// Returns a representation of [duration] as `MM:SS`.
+ String _timeString(Duration duration) {
+ return "${duration.inMinutes.toString().padLeft(2, '0')}:"
+ "${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
+ }
+
+ /// Truncates [text] to fit within [maxLength].
+ ///
+ /// This will try to truncate along word boundaries and preserve words both at
+ /// the beginning and the end of [text].
+ String _truncate(String text, int maxLength) {
+ // Return the full message if it fits.
+ if (text.length <= maxLength) return text;
+
+ // If we can fit the first and last three words, do so.
+ var words = text.split(' ');
+ if (words.length > 1) {
+ var i = words.length;
+ var length = words.first.length + 4;
+ do {
+ i--;
+ length += 1 + words[i].length;
+ } while (length <= maxLength && i > 0);
+ if (length > maxLength || i == 0) i++;
+ if (i < words.length - 4) {
+ // Require at least 3 words at the end.
+ var buffer = new StringBuffer();
+ buffer.write(words.first);
+ buffer.write(' ...');
+ for ( ; i < words.length; i++) {
+ buffer.write(' ');
+ buffer.write(words[i]);
+ }
+ return buffer.toString();
+ }
+ }
+
+ // Otherwise truncate to return the trailing text, but attempt to start at
+ // the beginning of a word.
+ var result = text.substring(text.length - maxLength + 4);
+ var firstSpace = result.indexOf(' ');
+ if (firstSpace > 0) {
+ result = result.substring(firstSpace);
+ }
+ return '...$result';
+ }
+
+ /// Returns a description of [liveTest].
+ ///
+ /// This differs from the test's own description in that it may also include
+ /// the suite's name.
+ String _description(LiveTest liveTest) {
+ if (_multipleSuites) return "${liveTest.suite.name}: ${liveTest.test.name}";
+ return liveTest.test.name;
+ }
+}
« 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