OLD | NEW |
| (Empty) |
1 //#!/usr/bin/env dart | |
2 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
3 // for details. All rights reserved. Use of this source code is governed by a | |
4 // BSD-style license that can be found in the LICENSE file. | |
5 | |
6 /** | |
7 * testrunner is a program to run Dart unit tests. Unlike $DART/tools/test.dart, | |
8 * this program is intended for 3rd parties to be able to run unit tests in | |
9 * a batched fashion. As such, it adds some features and removes others. Some | |
10 * of the removed features are: | |
11 * | |
12 * - No support for test.status files. The assumption is that tests are | |
13 * expected to pass. Status file support will be added in the future. | |
14 * - A restricted set of runtimes. The assumption here is that the Dart | |
15 * libraries deal with platform dependencies, and so the primary | |
16 * SKUs that a user of this app would be concerned with would be | |
17 * Dart-native versus compiled, and client (browser) vs server. To | |
18 * support these, three runtimes are allowed: 'drt-dart' and 'drt-js' (for | |
19 * client native and client-compiled, respectively), and 'vm' | |
20 * (for server-side native). | |
21 * - No sharding of test processes. | |
22 * | |
23 * On the other hand, a number of features have been added: | |
24 * | |
25 * - The ability to filter tests by group or name. | |
26 * - The ability to run tests in isolates. | |
27 * - The ability to customize the format of the test result messages. | |
28 * - The ability to list the tests available. | |
29 * | |
30 * By default, testrunner will run all tests in the current directory. | |
31 * With a -R option, it will recurse into subdirectories. | |
32 * Directories can also be specified on the command line; if | |
33 * any are specified they will override the use of the current directory. | |
34 * All files that match the `--test-file-pattern` will be included; by default | |
35 * this is files with names that end in _test.dart. | |
36 * | |
37 * Options can be specified on the command line, via a configuration | |
38 * file (`--config`) or via a test.config file in the test directory, | |
39 * in decreasing order of priority. | |
40 * | |
41 * The three runtimes are: | |
42 * | |
43 * vm - run native Dart in the VM; i.e. using $DARTSDK/dart-sdk/bin/dart. | |
44 * TODO(antonm): fix the option name. | |
45 * drt-dart - run native Dart in content shell, the headless version of | |
46 * Dartium, which is located in $DARTSDK/chromium/content_shell, if | |
47 * you installed the SDK that is bundled with the editor, or available | |
48 * from http://gsdview.appspot.com/dartium-archive/continuous/ | |
49 * otherwise. | |
50 * | |
51 * TODO(antonm): fix the option name. | |
52 * drt-js - run Dart compiled to Javascript in content shell. | |
53 * | |
54 * testrunner supports simple DOM render tests. These can use expected values | |
55 * for the render output from content shell, either are textual DOM | |
56 * descriptions (`--layout-tests`) or pixel renderings (`--pixel-tests`). | |
57 * When running layout tests, testrunner will see if there is a file with | |
58 * a .png or a .txt extension in a directory with the same name as the | |
59 * test file (without extension) and with the test name as the file name. | |
60 * For example, if there is a test file foo_test.dart with tests 'test1' | |
61 * and 'test2', it will look for foo_test/test1.txt and foo_test/test2.txt | |
62 * for text render layout files. If these exist it will do additional checks | |
63 * of the rendered layout; if not, the test will fail. | |
64 * | |
65 * Layout file (re)generation can be done using `--regenerate`. This will | |
66 * create or update the layout files (and implicitly pass the tests). | |
67 * | |
68 * The wrapping and execution of test files is handled by test_pipeline.dart, | |
69 * which is run in an isolate. The `--pipeline` argument can be used to | |
70 * specify a different script for running a test file pipeline, allowing | |
71 * customization of the pipeline. | |
72 * | |
73 * Wrapper files are created for tests in the tmp directory, which can be | |
74 * overridden with --tempdir. These files are not removed after the tests | |
75 * are complete, primarily to reduce the amount of times pub must be | |
76 * executed. You can use --clean-files to force file cleanup. The temp | |
77 * directories will have pubspec.yaml files auto-generated unless the | |
78 * original test file directories have such files; in that case the existing | |
79 * files will be copied. Whenever a new pubspec file is copied or | |
80 * created pub will be run (but not otherwise - so if you want to do | |
81 * the equivelent of pub update you should use --clean-files and the rerun | |
82 * the tests). | |
83 * | |
84 * TODO(gram): if the user has a pubspec.yaml file, we should inspect the | |
85 * pubspec.lock file and give useful errors: | |
86 * - if the lock file doesn't exit, then run pub install | |
87 * - if it exists and it doesn't have the required packages (unittest or | |
88 * browser), ask the user to add them and run pub install again. | |
89 */ | |
90 | |
91 library testrunner; | |
92 import 'dart:async'; | |
93 import 'dart:io'; | |
94 import 'dart:isolate'; | |
95 import 'dart:math'; | |
96 import 'package:args/args.dart'; | |
97 | |
98 part 'options.dart'; | |
99 part 'utils.dart'; | |
100 | |
101 /** The set of [PipelineRunner]s to execute. */ | |
102 List _tasks; | |
103 | |
104 /** The maximum number of pipelines that can run concurrently. */ | |
105 int _maxTasks; | |
106 | |
107 /** The number of pipelines currently running. */ | |
108 int _numTasks; | |
109 | |
110 /** The index of the next pipeline runner to execute. */ | |
111 int _nextTask; | |
112 | |
113 /** The sink to use for high-value messages, like test results. */ | |
114 IOSink _outSink; | |
115 | |
116 /** The sink to use for low-value messages, like verbose output. */ | |
117 IOSink _logSink; | |
118 | |
119 /** | |
120 * The last temp test directory we accessed; we use this to know if we | |
121 * need to check the pub configuration. | |
122 */ | |
123 String _testDir; | |
124 | |
125 /** | |
126 * The user can specify output streams on the command line, using 'none', | |
127 * 'stdout', 'stderr', or a file path; [getSink] will take such a name | |
128 * and return an appropriate [IOSink]. | |
129 */ | |
130 IOSink getSink(String name) { | |
131 if (name == null || name == 'none') { | |
132 return null; | |
133 } | |
134 if (name == 'stdout') { | |
135 return stdout; | |
136 } | |
137 if (name == 'stderr') { | |
138 return stderr; | |
139 } | |
140 var f = new File(name); | |
141 return f.openWrite(); | |
142 } | |
143 | |
144 /** | |
145 * Given a [List] of [testFiles], either print the list or create | |
146 * and execute pipelines for the files. | |
147 */ | |
148 void processTests(Map config, List testFiles) { | |
149 _outSink = getSink(config['out']); | |
150 _logSink = getSink(config['log']); | |
151 if (config['list-files']) { | |
152 if (_outSink != null) { | |
153 for (var i = 0; i < testFiles.length; i++) { | |
154 _outSink.write(testFiles[i]); | |
155 _outSink.write('\n'); | |
156 } | |
157 } | |
158 } else { | |
159 _maxTasks = min(config['tasks'], testFiles.length); | |
160 _numTasks = 0; | |
161 _nextTask = 0; | |
162 spawnTasks(config, testFiles); | |
163 } | |
164 } | |
165 | |
166 /** | |
167 * Create or update a pubspec for the target test directory. We use the | |
168 * source directory pubspec if available; otherwise we create a minimal one. | |
169 * We return a Future if we are running pub install, or null otherwise. | |
170 */ | |
171 Future doPubConfig(Path sourcePath, String sourceDir, | |
172 Path targetPath, String targetDir, | |
173 String pub, String runtime) { | |
174 // Make sure the target directory exists. | |
175 var d = new Directory(targetDir); | |
176 if (!d.existsSync()) { | |
177 d.createSync(recursive: true); | |
178 } | |
179 | |
180 // If the source has no pubspec, but the dest does, leave | |
181 // things as they are. If neither do, create one in dest. | |
182 | |
183 var sourcePubSpecName = new Path(sourceDir).append("pubspec.yaml"). | |
184 toNativePath(); | |
185 var targetPubSpecName = new Path(targetDir).append("pubspec.yaml"). | |
186 toNativePath(); | |
187 var sourcePubSpec = new File(sourcePubSpecName); | |
188 var targetPubSpec = new File(targetPubSpecName); | |
189 | |
190 if (!sourcePubSpec.existsSync()) { | |
191 if (targetPubSpec.existsSync()) { | |
192 return null; | |
193 } else { | |
194 // Create one. | |
195 if (runtime == 'vm') { | |
196 writeFile(targetPubSpecName, | |
197 "name: testrunner\ndependencies:\n unittest: any\n"); | |
198 } else { | |
199 writeFile(targetPubSpecName, | |
200 "name: testrunner\ndependencies:\n unittest: any\n browser: any\n"); | |
201 } | |
202 } | |
203 } else { | |
204 if (targetPubSpec.existsSync()) { | |
205 // If there is a source one, and it is older than the target, | |
206 // leave the target as is. | |
207 if (sourcePubSpec.lastModifiedSync().millisecondsSinceEpoch < | |
208 targetPubSpec.lastModifiedSync().millisecondsSinceEpoch) { | |
209 return null; | |
210 } | |
211 } | |
212 // Source exists and is newer than target or there is no target; | |
213 // copy the source to the target. If there is a pubspec.lock file, | |
214 // copy that too. | |
215 var s = sourcePubSpec.readAsStringSync(); | |
216 targetPubSpec.writeAsStringSync(s); | |
217 var sourcePubLock = new File(sourcePubSpecName.replaceAll(".yaml", ".lock"))
; | |
218 if (sourcePubLock.existsSync()) { | |
219 var targetPubLock = | |
220 new File(targetPubSpecName.replaceAll(".yaml", ".lock")); | |
221 s = sourcePubLock.readAsStringSync(); | |
222 targetPubLock.writeAsStringSync(s); | |
223 } | |
224 } | |
225 // A new target pubspec was created so run pub install. | |
226 return _processHelper(pub, [ 'install' ], workingDir: targetDir); | |
227 } | |
228 | |
229 /** Execute as many tasks as possible up to the maxTasks limit. */ | |
230 void spawnTasks(Map config, List testFiles) { | |
231 var verbose = config['verbose']; | |
232 // If we were running in the VM and the immediate flag was set, we have | |
233 // already printed the important messages (i.e. prefixed with ###), | |
234 // so we should skip them now. | |
235 var skipNonVerbose = config['immediate'] && config['runtime'] == 'vm'; | |
236 while (_numTasks < _maxTasks && _nextTask < testFiles.length) { | |
237 ++_numTasks; | |
238 var testfile = testFiles[_nextTask++]; | |
239 config['testfile'] = testfile; | |
240 ReceivePort port = new ReceivePort(); | |
241 port.receive((msg, _) { | |
242 List stdout = msg[0]; | |
243 List stderr = msg[1]; | |
244 List log = msg[2]; | |
245 int exitCode = msg[3]; | |
246 writelog(stdout, _outSink, _logSink, verbose, skipNonVerbose); | |
247 writelog(stderr, _outSink, _logSink, true, skipNonVerbose); | |
248 writelog(log, _outSink, _logSink, verbose, skipNonVerbose); | |
249 port.close(); | |
250 --_numTasks; | |
251 if (exitCode == 0 || !config['stop-on-failure']) { | |
252 spawnTasks(config, testFiles); | |
253 } | |
254 if (_numTasks == 0) { | |
255 // No outstanding tasks; we're all done. | |
256 // We could later print a summary report here. | |
257 } | |
258 }); | |
259 // Get the names of the source and target test files and containing | |
260 // directories. | |
261 var testPath = new Path(testfile); | |
262 var sourcePath = testPath.directoryPath; | |
263 var sourceDir = sourcePath.toNativePath(); | |
264 | |
265 var targetPath = new Path(config["tempdir"]); | |
266 var normalizedTarget = testPath.directoryPath.toNativePath() | |
267 .replaceAll(Platform.pathSeparator, '_') | |
268 .replaceAll(':', '_'); | |
269 targetPath = targetPath.append("${normalizedTarget}_${config['runtime']}"); | |
270 var targetDir = targetPath.toNativePath(); | |
271 | |
272 config['targetDir'] = targetDir; | |
273 // If this is a new target dir, we need to redo the pub check. | |
274 var f = null; | |
275 if (targetDir != _testDir) { | |
276 f = doPubConfig(sourcePath, sourceDir, targetPath, targetDir, | |
277 config['pub'], config['runtime']); | |
278 _testDir = targetDir; | |
279 } | |
280 var response = new ReceivePort(); | |
281 spawnUri(config['pipeline'], [], response) | |
282 .then((_) => f) | |
283 .then((_) => response.first) | |
284 .then((s) { s.send([config, port.sendPort]); }); | |
285 if (f != null) break; // Don't do any more until pub is done. | |
286 } | |
287 } | |
288 | |
289 /** | |
290 * Our tests are configured so that critical messages have a '###' prefix. | |
291 * [writelog] takes the output from a pipeline execution and writes it to | |
292 * our output sinks. It will strip the '###' if necessary on critical | |
293 * messages; other messages will only be written if verbose output was | |
294 * specified. | |
295 */ | |
296 void writelog(List messages, IOSink out, IOSink log, | |
297 bool includeVerbose, bool skipNonVerbose) { | |
298 for (var i = 0; i < messages.length; i++) { | |
299 var msg = messages[i]; | |
300 if (msg.startsWith('###')) { | |
301 if (!skipNonVerbose && out != null) { | |
302 out.write(msg.substring(3)); | |
303 out.write('\n'); | |
304 } | |
305 } else if (msg.startsWith('CONSOLE MESSAGE:')) { | |
306 if (!skipNonVerbose && out != null) { | |
307 int idx = msg.indexOf('###'); | |
308 if (idx > 0) { | |
309 out.write(msg.substring(idx + 3)); | |
310 out.write('\n'); | |
311 } | |
312 } | |
313 } else if (includeVerbose && log != null) { | |
314 log.write(msg); | |
315 log.write('\n'); | |
316 } | |
317 } | |
318 } | |
319 | |
320 normalizeFilter(List filter) { | |
321 // We want the filter to be a quoted string or list of quoted | |
322 // strings. | |
323 for (var i = 0; i < filter.length; i++) { | |
324 var f = filter[i]; | |
325 if (f[0] != "'" && f[0] != '"') { | |
326 filter[i] = "'$f'"; // TODO(gram): Quote embedded quotes. | |
327 } | |
328 } | |
329 return filter; | |
330 } | |
331 | |
332 void sanitizeConfig(Map config, ArgParser parser) { | |
333 config['layout'] = config['layout-text'] || config['layout-pixel']; | |
334 config['verbose'] = (config['log'] != 'none' && !config['list-groups']); | |
335 config['timeout'] = int.parse(config['timeout']); | |
336 config['tasks'] = int.parse(config['tasks']); | |
337 | |
338 var dartsdk = config['dartsdk']; | |
339 var pathSep = Platform.pathSeparator; | |
340 | |
341 if (dartsdk == null) { | |
342 var runner = Platform.executable; | |
343 var idx = runner.indexOf('dart-sdk'); | |
344 if (idx < 0) { | |
345 print("Please use --dartsdk option or run using the dart executable " | |
346 "from the Dart SDK"); | |
347 exit(0); | |
348 } | |
349 dartsdk = runner.substring(0, idx); | |
350 } | |
351 if (Platform.operatingSystem == 'macos') { | |
352 config['dart2js'] = | |
353 '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js'; | |
354 config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart'; | |
355 config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub'; | |
356 config['drt'] = | |
357 '$dartsdk/chromium/Content Shell.app/Contents/MacOS/Content Shell'; | |
358 } else if (Platform.operatingSystem == 'linux') { | |
359 config['dart2js'] = | |
360 '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js'; | |
361 config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart'; | |
362 config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub'; | |
363 config['drt'] = '$dartsdk${pathSep}chromium${pathSep}content_shell'; | |
364 } else { | |
365 config['dart2js'] = | |
366 '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js.bat'; | |
367 config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart.exe'
; | |
368 config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub.bat'; | |
369 config['drt'] = '$dartsdk${pathSep}chromium${pathSep}content_shell.exe'; | |
370 } | |
371 | |
372 for (var prog in [ 'drt', 'dart', 'pub', 'dart2js' ]) { | |
373 config[prog] = makePathAbsolute(config[prog]); | |
374 } | |
375 config['runnerDir'] = runnerDirectory; | |
376 config['include'] = normalizeFilter(config['include']); | |
377 config['exclude'] = normalizeFilter(config['exclude']); | |
378 } | |
379 | |
380 main(List<String> arguments) { | |
381 var optionsParser = getOptionParser(); | |
382 var options = loadConfiguration(optionsParser, arguments); | |
383 if (isSane(options)) { | |
384 if (options['list-options']) { | |
385 printOptions(optionsParser, options, false, stdout); | |
386 } else if (options['list-all-options']) { | |
387 printOptions(optionsParser, options, true, stdout); | |
388 } else { | |
389 var config = new Map(); | |
390 for (var option in options.options) { | |
391 config[option] = options[option]; | |
392 } | |
393 var rest = []; | |
394 // Process the remmaining command line args. If they look like | |
395 // options then split them up and add them to the map; they may be for | |
396 // custom pipelines. | |
397 for (var other in options.rest) { | |
398 var idx; | |
399 if (other.startsWith('--') && (idx = other.indexOf('=')) > 0) { | |
400 var optName = other.substring(2, idx); | |
401 var optValue = other.substring(idx+1); | |
402 config[optName] = optValue; | |
403 } else { | |
404 rest.add(other); | |
405 } | |
406 } | |
407 | |
408 sanitizeConfig(config, optionsParser); | |
409 | |
410 // Build the list of tests and then execute them. | |
411 List dirs = rest; | |
412 if (dirs.length == 0) { | |
413 dirs.add('.'); // Use current working directory as default. | |
414 } | |
415 var f = buildFileList(dirs, | |
416 new RegExp(config['test-file-pattern']), config['recurse']); | |
417 if (config['sort']) f.sort(); | |
418 processTests(config, f); | |
419 } | |
420 } | |
421 } | |
OLD | NEW |