| 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; | |
| 13 | 12 |
| 14 import 'package:args/args.dart'; | |
| 15 import 'package:async/async.dart'; | 13 import 'package:async/async.dart'; |
| 16 import 'package:stack_trace/stack_trace.dart'; | 14 import 'package:stack_trace/stack_trace.dart'; |
| 17 import 'package:yaml/yaml.dart'; | 15 import 'package:yaml/yaml.dart'; |
| 18 | 16 |
| 19 import 'backend/metadata.dart'; | 17 import 'backend/metadata.dart'; |
| 20 import 'backend/test_platform.dart'; | 18 import 'runner/application_exception.dart'; |
| 19 import 'runner/configuration.dart'; |
| 21 import 'runner/engine.dart'; | 20 import 'runner/engine.dart'; |
| 22 import 'runner/application_exception.dart'; | |
| 23 import 'runner/load_exception.dart'; | 21 import 'runner/load_exception.dart'; |
| 24 import 'runner/load_suite.dart'; | 22 import 'runner/load_suite.dart'; |
| 25 import 'runner/loader.dart'; | 23 import 'runner/loader.dart'; |
| 26 import 'runner/reporter/compact.dart'; | 24 import 'runner/reporter/compact.dart'; |
| 27 import 'runner/reporter/expanded.dart'; | 25 import 'runner/reporter/expanded.dart'; |
| 28 import 'util/exit_codes.dart' as exit_codes; | 26 import 'util/exit_codes.dart' as exit_codes; |
| 29 import 'util/io.dart'; | |
| 30 import 'utils.dart'; | 27 import 'utils.dart'; |
| 31 | 28 |
| 32 /// The argument parser used to parse the executable arguments. | |
| 33 final _parser = new ArgParser(allowTrailingOptions: true); | |
| 34 | |
| 35 /// The default number of test suites to run at once. | |
| 36 /// | |
| 37 /// This defaults to half the available processors, since presumably some of | |
| 38 /// them will be used for the OS and other processes. | |
| 39 final _defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2); | |
| 40 | |
| 41 /// A merged stream of all signals that tell the test runner to shut down | 29 /// A merged stream of all signals that tell the test runner to shut down |
| 42 /// gracefully. | 30 /// gracefully. |
| 43 /// | 31 /// |
| 44 /// Signals will only be captured as long as this has an active subscription. | 32 /// Signals will only be captured as long as this has an active subscription. |
| 45 /// Otherwise, they'll be handled by Dart's default signal handler, which | 33 /// Otherwise, they'll be handled by Dart's default signal handler, which |
| 46 /// terminates the program immediately. | 34 /// terminates the program immediately. |
| 47 final _signals = Platform.isWindows | 35 final _signals = Platform.isWindows |
| 48 ? ProcessSignal.SIGINT.watch() | 36 ? ProcessSignal.SIGINT.watch() |
| 49 : mergeStreams([ | 37 : mergeStreams([ |
| 50 ProcessSignal.SIGTERM.watch(), | 38 ProcessSignal.SIGTERM.watch(), |
| (...skipping 20 matching lines...) Expand all Loading... |
| 71 if (transformers is! List) return false; | 59 if (transformers is! List) return false; |
| 72 | 60 |
| 73 return transformers.any((transformer) { | 61 return transformers.any((transformer) { |
| 74 if (transformer is String) return transformer == 'test/pub_serve'; | 62 if (transformer is String) return transformer == 'test/pub_serve'; |
| 75 if (transformer is! Map) return false; | 63 if (transformer is! Map) return false; |
| 76 if (transformer.keys.length != 1) return false; | 64 if (transformer.keys.length != 1) return false; |
| 77 return transformer.keys.single == 'test/pub_serve'; | 65 return transformer.keys.single == 'test/pub_serve'; |
| 78 }); | 66 }); |
| 79 } | 67 } |
| 80 | 68 |
| 69 Configuration _configuration; |
| 70 |
| 81 main(List<String> args) async { | 71 main(List<String> args) async { |
| 82 var allPlatforms = TestPlatform.all.toList(); | |
| 83 if (!Platform.isMacOS) allPlatforms.remove(TestPlatform.safari); | |
| 84 if (!Platform.isWindows) allPlatforms.remove(TestPlatform.internetExplorer); | |
| 85 | |
| 86 _parser.addFlag("help", abbr: "h", negatable: false, | |
| 87 help: "Shows this usage information."); | |
| 88 _parser.addFlag("version", negatable: false, | |
| 89 help: "Shows the package's version."); | |
| 90 _parser.addOption("package-root", hide: true); | |
| 91 _parser.addOption("name", | |
| 92 abbr: 'n', | |
| 93 help: 'A substring of the name of the test to run.\n' | |
| 94 'Regular expression syntax is supported.'); | |
| 95 _parser.addOption("plain-name", | |
| 96 abbr: 'N', | |
| 97 help: 'A plain-text substring of the name of the test to run.'); | |
| 98 _parser.addOption("platform", | |
| 99 abbr: 'p', | |
| 100 help: 'The platform(s) on which to run the tests.', | |
| 101 allowed: allPlatforms.map((platform) => platform.identifier).toList(), | |
| 102 defaultsTo: 'vm', | |
| 103 allowMultiple: true); | |
| 104 _parser.addOption("concurrency", | |
| 105 abbr: 'j', | |
| 106 help: 'The number of concurrent test suites run.\n' | |
| 107 '(defaults to $_defaultConcurrency)', | |
| 108 valueHelp: 'threads'); | |
| 109 _parser.addOption("pub-serve", | |
| 110 help: 'The port of a pub serve instance serving "test/".', | |
| 111 hide: !supportsPubServe, | |
| 112 valueHelp: 'port'); | |
| 113 _parser.addOption("reporter", | |
| 114 abbr: 'r', | |
| 115 help: 'The runner used to print test results.', | |
| 116 allowed: ['compact', 'expanded'], | |
| 117 defaultsTo: Platform.isWindows ? 'expanded' : 'compact', | |
| 118 allowedHelp: { | |
| 119 'compact': 'A single line, updated continuously.', | |
| 120 'expanded': 'A separate line for each update.' | |
| 121 }); | |
| 122 _parser.addFlag("verbose-trace", negatable: false, | |
| 123 help: 'Whether to emit stack traces with core library frames.'); | |
| 124 _parser.addFlag("js-trace", negatable: false, | |
| 125 help: 'Whether to emit raw JavaScript stack traces for browser tests.'); | |
| 126 _parser.addFlag("color", defaultsTo: null, | |
| 127 help: 'Whether to use terminal colors.\n(auto-detected by default)'); | |
| 128 | |
| 129 var options; | |
| 130 try { | 72 try { |
| 131 options = _parser.parse(args); | 73 _configuration = new Configuration.parse(args); |
| 132 } on FormatException catch (error) { | 74 } on FormatException catch (error) { |
| 133 _printUsage(error.message); | 75 _printUsage(error.message); |
| 134 exitCode = exit_codes.usage; | 76 exitCode = exit_codes.usage; |
| 135 return; | 77 return; |
| 136 } | 78 } |
| 137 | 79 |
| 138 if (options["help"]) { | 80 if (_configuration.help) { |
| 139 _printUsage(); | 81 _printUsage(); |
| 140 return; | 82 return; |
| 141 } | 83 } |
| 142 | 84 |
| 143 if (options["version"]) { | 85 if (_configuration.version) { |
| 144 if (!_printVersion()) { | 86 if (!_printVersion()) { |
| 145 stderr.writeln("Couldn't find version number."); | 87 stderr.writeln("Couldn't find version number."); |
| 146 exitCode = exit_codes.data; | 88 exitCode = exit_codes.data; |
| 147 } | 89 } |
| 148 return; | 90 return; |
| 149 } | 91 } |
| 150 | 92 |
| 151 var color = options["color"]; | 93 if (_configuration.pubServeUrl != null && !_usesTransformer) { |
| 152 if (color == null) color = canUseSpecialChars; | 94 stderr.write(''' |
| 153 | |
| 154 var pubServeUrl; | |
| 155 if (options["pub-serve"] != null) { | |
| 156 pubServeUrl = Uri.parse("http://localhost:${options['pub-serve']}"); | |
| 157 if (!_usesTransformer) { | |
| 158 stderr.write(''' | |
| 159 When using --pub-serve, you must include the "test/pub_serve" transformer in | 95 When using --pub-serve, you must include the "test/pub_serve" transformer in |
| 160 your pubspec: | 96 your pubspec: |
| 161 | 97 |
| 162 transformers: | 98 transformers: |
| 163 - test/pub_serve: | 99 - test/pub_serve: |
| 164 \$include: test/**_test.dart | 100 \$include: test/**_test.dart |
| 165 '''); | 101 '''); |
| 166 exitCode = exit_codes.data; | 102 exitCode = exit_codes.data; |
| 167 return; | 103 return; |
| 168 } | |
| 169 } | 104 } |
| 170 | 105 |
| 171 var concurrency = _defaultConcurrency; | 106 if (!_configuration.explicitPaths && |
| 172 if (options["concurrency"] != null) { | 107 !new Directory(_configuration.paths.single).existsSync()) { |
| 173 try { | 108 _printUsage('No test files were passed and the default "test/" ' |
| 174 concurrency = int.parse(options["concurrency"]); | 109 "directory doesn't exist."); |
| 175 } catch (error) { | 110 exitCode = exit_codes.data; |
| 176 _printUsage('Couldn\'t parse --concurrency "${options["concurrency"]}":' | 111 return; |
| 177 ' ${error.message}'); | |
| 178 exitCode = exit_codes.usage; | |
| 179 return; | |
| 180 } | |
| 181 } | 112 } |
| 182 | 113 |
| 183 var paths = options.rest; | 114 var metadata = new Metadata( |
| 184 if (paths.isEmpty) { | 115 verboseTrace: _configuration.verboseTrace); |
| 185 if (!new Directory("test").existsSync()) { | 116 var loader = new Loader(_configuration.platforms, |
| 186 _printUsage('No test files were passed and the default "test/" ' | 117 pubServeUrl: _configuration.pubServeUrl, |
| 187 "directory doesn't exist."); | 118 packageRoot: _configuration.packageRoot, |
| 188 exitCode = exit_codes.data; | 119 color: _configuration.color, |
| 189 return; | |
| 190 } | |
| 191 paths = ["test"]; | |
| 192 } | |
| 193 | |
| 194 var pattern; | |
| 195 if (options["name"] != null) { | |
| 196 if (options["plain-name"] != null) { | |
| 197 _printUsage("--name and --plain-name may not both be passed."); | |
| 198 exitCode = exit_codes.data; | |
| 199 return; | |
| 200 } | |
| 201 pattern = new RegExp(options["name"]); | |
| 202 } else if (options["plain-name"] != null) { | |
| 203 pattern = options["plain-name"]; | |
| 204 } | |
| 205 | |
| 206 var metadata = new Metadata(verboseTrace: options["verbose-trace"]); | |
| 207 var platforms = options["platform"].map(TestPlatform.find); | |
| 208 var loader = new Loader(platforms, | |
| 209 pubServeUrl: pubServeUrl, | |
| 210 packageRoot: options["package-root"], | |
| 211 color: color, | |
| 212 metadata: metadata, | 120 metadata: metadata, |
| 213 jsTrace: options["js-trace"]); | 121 jsTrace: _configuration.jsTrace); |
| 214 | 122 |
| 215 var closed = false; | 123 var closed = false; |
| 216 var signalSubscription; | 124 var signalSubscription; |
| 217 signalSubscription = _signals.listen((_) { | 125 signalSubscription = _signals.listen((_) { |
| 218 closed = true; | 126 closed = true; |
| 219 signalSubscription.cancel(); | 127 signalSubscription.cancel(); |
| 220 loader.close(); | 128 loader.close(); |
| 221 }); | 129 }); |
| 222 | 130 |
| 223 try { | 131 try { |
| 224 var engine = new Engine(concurrency: concurrency); | 132 var engine = new Engine(concurrency: _configuration.concurrency); |
| 225 | 133 |
| 226 var watch = options["reporter"] == "compact" | 134 var watch = _configuration.reporter == "compact" |
| 227 ? CompactReporter.watch | 135 ? CompactReporter.watch |
| 228 : ExpandedReporter.watch; | 136 : ExpandedReporter.watch; |
| 229 | 137 |
| 230 watch( | 138 watch( |
| 231 engine, | 139 engine, |
| 232 color: color, | 140 color: _configuration.color, |
| 233 verboseTrace: options["verbose-trace"], | 141 verboseTrace: _configuration.verboseTrace, |
| 234 printPath: paths.length > 1 || | 142 printPath: _configuration.paths.length > 1 || |
| 235 new Directory(paths.single).existsSync(), | 143 new Directory(_configuration.paths.single).existsSync(), |
| 236 printPlatform: platforms.length > 1); | 144 printPlatform: _configuration.platforms.length > 1); |
| 237 | 145 |
| 238 // Override the signal handler to close [reporter]. [loader] will still be | 146 // Override the signal handler to close [reporter]. [loader] will still be |
| 239 // closed in the [whenComplete] below. | 147 // closed in the [whenComplete] below. |
| 240 signalSubscription.onData((_) async { | 148 signalSubscription.onData((_) async { |
| 241 closed = true; | 149 closed = true; |
| 242 signalSubscription.cancel(); | 150 signalSubscription.cancel(); |
| 243 | 151 |
| 244 // Wait a bit to print this message, since printing it eagerly looks weird | 152 // Wait a bit to print this message, since printing it eagerly looks weird |
| 245 // if the tests then finish immediately. | 153 // if the tests then finish immediately. |
| 246 var timer = new Timer(new Duration(seconds: 1), () { | 154 var timer = new Timer(new Duration(seconds: 1), () { |
| 247 // Print a blank line first to ensure that this doesn't interfere with | 155 // Print a blank line first to ensure that this doesn't interfere with |
| 248 // the compact reporter's unfinished line. | 156 // the compact reporter's unfinished line. |
| 249 print(""); | 157 print(""); |
| 250 print("Waiting for current test(s) to finish."); | 158 print("Waiting for current test(s) to finish."); |
| 251 print("Press Control-C again to terminate immediately."); | 159 print("Press Control-C again to terminate immediately."); |
| 252 }); | 160 }); |
| 253 | 161 |
| 254 // Make sure we close the engine *before* the loader. Otherwise, | 162 // Make sure we close the engine *before* the loader. Otherwise, |
| 255 // LoadSuites provided by the loader may get into bad states. | 163 // LoadSuites provided by the loader may get into bad states. |
| 256 await engine.close(); | 164 await engine.close(); |
| 257 timer.cancel(); | 165 timer.cancel(); |
| 258 await loader.close(); | 166 await loader.close(); |
| 259 }); | 167 }); |
| 260 | 168 |
| 261 try { | 169 try { |
| 262 var results = await Future.wait([ | 170 var results = await Future.wait([ |
| 263 _loadSuites(paths, pattern, loader, engine), | 171 _loadSuites(loader, engine), |
| 264 engine.run() | 172 engine.run() |
| 265 ], eagerError: true); | 173 ], eagerError: true); |
| 266 | 174 |
| 267 if (closed) return; | 175 if (closed) return; |
| 268 | 176 |
| 269 // Explicitly check "== true" here because [engine.run] can return `null` | 177 // Explicitly check "== true" here because [engine.run] can return `null` |
| 270 // if the engine was closed prematurely. | 178 // if the engine was closed prematurely. |
| 271 exitCode = results.last == true ? 0 : 1; | 179 exitCode = results.last == true ? 0 : 1; |
| 272 } finally { | 180 } finally { |
| 273 signalSubscription.cancel(); | 181 signalSubscription.cancel(); |
| 274 await engine.close(); | 182 await engine.close(); |
| 275 } | 183 } |
| 276 | 184 |
| 277 if (engine.passed.length == 0 && engine.failed.length == 0 && | 185 if (engine.passed.length == 0 && engine.failed.length == 0 && |
| 278 engine.skipped.length == 0 && pattern != null) { | 186 engine.skipped.length == 0 && _configuration.pattern != null) { |
| 279 stderr.write('No tests match '); | 187 stderr.write('No tests match '); |
| 280 | 188 |
| 281 if (pattern is RegExp) { | 189 if (_configuration.pattern is RegExp) { |
| 282 stderr.writeln('regular expression "${pattern.pattern}".'); | 190 var pattern = (_configuration.pattern as RegExp).pattern; |
| 191 stderr.writeln('regular expression "$pattern".'); |
| 283 } else { | 192 } else { |
| 284 stderr.writeln('"$pattern".'); | 193 stderr.writeln('"${_configuration.pattern}".'); |
| 285 } | 194 } |
| 286 exitCode = exit_codes.data; | 195 exitCode = exit_codes.data; |
| 287 } | 196 } |
| 288 } on ApplicationException catch (error) { | 197 } on ApplicationException catch (error) { |
| 289 stderr.writeln(error.message); | 198 stderr.writeln(error.message); |
| 290 exitCode = exit_codes.data; | 199 exitCode = exit_codes.data; |
| 291 } catch (error, stackTrace) { | 200 } catch (error, stackTrace) { |
| 292 stderr.writeln(getErrorMessage(error)); | 201 stderr.writeln(getErrorMessage(error)); |
| 293 stderr.writeln(new Trace.from(stackTrace).terse); | 202 stderr.writeln(new Trace.from(stackTrace).terse); |
| 294 stderr.writeln( | 203 stderr.writeln( |
| 295 "This is an unexpected error. Please file an issue at " | 204 "This is an unexpected error. Please file an issue at " |
| 296 "http://github.com/dart-lang/test\n" | 205 "http://github.com/dart-lang/test\n" |
| 297 "with the stack trace and instructions for reproducing the error."); | 206 "with the stack trace and instructions for reproducing the error."); |
| 298 exitCode = exit_codes.software; | 207 exitCode = exit_codes.software; |
| 299 } finally { | 208 } finally { |
| 300 signalSubscription.cancel(); | 209 signalSubscription.cancel(); |
| 301 await loader.close(); | 210 await loader.close(); |
| 302 } | 211 } |
| 303 } | 212 } |
| 304 | 213 |
| 305 /// Load the test suites in [paths] that match [pattern] and pass them to | 214 /// Load the test suites in [_configuration.paths] that match |
| 306 /// [engine]. | 215 /// [_configuration.pattern] and pass them to [engine]. |
| 307 /// | 216 /// |
| 308 /// This completes once all the tests have been added to the engine. It does not | 217 /// This completes once all the tests have been added to the engine. It does not |
| 309 /// run the engine. | 218 /// run the engine. |
| 310 Future _loadSuites(List<String> paths, Pattern pattern, Loader loader, | 219 Future _loadSuites(Loader loader, Engine engine) async { |
| 311 Engine engine) async { | |
| 312 var group = new FutureGroup(); | 220 var group = new FutureGroup(); |
| 313 | 221 |
| 314 mergeStreams(paths.map((path) { | 222 mergeStreams(_configuration.paths.map((path) { |
| 315 if (new Directory(path).existsSync()) return loader.loadDir(path); | 223 if (new Directory(path).existsSync()) return loader.loadDir(path); |
| 316 if (new File(path).existsSync()) return loader.loadFile(path); | 224 if (new File(path).existsSync()) return loader.loadFile(path); |
| 317 | 225 |
| 318 return new Stream.fromIterable([ | 226 return new Stream.fromIterable([ |
| 319 new LoadSuite("loading $path", () => | 227 new LoadSuite("loading $path", () => |
| 320 throw new LoadException(path, 'Does not exist.')) | 228 throw new LoadException(path, 'Does not exist.')) |
| 321 ]); | 229 ]); |
| 322 })).listen((loadSuite) { | 230 })).listen((loadSuite) { |
| 323 group.add(new Future.sync(() { | 231 group.add(new Future.sync(() { |
| 324 engine.suiteSink.add(loadSuite.changeSuite((suite) { | 232 engine.suiteSink.add(loadSuite.changeSuite((suite) { |
| 325 if (pattern == null) return suite; | 233 if (_configuration.pattern == null) return suite; |
| 326 return suite.change( | 234 return suite.change(tests: suite.tests.where( |
| 327 tests: suite.tests.where((test) => test.name.contains(pattern))); | 235 (test) => test.name.contains(_configuration.pattern))); |
| 328 })); | 236 })); |
| 329 })); | 237 })); |
| 330 }, onError: (error, stackTrace) { | 238 }, onError: (error, stackTrace) { |
| 331 group.add(new Future.error(error, stackTrace)); | 239 group.add(new Future.error(error, stackTrace)); |
| 332 }, onDone: group.close); | 240 }, onDone: group.close); |
| 333 | 241 |
| 334 await group.future; | 242 await group.future; |
| 335 | 243 |
| 336 // Once we've loaded all the suites, notify the engine that no more will be | 244 // Once we've loaded all the suites, notify the engine that no more will be |
| 337 // coming. | 245 // coming. |
| (...skipping 10 matching lines...) Expand all Loading... |
| 348 var message = "Runs tests in this package."; | 256 var message = "Runs tests in this package."; |
| 349 if (error != null) { | 257 if (error != null) { |
| 350 message = error; | 258 message = error; |
| 351 output = stderr; | 259 output = stderr; |
| 352 } | 260 } |
| 353 | 261 |
| 354 output.write("""$message | 262 output.write("""$message |
| 355 | 263 |
| 356 Usage: pub run test:test [files or directories...] | 264 Usage: pub run test:test [files or directories...] |
| 357 | 265 |
| 358 ${_parser.usage} | 266 ${Configuration.usage} |
| 359 """); | 267 """); |
| 360 } | 268 } |
| 361 | 269 |
| 362 /// Prints the version number of the test package. | 270 /// Prints the version number of the test package. |
| 363 /// | 271 /// |
| 364 /// This loads the version number from the current package's lockfile. It | 272 /// This loads the version number from the current package's lockfile. It |
| 365 /// returns true if it successfully printed the version number and false if it | 273 /// returns true if it successfully printed the version number and false if it |
| 366 /// couldn't be loaded. | 274 /// couldn't be loaded. |
| 367 bool _printVersion() { | 275 bool _printVersion() { |
| 368 var lockfile; | 276 var lockfile; |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 409 if (description is! Map) return false; | 317 if (description is! Map) return false; |
| 410 var path = description["path"]; | 318 var path = description["path"]; |
| 411 if (path is! String) return false; | 319 if (path is! String) return false; |
| 412 | 320 |
| 413 print("$version (from $path)"); | 321 print("$version (from $path)"); |
| 414 return true; | 322 return true; |
| 415 | 323 |
| 416 default: return false; | 324 default: return false; |
| 417 } | 325 } |
| 418 } | 326 } |
| OLD | NEW |