From 4dddf1146ff6e207d2e299dd5a375f5110f05bc1 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum <nweiz@google.com> Date: Tue, 3 Mar 2015 12:05:18 -0800 Subject: [PATCH] Add a variant on ConsoleReporter that doesn't import dart:io. This is necessary to avoid an IO dependency in lib/unittest.dart, which would prevent browser tests from running. R=kevmoo@google.com Review URL: https://codereview.chromium.org//971123002 --- bin/unittest.dart | 4 +- .../compact.dart} | 61 +----- lib/src/runner/reporter/no_io_compact.dart | 190 ++++++++++++++++++ lib/src/utils.dart | 41 ++++ lib/unittest.dart | 5 +- ...r_test.dart => compact_reporter_test.dart} | 0 6 files changed, 245 insertions(+), 56 deletions(-) rename lib/src/runner/{console_reporter.dart => reporter/compact.dart} (77%) create mode 100644 lib/src/runner/reporter/no_io_compact.dart rename test/{console_reporter_test.dart => compact_reporter_test.dart} (100%) diff --git a/bin/unittest.dart b/bin/unittest.dart index 6836ee4e..b2d70acc 100644 --- a/bin/unittest.dart +++ b/bin/unittest.dart @@ -11,7 +11,7 @@ import 'dart:isolate'; import 'package:args/args.dart'; import 'package:stack_trace/stack_trace.dart'; -import 'package:unittest/src/runner/console_reporter.dart'; +import 'package:unittest/src/runner/reporter/compact.dart'; import 'package:unittest/src/runner/load_exception.dart'; import 'package:unittest/src/runner/loader.dart'; import 'package:unittest/src/util/exit_codes.dart' as exit_codes; @@ -62,7 +62,7 @@ void main(List<String> args) { }).then((suites) { var color = options["color"]; if (color == null) color = canUseSpecialChars; - var reporter = new ConsoleReporter(flatten(suites), color: color); + var reporter = new CompactReporter(flatten(suites), color: color); return reporter.run().then((success) { exitCode = success ? 0 : 1; }).whenComplete(() => reporter.close()); diff --git a/lib/src/runner/console_reporter.dart b/lib/src/runner/reporter/compact.dart similarity index 77% rename from lib/src/runner/console_reporter.dart rename to lib/src/runner/reporter/compact.dart index 4680146b..a0335a38 100644 --- a/lib/src/runner/console_reporter.dart +++ b/lib/src/runner/reporter/compact.dart @@ -2,16 +2,16 @@ // 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.runner.console_reporter; +library unittest.runner.reporter.compact; import 'dart:async'; import 'dart:io'; -import '../backend/live_test.dart'; -import '../backend/state.dart'; -import '../backend/suite.dart'; -import '../utils.dart'; -import 'engine.dart'; +import '../../backend/live_test.dart'; +import '../../backend/state.dart'; +import '../../backend/suite.dart'; +import '../../utils.dart'; +import '../engine.dart'; /// The maximum console line length. /// @@ -20,7 +20,7 @@ const _lineLength = 100; /// A reporter that prints test results to the console in a single /// continuously-updating line. -class ConsoleReporter { +class CompactReporter { /// The terminal escape for green text, or the empty string if this is Windows /// or not outputting to a terminal. final String _green; @@ -61,7 +61,7 @@ class ConsoleReporter { /// /// If [color] is `true`, this will use terminal colors; if it's `false`, it /// won't. - ConsoleReporter(Iterable<Suite> suites, {bool color: true}) + CompactReporter(Iterable<Suite> suites, {bool color: true}) : _multipleSuites = suites.length > 1, _engine = new Engine(suites), _green = color ? '\u001b[32m' : '', @@ -97,7 +97,7 @@ class ConsoleReporter { /// 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 " + throw new StateError("CompactReporter.run() may not be called more than " "once."); } @@ -168,7 +168,7 @@ class ConsoleReporter { 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(truncate(message, _lineLength - length)); buffer.write(_noColor); // Pad the rest of the line so that it looks erased. @@ -183,47 +183,6 @@ class ConsoleReporter { "${(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 diff --git a/lib/src/runner/reporter/no_io_compact.dart b/lib/src/runner/reporter/no_io_compact.dart new file mode 100644 index 00000000..a947730e --- /dev/null +++ b/lib/src/runner/reporter/no_io_compact.dart @@ -0,0 +1,190 @@ +// 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.runner.reporter.no_io_compact; + +import 'dart:async'; + +import '../../backend/live_test.dart'; +import '../../backend/state.dart'; +import '../../backend/suite.dart'; +import '../../utils.dart'; +import '../engine.dart'; + +/// The maximum console line length. +/// +/// Lines longer than this will be cropped. +const _lineLength = 100; + +// TODO(nweiz): Get rid of this when issue 6943 is fixed. +/// A reporter that doesn't import `dart:io`, even transitively. +/// +/// This is used in place of [CompactReporter] by `lib/unittest.dart`, which +/// can't transitively import `dart:io` but still needs access to a runner so +/// that test files can be run directly. +class NoIoCompactReporter { + /// The terminal escape for green text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _green; + + /// The terminal escape for red text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _red; + + /// The terminal escape for removing test coloring, or the empty string if + /// this is Windows or not outputting to a terminal. + final String _noColor; + + /// 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>(); + + /// The size of [_passed] last time a progress notification was printed. + int _lastProgressPassed; + + /// The size of [_failed] last time a progress notification was printed. + int _lastProgressFailed; + + /// The message printed for the last progress notification. + String _lastProgressMessage; + + /// Creates a [NoIoCompactReporter] that will run all tests in [suites]. + /// + /// If [color] is `true`, this will use terminal colors; if it's `false`, it + /// won't. + NoIoCompactReporter(Iterable<Suite> suites, {bool color: true}) + : _multipleSuites = suites.length > 1, + _engine = new Engine(suites), + _green = color ? '\u001b[32m' : '', + _red = color ? '\u001b[31m' : '', + _noColor = color ? '\u001b[0m' : '' { + _engine.onTestStarted.listen((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; + + _progressLine(_description(liveTest)); + print(indent(error.error.toString())); + print(indent(terseChain(error.stackTrace).toString())); + }); + }); + } + + /// 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("CompactReporter.run() may not be called more than " + "once."); + } + + if (_engine.liveTests.isEmpty) { + print("No tests ran."); + return new Future.value(true); + } + + _stopwatch.start(); + return _engine.run().then((success) { + if (success) { + _progressLine("All tests passed!"); + } else { + _progressLine('Some tests failed.', color: _red); + } + + return success; + }); + } + + /// Signals that the caller is done with any test output and the reporter + /// should release any resources it has allocated. + Future close() => _engine.close(); + + /// 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}) { + // Print nothing if nothing has changed since the last progress line. + if (_passed.length == _lastProgressPassed && + _failed.length == _lastProgressFailed && + message == _lastProgressMessage) { + return; + } + + _lastProgressPassed = _passed.length; + _lastProgressFailed = _failed.length; + _lastProgressMessage = message; + + 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); + + print(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')}"; + } + + /// 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; + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 6b0939fe..06bd8dff 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -63,3 +63,44 @@ List flatten(Iterable nested) { helper(nested); return result; } + +/// 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'; +} diff --git a/lib/unittest.dart b/lib/unittest.dart index e4f36b59..07bc46d4 100644 --- a/lib/unittest.dart +++ b/lib/unittest.dart @@ -13,7 +13,7 @@ import 'src/backend/invoker.dart'; import 'src/backend/suite.dart'; import 'src/deprecated/configuration.dart'; import 'src/deprecated/test_case.dart'; -import 'src/runner/console_reporter.dart'; +import 'src/runner/reporter/no_io_compact.dart'; export 'package:matcher/matcher.dart'; @@ -49,9 +49,8 @@ Declarer get _declarer { _globalDeclarer = new Declarer(); scheduleMicrotask(() { var suite = new Suite(p.prettyUri(Uri.base), _globalDeclarer.tests); - // TODO(nweiz): Use a reporter that doesn't import dart:io here. // TODO(nweiz): Set the exit code on the VM when issue 6943 is fixed. - new ConsoleReporter([suite]).run(); + new NoIoCompactReporter([suite], color: true).run(); }); return _globalDeclarer; } diff --git a/test/console_reporter_test.dart b/test/compact_reporter_test.dart similarity index 100% rename from test/console_reporter_test.dart rename to test/compact_reporter_test.dart -- GitLab