| Index: utils/testrunner/testrunner.dart
|
| ===================================================================
|
| --- utils/testrunner/testrunner.dart (revision 21957)
|
| +++ utils/testrunner/testrunner.dart (working copy)
|
| @@ -10,7 +10,7 @@
|
| * of the removed features are:
|
| *
|
| * - No support for test.status files. The assumption is that tests are
|
| - * expected to pass.
|
| + * expected to pass. Status file support will be added in the future.
|
| * - A restricted set of runtimes. The assumption here is that the Dart
|
| * libraries deal with platform dependencies, and so the primary
|
| * SKUs that a user of this app would be concerned with would be
|
| @@ -43,7 +43,7 @@
|
| * vm - run native Dart in the VM; i.e. using $DARTSDK/dart-sdk/bin/dart.
|
| * drt-dart - run native Dart in DumpRenderTree, the headless version of
|
| * Dartium, which is located in $DARTSDK/chromium/DumpRenderTree, if
|
| - * you intsalled the SDK that is bundled with the editor, or available
|
| + * you installed the SDK that is bundled with the editor, or available
|
| * from http://gsdview.appspot.com/dartium-archive/continuous/
|
| * otherwise.
|
| *
|
| @@ -67,10 +67,27 @@
|
| * which is run in an isolate. The `--pipeline` argument can be used to
|
| * specify a different script for running a test file pipeline, allowing
|
| * customization of the pipeline.
|
| + *
|
| + * Wrapper files are created for tests in the tmp directory, which can be
|
| + * overridden with --tempdir. These files are not removed after the tests
|
| + * are complete, primarily to reduce the amount of times pub must be
|
| + * executed. You can use --clean-files to force file cleanup. The temp
|
| + * directories will have pubspec.yaml files auto-generated unless the
|
| + * original test file directories have such files; in that case the existing
|
| + * files will be copied. Whenever a new pubspec file is copied or
|
| + * created pub will be run (but not otherwise - so if you want to do
|
| + * the equivelent of pub update you should use --clean-files and the rerun
|
| + * the tests).
|
| + *
|
| + * TODO(gram): if the user has a pubspec.yaml file, we should inspect the
|
| + * pubspec.lock file and give useful errors:
|
| + * - if the lock file doesn't exit, then run pub install
|
| + * - if it exists and it doesn't have the required packages (unittest or
|
| + * browser), ask the user to add them and run pub install again.
|
| */
|
|
|
| -// TODO - layout tests that use PNGs rather than DRT text render dumps.
|
| library testrunner;
|
| +import 'dart:async';
|
| import 'dart:io';
|
| import 'dart:isolate';
|
| import 'dart:math';
|
| @@ -91,18 +108,24 @@
|
| /** The index of the next pipeline runner to execute. */
|
| int _nextTask;
|
|
|
| -/** The stream to use for high-value messages, like test results. */
|
| -OutputStream _outStream;
|
| +/** The sink to use for high-value messages, like test results. */
|
| +IOSink _outSink;
|
|
|
| -/** The stream to use for low-value messages, like verbose output. */
|
| -OutputStream _logStream;
|
| +/** The sink to use for low-value messages, like verbose output. */
|
| +IOSink _logSink;
|
|
|
| /**
|
| + * The last temp test directory we accessed; we use this to know if we
|
| + * need to check the pub configuration.
|
| + */
|
| +String _testDir;
|
| +
|
| +/**
|
| * The user can specify output streams on the command line, using 'none',
|
| - * 'stdout', 'stderr', or a file path; [getStream] will take such a name
|
| - * and return an appropriate [OutputStream].
|
| + * 'stdout', 'stderr', or a file path; [getSink] will take such a name
|
| + * and return an appropriate [IOSink].
|
| */
|
| -OutputStream getStream(String name) {
|
| +IOSink getSink(String name) {
|
| if (name == null || name == 'none') {
|
| return null;
|
| }
|
| @@ -112,7 +135,8 @@
|
| if (name == 'stderr') {
|
| return stderr;
|
| }
|
| - return new File(name).openOutputStream(FileMode.WRITE);
|
| + var f = new File(name);
|
| + return f.openWrite();
|
| }
|
|
|
| /**
|
| @@ -120,13 +144,13 @@
|
| * and execute pipelines for the files.
|
| */
|
| void processTests(Map config, List testFiles) {
|
| - _outStream = getStream(config['out']);
|
| - _logStream = getStream(config['log']);
|
| + _outSink = getSink(config['out']);
|
| + _logSink = getSink(config['log']);
|
| if (config['list-files']) {
|
| - if (_outStream != null) {
|
| + if (_outSink != null) {
|
| for (var i = 0; i < testFiles.length; i++) {
|
| - _outStream.writeString(testFiles[i]);
|
| - _outStream.writeString('\n');
|
| + _outSink.write(testFiles[i]);
|
| + _outSink.write('\n');
|
| }
|
| }
|
| } else {
|
| @@ -137,6 +161,69 @@
|
| }
|
| }
|
|
|
| +/**
|
| + * Create or update a pubspec for the target test directory. We use the
|
| + * source directory pubspec if available; otherwise we create a minimal one.
|
| + * We return a Future if we are running pub install, or null otherwise.
|
| + */
|
| +Future doPubConfig(Path sourcePath, String sourceDir,
|
| + Path targetPath, String targetDir,
|
| + String pub, String runtime) {
|
| + // Make sure the target directory exists.
|
| + var d = new Directory(targetDir);
|
| + if (!d.existsSync()) {
|
| + d.createSync(recursive: true);
|
| + }
|
| +
|
| + // If the source has no pubspec, but the dest does, leave
|
| + // things as they are. If neither do, create one in dest.
|
| +
|
| + var sourcePubSpecName = new Path(sourceDir).append("pubspec.yaml").
|
| + toNativePath();
|
| + var targetPubSpecName = new Path(targetDir).append("pubspec.yaml").
|
| + toNativePath();
|
| + var sourcePubSpec = new File(sourcePubSpecName);
|
| + var targetPubSpec = new File(targetPubSpecName);
|
| +
|
| + if (!sourcePubSpec.existsSync()) {
|
| + if (targetPubSpec.existsSync()) {
|
| + return null;
|
| + } else {
|
| + // Create one.
|
| + if (runtime == 'vm') {
|
| + writeFile(targetPubSpecName,
|
| + "name: testrunner\ndependencies:\n unittest: any\n");
|
| + } else {
|
| + writeFile(targetPubSpecName,
|
| + "name: testrunner\ndependencies:\n unittest: any\n browser: any\n");
|
| + }
|
| + }
|
| + } else {
|
| + if (targetPubSpec.existsSync()) {
|
| + // If there is a source one, and it is older than the target,
|
| + // leave the target as is.
|
| + if (sourcePubSpec.lastModifiedSync().millisecondsSinceEpoch <
|
| + targetPubSpec.lastModifiedSync().millisecondsSinceEpoch) {
|
| + return null;
|
| + }
|
| + }
|
| + // Source exists and is newer than target or there is no target;
|
| + // copy the source to the target. If there is a pubspec.lock file,
|
| + // copy that too.
|
| + var s = sourcePubSpec.readAsStringSync();
|
| + targetPubSpec.writeAsStringSync(s);
|
| + var sourcePubLock = new File(sourcePubSpecName.replaceAll(".yaml", ".lock"));
|
| + if (sourcePubLock.existsSync()) {
|
| + var targetPubLock =
|
| + new File(targetPubSpecName.replaceAll(".yaml", ".lock"));
|
| + s = sourcePubLock.readAsStringSync();
|
| + targetPubLock.writeAsStringSync(s);
|
| + }
|
| + }
|
| + // A new target pubspec was created so run pub install.
|
| + return _processHelper(pub, [ 'install' ], workingDir: targetDir);
|
| +}
|
| +
|
| /** Execute as many tasks as possible up to the maxTasks limit. */
|
| void spawnTasks(Map config, List testFiles) {
|
| var verbose = config['verbose'];
|
| @@ -154,12 +241,12 @@
|
| List stderr = msg[1];
|
| List log = msg[2];
|
| int exitCode = msg[3];
|
| - writelog(stdout, _outStream, _logStream, verbose, skipNonVerbose);
|
| - writelog(stderr, _outStream, _logStream, true, skipNonVerbose);
|
| - writelog(log, _outStream, _logStream, verbose, skipNonVerbose);
|
| + writelog(stdout, _outSink, _logSink, verbose, skipNonVerbose);
|
| + writelog(stderr, _outSink, _logSink, true, skipNonVerbose);
|
| + writelog(log, _outSink, _logSink, verbose, skipNonVerbose);
|
| port.close();
|
| --_numTasks;
|
| - if (exitCode == 0 || !config['stopOnFailure']) {
|
| + if (exitCode == 0 || !config['stop-on-failure']) {
|
| spawnTasks(config, testFiles);
|
| }
|
| if (_numTasks == 0) {
|
| @@ -168,74 +255,129 @@
|
| }
|
| });
|
| SendPort s = spawnUri(config['pipeline']);
|
| - s.send(config, port.toSendPort());
|
| +
|
| + // Get the names of the source and target test files and containing
|
| + // directories.
|
| + var testPath = new Path(testfile);
|
| + var sourcePath = testPath.directoryPath;
|
| + var sourceDir = sourcePath.toNativePath();
|
| +
|
| + var targetPath = new Path(config["tempdir"]);
|
| + var normalizedTarget = testPath.directoryPath.toNativePath()
|
| + .replaceAll(Platform.pathSeparator, '_')
|
| + .replaceAll(':', '_');
|
| + targetPath = targetPath.append("${normalizedTarget}_${config['runtime']}");
|
| + var targetDir = targetPath.toNativePath();
|
| +
|
| + config['targetDir'] = targetDir;
|
| + // If this is a new target dir, we need to redo the pub check.
|
| + var f = null;
|
| + if (targetDir != _testDir) {
|
| + f = doPubConfig(sourcePath, sourceDir, targetPath, targetDir,
|
| + config['pub'], config['runtime']);
|
| + _testDir = targetDir;
|
| + }
|
| + if (f == null) {
|
| + s.send(config, port.toSendPort());
|
| + } else {
|
| + f.then((_) {
|
| + s.send(config, port.toSendPort());
|
| + });
|
| + break; // Don't do any more until pub is done.
|
| + }
|
| }
|
| }
|
|
|
| /**
|
| * Our tests are configured so that critical messages have a '###' prefix.
|
| - * [writeLog] takes the output from a pipeline execution and writes it to
|
| - * our output streams. It will strip the '###' if necessary on critical
|
| + * [writelog] takes the output from a pipeline execution and writes it to
|
| + * our output sinks. It will strip the '###' if necessary on critical
|
| * messages; other messages will only be written if verbose output was
|
| * specified.
|
| */
|
| -void writelog(List messages, OutputStream out, OutputStream log,
|
| +void writelog(List messages, IOSink out, IOSink log,
|
| bool includeVerbose, bool skipNonVerbose) {
|
| for (var i = 0; i < messages.length; i++) {
|
| var msg = messages[i];
|
| if (msg.startsWith('###')) {
|
| if (!skipNonVerbose && out != null) {
|
| - out.writeString(msg.substring(3));
|
| - out.writeString('\n');
|
| + out.write(msg.substring(3));
|
| + out.write('\n');
|
| }
|
| } else if (msg.startsWith('CONSOLE MESSAGE:')) {
|
| if (!skipNonVerbose && out != null) {
|
| int idx = msg.indexOf('###');
|
| if (idx > 0) {
|
| - out.writeString(msg.substring(idx + 3));
|
| - out.writeString('\n');
|
| + out.write(msg.substring(idx + 3));
|
| + out.write('\n');
|
| }
|
| }
|
| } else if (includeVerbose && log != null) {
|
| - log.writeString(msg);
|
| - log.writeString('\n');
|
| + log.write(msg);
|
| + log.write('\n');
|
| }
|
| }
|
| }
|
|
|
| -sanitizeConfig(Map config, ArgParser parser) {
|
| +normalizeFilter(List filter) {
|
| + // We want the filter to be a quoted string or list of quoted
|
| + // strings.
|
| + for (var i = 0; i < filter.length; i++) {
|
| + var f = filter[i];
|
| + if (f[0] != "'" && f[0] != '"') {
|
| + filter[i] = "'$f'"; // TODO(gram): Quote embedded quotes.
|
| + }
|
| + }
|
| + return filter;
|
| +}
|
| +
|
| +void sanitizeConfig(Map config, ArgParser parser) {
|
| config['layout'] = config['layout-text'] || config['layout-pixel'];
|
| -
|
| - // TODO - check if next three are actually used.
|
| - config['runInBrowser'] = (config['runtime'] != 'vm');
|
| config['verbose'] = (config['log'] != 'none' && !config['list-groups']);
|
| - config['filtering'] = (config['include'].length > 0 ||
|
| - config['exclude'].length > 0);
|
| -
|
| config['timeout'] = int.parse(config['timeout']);
|
| config['tasks'] = int.parse(config['tasks']);
|
|
|
| var dartsdk = config['dartsdk'];
|
| var pathSep = Platform.pathSeparator;
|
|
|
| - if (dartsdk != null) {
|
| - if (parser.getDefault('dart2js') == config['dart2js']) {
|
| - config['dart2js'] =
|
| - '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js';
|
| + if (dartsdk == null) {
|
| + var opt = new Options();
|
| + var runner = opt.executable;
|
| + var idx = runner.indexOf('dart-sdk');
|
| + if (idx < 0) {
|
| + print("Please use --dartsdk option or run using the dart executable "
|
| + "from the Dart SDK");
|
| + exit(0);
|
| }
|
| - if (parser.getDefault('dart') == config['dart']) {
|
| - config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart';
|
| - }
|
| - if (parser.getDefault('drt') == config['drt']) {
|
| - config['drt'] = '$dartsdk${pathSep}chromium${pathSep}DumpRenderTree';
|
| - }
|
| + dartsdk = runner.substring(0, idx);
|
| }
|
| + if (Platform.operatingSystem == 'macos') {
|
| + config['dart2js'] =
|
| + '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js';
|
| + config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart';
|
| + config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub';
|
| + config['drt'] =
|
| + '$dartsdk/chromium/DumpRenderTree.app/Contents/MacOS/DumpRenderTree';
|
| + } else if (Platform.operatingSystem == 'linux') {
|
| + config['dart2js'] =
|
| + '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js';
|
| + config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart';
|
| + config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub';
|
| + config['drt'] = '$dartsdk${pathSep}chromium${pathSep}DumpRenderTree';
|
| + } else {
|
| + config['dart2js'] =
|
| + '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart2js.bat';
|
| + config['dart'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}dart.exe';
|
| + config['pub'] = '$dartsdk${pathSep}dart-sdk${pathSep}bin${pathSep}pub.bat';
|
| + config['drt'] = '$dartsdk${pathSep}chromium${pathSep}DumpRenderTree.exe';
|
| + }
|
|
|
| - config['unittest'] = makePathAbsolute(config['unittest']);
|
| - config['drt'] = makePathAbsolute(config['drt']);
|
| - config['dart'] = makePathAbsolute(config['dart']);
|
| - config['dart2js'] = makePathAbsolute(config['dart2js']);
|
| + for (var prog in [ 'drt', 'dart', 'pub', 'dart2js' ]) {
|
| + config[prog] = makePathAbsolute(config[prog]);
|
| + }
|
| config['runnerDir'] = runnerDirectory;
|
| + config['include'] = normalizeFilter(config['include']);
|
| + config['exclude'] = normalizeFilter(config['exclude']);
|
| }
|
|
|
| main() {
|
| @@ -245,7 +387,7 @@
|
| if (options['list-options']) {
|
| printOptions(optionsParser, options, false, stdout);
|
| } else if (options['list-all-options']) {
|
| - printOptions(optionsParser, options, true, stdout);
|
| + printOptions(optionsParser, options, true, stdout);
|
| } else {
|
| var config = new Map();
|
| for (var option in options.options) {
|
| @@ -273,9 +415,10 @@
|
| if (dirs.length == 0) {
|
| dirs.add('.'); // Use current working directory as default.
|
| }
|
| - buildFileList(dirs,
|
| - new RegExp(options['test-file-pattern']), options['recurse'],
|
| - (f) => processTests(config, f));
|
| + var f = buildFileList(dirs,
|
| + new RegExp(config['test-file-pattern']), config['recurse']);
|
| + if (config['sort']) f.sort();
|
| + processTests(config, f);
|
| }
|
| }
|
| }
|
|
|