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 |