Skip to content

Add in-browser/webdriver hybrid test runner #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 360 additions & 0 deletions bin/test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// TODO(DrMarcII) This is forked from executable.dart in the 'test' package.
// When 'test' supports more extension points, remove/replace this.
library test.executable;

import 'dart:async';
import 'dart:io';

import 'package:args/args.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:yaml/yaml.dart';

import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/runner/reporter/compact.dart';
import 'package:test/src/runner/reporter/expanded.dart';
import 'package:test/src/runner/application_exception.dart';
import 'package:test/src/runner/load_exception.dart';
import 'package:test/src/runner/load_exception_suite.dart';
import 'package:test/src/util/exit_codes.dart' as exit_codes;
import 'package:test/src/util/io.dart';
import 'package:test/src/utils.dart';
import 'package:webdriver/test/loader.dart';

/// The argument parser used to parse the executable arguments.
final _parser = new ArgParser(allowTrailingOptions: true);

/// A merged stream of all signals that tell the test runner to shut down
/// gracefully.
///
/// Signals will only be captured as long as this has an active subscription.
/// Otherwise, they'll be handled by Dart's default signal handler, which
/// terminates the program immediately.
final _signals = Platform.isWindows
? ProcessSignal.SIGINT.watch()
: mergeStreams(
[ProcessSignal.SIGTERM.watch(), ProcessSignal.SIGINT.watch()]);

/// Returns whether the current package has a pubspec which uses the
/// `test/pub_serve` transformer.
bool get _usesTransformer {
if (!new File('pubspec.yaml').existsSync()) return false;
var contents = new File('pubspec.yaml').readAsStringSync();

var yaml;
try {
yaml = loadYaml(contents);
} on FormatException {
return false;
}

if (yaml is! Map) return false;

var transformers = yaml['transformers'];
if (transformers == null) return false;
if (transformers is! List) return false;

return transformers.any((transformer) {
if (transformer is String) return transformer == 'test/pub_serve';
if (transformer is! Map) return false;
if (transformer.keys.length != 1) return false;
return transformer.keys.single == 'test/pub_serve';
});
}

