@@ -7,20 +7,26 @@ library test.runner;
7
7
import 'dart:async' ;
8
8
import 'dart:io' ;
9
9
10
- import 'package:async/async.dart' ;
11
-
12
10
import 'backend/metadata.dart' ;
11
+ import 'backend/suite.dart' ;
12
+ import 'backend/test_platform.dart' ;
13
13
import 'runner/application_exception.dart' ;
14
14
import 'runner/configuration.dart' ;
15
15
import 'runner/engine.dart' ;
16
16
import 'runner/load_exception.dart' ;
17
17
import 'runner/load_suite.dart' ;
18
18
import 'runner/loader.dart' ;
19
+ import 'runner/reporter.dart' ;
19
20
import 'runner/reporter/compact.dart' ;
20
21
import 'runner/reporter/expanded.dart' ;
21
22
import 'util/async_thunk.dart' ;
23
+ import 'util/io.dart' ;
22
24
import 'utils.dart' ;
23
25
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
+
24
30
/// A class that loads and runs tests based on a [Configuration] .
25
31
///
26
32
/// This maintains a [Loader] and an [Engine] and passes test suites from one to
@@ -36,13 +42,16 @@ class Runner {
36
42
/// The engine that runs the test suites.
37
43
final Engine _engine;
38
44
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
+
39
51
/// The thunk for ensuring [close] only runs once.
40
52
final _closeThunk = new AsyncThunk ();
41
53
bool get _closed => _closeThunk.hasRun;
42
54
43
- /// Whether [run] has been called.
44
- bool _hasRun = false ;
45
-
46
55
/// Creates a new runner based on [configuration] .
47
56
factory Runner (Configuration configuration) {
48
57
var metadata = new Metadata (
@@ -60,36 +69,42 @@ class Runner {
60
69
? CompactReporter .watch
61
70
: ExpandedReporter .watch;
62
71
63
- watch (
72
+ var reporter = watch (
64
73
engine,
65
74
color: configuration.color,
66
75
verboseTrace: configuration.verboseTrace,
67
76
printPath: configuration.paths.length > 1 ||
68
77
new Directory (configuration.paths.single).existsSync (),
69
78
printPlatform: configuration.platforms.length > 1 );
70
79
71
- return new Runner ._(configuration, loader, engine);
80
+ return new Runner ._(configuration, loader, engine, reporter );
72
81
}
73
82
74
- Runner ._(this ._configuration, this ._loader, this ._engine);
83
+ Runner ._(this ._configuration, this ._loader, this ._engine, this ._reporter );
75
84
76
85
/// Starts the runner.
77
86
///
78
87
/// This starts running tests and printing their progress. It returns whether
79
88
/// or not they ran successfully.
80
89
Future <bool > run () async {
81
- _hasRun = true ;
82
-
83
90
if (_closed) {
84
91
throw new StateError ("run() may not be called on a closed Runner." );
85
92
}
86
93
94
+ var suites = _loadSuites ();
95
+
87
96
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
+ }
93
108
94
109
if (_closed) return false ;
95
110
@@ -118,54 +133,115 @@ class Runner {
118
133
/// filesystem.
119
134
Future close () => _closeThunk.run (() async {
120
135
var timer;
121
- if (_hasRun ) {
136
+ if (! _engine.isIdle ) {
122
137
// Wait a bit to print this message, since printing it eagerly looks weird
123
138
// if the tests then finish immediately.
124
139
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 ( );
128
143
print ("Waiting for current test(s) to finish." );
129
144
print ("Press Control-C again to terminate immediately." );
145
+ _reporter.resume ();
130
146
});
131
147
}
132
148
149
+ if (_suiteSubscription != null ) _suiteSubscription.cancel ();
150
+ _suiteSubscription = null ;
151
+
133
152
// Make sure we close the engine *before* the loader. Otherwise,
134
153
// LoadSuites provided by the loader may get into bad states.
135
154
await _engine.close ();
136
155
if (timer != null ) timer.cancel ();
137
156
await _loader.close ();
138
157
});
139
158
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) {
146
165
if (new Directory (path).existsSync ()) return _loader.loadDir (path);
147
166
if (new File (path).existsSync ()) return _loader.loadFile (path);
148
167
149
168
return new Stream .fromIterable ([
150
169
new LoadSuite ("loading $path " , () =>
151
170
throw new LoadException (path, 'Does not exist.' ))
152
171
]);
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 ? '\u 001b[1m' : '' ;
232
+ var noColor = _configuration.color ? '\u 001b[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
+ }
170
246
}
171
247
}
0 commit comments