OLD | NEW |
---|---|
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 // TODO(nweiz): This is under lib so that it can be used by the unittest dummy | 5 // TODO(nweiz): This is under lib so that it can be used by the unittest dummy |
6 // package. Once that package is no longer being updated, move this back into | 6 // package. Once that package is no longer being updated, move this back into |
7 // bin. | 7 // bin. |
8 library test.executable; | 8 library test.executable; |
9 | 9 |
10 import 'dart:async'; | 10 import 'dart:async'; |
11 import 'dart:io'; | 11 import 'dart:io'; |
12 import 'dart:math' as math; | 12 import 'dart:math' as math; |
13 | 13 |
14 import 'package:args/args.dart'; | 14 import 'package:args/args.dart'; |
15 import 'package:stack_trace/stack_trace.dart'; | 15 import 'package:stack_trace/stack_trace.dart'; |
16 import 'package:yaml/yaml.dart'; | 16 import 'package:yaml/yaml.dart'; |
17 | 17 |
18 import 'backend/metadata.dart'; | 18 import 'backend/metadata.dart'; |
19 import 'backend/test_platform.dart'; | 19 import 'backend/test_platform.dart'; |
20 import 'runner/reporter/compact.dart'; | 20 import 'runner/engine.dart'; |
21 import 'runner/reporter/expanded.dart'; | |
22 import 'runner/application_exception.dart'; | 21 import 'runner/application_exception.dart'; |
23 import 'runner/load_exception.dart'; | 22 import 'runner/load_exception.dart'; |
24 import 'runner/load_exception_suite.dart'; | 23 import 'runner/load_exception_suite.dart'; |
25 import 'runner/loader.dart'; | 24 import 'runner/loader.dart'; |
25 import 'runner/reporter/compact.dart'; | |
26 import 'runner/reporter/expanded.dart'; | |
26 import 'util/exit_codes.dart' as exit_codes; | 27 import 'util/exit_codes.dart' as exit_codes; |
27 import 'util/io.dart'; | 28 import 'util/io.dart'; |
28 import 'utils.dart'; | 29 import 'utils.dart'; |
29 | 30 |
30 /// The argument parser used to parse the executable arguments. | 31 /// The argument parser used to parse the executable arguments. |
31 final _parser = new ArgParser(allowTrailingOptions: true); | 32 final _parser = new ArgParser(allowTrailingOptions: true); |
32 | 33 |
33 /// The default number of test suites to run at once. | 34 /// The default number of test suites to run at once. |
34 /// | 35 /// |
35 /// This defaults to half the available processors, since presumably some of | 36 /// This defaults to half the available processors, since presumably some of |
(...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
159 | 160 |
160 transformers: | 161 transformers: |
161 - test/pub_serve: | 162 - test/pub_serve: |
162 \$include: test/**_test.dart | 163 \$include: test/**_test.dart |
163 '''); | 164 '''); |
164 exitCode = exit_codes.data; | 165 exitCode = exit_codes.data; |
165 return; | 166 return; |
166 } | 167 } |
167 } | 168 } |
168 | 169 |
169 var metadata = new Metadata(verboseTrace: options["verbose-trace"]); | |
170 var platforms = options["platform"].map(TestPlatform.find); | |
171 var loader = new Loader(platforms, | |
172 pubServeUrl: pubServeUrl, | |
173 packageRoot: options["package-root"], | |
174 color: color, | |
175 metadata: metadata, | |
176 jsTrace: options["js-trace"]); | |
177 | |
178 var concurrency = _defaultConcurrency; | 170 var concurrency = _defaultConcurrency; |
179 if (options["concurrency"] != null) { | 171 if (options["concurrency"] != null) { |
180 try { | 172 try { |
181 concurrency = int.parse(options["concurrency"]); | 173 concurrency = int.parse(options["concurrency"]); |
182 } catch (error) { | 174 } catch (error) { |
183 _printUsage('Couldn\'t parse --concurrency "${options["concurrency"]}":' | 175 _printUsage('Couldn\'t parse --concurrency "${options["concurrency"]}":' |
184 ' ${error.message}'); | 176 ' ${error.message}'); |
185 exitCode = exit_codes.usage; | 177 exitCode = exit_codes.usage; |
186 return; | 178 return; |
187 } | 179 } |
188 } | 180 } |
189 | 181 |
190 var paths = options.rest; | 182 var paths = options.rest; |
191 if (paths.isEmpty) { | 183 if (paths.isEmpty) { |
192 if (!new Directory("test").existsSync()) { | 184 if (!new Directory("test").existsSync()) { |
193 _printUsage('No test files were passed and the default "test/" ' | 185 _printUsage('No test files were passed and the default "test/" ' |
194 "directory doesn't exist."); | 186 "directory doesn't exist."); |
195 exitCode = exit_codes.data; | 187 exitCode = exit_codes.data; |
196 return; | 188 return; |
197 } | 189 } |
198 paths = ["test"]; | 190 paths = ["test"]; |
199 } | 191 } |
200 | 192 |
193 var pattern; | |
194 if (options["name"] != null) { | |
195 if (options["plain-name"] != null) { | |
196 _printUsage("--name and --plain-name may not both be passed."); | |
197 exitCode = exit_codes.data; | |
198 return; | |
199 } | |
200 pattern = new RegExp(options["name"]); | |
201 } else if (options["plain-name"] != null) { | |
202 pattern = options["plain-name"]; | |
203 } | |
204 | |
205 var metadata = new Metadata(verboseTrace: options["verbose-trace"]); | |
206 var platforms = options["platform"].map(TestPlatform.find); | |
207 var loader = new Loader(platforms, | |
208 pubServeUrl: pubServeUrl, | |
209 packageRoot: options["package-root"], | |
210 color: color, | |
211 metadata: metadata, | |
212 jsTrace: options["js-trace"]); | |
213 | |
201 var signalSubscription; | 214 var signalSubscription; |
202 var closed = false; | |
203 signalSubscription = _signals.listen((_) { | 215 signalSubscription = _signals.listen((_) { |
204 signalSubscription.cancel(); | 216 signalSubscription.cancel(); |
205 closed = true; | |
206 loader.close(); | 217 loader.close(); |
207 }); | 218 }); |
208 | 219 |
209 try { | 220 try { |
210 var suites = await mergeStreams(paths.map((path) { | 221 var engine = new Engine(concurrency: concurrency); |
211 if (new Directory(path).existsSync()) return loader.loadDir(path); | |
212 if (new File(path).existsSync()) return loader.loadFile(path); | |
213 return new Stream.fromFuture(new Future.error( | |
214 new LoadException(path, 'Does not exist.'), | |
215 new Trace.current())); | |
216 })).transform(new StreamTransformer.fromHandlers( | |
217 handleError: (error, stackTrace, sink) { | |
218 if (error is! LoadException) { | |
219 sink.addError(error, stackTrace); | |
220 } else { | |
221 sink.add(new LoadExceptionSuite(error, stackTrace)); | |
222 } | |
223 })).toList(); | |
224 | 222 |
225 if (closed) return; | 223 var watch = options["reporter"] == "compact" |
226 suites = flatten(suites); | 224 ? CompactReporter.watch |
225 : ExpandedReporter.watch; | |
227 | 226 |
228 var pattern; | 227 watch( |
229 if (options["name"] != null) { | 228 engine, |
230 if (options["plain-name"] != null) { | 229 color: color, |
231 _printUsage("--name and --plain-name may not both be passed."); | 230 verboseTrace: options["verbose-trace"], |
232 exitCode = exit_codes.data; | 231 printPath: paths.length > 1 || |
233 return; | 232 new Directory(paths.single).existsSync(), |
234 } | 233 printPlatform: platforms.length > 1); |
235 pattern = new RegExp(options["name"]); | |
236 } else if (options["plain-name"] != null) { | |
237 pattern = options["plain-name"]; | |
238 } | |
239 | |
240 if (pattern != null) { | |
241 suites = suites.map((suite) { | |
242 // Don't ever filter out load errors. | |
243 if (suite is LoadExceptionSuite) return suite; | |
244 return suite.change( | |
245 tests: suite.tests.where((test) => test.name.contains(pattern))); | |
246 }).toList(); | |
247 | |
248 if (suites.every((suite) => suite.tests.isEmpty)) { | |
249 stderr.write('No tests match '); | |
250 | |
251 if (pattern is RegExp) { | |
252 stderr.writeln('regular expression "${pattern.pattern}".'); | |
253 } else { | |
254 stderr.writeln('"$pattern".'); | |
255 } | |
256 exitCode = exit_codes.data; | |
257 return; | |
258 } | |
259 } | |
260 | |
261 var reporter = options["reporter"] == "compact" | |
262 ? new CompactReporter( | |
263 flatten(suites), | |
264 concurrency: concurrency, | |
265 color: color, | |
266 verboseTrace: options["verbose-trace"]) | |
267 : new ExpandedReporter( | |
268 flatten(suites), | |
269 concurrency: concurrency, | |
270 color: color, | |
271 verboseTrace: options["verbose-trace"]); | |
272 | 234 |
273 // Override the signal handler to close [reporter]. [loader] will still be | 235 // Override the signal handler to close [reporter]. [loader] will still be |
274 // closed in the [whenComplete] below. | 236 // closed in the [whenComplete] below. |
275 signalSubscription.onData((_) { | 237 signalSubscription.onData((_) async { |
276 signalSubscription.cancel(); | 238 signalSubscription.cancel(); |
277 closed = true; | |
278 | 239 |
279 // Wait a bit to print this message, since printing it eagerly looks weird | 240 // Wait a bit to print this message, since printing it eagerly looks weird |
280 // if the tests then finish immediately. | 241 // if the tests then finish immediately. |
281 var timer = new Timer(new Duration(seconds: 1), () { | 242 var timer = new Timer(new Duration(seconds: 1), () { |
282 // Print a blank line first to ensure that this doesn't interfere with | 243 // Print a blank line first to ensure that this doesn't interfere with |
283 // the compact reporter's unfinished line. | 244 // the compact reporter's unfinished line. |
284 print(""); | 245 print(""); |
285 print("Waiting for current test(s) to finish."); | 246 print("Waiting for current test(s) to finish."); |
286 print("Press Control-C again to terminate immediately."); | 247 print("Press Control-C again to terminate immediately."); |
287 }); | 248 }); |
288 | 249 |
289 reporter.close().then((_) => timer.cancel()); | 250 await engine.close(); |
251 timer.cancel(); | |
252 await loader.close(); | |
290 }); | 253 }); |
291 | 254 |
292 try { | 255 try { |
293 var success = await reporter.run(); | 256 var results = await Future.wait([ |
294 exitCode = success ? 0 : 1; | 257 _loadSuites(paths, pattern, loader, engine), |
258 engine.run() | |
259 ], eagerError: true); | |
260 | |
261 // Explicitly check "== true" here because [engine.run] can return `null` | |
262 // if the engine was closed prematurely. | |
263 exitCode = results.last == true ? 0 : 1; | |
295 } finally { | 264 } finally { |
296 signalSubscription.cancel(); | 265 signalSubscription.cancel(); |
297 await reporter.close(); | 266 await engine.close(); |
267 } | |
268 | |
269 if (engine.passed.length == 0 && engine.failed.length == 0 && | |
270 engine.skipped.length == 0 && pattern != null) { | |
271 stderr.write('No tests match '); | |
272 | |
273 if (pattern is RegExp) { | |
274 stderr.writeln('regular expression "${pattern.pattern}".'); | |
275 } else { | |
276 stderr.writeln('"$pattern".'); | |
277 } | |
278 exitCode = exit_codes.data; | |
298 } | 279 } |
299 } on ApplicationException catch (error) { | 280 } on ApplicationException catch (error) { |
300 stderr.writeln(error.message); | 281 stderr.writeln(error.message); |
301 exitCode = exit_codes.data; | 282 exitCode = exit_codes.data; |
302 } catch (error, stackTrace) { | 283 } catch (error, stackTrace) { |
303 stderr.writeln(getErrorMessage(error)); | 284 stderr.writeln(getErrorMessage(error)); |
304 stderr.writeln(new Trace.from(stackTrace).terse); | 285 stderr.writeln(new Trace.from(stackTrace).terse); |
305 stderr.writeln( | 286 stderr.writeln( |
306 "This is an unexpected error. Please file an issue at " | 287 "This is an unexpected error. Please file an issue at " |
307 "http://github.com/dart-lang/test\n" | 288 "http://github.com/dart-lang/test\n" |
308 "with the stack trace and instructions for reproducing the error."); | 289 "with the stack trace and instructions for reproducing the error."); |
309 exitCode = exit_codes.software; | 290 exitCode = exit_codes.software; |
310 } finally { | 291 } finally { |
311 signalSubscription.cancel(); | 292 signalSubscription.cancel(); |
312 await loader.close(); | 293 await loader.close(); |
313 } | 294 } |
314 } | 295 } |
315 | 296 |
297 /// Load the test suites in [paths] that match [pattern] and pass them to | |
298 /// [engine]. | |
299 /// | |
300 /// This completes once all the tests have been added to the engine. It does not | |
301 /// run the engine. | |
302 Future _loadSuites(List<String> paths, Pattern pattern, Loader loader, | |
303 Engine engine) async { | |
304 var completer = new Completer(); | |
305 | |
306 mergeStreams(paths.map((path) { | |
307 if (new Directory(path).existsSync()) return loader.loadDir(path); | |
308 if (new File(path).existsSync()) return loader.loadFile(path); | |
309 | |
310 return new Stream.fromFuture(new Future.error( | |
311 new LoadException(path, 'Does not exist.'), | |
312 new Trace.current())); | |
313 })).map((suite) { | |
314 if (pattern == null) return suite; | |
315 return suite.change( | |
316 tests: suite.tests.where((test) => test.name.contains(pattern))); | |
317 }).listen((suite) => engine.suiteSink.add(suite), | |
kevmoo
2015/06/17 22:20:04
consider using await async here – won't need a com
nweiz
2015/06/17 22:41:08
We can't use it here, because we need to intercept
| |
318 onError: (error, stackTrace) { | |
319 if (error is LoadException) { | |
320 engine.suiteSink.add(new LoadExceptionSuite(error, stackTrace)); | |
321 } else if (!completer.isCompleted) { | |
322 completer.completeError(error, stackTrace); | |
323 } | |
324 }, onDone: () => completer.complete()); | |
325 | |
326 await completer.future; | |
327 | |
328 // Once we've loaded all the suites, notify the engine that no more will be | |
329 // coming. | |
330 engine.suiteSink.close(); | |
331 } | |
332 | |
316 /// Print usage information for this command. | 333 /// Print usage information for this command. |
317 /// | 334 /// |
318 /// If [error] is passed, it's used in place of the usage message and the whole | 335 /// If [error] is passed, it's used in place of the usage message and the whole |
319 /// thing is printed to stderr instead of stdout. | 336 /// thing is printed to stderr instead of stdout. |
320 void _printUsage([String error]) { | 337 void _printUsage([String error]) { |
321 var output = stdout; | 338 var output = stdout; |
322 | 339 |
323 var message = "Runs tests in this package."; | 340 var message = "Runs tests in this package."; |
324 if (error != null) { | 341 if (error != null) { |
325 message = error; | 342 message = error; |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
384 if (description is! Map) return false; | 401 if (description is! Map) return false; |
385 var path = description["path"]; | 402 var path = description["path"]; |
386 if (path is! String) return false; | 403 if (path is! String) return false; |
387 | 404 |
388 print("$version (from $path)"); | 405 print("$version (from $path)"); |
389 return true; | 406 return true; |
390 | 407 |
391 default: return false; | 408 default: return false; |
392 } | 409 } |
393 } | 410 } |
OLD | NEW |