Gracefully handle tests that print.
Prints from browser tests will be forwarded to the command-line, and the compact
reporter will ensure that printed lines don't interfere with its status lines.
Closes #13
R=kevmoo@google.com
Review URL: https://codereview.chromium.org//1044953004
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3de4593..5b75883 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@
* Add a `--name` (shorthand `-n`) flag to the test runner for selecting which
test to run.
+* Ensure that `print()` in tests always prints on its own line.
+
+* Forward `print()`s from browser tests to the command-line reporter.
+
* Add a missing dependency on `string_scanner`.
## 0.12.0-beta.0
diff --git a/lib/src/backend/invoker.dart b/lib/src/backend/invoker.dart
index 53d9ad0..fe4d0fb 100644
--- a/lib/src/backend/invoker.dart
+++ b/lib/src/backend/invoker.dart
@@ -178,7 +178,11 @@
// non-microtask events.
Timer.run(_controller.completer.complete);
});
- }, zoneValues: {#unittest.invoker: this}, onError: handleError);
+ },
+ zoneSpecification: new ZoneSpecification(
+ print: (self, parent, zone, line) => _controller.print(line)),
+ zoneValues: {#unittest.invoker: this},
+ onError: handleError);
});
}
}
diff --git a/lib/src/backend/live_test.dart b/lib/src/backend/live_test.dart
index fef0c11..28a2a8d 100644
--- a/lib/src/backend/live_test.dart
+++ b/lib/src/backend/live_test.dart
@@ -79,6 +79,9 @@
/// will be [Chain]s.
Stream<AsyncError> get onError;
+ /// A stream that emits lines printed by the test.
+ Stream<String> onPrint;
+
/// A [Future] that completes once the test is complete.
///
/// This will complete after [onStateChange] has fired, and after [onError]
diff --git a/lib/src/backend/live_test_controller.dart b/lib/src/backend/live_test_controller.dart
index 17b8bcf..a1a0d3a 100644
--- a/lib/src/backend/live_test_controller.dart
+++ b/lib/src/backend/live_test_controller.dart
@@ -32,6 +32,8 @@
Stream<AsyncError> get onError => _controller._onErrorController.stream;
+ Stream<String> get onPrint => _controller._onPrintController.stream;
+
Future get onComplete => _controller.completer.future;
Future run() => _controller._run();
@@ -76,10 +78,24 @@
var _state = const State(Status.pending, Result.success);
/// The controller for [LiveTest.onStateChange].
- final _onStateChangeController = new StreamController<State>.broadcast();
+ ///
+ /// This is synchronous to ensure that events are well-ordered across multiple
+ /// streams.
+ final _onStateChangeController = new StreamController<State>
+ .broadcast(sync: true);
/// The controller for [LiveTest.onError].
- final _onErrorController = new StreamController<AsyncError>.broadcast();
+ ///
+ /// This is synchronous to ensure that events are well-ordered across multiple
+ /// streams.
+ final _onErrorController = new StreamController<AsyncError>
+ .broadcast(sync: true);
+
+ /// The controller for [LiveTest.onPrint].
+ ///
+ /// This is synchronous to ensure that events are well-ordered across multiple
+ /// streams.
+ final _onPrintController = new StreamController<String>.broadcast(sync: true);
/// The completer for [LiveTest.onComplete];
final completer = new Completer();
@@ -130,6 +146,9 @@
_onStateChangeController.add(newState);
}
+ /// Emits a line printed by the test over [LiveTest.onPrint].
+ void print(String line) => _onPrintController.add(line);
+
/// A wrapper for [_onRun] that ensures that it follows the guarantees for
/// [LiveTest.run].
Future _run() {
diff --git a/lib/src/runner/browser/iframe_listener.dart b/lib/src/runner/browser/iframe_listener.dart
index 49ab7d9..9501959 100644
--- a/lib/src/runner/browser/iframe_listener.dart
+++ b/lib/src/runner/browser/iframe_listener.dart
@@ -159,6 +159,11 @@
});
});
+ liveTest.onPrint.listen((line) {
+ print(line);
+ channel.sink.add({"type": "print", "line": line});
+ });
+
liveTest.run().then((_) => channel.sink.add({"type": "complete"}));
}
}
diff --git a/lib/src/runner/browser/iframe_test.dart b/lib/src/runner/browser/iframe_test.dart
index fa7b023..81c2a23 100644
--- a/lib/src/runner/browser/iframe_test.dart
+++ b/lib/src/runner/browser/iframe_test.dart
@@ -43,6 +43,8 @@
new State(
new Status.parse(message['status']),
new Result.parse(message['result'])));
+ } else if (message['type'] == 'print') {
+ controller.print(message['line']);
} else {
assert(message['type'] == 'complete');
controller.completer.complete();
diff --git a/lib/src/runner/reporter/compact.dart b/lib/src/runner/reporter/compact.dart
index 0a75d7c..49817ac 100644
--- a/lib/src/runner/reporter/compact.dart
+++ b/lib/src/runner/reporter/compact.dart
@@ -72,6 +72,8 @@
_green = color ? '\u001b[32m' : '',
_red = color ? '\u001b[31m' : '',
_noColor = color ? '\u001b[0m' : '' {
+ // Whether a newline has been printed since the last progress line.
+ var printedNewline = false;
_engine.onTestStarted.listen((liveTest) {
_progressLine(_description(liveTest));
liveTest.onStateChange.listen((state) {
@@ -83,16 +85,27 @@
_failed.add(liveTest);
}
_progressLine(_description(liveTest));
+ printedNewline = false;
});
liveTest.onError.listen((error) {
if (liveTest.state.status != Status.complete) return;
_progressLine(_description(liveTest));
- print('');
+ if (!printedNewline) print('');
+ printedNewline = true;
+
print(indent(error.error.toString()));
print(indent(terseChain(error.stackTrace).toString()));
});
+
+ liveTest.onPrint.listen((line) {
+ _progressLine(_description(liveTest));
+ if (!printedNewline) print('');
+ printedNewline = true;
+
+ print(line);
+ });
});
}
@@ -134,12 +147,12 @@
/// [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}) {
+ bool _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;
+ return false;
}
_lastProgressPassed = _passed.length;
@@ -180,6 +193,7 @@
length = buffer.length - nonVisible - _noColor.length;
buffer.write(' ' * (_lineLength - length));
stdout.write(buffer.toString());
+ return true;
}
/// Returns a representation of [duration] as `MM:SS`.
diff --git a/lib/src/runner/reporter/no_io_compact.dart b/lib/src/runner/reporter/no_io_compact.dart
index 91dae81..3afef34 100644
--- a/lib/src/runner/reporter/no_io_compact.dart
+++ b/lib/src/runner/reporter/no_io_compact.dart
@@ -94,6 +94,11 @@
print(indent(error.error.toString()));
print(indent(terseChain(error.stackTrace).toString()));
});
+
+ liveTest.onPrint.listen((line) {
+ _progressLine(_description(liveTest));
+ print(line);
+ });
});
}
diff --git a/lib/src/runner/vm/isolate_listener.dart b/lib/src/runner/vm/isolate_listener.dart
index 352490f..efaaea4 100644
--- a/lib/src/runner/vm/isolate_listener.dart
+++ b/lib/src/runner/vm/isolate_listener.dart
@@ -113,6 +113,9 @@
});
});
+ liveTest.onPrint.listen((line) =>
+ sendPort.send({"type": "print", "line": line}));
+
liveTest.run().then((_) => sendPort.send({"type": "complete"}));
}
}
diff --git a/lib/src/runner/vm/isolate_test.dart b/lib/src/runner/vm/isolate_test.dart
index 76f8f77..af99a25 100644
--- a/lib/src/runner/vm/isolate_test.dart
+++ b/lib/src/runner/vm/isolate_test.dart
@@ -46,6 +46,8 @@
new State(
new Status.parse(message['status']),
new Result.parse(message['result'])));
+ } else if (message['type'] == 'print') {
+ controller.print(message['line']);
} else {
assert(message['type'] == 'complete');
controller.completer.complete();
diff --git a/pubspec.yaml b/pubspec.yaml
index 9ef9b14..4003429 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: unittest
-version: 0.12.0-dev
+version: 0.12.0-beta.1
author: Dart Team <misc@dartlang.org>
description: A library for writing dart unit tests.
homepage: https://github.com/dart-lang/unittest
diff --git a/test/backend/invoker_test.dart b/test/backend/invoker_test.dart
index 6cbe20d..7ba7c77 100644
--- a/test/backend/invoker_test.dart
+++ b/test/backend/invoker_test.dart
@@ -493,6 +493,20 @@
async.elapse(new Duration(seconds: 30));
});
});
+
+ test("a test's prints are captured and reported", () {
+ expect(() {
+ var liveTest = _localTest(() {
+ print("Hello,");
+ return new Future(() => print("world!"));
+ }).load(suite);
+
+ expect(liveTest.onPrint.take(2).toList(),
+ completion(equals(["Hello,", "world!"])));
+
+ return liveTest.run();
+ }, prints(isEmpty));
+ });
}
LocalTest _localTest(body(), {tearDown()}) =>
diff --git a/test/runner/browser/runner_test.dart b/test/runner/browser/runner_test.dart
index bf90243..2627a13 100644
--- a/test/runner/browser/runner_test.dart
+++ b/test/runner/browser/runner_test.dart
@@ -157,6 +157,25 @@
expect(result.exitCode, equals(1));
});
});
+
+ test("forwards prints from the browser test", () {
+ new File(p.join(_sandbox, "test.dart")).writeAsStringSync("""
+import 'dart:async';
+
+import 'package:unittest/unittest.dart';
+
+void main() {
+ test("test", () {
+ print("Hello,");
+ return new Future(() => print("world!"));
+ });
+}
+""");
+
+ var result = _runUnittest(["-p", "chrome", "test.dart"]);
+ expect(result.stdout, contains("Hello,\nworld!\n"));
+ expect(result.exitCode, equals(0));
+ });
}
ProcessResult _runUnittest(List<String> args) =>
diff --git a/test/runner/compact_reporter_test.dart b/test/runner/compact_reporter_test.dart
index f672971..35c5079 100644
--- a/test/runner/compact_reporter_test.dart
+++ b/test/runner/compact_reporter_test.dart
@@ -110,14 +110,12 @@
dart:async Future.Future.microtask
test.dart 10:15 main.<fn>
-
second error
test.dart 11:38 main.<fn>.<fn>
===== asynchronous gap ===========================
dart:async Future.Future.microtask
test.dart 11:15 main.<fn>
-
third error
test.dart 12:38 main.<fn>.<fn>
===== asynchronous gap ===========================
@@ -129,6 +127,109 @@
+1 -1: wait
+1 -1: Some tests failed.""");
});
+
+ group("print:", () {
+ test("handles multiple prints", () {
+ _expectReport("""
+ test('test', () {
+ print("one");
+ print("two");
+ print("three");
+ print("four");
+ });""",
+ """
+ +0: test
+ one
+ two
+ three
+ four
+
+ +1: test
+ +1: All tests passed!""");
+ });
+
+ test("handles a print after the test completes", () {
+ _expectReport("""
+ // This completer ensures that the test isolate isn't killed until all
+ // prints have happened.
+ var testDone = new Completer();
+ var waitStarted = new Completer();
+ test('test', () {
+ waitStarted.future.then((_) {
+ new Future(() => print("one"));
+ new Future(() => print("two"));
+ new Future(() => print("three"));
+ new Future(() => print("four"));
+ new Future(testDone.complete);
+ });
+ });
+
+ test('wait', () {
+ waitStarted.complete();
+ return testDone.future;
+ });""", """
+ +0: test
+ +1: test
+ +1: wait
+ +1: test
+ one
+ two
+ three
+ four
+
+ +2: wait
+ +2: All tests passed!""");
+ });
+
+ test("interleaves prints and errors", () {
+ _expectReport("""
+ // This completer ensures that the test isolate isn't killed until all
+ // prints have happened.
+ var completer = new Completer();
+ test('test', () {
+ scheduleMicrotask(() {
+ print("three");
+ print("four");
+ throw "second error";
+ });
+
+ scheduleMicrotask(() {
+ print("five");
+ print("six");
+ completer.complete();
+ });
+
+ print("one");
+ print("two");
+ throw "first error";
+ });
+
+ test('wait', () => completer.future);""",
+ """
+ +0: test
+ one
+ two
+
+ +0 -1: test
+ first error
+ test.dart 24:11 main.<fn>
+
+ three
+ four
+ second error
+ test.dart 13:13 main.<fn>.<fn>
+ ===== asynchronous gap ===========================
+ dart:async scheduleMicrotask
+ test.dart 10:28 main.<fn>
+
+ five
+ six
+
+ +0 -1: wait
+ +1 -1: wait
+ +1 -1: Some tests failed.""");
+ });
+ });
}
void _expectReport(String tests, String expected, {List<String> args}) {
diff --git a/test/runner/isolate_listener_test.dart b/test/runner/isolate_listener_test.dart
index 184c86d..06d9244 100644
--- a/test/runner/isolate_listener_test.dart
+++ b/test/runner/isolate_listener_test.dart
@@ -262,6 +262,15 @@
});
});
});
+
+ test("forwards a test's prints", () {
+ return _isolateTest(_printTest).then((liveTest) {
+ expect(liveTest.onPrint.take(2).toList(),
+ completion(equals(["Hello,", "world!"])));
+
+ return liveTest.run();
+ });
+ });
}
/// Loads the first test defined in [entryPoint] in another isolate.
@@ -380,3 +389,14 @@
});
});
}
+
+/// An isolate entrypoint that defines a test that prints twice.
+void _printTest(SendPort sendPort) {
+ IsolateListener.start(sendPort, () => () {
+ test("prints", () {
+ print("Hello,");
+ return new Future(() => print("world!"));
+ });
+ });
+}
+