Skip to content

Commit 653ef17

Browse files
committed
Add a --pause-after-load flag for debugging.
Currently this only support windowed browsers and the runner can only be unpaused from the command line. See #294 [email protected] Review URL: https://codereview.chromium.org//1248073003 .
1 parent c1e76b4 commit 653ef17

15 files changed

+607
-98
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.12.4
2+
3+
* Add a `--pause-after-load` flag that pauses the test runner after each suite
4+
is loaded so that breakpoints and other debugging annotations can be added.
5+
Currently this is only supported on browsers.
6+
17
## 0.12.3+8
28

39
* Fix an uncaught error that could crop up when killing the test runner process

lib/src/backend/live_test_controller.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,15 @@ class LiveTestController {
151151
}
152152

153153
/// Emits a line printed by the test over [LiveTest.onPrint].
154-
void print(String line) => _onPrintController.add(line);
154+
void print(String line) {
155+
if (_onPrintController.hasListener) {
156+
_onPrintController.add(line);
157+
} else {
158+
// Make sure all prints get surfaced one way or another to aid in
159+
// debugging.
160+
Zone.ROOT.print(line);
161+
}
162+
}
155163

156164
/// A wrapper for [_onRun] that ensures that it follows the guarantees for
157165
/// [LiveTest.run].

lib/src/executable.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'runner.dart';
1616
import 'runner/application_exception.dart';
1717
import 'runner/configuration.dart';
1818
import 'util/exit_codes.dart' as exit_codes;
19+
import 'util/io.dart';
1920
import 'utils.dart';
2021

2122
/// A merged stream of all signals that tell the test runner to shut down
@@ -109,6 +110,7 @@ transformers:
109110
if (signalSubscription == null) return;
110111
signalSubscription.cancel();
111112
signalSubscription = null;
113+
stdinLines.cancel(immediate: true);
112114
await runner.close();
113115
}
114116

lib/src/runner.dart

Lines changed: 118 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,26 @@ library test.runner;
77
import 'dart:async';
88
import 'dart:io';
99

10-
import 'package:async/async.dart';
11-
1210
import 'backend/metadata.dart';
11+
import 'backend/suite.dart';
12+
import 'backend/test_platform.dart';
1313
import 'runner/application_exception.dart';
1414
import 'runner/configuration.dart';
1515
import 'runner/engine.dart';
1616
import 'runner/load_exception.dart';
1717
import 'runner/load_suite.dart';
1818
import 'runner/loader.dart';
19+
import 'runner/reporter.dart';
1920
import 'runner/reporter/compact.dart';
2021
import 'runner/reporter/expanded.dart';
2122
import 'util/async_thunk.dart';
23+
import 'util/io.dart';
2224
import 'utils.dart';
2325

