Chromium Code Reviews| 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 |