Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(477)

Side by Side Diff: utils/testrunner/testrunner.dart

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

Powered by Google App Engine
This is Rietveld 408576698