26+
/// The set of platforms for which debug flags are (currently) not supported.
27+
final _debugUnsupportedPlatforms = new Set.from(
28+
[TestPlatform.vm, TestPlatform.phantomJS, TestPlatform.contentShell]);
29+
2430
/// A class that loads and runs tests based on a [Configuration].
2531
///
2632
/// This maintains a [Loader] and an [Engine] and passes test suites from one to
@@ -36,13 +42,16 @@ class Runner {
3642
/// The engine that runs the test suites.
3743
final Engine _engine;
3844

45+
/// The reporter that's emitting the test runner's results.
46+
final Reporter _reporter;
47+
48+
/// The subscription to the stream returned by [_loadSuites].
49+
StreamSubscription _suiteSubscription;
50+
3951
/// The thunk for ensuring [close] only runs once.
4052
final _closeThunk = new AsyncThunk();
4153
bool get _closed => _closeThunk.hasRun;
4254

43-
/// Whether [run] has been called.
44-
bool _hasRun = false;
45-
4655
/// Creates a new runner based on [configuration].
4756
factory Runner(Configuration configuration) {
4857
var metadata = new Metadata(
@@ -60,36 +69,42 @@ class Runner {
6069
? CompactReporter.watch
6170
: ExpandedReporter.watch;
6271

63-
watch(
72+
var reporter = watch(
6473
engine,
6574
color: configuration.color,
6675
verboseTrace: configuration.verboseTrace,
6776
printPath: configuration.paths.length > 1 ||
6877
new Directory(configuration.paths.single).existsSync(),
6978
printPlatform: configuration.platforms.length > 1);
7079

71-
return new Runner._(configuration, loader, engine);
80+
return new Runner._(configuration, loader, engine, reporter);
7281
}
7382

74-
Runner._(this._configuration, this._loader, this._engine);
83+
Runner._(this._configuration, this._loader, this._engine, this._reporter);
7584

7685
/// Starts the runner.
7786
///
7887
/// This starts running tests and printing their progress. It returns whether
7988
/// or not they ran successfully.
8089
Future<bool> run() async {
81-
_hasRun = true;
82-
8390
if (_closed) {
8491
throw new StateError("run() may not be called on a closed Runner.");
8592
}
8693

94+
var suites = _loadSuites();
95+
8796
var success;
88-
var results = await Future.wait([
89-
_loadSuites(),
90-
_engine.run()
91-
], eagerError: true);
92-
success = results.last;
97+
if (_configuration.pauseAfterLoad) {
98+
// TODO(nweiz): disable timeouts when debugging.
99+
success = await _loadThenPause(suites);
100+
} else {
101+
_suiteSubscription = suites.listen(_engine.suiteSink.add);
102+
var results = await Future.wait([
103+
_suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()),
104+
_engine.run()
105+
], eagerError: true);
106+
success = results.last;
107+
}
93108

94109
if (_closed) return false;
95110

@@ -118,54 +133,115 @@ class Runner {
118133
/// filesystem.
119134
Future close() => _closeThunk.run(() async {
120135
var timer;
121-
if (_hasRun) {
136+
if (!_engine.isIdle) {
122137
// Wait a bit to print this message, since printing it eagerly looks weird
123138
// if the tests then finish immediately.
124139
timer = new Timer(new Duration(seconds: 1), () {
125-
// Print a blank line first to ensure that this doesn't interfere with
126-
// the compact reporter's unfinished line.
127-
print('');
140+
// Pause the reporter while we print to ensure that we don't interfere
141+
// with its output.
142+
_reporter.pause();
128143
print("Waiting for current test(s) to finish.");
129144
print("Press Control-C again to terminate immediately.");
145+
_reporter.resume();
130146
});
131147
}
132148

149+
if (_suiteSubscription != null) _suiteSubscription.cancel();
150+
_suiteSubscription = null;
151+
133152
// Make sure we close the engine *before* the loader. Otherwise,
134153
// LoadSuites provided by the loader may get into bad states.
135154
await _engine.close();
136155
if (timer != null) timer.cancel();
137156
await _loader.close();
138157
});
139158

140-
/// Load the test suites in [_configuration.paths] that match
141-
/// [_configuration.pattern].
142-
Future _loadSuites() async {
143-
var group = new FutureGroup();
144-
145-
mergeStreams(_configuration.paths.map((path) {
159+
/// Return a stream of [LoadSuite]s in [_configuration.paths].
160+
///
161+
/// Only tests that match [_configuration.pattern] will be included in the
162+
/// suites once they're loaded.
163+
Stream<LoadSuite> _loadSuites() {
164+
return mergeStreams(_configuration.paths.map((path) {
146165
if (new Directory(path).existsSync()) return _loader.loadDir(path);
147166
if (new File(path).existsSync()) return _loader.loadFile(path);
148167

149168
return new Stream.fromIterable([
150169
new LoadSuite("loading $path", () =>
151170
throw new LoadException(path, 'Does not exist.'))
152171
]);
153-
})).listen((loadSuite) {
154-
group.add(new Future.sync(() {
155-
_engine.suiteSink.add(loadSuite.changeSuite((suite) {
156-
if (_configuration.pattern == null) return suite;
157-
return suite.change(tests: suite.tests.where(
158-
(test) => test.name.contains(_configuration.pattern)));
159-
}));
160-
}));
161-
}, onError: (error, stackTrace) {
162-
group.add(new Future.error(error, stackTrace));
163-
}, onDone: group.close);
164-
165-
await group.future;
166-
167-
// Once we've loaded all the suites, notify the engine that no more will be
168-
// coming.
169-
_engine.suiteSink.close();
172+
})).map((loadSuite) {
173+
return loadSuite.changeSuite((suite) {
174+
if (_configuration.pattern == null) return suite;
175+
return suite.change(tests: suite.tests.where((test) =>
176+
test.name.contains(_configuration.pattern)));
177+
});
178+
});
179+
}
180+
181+
/// Loads each suite in [suites] in order, pausing after load for platforms
182+
/// that support debugging.
183+
Future<bool> _loadThenPause(Stream<LoadSuite> suites) async {
184+
var unsupportedPlatforms = _configuration.platforms
185+
.where(_debugUnsupportedPlatforms.contains)
186+
.map((platform) =>
187+
platform == TestPlatform.vm ? "the Dart VM" : platform.name)
188+
.toList();
189+
190+
if (unsupportedPlatforms.isNotEmpty) {
191+
warn(
192+
wordWrap("Debugging is currently unsupported on "
193+
"${toSentence(unsupportedPlatforms)}."),
194+
color: _configuration.color);
195+
}
196+
197+
_suiteSubscription = suites.asyncMap((loadSuite) async {
198+
// Make the underlying suite null so that the engine doesn't start running
199+
// it immediately.
200+
_engine.suiteSink.add(loadSuite.changeSuite((_) => null));
201+
202+
var suite = await loadSuite.suite;
203+
if (suite == null) return;
204+
205+
await _pause(suite);
206+
if (_closed) return;
207+
208+
_engine.suiteSink.add(suite);
209+
await _engine.onIdle.first;
210+
}).listen(null);
211+
212+
var results = await Future.wait([
213+
_suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()),
214+
_engine.run()
215+
]);
216+
return results.last;
217+
}
218+
219+
/// Pauses the engine and the reporter so that the user can set breakpoints as
220+
/// necessary.
221+
///
222+
/// This is a no-op for test suites that aren't on platforms where debugging
223+
/// is supported.
224+
Future _pause(Suite suite) async {
225+
if (suite.platform == null) return;
226+
if (_debugUnsupportedPlatforms.contains(suite.platform)) return;
227+
228+
try {
229+
_reporter.pause();
230+
231+
var bold = _configuration.color ? '\u001b[1m' : '';
232+
var noColor = _configuration.color ? '\u001b[0m' : '';
233+
print('');
234+
print(wordWrap(
235+
"${bold}The test runner is paused.${noColor} Open the dev console in "
236+
"${suite.platform} and set breakpoints. Once you're finished, "
237+
"return to this terminal and press Enter."));
238+
239+
// TODO(nweiz): Display something in the paused browsers indicating that
240+
// they're paused.
241+
242+
await stdinLines.next;
243+
} finally {
244+
_reporter.resume();
245+
}
170246
}
171247
}

lib/src/runner/configuration.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ class Configuration {
5656
help: 'The port of a pub serve instance serving "test/".',
5757
hide: !supportsPubServe,
5858
valueHelp: 'port');
59+
parser.addFlag("pause-after-load",
60+
help: 'Pauses for debugging before any tests execute.\n'
61+
'Implies --concurrency=1.\n'
62+
'Currently only supported for browser tests.',
63+
negatable: false);
5964
parser.addOption("reporter",
6065
abbr: 'r',
6166
help: 'The runner used to print test results.',
@@ -95,6 +100,9 @@ class Configuration {
95100
/// Dart-like traces.
96101
bool get jsTrace => _options['js-trace'];
97102

103+
/// Whether to pause for debugging after loading each test suite.
104+
bool get pauseAfterLoad => _options['pause-after-load'];
105+
98106
/// The package root for resolving "package:" URLs.
99107
String get packageRoot => _options['package-root'] == null
100108
? p.join(p.current, 'packages')

lib/src/runner/engine.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import '../backend/live_test_controller.dart';
1616
import '../backend/state.dart';
1717
import '../backend/suite.dart';
1818
import '../backend/test.dart';
19-
import '../util/delegating_sink.dart';
2019
import 'load_suite.dart';
2120

2221
/// An [Engine] manages a run that encompasses multiple test suites.
@@ -136,6 +135,13 @@ class Engine {
136135
/// This is separate from [active] because load tests aren't always surfaced.
137136
final _activeLoadTests = new List<LiveTest>();
138137

138+
/// Whether this engine is idle—that is, not currently executing a test.
139+
bool get isIdle => _group.isIdle;
140+
141+
/// A broadcast stream that fires an event whenever [isIdle] switches from
142+
/// `false` to `true`.
143+
Stream get onIdle => _group.onIdle;
144+
139145
/// Creates an [Engine] that will run all tests provided via [suiteSink].
140146
///
141147
/// [concurrency] controls how many suites are run at once, and defaults to 1.

lib/src/runner/reporter.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 test.runner.reporter;
6+
7+
/// An interface for classes that watch the progress of an Engine and report it
8+
/// to the user.
9+
///
10+
/// A reporter should subscribe to the Engine's events as soon as it's created.
11+
abstract class Reporter {
12+
/// Pauses the reporter's output.
13+
///
14+
/// Subclasses should buffer any events from the engine while they're paused.
15+
/// They should also ensure that this does nothing if the reporter is already
16+
/// paused.
17+
void pause();
18+
19+
/// Resumes the reporter's output after being [paused].
20+
///
21+
/// Subclasses should ensure that this does nothing if the reporter isn't
22+
/// paused.
23+
void resume();
24+
25+
/// Cancels the reporter's output.
26+
void cancel();
27+
}

0 commit comments

Comments
 (0)