| OLD | NEW |
| 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 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 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 * |
| 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 |
| 68 * specify a different script for running a test file pipeline, allowing |
| 69 * customization of the pipeline. |
| 65 */ | 70 */ |
| 66 | 71 |
| 67 // TODO - layout tests that use PNGs rather than DRT text render dumps. | 72 // TODO - layout tests that use PNGs rather than DRT text render dumps. |
| 68 #library('testrunner'); | 73 #library('testrunner'); |
| 69 #import('dart:io'); | 74 #import('dart:io'); |
| 70 #import('dart:isolate'); | 75 #import('dart:isolate'); |
| 71 #import('dart:math'); | 76 #import('dart:math'); |
| 72 #import('../../pkg/args/lib/args.dart'); | 77 #import('../../pkg/args/lib/args.dart'); |
| 73 | 78 |
| 74 #source('client_server_task.dart'); | |
| 75 #source('configuration.dart'); | |
| 76 #source('dart_wrap_task.dart'); | |
| 77 #source('dart2js_task.dart'); | |
| 78 #source('delete_task.dart'); | |
| 79 #source('html_wrap_task.dart'); | |
| 80 #source('macros.dart'); | |
| 81 #source('options.dart'); | 79 #source('options.dart'); |
| 82 #source('pipeline_runner.dart'); | |
| 83 #source('pipeline_task.dart'); | |
| 84 #source('run_process_task.dart'); | |
| 85 #source('utils.dart'); | 80 #source('utils.dart'); |
| 86 | 81 |
| 87 /** The set of [PipelineRunner]s to execute. */ | 82 /** The set of [PipelineRunner]s to execute. */ |
| 88 List _tasks; | 83 List _tasks; |
| 89 | 84 |
| 90 /** The maximum number of pipelines that can run concurrently. */ | 85 /** The maximum number of pipelines that can run concurrently. */ |
| 91 int _maxTasks; | 86 int _maxTasks; |
| 92 | 87 |
| 93 /** The number of pipelines currently running. */ | 88 /** The number of pipelines currently running. */ |
| 94 int _numTasks; | 89 int _numTasks; |
| 95 | 90 |
| 96 /** The index of the next pipeline runner to execute. */ | 91 /** The index of the next pipeline runner to execute. */ |
| 97 int _nextTask; | 92 int _nextTask; |
| 98 | 93 |
| 99 /** The stream to use for high-value messages, like test results. */ | 94 /** The stream to use for high-value messages, like test results. */ |
| 100 OutputStream _outStream; | 95 OutputStream _outStream; |
| 101 | 96 |
| 102 /** The stream to use for low-value messages, like verbose output. */ | 97 /** The stream to use for low-value messages, like verbose output. */ |
| 103 OutputStream _logStream; | 98 OutputStream _logStream; |
| 104 | 99 |
| 105 /** The full set of options. */ | |
| 106 Configuration config; | |
| 107 | |
| 108 /** | 100 /** |
| 109 * The user can specify output streams on the command line, using 'none', | 101 * The user can specify output streams on the command line, using 'none', |
| 110 * 'stdout', 'stderr', or a file path; [getStream] will take such a name | 102 * 'stdout', 'stderr', or a file path; [getStream] will take such a name |
| 111 * and return an appropriate [OutputStream]. | 103 * and return an appropriate [OutputStream]. |
| 112 */ | 104 */ |
| 113 OutputStream getStream(String name) { | 105 OutputStream getStream(String name) { |
| 114 if (name == 'none') { | 106 if (name == null || name == 'none') { |
| 115 return null; | 107 return null; |
| 116 } | 108 } |
| 117 if (name == 'stdout') { | 109 if (name == 'stdout') { |
| 118 return stdout; | 110 return stdout; |
| 119 } | 111 } |
| 120 if (name == 'stderr') { | 112 if (name == 'stderr') { |
| 121 return stderr; | 113 return stderr; |
| 122 } | 114 } |
| 123 return new File(name).openOutputStream(FileMode.WRITE); | 115 return new File(name).openOutputStream(FileMode.WRITE); |
| 124 } | 116 } |
| 125 | 117 |
| 126 /** | 118 /** |
| 127 * Generate a templated list of commands that should be executed for each test | |
| 128 * file. Each command is an instance of a [PipelineTask]. | |
| 129 * The commands can make use of a number of metatokens that will be | |
| 130 * expanded before execution (see the [Meta] class for details). | |
| 131 */ | |
| 132 List getPipelineTemplate(String runtime, bool checkedMode, bool keepTests) { | |
| 133 var pipeline = new List(); | |
| 134 var pathSep = Platform.pathSeparator; | |
| 135 Directory tempDir = new Directory(config.tempDir); | |
| 136 | |
| 137 if (!tempDir.existsSync()) { | |
| 138 tempDir.createSync(); | |
| 139 } | |
| 140 | |
| 141 // Templates for the generated files that are used to run the wrapped test. | |
| 142 var basePath = | |
| 143 '${config.tempDir}$pathSep${Macros.flattenedDirectory}_' | |
| 144 '${Macros.filenameNoExtension}'; | |
| 145 var tempDartFile = '${basePath}.dart'; | |
| 146 var tempJsFile = '${basePath}.js'; | |
| 147 var tempHTMLFile = '${basePath}.html'; | |
| 148 var tempCSSFile = '${basePath}.css'; | |
| 149 | |
| 150 // Add step for wrapping in Dart scaffold. | |
| 151 pipeline.add(new DartWrapTask(Macros.fullFilePath, tempDartFile)); | |
| 152 | |
| 153 // Add the compiler step, unless we are running native Dart. | |
| 154 if (runtime == 'drt-js') { | |
| 155 if (checkedMode) { | |
| 156 pipeline.add(new Dart2jsTask.checked(tempDartFile, tempJsFile)); | |
| 157 } else { | |
| 158 pipeline.add(new Dart2jsTask(tempDartFile, tempJsFile)); | |
| 159 } | |
| 160 } | |
| 161 | |
| 162 // Add step for wrapping in HTML, if we are running in DRT. | |
| 163 if (runtime != 'vm') { | |
| 164 // The user can have pre-existing HTML and CSS files for the test in the | |
| 165 // same directory and using the same name. The paths to these are matched | |
| 166 // by these two templates. | |
| 167 var HTMLFile = | |
| 168 '${Macros.directory}$pathSep${Macros.filenameNoExtension}.html'; | |
| 169 var CSSFile = | |
| 170 '${Macros.directory}$pathSep${Macros.filenameNoExtension}.css'; | |
| 171 pipeline.add(new HtmlWrapTask(Macros.fullFilePath, | |
| 172 HTMLFile, tempHTMLFile, CSSFile, tempCSSFile)); | |
| 173 } | |
| 174 | |
| 175 // Add the execution step. | |
| 176 var command; | |
| 177 var flags; | |
| 178 var task; | |
| 179 if (runtime == 'vm' || config.layoutPixel || config.layoutText) { | |
| 180 command = config.dartPath; | |
| 181 if (checkedMode) { | |
| 182 flags = ['--enable_asserts', '--enable_type_checks', tempDartFile]; | |
| 183 } else { | |
| 184 flags = [tempDartFile]; | |
| 185 } | |
| 186 } else { | |
| 187 command = config.drtPath; | |
| 188 flags = ['--no-timeout', tempHTMLFile]; | |
| 189 } | |
| 190 if (config.runServer) { | |
| 191 task = new RunClientServerTask(command, flags, config.timeout); | |
| 192 } else { | |
| 193 task = new RunProcessTask(command, flags, config.timeout); | |
| 194 } | |
| 195 pipeline.add(task); | |
| 196 return pipeline; | |
| 197 } | |
| 198 | |
| 199 /** | |
| 200 * Given a [List] of [testFiles], either print the list or create | 119 * Given a [List] of [testFiles], either print the list or create |
| 201 * and execute pipelines for the files. | 120 * and execute pipelines for the files. |
| 202 */ | 121 */ |
| 203 void processTests(List pipelineTemplate, List testFiles) { | 122 void processTests(Map config, List testFiles) { |
| 204 _outStream = getStream(config.outputStream); | 123 _outStream = getStream(config['out']); |
| 205 _logStream = getStream(config.logStream); | 124 _logStream = getStream(config['log']); |
| 206 if (config.listFiles) { | 125 if (config['list-files']) { |
| 207 if (_outStream != null) { | 126 if (_outStream != null) { |
| 208 for (var i = 0; i < testFiles.length; i++) { | 127 for (var i = 0; i < testFiles.length; i++) { |
| 209 _outStream.writeString(testFiles[i]); | 128 _outStream.writeString(testFiles[i]); |
| 210 _outStream.writeString('\n'); | 129 _outStream.writeString('\n'); |
| 211 } | 130 } |
| 212 } | 131 } |
| 213 } else { | 132 } else { |
| 214 // Create execution pipelines for each test file from the pipeline | 133 _maxTasks = min(config['tasks'], testFiles.length); |
| 215 // template and the concrete test file path, and then kick | |
| 216 // off execution of the first batch. | |
| 217 _tasks = new List(); | |
| 218 for (var i = 0; i < testFiles.length; i++) { | |
| 219 _tasks.add(new PipelineRunner(pipelineTemplate, testFiles[i], | |
| 220 config.verbose, completeHandler)); | |
| 221 } | |
| 222 | |
| 223 _maxTasks = min(config.maxTasks, testFiles.length); | |
| 224 _numTasks = 0; | 134 _numTasks = 0; |
| 225 _nextTask = 0; | 135 _nextTask = 0; |
| 226 spawnTasks(); | 136 spawnTasks(config, testFiles); |
| 227 } | 137 } |
| 228 } | 138 } |
| 229 | 139 |
| 230 /** Execute as many tasks as possible up to the maxTasks limit. */ | 140 /** Execute as many tasks as possible up to the maxTasks limit. */ |
| 231 void spawnTasks() { | 141 void spawnTasks(Map config, List testFiles) { |
| 232 while (_numTasks < _maxTasks && _nextTask < _tasks.length) { | 142 var verbose = config['verbose']; |
| 143 // 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 ###), |
| 145 // so we should skip them now. |
| 146 var skipNonVerbose = config['immediate'] && config['runtime'] == 'vm'; |
| 147 while (_numTasks < _maxTasks && _nextTask < testFiles.length) { |
| 233 ++_numTasks; | 148 ++_numTasks; |
| 234 _tasks[_nextTask++].execute(); | 149 var testfile = testFiles[_nextTask++]; |
| 235 } | 150 config['testfile'] = testfile; |
| 236 } | 151 ReceivePort port = new ReceivePort(); |
| 237 | 152 port.receive((msg, _) { |
| 238 /** | 153 port.close(); |
| 239 * Handle the completion of a task. Kick off more tasks if we | 154 List stdout = msg[0]; |
| 240 * have them. | 155 List stderr = msg[1]; |
| 241 */ | 156 List log = msg[2]; |
| 242 void completeHandler(String testFile, | 157 int exitCode = msg[3]; |
| 243 int exitCode, | 158 writelog(stdout, _outStream, _logStream, verbose, skipNonVerbose); |
| 244 List _stdout, | 159 writelog(stderr, _outStream, _logStream, true, skipNonVerbose); |
| 245 List _stderr) { | 160 writelog(log, _outStream, _logStream, verbose, skipNonVerbose); |
| 246 writelog(_stdout, _outStream, _logStream); | 161 --_numTasks; |
| 247 writelog(_stderr, _outStream, _logStream); | 162 if (exitCode == 0 || !config['stopOnFailure']) { |
| 248 --_numTasks; | 163 spawnTasks(config, testFiles); |
| 249 if (exitCode == 0 || !config.stopOnFailure) { | 164 } |
| 250 spawnTasks(); | 165 if (_numTasks == 0) { |
| 251 } | 166 // No outstanding tasks; we're all done. |
| 252 if (_numTasks == 0) { | 167 // We could later print a summary report here. |
| 253 // No outstanding tasks; we're all done. | 168 } |
| 254 // We could later print a summary report here. | 169 }); |
| 170 SendPort s = spawnUri(config['pipeline']); |
| 171 s.send(config, port.toSendPort()); |
| 255 } | 172 } |
| 256 } | 173 } |
| 257 | 174 |
| 258 /** | 175 /** |
| 259 * Our tests are configured so that critical messages have a '###' prefix. | 176 * Our tests are configured so that critical messages have a '###' prefix. |
| 260 * [writeLog] takes the output from a pipeline execution and writes it to | 177 * [writeLog] takes the output from a pipeline execution and writes it to |
| 261 * our output streams. It will strip the '###' if necessary on critical | 178 * our output streams. It will strip the '###' if necessary on critical |
| 262 * messages; other messages will only be written if verbose output was | 179 * messages; other messages will only be written if verbose output was |
| 263 * specified. | 180 * specified. |
| 264 */ | 181 */ |
| 265 void writelog(List messages, OutputStream out, OutputStream log) { | 182 void writelog(List messages, OutputStream out, OutputStream log, |
| 183 bool includeVerbose, bool skipNonVerbose) { |
| 266 for (var i = 0; i < messages.length; i++) { | 184 for (var i = 0; i < messages.length; i++) { |
| 267 var msg = messages[i]; | 185 var msg = messages[i]; |
| 268 if (msg.startsWith('###')) { | 186 if (msg.startsWith('###')) { |
| 269 if (out != null) { | 187 if (!skipNonVerbose && out != null) { |
| 270 out.writeString(msg.substring(3)); | 188 out.writeString(msg.substring(3)); |
| 271 out.writeString('\n'); | 189 out.writeString('\n'); |
| 272 } | 190 } |
| 273 } else if (config.verbose) { | 191 } else if (includeVerbose && log != null) { |
| 274 if (log != null) { | 192 log.writeString(msg); |
| 275 log.writeString(msg); | 193 log.writeString('\n'); |
| 276 log.writeString('\n'); | |
| 277 } | |
| 278 } | 194 } |
| 279 } | 195 } |
| 280 } | 196 } |
| 281 | 197 |
| 198 sanitizeConfig(Map config, ArgParser parser) { |
| 199 config['layout'] = config['layout-text'] || config['layout-pixel']; |
| 200 |
| 201 // TODO - check if next three are actually used. |
| 202 config['runInBrowser'] = (config['runtime'] != 'vm'); |
| 203 config['verbose'] = (config['log'] != 'none' && !config['list-groups']); |
| 204 config['filtering'] = (config['include'].length > 0 || |
| 205 config['exclude'].length > 0); |
| 206 |
| 207 config['timeout'] = int.parse(config['timeout']); |
| 208 config['tasks'] = int.parse(config['tasks']); |
| 209 |
| 210 config['keep-files'] = (config['keep-files'] && |
| 211 !(config['list-groups'] || config['list-tests'])); |
| 212 |
| 213 var dartsdk = config['dartsdk']; |
| 214 var pathSep = Platform.pathSeparator; |
| 215 |
| 216 if (dartsdk != null) { |
| 217 if (parser.getDefault('dart2js') == config['dart2js']) { |
| 218 config['dart2js'] = |
| 219 '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js'; |
| 220 } |
| 221 if (parser.getDefault('dart') == config['dart']) { |
| 222 config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart'; |
| 223 } |
| 224 if (parser.getDefault('drt') == config['drt']) { |
| 225 config['drt'] = '$dartsdk${pathSep}chromium${pathSep}DumpRenderTree'; |
| 226 } |
| 227 } |
| 228 |
| 229 config['unittest'] = makePathAbsolute(config['unittest']); |
| 230 config['drt'] = makePathAbsolute(config['drt']); |
| 231 config['dart'] = makePathAbsolute(config['dart']); |
| 232 config['dart2js'] = makePathAbsolute(config['dart2js']); |
| 233 config['runnerDir'] = runnerDirectory; |
| 234 } |
| 235 |
| 282 main() { | 236 main() { |
| 283 var optionsParser = getOptionParser(); | 237 var optionsParser = getOptionParser(); |
| 284 var options = loadConfiguration(optionsParser); | 238 var options = loadConfiguration(optionsParser); |
| 285 if (isSane(options)) { | 239 if (isSane(options)) { |
| 286 if (options['list-options']) { | 240 if (options['list-options']) { |
| 287 printOptions(optionsParser, options, false, stdout); | 241 printOptions(optionsParser, options, false, stdout); |
| 288 } else if (options['list-all-options']) { | 242 } else if (options['list-all-options']) { |
| 289 printOptions(optionsParser, options, true, stdout); | 243 printOptions(optionsParser, options, true, stdout); |
| 290 } else { | 244 } else { |
| 291 config = new Configuration(optionsParser, options); | 245 var config = new Map(); |
| 292 // Build the command templates needed for test compile and execute. | 246 for (var option in options.options) { |
| 293 var pipelineTemplate = getPipelineTemplate(config.runtime, | 247 config[option] = options[option]; |
| 294 config.checkedMode, | 248 } |
| 295 config.keepTests); | 249 var rest = []; |
| 296 if (pipelineTemplate != null) { | 250 // Process the remmaining command line args. If they look like |
| 297 // Build the list of tests and then execute them. | 251 // options then split them up and add them to the map; they may be for |
| 298 List dirs = options.rest; | 252 // custom pipelines. |
| 299 if (dirs.length == 0) { | 253 for (var other in options.rest) { |
| 300 dirs.add('.'); // Use current working directory as default. | 254 var idx; |
| 255 if (other.startsWith('--') && (idx = other.indexOf('=')) > 0) { |
| 256 var optName = other.substring(2, idx); |
| 257 var optValue = other.substring(idx+1); |
| 258 config[optName] = optValue; |
| 259 } else { |
| 260 rest.add(other); |
| 301 } | 261 } |
| 302 buildFileList(dirs, | |
| 303 new RegExp(options['test-file-pattern']), options['recurse'], | |
| 304 (f) => processTests(pipelineTemplate, f)); | |
| 305 } | 262 } |
| 263 |
| 264 sanitizeConfig(config, optionsParser); |
| 265 |
| 266 // Build the list of tests and then execute them. |
| 267 List dirs = rest; |
| 268 if (dirs.length == 0) { |
| 269 dirs.add('.'); // Use current working directory as default. |
| 270 } |
| 271 buildFileList(dirs, |
| 272 new RegExp(options['test-file-pattern']), options['recurse'], |
| 273 (f) => processTests(config, f)); |
| 306 } | 274 } |
| 307 } | 275 } |
| 308 } | 276 } |
| 309 | 277 |
| 310 | 278 |
| OLD | NEW |