void main(List<String> args) {
var allPlatforms = TestPlatform.all.toList();
allPlatforms.removeWhere((p) => !p.isBrowser);

_parser.addFlag("help",
abbr: "h", negatable: false, help: "Shows this usage information.");
_parser.addFlag("version",
negatable: false, help: "Shows the package's version.");
_parser.addOption("package-root", hide: true);
_parser.addOption("name",
abbr: 'n', help: 'A substring of the name of the test to run.\n'
'Regular expression syntax is supported.');
_parser.addOption("plain-name",
abbr: 'N',
help: 'A plain-text substring of the name of the test to run.');
_parser.addOption("platform",
abbr: 'p',
help: 'The platform on which to run the tests.',
allowed: allPlatforms.map((platform) => platform.identifier).toList(),
defaultsTo: 'chrome');
_parser.addOption("pub-serve",
help: 'The port of a pub serve instance serving "test/".',
hide: !supportsPubServe,
valueHelp: 'port');
_parser.addOption("reporter",
abbr: 'r',
help: 'The runner used to print test results.',
allowed: ['compact', 'expanded'],
defaultsTo: Platform.isWindows ? 'expanded' : 'compact',
allowedHelp: {
'compact': 'A single line, updated continuously.',
'expanded': 'A separate line for each update.'
});
_parser.addFlag("color",
defaultsTo: null,
help: 'Whether to use terminal colors.\n(auto-detected by default)');
_parser.addOption("config-dir",
defaultsTo: 'packages/webdriver/test/configs',
help: 'Directory with browser config JSON files.');

var options;
try {
options = _parser.parse(args);
} on FormatException catch (error) {
_printUsage(error.message);
exitCode = exit_codes.usage;
return;
}

if (options["help"]) {
_printUsage();
return;
}

if (options["version"]) {
if (!_printVersion()) {
stderr.writeln("Couldn't find version number.");
exitCode = exit_codes.data;
}
return;
}

var color = options["color"];
if (color == null) color = canUseSpecialChars;

var pubServeUrl;
if (options["pub-serve"] != null) {
pubServeUrl = Uri.parse("http://localhost:${options['pub-serve']}");
if (!_usesTransformer) {
stderr.write('''
When using --pub-serve, you must include the "test/pub_serve" transformer in
your pubspec:

transformers:
- test/pub_serve:
\$include: test/**_test.dart
''');
exitCode = exit_codes.data;
return;
}
}

var platforms;
if (options["platform"] is List) {
platforms = options["platform"].map(TestPlatform.find);
} else {
platforms = [TestPlatform.find(options["platform"])];
}
var loader = new Loader(platforms,
pubServeUrl: pubServeUrl,
packageRoot: options["package-root"],
color: color,
configDir: options["config-dir"]);

var concurrency = 1;

var paths = options.rest;
if (paths.isEmpty) {
if (!new Directory("test").existsSync()) {
_printUsage('No test files were passed and the default "test/" '
"directory doesn't exist.");
exitCode = exit_codes.data;
return;
}
paths = ["test"];
}

var signalSubscription;
var closed = false;
signalSubscription = _signals.listen((_) {
signalSubscription.cancel();
closed = true;
loader.close();
});

mergeStreams(paths.map((path) {
if (new Directory(path).existsSync()) return loader.loadDir(path);
if (new File(path).existsSync()) return loader.loadFile(path);
return new Stream.fromFuture(new Future.error(
new LoadException(path, 'Does not exist.'), new Trace.current()));
}))
.transform(new StreamTransformer.fromHandlers(
handleError: (error, stackTrace, sink) {
if (error is! LoadException) {
sink.addError(error, stackTrace);
} else {
sink.add(new LoadExceptionSuite(error, stackTrace));
}
})).toList().then((suites) {
if (closed) return null;
suites = flatten(suites);

var pattern;
if (options["name"] != null) {
if (options["plain-name"] != null) {
_printUsage("--name and --plain-name may not both be passed.");
exitCode = exit_codes.data;
return null;
}
pattern = new RegExp(options["name"]);
} else if (options["plain-name"] != null) {
pattern = options["plain-name"];
}

if (pattern != null) {
suites = suites.map((suite) {
// Don't ever filter out load errors.
if (suite is LoadExceptionSuite) return suite;
return suite.change(
tests: suite.tests.where((test) => test.name.contains(pattern)));
}).toList();

if (suites.every((suite) => suite.tests.isEmpty)) {
stderr.write('No tests match ');

if (pattern is RegExp) {
stderr.writeln('regular expression "${pattern.pattern}".');
} else {
stderr.writeln('"$pattern".');
}
exitCode = exit_codes.data;
return null;
}
}

var reporter = options["reporter"] == "compact"
? new CompactReporter(flatten(suites),
concurrency: concurrency, color: color)
: new ExpandedReporter(flatten(suites),
concurrency: concurrency, color: color);

// Override the signal handler to close [reporter]. [loader] will still be
// closed in the [whenComplete] below.
signalSubscription.onData((_) {
signalSubscription.cancel();
closed = true;

// Wait a bit to print this message, since printing it eagerly looks weird
// if the tests then finish immediately.
var timer = new Timer(new Duration(seconds: 1), () {
// Print a blank line first to ensure that this doesn't interfere with
// the compact reporter's unfinished line.
print("");
print("Waiting for current test(s) to finish.");
print("Press Control-C again to terminate immediately.");
});

reporter.close().then((_) => timer.cancel());
});

return reporter.run().then((success) {
exitCode = success ? 0 : 1;
}).whenComplete(() {
signalSubscription.cancel();
return reporter.close();
});
}).whenComplete(signalSubscription.cancel).catchError((error, stackTrace) {
if (error is ApplicationException) {
stderr.writeln(error.message);
exitCode = exit_codes.data;
return;
}

stderr.writeln(getErrorMessage(error));
stderr.writeln(new Trace.from(stackTrace).terse);
stderr.writeln("This is an unexpected error. Please file an issue at "
"http://github.com/dart-lang/test\n"
"with the stack trace and instructions for reproducing the error.");
exitCode = exit_codes.software;
}).whenComplete(() {
return loader.close();
});
}

/// Print usage information for this command.
///
/// If [error] is passed, it's used in place of the usage message and the whole
/// thing is printed to stderr instead of stdout.
void _printUsage([String error]) {
var output = stdout;

var message = "Runs tests in this package.";
if (error != null) {
message = error;
output = stderr;
}

output.write("""$message

Usage: pub run webdriver:test [files or directories...]

${_parser.usage}
""");
}

/// Prints the version number of the test package.
///
/// This loads the version number from the current package's lockfile. It
/// returns true if it successfully printed the version number and false if it
/// couldn't be loaded.
bool _printVersion() {
var lockfile;
try {
lockfile = loadYaml(new File("pubspec.lock").readAsStringSync());
} on FormatException catch (_) {
return false;
} on IOException catch (_) {
return false;
}

if (lockfile is! Map) return false;
var packages = lockfile["packages"];
if (packages is! Map) return false;
var package = packages["test"];
if (package is! Map) return false;

var source = package["source"];
if (source is! String) return false;

switch (source) {
case "hosted":
var version = package["version"];
if (version is! String) return false;

print(version);
return true;

case "git":
var version = package["version"];
if (version is! String) return false;
var description = package["description"];
if (description is! Map) return false;
var ref = description["resolved-ref"];
if (ref is! String) return false;

print("$version (${ref.substring(0, 7)})");
return true;

case "path":
var version = package["version"];
if (version is! String) return false;
var description = package["description"];
if (description is! Map) return false;
var path = description["path"];
if (path is! String) return false;

print("$version (from $path)");
return true;

default:
return false;
}
}
Loading