From fe0b4ce81fdcad810ecd07855afb0893b1257dcd Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum <nweiz@google.com> Date: Wed, 3 Feb 2016 16:07:30 -0800 Subject: [PATCH] Add basic support for a configuration file. This just loads a subset of the config that can be pased on the command line from the configuration file. The command line takes precedence over the config. See #46 R=kevmoo@google.com Review URL: https://codereview.chromium.org//1649663003 . --- README.md | 26 ++ dart_test.yaml | 2 + doc/package_config.md | 116 +++++++ lib/src/executable.dart | 20 ++ lib/src/runner/configuration.dart | 302 ++++++------------ lib/src/runner/configuration/args.dart | 177 ++++++++++ lib/src/runner/configuration/load.dart | 156 +++++++++ lib/src/runner/configuration/values.dart | 22 ++ lib/src/util/io.dart | 6 +- lib/src/utils.dart | 7 + test/io.dart | 10 +- .../configuration/configuration_test.dart | 213 ++++++++++++ .../configuration/top_level_error_test.dart | 161 ++++++++++ test/runner/configuration/top_level_test.dart | 234 ++++++++++++++ test/runner/pub_serve_test.dart | 2 - test/runner/runner_test.dart | 4 +- 16 files changed, 1248 insertions(+), 210 deletions(-) create mode 100644 dart_test.yaml create mode 100644 doc/package_config.md create mode 100644 lib/src/runner/configuration/args.dart create mode 100644 lib/src/runner/configuration/load.dart create mode 100644 lib/src/runner/configuration/values.dart create mode 100644 test/runner/configuration/configuration_test.dart create mode 100644 test/runner/configuration/top_level_error_test.dart create mode 100644 test/runner/configuration/top_level_test.dart diff --git a/README.md b/README.md index beda9ef1..cabbd1f7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ * [Skipping Tests](#skipping-tests) * [Timeouts](#timeouts) * [Platform-Specific Configuration](#platform-specific-configuration) + * [Whole-Package Configuration](#whole-package-configuration) * [Debugging](#debugging) * [Testing with `barback`](#testing-with-barback) * [Further Reading](#further-reading) @@ -486,6 +487,31 @@ If multiple platforms match, the configuration is applied in order from first to last, just as they would in nested groups. This means that for configuration like duration-based timeouts, the last matching value wins. +### Whole-Package Configuration + +For configuration that applies across multiple files, or even the entire +package, `test` supports a configuration file called `dart_test.yaml`. At its +simplest, this file can contain the same sort of configuration that can be +passed as command-line arguments: + +```yaml +# This package's tests are very slow. Double the default timeout. +timeout: 2x + +# This is a browser-only package, so test on content shell by default. +platforms: [content-shell] +``` + +The configuration file sets new defaults. These defaults can still be overridden +by command-line arguments, just like the built-in defaults. In the example +above, you could pass `--platform chrome` to run on Chrome instead of the +Dartium content shell. + +A configuration file can do much more than just set global defaults. See +[the full documentation][package config] for more details. + +[package config]: https://github.com/dart-lang/test/blob/master/doc/package_config.md + ## Debugging Tests can be debugged interactively using browsers' built-in development tools, diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 00000000..1df83980 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,2 @@ +# Terse traces hide frames from test itself, which is bad. +verbose_trace: true diff --git a/doc/package_config.md b/doc/package_config.md new file mode 100644 index 00000000..b0fb74e4 --- /dev/null +++ b/doc/package_config.md @@ -0,0 +1,116 @@ +Each package may include a configuration file that applies to the package as a +whole. This file can be used to provide custom defaults for various options, to +define configuration for multiple files, and more. + +The file is named `dart_test.yaml` and lives at the root of the package, next to +the package's pubspec. Like the pubspec, it's a [YAML][] file. Here's an +example: + +[YAML]: http://yaml.org/ + +```yaml +# This package's tests are very slow. Double the default timeout. +timeout: 2x + +# This is a browser-only package, so test on content shell by default. +platforms: [content-shell] +``` + +* [Config Fields](#config-fields) + +## Config Fields + +These fields directly affect the behavior of the test runner. They go at the +root of the configuration file. + +### `platforms` + +This field indicates which platforms tests should run on by default. It allows +the same paltform identifiers that can be passed to `--platform`. If multiple +platforms are included, the test runner will default to running tests on all of +them. This defaults to `[vm]`. + +```yaml +platforms: [content_shell] + +platforms: +- chrome +- firefox +``` + +### `concurrency` + +This field indicates the default number of test suites to run in parallel. More +parallelism can improve the overall speed of running tests up to a point, but +eventually it just adds more memory overhead without any performance gain. This +defaults to approximately half the number of processors on the current machine. +If it's set to 1, only one test suite will run at a time. + +```yaml +concurrency: 3 +``` + +### `pub_serve` + +This field indicates that the test runner should run against a `pub serve` +instance by default, and provides the port number for that instance. Note that +if there is no `pub serve` instance running at that port, running the tests will +fail by default. + +```yaml +pub_serve: 8081 +``` + +### `timeout` + +This field indicates how much time the test runner should allow a test to remain +inactive before it considers that test to have failed. It has three possible +formats: + +* The string "none" indicates that tests should never time out. + +* A number followed by a unit abbreviation indicates an exact time. For example, + "1m" means a timeout of one minute, and "30s" means a timeout of thirty + seconds. Multiple numbers can be combined, as in "1m 30s". + +* A number followed by "x" indicates a multiple. This is applied to the default + value of 30s. + +```yaml +timeout: 1m +``` + +### `reporter` + +This field indicates the default reporter to use. It may be set to "compact", +"expanded", or "json" (although why anyone would want to default to JSON is +beyond me). It defaults to "expanded" on Windows and "compact" everywhere else. + +```yaml +reporter: expanded +``` + +### `verbose_trace` + +This boolean field controls whether or not stack traces caused by errors are +trimmed to remove internal stack frames. This includes frames from the Dart core +libraries, the [`stack_trace`][stack_trace] package, and the `test` package +itself. It defaults to `false`. + +[stack_trace]: https://pub.dartlang.org/packages/stack_trace + +```yaml +verbose_trace: true +``` + +### `js_trace` + +This boolean field controls whether or not stack traces caused by errors that +occur while running Dart compiled to JS are converted back to Dart style. This +conversion uses the source map generated by `dart2js` to approximate the +original Dart line, column, and in some cases member name for each stack frame. +It defaults to `false`. + +```yaml +js_trace: true +``` diff --git a/lib/src/executable.dart b/lib/src/executable.dart index fa9e2f19..8dd546ec 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -7,6 +7,7 @@ // bin. import 'dart:io'; +import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:yaml/yaml.dart'; @@ -84,6 +85,25 @@ main(List<String> args) async { return; } + try { + if (new File("dart_test.yaml").existsSync()) { + var fileConfiguration = new Configuration.load("dart_test.yaml"); + configuration = fileConfiguration.merge(configuration); + } + } on SourceSpanFormatException catch (error) { + stderr.writeln(error.toString(color: configuration.color)); + exitCode = exit_codes.data; + return; + } on FormatException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.data; + return; + } on IOException catch (error) { + stderr.writeln(error.toString()); + exitCode = exit_codes.noInput; + return; + } + if (configuration.pubServeUrl != null && !_usesTransformer) { stderr.write(''' When using --pub-serve, you must include the "test/pub_serve" transformer in diff --git a/lib/src/runner/configuration.dart b/lib/src/runner/configuration.dart index b9e84945..28f42788 100644 --- a/lib/src/runner/configuration.dart +++ b/lib/src/runner/configuration.dart @@ -3,165 +3,99 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:io'; -import 'dart:math' as math; -import 'package:args/args.dart'; import 'package:path/path.dart' as p; import '../frontend/timeout.dart'; import '../backend/metadata.dart'; import '../backend/test_platform.dart'; -import '../utils.dart'; import '../util/io.dart'; - -/// The default number of test suites to run at once. -/// -/// This defaults to half the available processors, since presumably some of -/// them will be used for the OS and other processes. -final _defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2); +import 'configuration/args.dart' as args; +import 'configuration/load.dart'; +import 'configuration/values.dart'; /// A class that encapsulates the command-line configuration of the test runner. class Configuration { - /// The parser used to parse the command-line arguments. - static final ArgParser _parser = (() { - var parser = new ArgParser(allowTrailingOptions: true); - - var allPlatforms = TestPlatform.all.toList(); - if (!Platform.isMacOS) allPlatforms.remove(TestPlatform.safari); - if (!Platform.isWindows) allPlatforms.remove(TestPlatform.internetExplorer); - - 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.addSeparator("======== Selecting Tests"); - 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.'); - // TODO(nweiz): Support the full platform-selector syntax for choosing which - // tags to run. In the shorter term, disallow non-"identifier" tags. - parser.addOption("tags", - abbr: 't', - help: 'Run only tests with all of the specified tags.', - allowMultiple: true); - parser.addOption("tag", hide: true, allowMultiple: true); - parser.addOption("exclude-tags", - abbr: 'x', - help: "Don't run tests with any of the specified tags.", - allowMultiple: true); - parser.addOption("exclude-tag", hide: true, allowMultiple: true); - - parser.addSeparator("======== Running Tests"); - parser.addOption("platform", - abbr: 'p', - help: 'The platform(s) on which to run the tests.', - allowed: allPlatforms.map((platform) => platform.identifier).toList(), - defaultsTo: 'vm', - allowMultiple: true); - parser.addOption("concurrency", - abbr: 'j', - help: 'The number of concurrent test suites run.\n' - '(defaults to $_defaultConcurrency)', - valueHelp: 'threads'); - parser.addOption("pub-serve", - help: 'The port of a pub serve instance serving "test/".', - valueHelp: 'port'); - - // Note: although we list the 30s default timeout as though it were a - // default value for this argument, it's actually encoded in the [Invoker]'s - // call to [Timeout.apply]. - parser.addOption("timeout", - help: 'The default test timeout. For example: 15s, 2x, none\n' - '(defaults to 30s)'); - parser.addFlag("pause-after-load", - help: 'Pauses for debugging before any tests execute.\n' - 'Implies --concurrency=1 and --timeout=none.\n' - 'Currently only supported for browser tests.', - negatable: false); - - parser.addSeparator("======== Output"); - parser.addOption("reporter", - abbr: 'r', - help: 'The runner used to print test results.', - allowed: ['compact', 'expanded', 'json'], - defaultsTo: Platform.isWindows ? 'expanded' : 'compact', - allowedHelp: { - 'compact': 'A single line, updated continuously.', - 'expanded': 'A separate line for each update.', - 'json': 'A machine-readable format (see https://goo.gl/0HRhdZ).' - }); - parser.addFlag("verbose-trace", negatable: false, - help: 'Whether to emit stack traces with core library frames.'); - parser.addFlag("js-trace", negatable: false, - help: 'Whether to emit raw JavaScript stack traces for browser tests.'); - parser.addFlag("color", defaultsTo: null, - help: 'Whether to use terminal colors.\n(auto-detected by default)'); - - return parser; - })(); - /// The usage string for the command-line arguments. - static String get usage => _parser.usage; + static String get usage => args.usage; /// Whether `--help` was passed. - final bool help; + bool get help => _help ?? false; + final bool _help; /// Whether `--version` was passed. - final bool version; + bool get version => _version ?? false; + final bool _version; /// Whether stack traces should be presented as-is or folded to remove /// irrelevant packages. - final bool verboseTrace; + bool get verboseTrace => _verboseTrace ?? false; + final bool _verboseTrace; /// Whether JavaScript stack traces should be left as-is or converted to /// Dart-like traces. - final bool jsTrace; + bool get jsTrace => _jsTrace ?? false; + final bool _jsTrace; /// Whether to pause for debugging after loading each test suite. - final bool pauseAfterLoad; + bool get pauseAfterLoad => _pauseAfterLoad ?? false; + final bool _pauseAfterLoad; /// The package root for resolving "package:" URLs. - final String packageRoot; + String get packageRoot => _packageRoot ?? p.join(p.current, 'packages'); + final String _packageRoot; /// The name of the reporter to use to display results. - final String reporter; + String get reporter => _reporter ?? defaultReporter; + final String _reporter; /// The URL for the `pub serve` instance from which to load tests, or `null` /// if tests should be loaded from the filesystem. final Uri pubServeUrl; /// The default test timeout. + /// + /// When [merge]d, this combines with the other configuration's timeout using + /// [Timeout.merge]. final Timeout timeout; /// Whether to use command-line color escapes. - final bool color; + bool get color => _color ?? canUseSpecialChars; + final bool _color; /// How many tests to run concurrently. - final int concurrency; + int get concurrency => + pauseAfterLoad ? 1 : (_concurrency ?? defaultConcurrency); + final int _concurrency; - /// The from which to load tests. - final List<String> paths; + /// The paths from which to load tests. + List<String> get paths => _paths ?? ["test"]; + final List<String> _paths; /// Whether the load paths were passed explicitly or the default was used. - final bool explicitPaths; + bool get explicitPaths => _paths != null; /// The pattern to match against test names to decide which to run, or `null` /// if all tests should be run. final Pattern pattern; /// The set of platforms on which to run tests. - final List<TestPlatform> platforms; + List<TestPlatform> get platforms => _platforms ?? [TestPlatform.vm]; + final List<TestPlatform> _platforms; - /// Restricts the set of tests to a set of tags + /// Restricts the set of tests to a set of tags. + /// + /// If this is empty, it applies no restrictions. + /// + /// When [merge]d, this is unioned with the other configuration's tags. final Set<String> tags; - /// Does not run tests with tags from this set + /// Does not run tests with tags from this set. + /// + /// If this is empty, it applies no restrictions. + /// + /// When [merge]d, this is unioned with the other configuration's excluded + /// tags. final Set<String> excludeTags; /// The global test metadata derived from this configuration. @@ -171,100 +105,70 @@ class Configuration { /// Parses the configuration from [args]. /// /// Throws a [FormatException] if [args] are invalid. - factory Configuration.parse(List<String> args) { - var options = _parser.parse(args); - - var pattern; - if (options['name'] != null) { - if (options["plain-name"] != null) { - throw new FormatException( - "--name and --plain-name may not both be passed."); - } + factory Configuration.parse(List<String> arguments) => args.parse(arguments); - pattern = _wrapFormatException( - options, 'name', (value) => new RegExp(value)); - } else if (options['plain-name'] != null) { - pattern = options['plain-name']; - } - - var tags = new Set(); - tags.addAll(options['tags'] ?? []); - tags.addAll(options['tag'] ?? []); - - var excludeTags = new Set(); - excludeTags.addAll(options['exclude-tags'] ?? []); - excludeTags.addAll(options['exclude-tag'] ?? []); - - var tagIntersection = tags.intersection(excludeTags); - if (tagIntersection.isNotEmpty) { - throw new FormatException( - 'The ${pluralize('tag', tagIntersection.length)} ' - '${toSentence(tagIntersection)} ' - '${pluralize('was', tagIntersection.length, plural: 'were')} ' - 'both included and excluded.'); - } - - return new Configuration( - help: options['help'], - version: options['version'], - verboseTrace: options['verbose-trace'], - jsTrace: options['js-trace'], - pauseAfterLoad: options['pause-after-load'], - color: options['color'], - packageRoot: options['package-root'], - reporter: options['reporter'], - pubServePort: _wrapFormatException(options, 'pub-serve', int.parse), - concurrency: _wrapFormatException(options, 'concurrency', int.parse, - orElse: () => _defaultConcurrency), - timeout: _wrapFormatException(options, 'timeout', - (value) => new Timeout.parse(value), - orElse: () => new Timeout.factor(1)), - pattern: pattern, - platforms: options['platform'].map(TestPlatform.find), - paths: options.rest.isEmpty ? null : options.rest, - tags: tags, - excludeTags: excludeTags); - } - - /// Runs [parse] on the value of the option [name], and wraps any - /// [FormatException] it throws with additional information. - static _wrapFormatException(ArgResults options, String name, parse(value), - {orElse()}) { - var value = options[name]; - if (value == null) return orElse == null ? null : orElse(); - - try { - return parse(value); - } on FormatException catch (error) { - throw new FormatException('Couldn\'t parse --$name "${options[name]}": ' - '${error.message}'); - } - } - - Configuration({this.help: false, this.version: false, - this.verboseTrace: false, this.jsTrace: false, - bool pauseAfterLoad: false, bool color, String packageRoot, - String reporter, int pubServePort, int concurrency, Timeout timeout, - this.pattern, Iterable<TestPlatform> platforms, - Iterable<String> paths, Set<String> tags, Set<String> excludeTags}) - : pauseAfterLoad = pauseAfterLoad, - color = color == null ? canUseSpecialChars : color, - packageRoot = packageRoot == null - ? p.join(p.current, 'packages') - : packageRoot, - reporter = reporter == null ? 'compact' : reporter, + /// Loads the configuration from [path]. + /// + /// Throws an [IOException] if [path] does not exist or cannot be read. Throws + /// a [FormatException] if its contents are invalid. + factory Configuration.load(String path) => load(path); + + Configuration({bool help, bool version, bool verboseTrace, bool jsTrace, + bool pauseAfterLoad, bool color, String packageRoot, String reporter, + int pubServePort, int concurrency, Timeout timeout, this.pattern, + Iterable<TestPlatform> platforms, Iterable<String> paths, + Iterable<String> tags, Iterable<String> excludeTags}) + : _help = help, + _version = version, + _verboseTrace = verboseTrace, + _jsTrace = jsTrace, + _pauseAfterLoad = pauseAfterLoad, + _color = color, + _packageRoot = packageRoot, + _reporter = reporter, pubServeUrl = pubServePort == null ? null : Uri.parse("http://localhost:$pubServePort"), - concurrency = pauseAfterLoad - ? 1 - : (concurrency == null ? _defaultConcurrency : concurrency), - timeout = pauseAfterLoad + _concurrency = concurrency, + timeout = (pauseAfterLoad ?? false) ? Timeout.none : (timeout == null ? new Timeout.factor(1) : timeout), - platforms = platforms == null ? [TestPlatform.vm] : platforms.toList(), - paths = paths == null ? ["test"] : paths.toList(), - explicitPaths = paths != null, - this.tags = tags, - this.excludeTags = excludeTags; + _platforms = _list(platforms), + _paths = _list(paths), + tags = tags?.toSet() ?? new Set(), + excludeTags = excludeTags?.toSet() ?? new Set(); + + /// Returns a [input] as a list or `null`. + /// + /// If [input] is `null` or empty, this returns `null`. Otherwise, it returns + /// `input.toList()`. + static List _list(Iterable input) { + if (input == null) return null; + input = input.toList(); + if (input.isEmpty) return null; + return input; + } + + /// Merges this with [other]. + /// + /// For most fields, if both configurations have values set, [other]'s value + /// takes precedence. However, certain fields are merged together instead. + /// This is indicated in those fields' documentation. + Configuration merge(Configuration other) => new Configuration( + help: other._help ?? _help, + version: other._version ?? _version, + verboseTrace: other._verboseTrace ?? _verboseTrace, + jsTrace: other._jsTrace ?? _jsTrace, + pauseAfterLoad: other._pauseAfterLoad ?? _pauseAfterLoad, + color: other._color ?? _color, + packageRoot: other._packageRoot ?? _packageRoot, + reporter: other._reporter ?? _reporter, + pubServePort: (other.pubServeUrl ?? pubServeUrl)?.port, + concurrency: other._concurrency ?? _concurrency, + timeout: timeout.merge(other.timeout), + pattern: other.pattern ?? pattern, + platforms: other._platforms ?? _platforms, + paths: other._paths ?? _paths, + tags: other.tags.union(tags), + excludeTags: other.excludeTags.union(excludeTags)); } diff --git a/lib/src/runner/configuration/args.dart b/lib/src/runner/configuration/args.dart new file mode 100644 index 00000000..47df545a --- /dev/null +++ b/lib/src/runner/configuration/args.dart @@ -0,0 +1,177 @@ +// Copyright (c) 2016, 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. + +import 'dart:io'; + +import 'package:args/args.dart'; + +import '../../frontend/timeout.dart'; +import '../../backend/test_platform.dart'; +import '../../utils.dart'; +import '../configuration.dart'; +import 'values.dart'; + +/// The parser used to parse the command-line arguments. +final ArgParser _parser = (() { + var parser = new ArgParser(allowTrailingOptions: true); + + var allPlatforms = TestPlatform.all.toList(); + if (!Platform.isMacOS) allPlatforms.remove(TestPlatform.safari); + if (!Platform.isWindows) allPlatforms.remove(TestPlatform.internetExplorer); + + 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); + + // Note that defaultsTo declarations here are only for documentation purposes. + // We pass null values rather than defaults to [new Configuration] so that it + // merges properly with the config file. + + parser.addSeparator("======== Selecting Tests"); + 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.'); + // TODO(nweiz): Support the full platform-selector syntax for choosing which + // tags to run. In the shorter term, disallow non-"identifier" tags. + parser.addOption("tags", + abbr: 't', + help: 'Run only tests with all of the specified tags.', + allowMultiple: true); + parser.addOption("tag", hide: true, allowMultiple: true); + parser.addOption("exclude-tags", + abbr: 'x', + help: "Don't run tests with any of the specified tags.", + allowMultiple: true); + parser.addOption("exclude-tag", hide: true, allowMultiple: true); + + parser.addSeparator("======== Running Tests"); + parser.addOption("platform", + abbr: 'p', + help: 'The platform(s) on which to run the tests.', + defaultsTo: 'vm', + allowed: allPlatforms.map((platform) => platform.identifier).toList(), + allowMultiple: true); + parser.addOption("concurrency", + abbr: 'j', + help: 'The number of concurrent test suites run.', + defaultsTo: defaultConcurrency.toString(), + valueHelp: 'threads'); + parser.addOption("pub-serve", + help: 'The port of a pub serve instance serving "test/".', + valueHelp: 'port'); + parser.addOption("timeout", + help: 'The default test timeout. For example: 15s, 2x, none', + defaultsTo: '30s'); + parser.addFlag("pause-after-load", + help: 'Pauses for debugging before any tests execute.\n' + 'Implies --concurrency=1 and --timeout=none.\n' + 'Currently only supported for browser tests.', + negatable: false); + + parser.addSeparator("======== Output"); + parser.addOption("reporter", + abbr: 'r', + help: 'The runner used to print test results.', + defaultsTo: defaultReporter, + allowed: allReporters, + allowedHelp: { + 'compact': 'A single line, updated continuously.', + 'expanded': 'A separate line for each update.', + 'json': 'A machine-readable format (see https://goo.gl/0HRhdZ).' + }); + parser.addFlag("verbose-trace", negatable: false, + help: 'Whether to emit stack traces with core library frames.'); + parser.addFlag("js-trace", negatable: false, + help: 'Whether to emit raw JavaScript stack traces for browser tests.'); + parser.addFlag("color", + help: 'Whether to use terminal colors.\n(auto-detected by default)'); + + return parser; +})(); + +/// The usage string for the command-line arguments. +String get usage => _parser.usage; + +/// Parses the configuration from [args]. +/// +/// Throws a [FormatException] if [args] are invalid. +Configuration parse(List<String> args) { + var options = _parser.parse(args); + + var pattern; + if (options['name'] != null) { + if (options["plain-name"] != null) { + throw new FormatException( + "--name and --plain-name may not both be passed."); + } + + pattern = _wrapFormatException( + options, 'name', (value) => new RegExp(value)); + } else if (options['plain-name'] != null) { + pattern = options['plain-name']; + } + + var tags = new Set(); + tags.addAll(options['tags'] ?? []); + tags.addAll(options['tag'] ?? []); + + var excludeTags = new Set(); + excludeTags.addAll(options['exclude-tags'] ?? []); + excludeTags.addAll(options['exclude-tag'] ?? []); + + var tagIntersection = tags.intersection(excludeTags); + if (tagIntersection.isNotEmpty) { + throw new FormatException( + 'The ${pluralize('tag', tagIntersection.length)} ' + '${toSentence(tagIntersection)} ' + '${pluralize('was', tagIntersection.length, plural: 'were')} ' + 'both included and excluded.'); + } + + // If the user hasn't explicitly chosen a value, we want to pass null values + // to [new Configuration] so that it considers those fields unset when merging + // with configuration from the config file. + ifParsed(name) => options.wasParsed(name) ? options[name] : null; + + return new Configuration( + help: ifParsed('help'), + version: ifParsed('version'), + verboseTrace: ifParsed('verbose-trace'), + jsTrace: ifParsed('js-trace'), + pauseAfterLoad: ifParsed('pause-after-load'), + color: ifParsed('color'), + packageRoot: ifParsed('package-root'), + reporter: ifParsed('reporter'), + pubServePort: _wrapFormatException(options, 'pub-serve', int.parse), + concurrency: _wrapFormatException(options, 'concurrency', int.parse), + timeout: _wrapFormatException(options, 'timeout', + (value) => new Timeout.parse(value)), + pattern: pattern, + platforms: ifParsed('platform')?.map(TestPlatform.find), + paths: options.rest.isEmpty ? null : options.rest, + tags: tags, + excludeTags: excludeTags); +} + +/// Runs [parse] on the value of the option [name], and wraps any +/// [FormatException] it throws with additional information. +_wrapFormatException(ArgResults options, String name, parse(value)) { + if (!options.wasParsed(name)) return null; + + var value = options[name]; + if (value == null) return null; + + try { + return parse(value); + } on FormatException catch (error) { + throw new FormatException('Couldn\'t parse --$name "${options[name]}": ' + '${error.message}'); + } +} diff --git a/lib/src/runner/configuration/load.dart b/lib/src/runner/configuration/load.dart new file mode 100644 index 00000000..d2def184 --- /dev/null +++ b/lib/src/runner/configuration/load.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2016, 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. + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import '../../utils.dart'; +import '../../frontend/timeout.dart'; +import '../../backend/test_platform.dart'; +import '../configuration.dart'; +import 'values.dart'; + +/// Loads configuration information from a YAML file at [path]. +/// +/// Throws a [FormatException] if the configuration is invalid, and a +/// [FileSystemException] if it can't be read. +Configuration load(String path) { + var source = new File(path).readAsStringSync(); + var document = loadYamlNode(source, sourceUrl: p.toUri(path)); + + if (document.value == null) return new Configuration(); + + if (document is! Map) { + throw new SourceSpanFormatException( + "The configuration must be a YAML map.", document.span, source); + } + + var loader = new _ConfigurationLoader(document, source); + return loader.load(); +} + +/// A helper for [load] that tracks the YAML document. +class _ConfigurationLoader { + /// The parsed configuration document. + final YamlMap _document; + + /// The source string for [_document]. + /// + /// Used for error reporting. + final String _source; + + _ConfigurationLoader(this._document, this._source); + + /// Loads the configuration in [_document]. + Configuration load() { + var verboseTrace = _getBool("verbose_trace"); + var jsTrace = _getBool("js_trace"); + + var reporter = _getString("reporter"); + if (reporter != null && !allReporters.contains(reporter)) { + _error('Unknown reporter "$reporter".', "reporter"); + } + + var pubServePort = _getInt("pub_serve"); + var concurrency = _getInt("concurrency"); + var timeout = _parseValue("timeout", (value) => new Timeout.parse(value)); + + var allPlatformIdentifiers = + TestPlatform.all.map((platform) => platform.identifier).toSet(); + var platforms = _getList("platforms", (platformNode) { + _validate(platformNode, "Platforms must be strings.", + (value) => value is String); + _validate(platformNode, 'Unknown platform "${platformNode.value}".', + allPlatformIdentifiers.contains); + + return TestPlatform.find(platformNode.value); + }); + + // TODO(nweiz): Add support for using globs to define defaults paths to run. + + return new Configuration( + verboseTrace: verboseTrace, + jsTrace: jsTrace, + reporter: reporter, + pubServePort: pubServePort, + concurrency: concurrency, + timeout: timeout, + platforms: platforms); + } + + /// Throws an exception with [message] if [test] returns `false` when passed + /// [node]'s value. + void _validate(YamlNode node, String message, bool test(value)) { + if (test(node.value)) return; + throw new SourceSpanFormatException(message, node.span, _source); + } + + /// Returns the value of the node at [field]. + /// + /// If [typeTest] returns `false` for that value, instead throws an error + /// complaining that the field is not a [typeName]. + _getValue(String field, String typeName, bool typeTest(value)) { + var value = _document[field]; + if (value == null || typeTest(value)) return value; + _error("$field must be ${a(typeName)}.", field); + } + + /// Returns the YAML node at [field]. + /// + /// If [typeTest] returns `false` for that node's value, instead throws an + /// error complaining that the field is not a [typeName]. + YamlNode _getNode(String field, String typeName, bool typeTest(value)) { + var node = _document.nodes[field]; + if (node == null) return null; + _validate(node, "$field must be ${a(typeName)}.", typeTest); + return node; + } + + /// Asserts that [field] is an int and returns its value. + int _getInt(String field) => + _getValue(field, "int", (value) => value is int); + + /// Asserts that [field] is a boolean and returns its value. + bool _getBool(String field) => + _getValue(field, "boolean", (value) => value is bool); + + /// Asserts that [field] is a string and returns its value. + String _getString(String field) => + _getValue(field, "string", (value) => value is String); + + /// Asserts that [field] is a list and runs [forElement] for each element it + /// contains. + /// + /// Returns a list of values returned by [forElement]. + List _getList(String field, forElement(YamlNode elementNode)) { + var node = _getNode(field, "list", (value) => value is List); + if (node == null) return []; + return node.nodes.map(forElement).toList(); + } + + /// Asserts that [field] is a string, passes it to [parse], and returns the + /// result. + /// + /// If [parse] throws a [FormatException], it's wrapped to include [field]'s + /// span. + _parseValue(String field, parse(value)) { + var value = _getString(field); + if (value == null) return null; + + try { + return parse(value); + } on FormatException catch (error) { + _error('Invalid $field: ${error.message}', field); + } + } + + /// Throws a [SourceSpanFormatException] with [message] about [field]. + void _error(String message, String field) { + throw new SourceSpanFormatException( + message, _document.nodes[field].span, _source); + } +} diff --git a/lib/src/runner/configuration/values.dart b/lib/src/runner/configuration/values.dart new file mode 100644 index 00000000..a266f090 --- /dev/null +++ b/lib/src/runner/configuration/values.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2016, 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. + +import 'dart:io'; +import 'dart:math' as math; + +import '../../util/io.dart'; + +/// The default number of test suites to run at once. +/// +/// This defaults to half the available processors, since presumably some of +/// them will be used for the OS and other processes. +final defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2); + +/// The reporters supported by the test runner. +const allReporters = const ["compact", "expanded", "json"]; + +/// The default reporter. +final defaultReporter = inTestTests + ? 'expanded' + : (Platform.isWindows ? 'expanded' : 'compact'); diff --git a/lib/src/util/io.dart b/lib/src/util/io.dart index 4a9f6a82..ab408b60 100644 --- a/lib/src/util/io.dart +++ b/lib/src/util/io.dart @@ -42,6 +42,9 @@ final OperatingSystem currentOS = (() { final stdinLines = new StreamQueue( UTF8.decoder.fuse(const LineSplitter()).bind(stdin)); +/// Whether this is being run as a subprocess in the test package's own tests. +bool inTestTests = Platform.environment["_DART_TEST_TESTING"] == "true"; + /// The root directory below which to nest temporary directories created by the /// test runner. /// @@ -64,8 +67,7 @@ String libDir({String packageRoot}) { /// On Windows or when not printing to a terminal, only printable ASCII /// characters should be used. bool get canUseSpecialChars => - Platform.operatingSystem != 'windows' && - Platform.environment["_UNITTEST_USE_COLOR"] != "false"; + Platform.operatingSystem != 'windows' && !inTestTests; /// Creates a temporary directory and returns its path. String createTempDir() => diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 4b554ee5..74675907 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -34,6 +34,9 @@ final lineSplitter = UTF8.decoder.fuse(const LineSplitter()); /// [Object.toString] values contain. final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): '); +/// A regular expression matching a single vowel. +final _vowel = new RegExp('[aeiou]'); + /// Directories that are specific to OS X. /// /// This is used to try to distinguish OS X and Linux in [currentOSGuess]. @@ -118,6 +121,10 @@ String pluralize(String name, int number, {String plural}) { return '${name}s'; } +/// Returns [noun] with an indefinite article ("a" or "an") added, based on +/// whether its first letter is a vowel. +String a(String noun) => noun.startsWith(_vowel) ? "an $noun" : "a $noun"; + /// Wraps [text] so that it fits within [lineLength], which defaults to 100 /// characters. /// diff --git a/test/io.dart b/test/io.dart index b3477430..1db00ed7 100644 --- a/test/io.dart +++ b/test/io.dart @@ -102,18 +102,18 @@ StreamMatcher containsInOrder(Iterable<String> strings) => ScheduledProcess runTest(List args, {String reporter, int concurrency, Map<String, String> environment, bool forwardStdio: false}) { - reporter ??= "expanded"; concurrency ??= 1; var allArgs = [ p.absolute(p.join(packageDir, 'bin/test.dart')), "--package-root=${p.join(packageDir, 'packages')}", - "--concurrency=$concurrency", - "--reporter=$reporter" - ]..addAll(args); + "--concurrency=$concurrency" + ]; + if (reporter != null) allArgs.add("--reporter=$reporter"); + allArgs.addAll(args); if (environment == null) environment = {}; - environment.putIfAbsent("_UNITTEST_USE_COLOR", () => "false"); + environment.putIfAbsent("_DART_TEST_TESTING", () => "true"); var process = runDart(allArgs, environment: environment, diff --git a/test/runner/configuration/configuration_test.dart b/test/runner/configuration/configuration_test.dart new file mode 100644 index 00000000..1fa22b54 --- /dev/null +++ b/test/runner/configuration/configuration_test.dart @@ -0,0 +1,213 @@ +// Copyright (c) 2016, 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. + +@TestOn("vm") + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/configuration.dart'; +import 'package:test/src/runner/configuration/values.dart'; +import 'package:test/src/util/io.dart'; + +void main() { + group("merge", () { + group("for most fields", () { + test("if neither is defined, preserves the default", () { + var merged = new Configuration().merge(new Configuration()); + expect(merged.help, isFalse); + expect(merged.version, isFalse); + expect(merged.verboseTrace, isFalse); + expect(merged.jsTrace, isFalse); + expect(merged.pauseAfterLoad, isFalse); + expect(merged.color, equals(canUseSpecialChars)); + expect(merged.packageRoot, equals(p.join(p.current, 'packages'))); + expect(merged.reporter, equals(defaultReporter)); + expect(merged.pubServeUrl, isNull); + expect(merged.pattern, isNull); + expect(merged.platforms, equals([TestPlatform.vm])); + expect(merged.paths, equals(["test"])); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = new Configuration( + help: true, + version: true, + verboseTrace: true, + jsTrace: true, + pauseAfterLoad: true, + color: true, + packageRoot: "root", + reporter: "json", + pubServePort: 1234, + pattern: "foo", + platforms: [TestPlatform.chrome], + paths: ["bar"]) + .merge(new Configuration()); + + expect(merged.help, isTrue); + expect(merged.version, isTrue); + expect(merged.verboseTrace, isTrue); + expect(merged.jsTrace, isTrue); + expect(merged.pauseAfterLoad, isTrue); + expect(merged.color, isTrue); + expect(merged.packageRoot, equals("root")); + expect(merged.reporter, equals("json")); + expect(merged.pubServeUrl.port, equals(1234)); + expect(merged.pattern, equals("foo")); + expect(merged.platforms, equals([TestPlatform.chrome])); + expect(merged.paths, equals(["bar"])); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = new Configuration().merge(new Configuration( + help: true, + version: true, + verboseTrace: true, + jsTrace: true, + pauseAfterLoad: true, + color: true, + packageRoot: "root", + reporter: "json", + pubServePort: 1234, + pattern: "foo", + platforms: [TestPlatform.chrome], + paths: ["bar"])); + + expect(merged.help, isTrue); + expect(merged.version, isTrue); + expect(merged.verboseTrace, isTrue); + expect(merged.jsTrace, isTrue); + expect(merged.pauseAfterLoad, isTrue); + expect(merged.color, isTrue); + expect(merged.packageRoot, equals("root")); + expect(merged.reporter, equals("json")); + expect(merged.pubServeUrl.port, equals(1234)); + expect(merged.pattern, equals("foo")); + expect(merged.platforms, equals([TestPlatform.chrome])); + expect(merged.paths, equals(["bar"])); + }); + + test("if the two configurations conflict, uses the new configuration's " + "values", () { + var older = new Configuration( + help: true, + version: false, + verboseTrace: true, + jsTrace: false, + pauseAfterLoad: true, + color: false, + packageRoot: "root", + reporter: "json", + pubServePort: 1234, + pattern: "foo", + platforms: [TestPlatform.chrome], + paths: ["bar"]); + var newer = new Configuration( + help: false, + version: true, + verboseTrace: false, + jsTrace: true, + pauseAfterLoad: false, + color: true, + packageRoot: "boot", + reporter: "compact", + pubServePort: 5678, + pattern: "gonk", + platforms: [TestPlatform.dartium], + paths: ["blech"]); + var merged = older.merge(newer); + + expect(merged.help, isFalse); + expect(merged.version, isTrue); + expect(merged.verboseTrace, isFalse); + expect(merged.jsTrace, isTrue); + expect(merged.pauseAfterLoad, isFalse); + expect(merged.color, isTrue); + expect(merged.packageRoot, equals("boot")); + expect(merged.reporter, equals("compact")); + expect(merged.pubServeUrl.port, equals(5678)); + expect(merged.pattern, equals("gonk")); + expect(merged.platforms, equals([TestPlatform.dartium])); + expect(merged.paths, equals(["blech"])); + }); + }); + + group("for tags", () { + test("if neither is defined, preserves the default", () { + var merged = new Configuration().merge(new Configuration()); + expect(merged.tags, isEmpty); + expect(merged.excludeTags, isEmpty); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = new Configuration( + tags: ["foo", "bar"], + excludeTags: ["baz", "bang"]) + .merge(new Configuration()); + + expect(merged.tags, unorderedEquals(["foo", "bar"])); + expect(merged.excludeTags, unorderedEquals(["baz", "bang"])); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = new Configuration().merge(new Configuration( + tags: ["foo", "bar"], + excludeTags: ["baz", "bang"])); + + expect(merged.tags, unorderedEquals(["foo", "bar"])); + expect(merged.excludeTags, unorderedEquals(["baz", "bang"])); + }); + + test("if both are defined, unions them", () { + var older = new Configuration( + tags: ["foo", "bar"], + excludeTags: ["baz", "bang"]); + var newer = new Configuration( + tags: ["bar", "blip"], + excludeTags: ["bang", "qux"]); + var merged = older.merge(newer); + + expect(merged.tags, unorderedEquals(["foo", "bar", "blip"])); + expect(merged.excludeTags, unorderedEquals(["baz", "bang", "qux"])); + }); + }); + + group("for timeout", () { + test("if neither is defined, preserves the default", () { + var merged = new Configuration().merge(new Configuration()); + expect(merged.timeout, equals(new Timeout.factor(1))); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = new Configuration(timeout: new Timeout.factor(2)) + .merge(new Configuration()); + expect(merged.timeout, equals(new Timeout.factor(2))); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = new Configuration() + .merge(new Configuration(timeout: new Timeout.factor(2))); + expect(merged.timeout, equals(new Timeout.factor(2))); + }); + + test("if both are defined, merges them", () { + var older = new Configuration(timeout: new Timeout.factor(2)); + var newer = new Configuration(timeout: new Timeout.factor(3)); + var merged = older.merge(newer); + expect(merged.timeout, equals(new Timeout.factor(6))); + }); + + test("if the merge conflicts, prefers the new value", () { + var older = new Configuration( + timeout: new Timeout(new Duration(seconds: 1))); + var newer = new Configuration( + timeout: new Timeout(new Duration(seconds: 2))); + var merged = older.merge(newer); + expect(merged.timeout, equals(new Timeout(new Duration(seconds: 2)))); + }); + }); + }); +} diff --git a/test/runner/configuration/top_level_error_test.dart b/test/runner/configuration/top_level_error_test.dart new file mode 100644 index 00000000..84ba460b --- /dev/null +++ b/test/runner/configuration/top_level_error_test.dart @@ -0,0 +1,161 @@ +// Copyright (c) 2016, 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. + +@TestOn("vm") + +import 'dart:convert'; + +import 'package:scheduled_test/descriptor.dart' as d; +import 'package:scheduled_test/scheduled_test.dart'; +import 'package:test/src/util/exit_codes.dart' as exit_codes; + +import '../../io.dart'; + +void main() { + useSandbox(); + + test("rejects an invalid verbose_trace", () { + d.file("dart_test.yaml", JSON.encode({ + "verbose_trace": "flup" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "verbose_trace must be a boolean", + "^^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid js_trace", + () { + d.file("dart_test.yaml", JSON.encode({ + "js_trace": "flup" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "js_trace must be a boolean", + "^^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid reporter type", () { + d.file("dart_test.yaml", JSON.encode({ + "reporter": 12 + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "reporter must be a string", + "^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid reporter name", () { + d.file("dart_test.yaml", JSON.encode({ + "reporter": "non-existent" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + 'Unknown reporter "non-existent"', + "^^^^^^^^^^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid pub serve port", () { + d.file("dart_test.yaml", JSON.encode({ + "pub_serve": "foo" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "pub_serve must be an int", + "^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid concurrency", () { + d.file("dart_test.yaml", JSON.encode({ + "concurrency": "foo" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "concurrency must be an int", + "^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid timeout type", () { + d.file("dart_test.yaml", JSON.encode({ + "timeout": 12 + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "timeout must be a string", + "^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid timeout format", () { + d.file("dart_test.yaml", JSON.encode({ + "timeout": "12p" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "Invalid timeout: expected unit", + "^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid platforms list type", () { + d.file("dart_test.yaml", JSON.encode({ + "platforms": "vm" + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "platforms must be a list", + "^^^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid platforms member type", () { + d.file("dart_test.yaml", JSON.encode({ + "platforms": [12] + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + "Platforms must be strings", + "^^" + ])); + test.shouldExit(exit_codes.data); + }); + + test("rejects an invalid platforms member name", () { + d.file("dart_test.yaml", JSON.encode({ + "platforms": ["foo"] + })).create(); + + var test = runTest(["test.dart"]); + test.stderr.expect(containsInOrder([ + 'Unknown platform "foo"', + "^^^^^" + ])); + test.shouldExit(exit_codes.data); + }); +} diff --git a/test/runner/configuration/top_level_test.dart b/test/runner/configuration/top_level_test.dart new file mode 100644 index 00000000..25104452 --- /dev/null +++ b/test/runner/configuration/top_level_test.dart @@ -0,0 +1,234 @@ +// Copyright (c) 2016, 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. + +@TestOn("vm") + +import 'dart:convert'; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/descriptor.dart' as d; +import 'package:scheduled_test/scheduled_stream.dart'; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../io.dart'; + +void main() { + useSandbox(); + + test("ignores an empty file", () { + d.file("dart_test.yaml", "").create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + """).create(); + + var test = runTest(["test.dart"]); + test.stdout.expect(consumeThrough(contains("+1: All tests passed!"))); + test.shouldExit(0); + }); + + test("includes the full stack with verbose_trace: true", () { + d.file("dart_test.yaml", JSON.encode({ + "verbose_trace": true + })).create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + """).create(); + + var test = runTest(["test.dart"], reporter: "compact"); + test.stdout.expect(consumeThrough(contains("dart:isolate-patch"))); + test.shouldExit(1); + }); + + test("doesn't dartify stack traces for JS-compiled tests with js_trace: true", + () { + d.file("dart_test.yaml", JSON.encode({ + "js_trace": true + })).create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + """).create(); + + var test = runTest(["-p", "chrome", "--verbose-trace", "test.dart"]); + test.stdout.fork().expect(never(endsWith(" main.<fn>"))); + test.stdout.fork().expect(never(contains("package:test"))); + test.stdout.fork().expect(never(contains("dart:async/zone.dart"))); + test.stdout.expect(consumeThrough(contains("-1: Some tests failed."))); + test.shouldExit(1); + }); + + test("uses the specified reporter", () { + d.file("dart_test.yaml", JSON.encode({ + "reporter": "json" + })).create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + """).create(); + + var test = runTest(["test.dart"]); + test.stdout.expect(consumeThrough(contains('"testStart"'))); + test.shouldExit(0); + }); + + test("uses the specified pub serve port", () { + d.file("pubspec.yaml", """ +name: myapp +dependencies: + barback: any + test: {path: ${p.current}} +transformers: +- myapp: + \$include: test/**_test.dart +- test/pub_serve: + \$include: test/**_test.dart +""").create(); + + d.dir("lib", [ + d.file("myapp.dart", """ + import 'package:barback/barback.dart'; + + class MyTransformer extends Transformer { + final allowedExtensions = '.dart'; + + MyTransformer.asPlugin(); + + Future apply(Transform transform) async { + var contents = await transform.primaryInput.readAsString(); + transform.addOutput(new Asset.fromString( + transform.primaryInput.id, + contents.replaceAll("isFalse", "isTrue"))); + } + } + """) + ]).create(); + + runPub(['get']).shouldExit(0); + + d.dir("test", [ + d.file("my_test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () => expect(true, isFalse)); + } + """) + ]).create(); + + var pub = runPubServe(); + + d.async(pubServePort.then((port) { + return d.file("dart_test.yaml", JSON.encode({ + "pub_serve": port + })); + })).create(); + + var test = runTest([]); + test.stdout.expect(consumeThrough(contains('+1: All tests passed!'))); + test.shouldExit(0); + pub.kill(); + }); + + test("uses the specified concurrency", () { + d.file("dart_test.yaml", JSON.encode({ + "concurrency": 2 + })).create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + """).create(); + + // We can't reliably test cthe concurrency, but this at least ensures that + // it doesn't fail to parse. + var test = runTest(["test.dart"]); + test.stdout.expect(consumeThrough(contains("+1: All tests passed!"))); + test.shouldExit(0); + }); + + test("uses the specified timeout", () { + d.file("dart_test.yaml", JSON.encode({ + "timeout": "0s" + })).create(); + + d.file("test.dart", """ + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () => new Future.delayed(Duration.ZERO)); + } + """).create(); + + var test = runTest(["test.dart"]); + test.stdout.expect(containsInOrder([ + "Test timed out after 0 seconds.", + "-1: Some tests failed." + ])); + test.shouldExit(1); + }); + + test("runs on the specified platforms", () { + d.file("dart_test.yaml", JSON.encode({ + "platforms": ["vm", "content-shell"] + })).create(); + + d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + """).create(); + + var test = runTest(["test.dart"]); + test.stdout.expect(containsInOrder([ + "[VM] success", + "[Dartium Content Shell] success" + ])); + test.shouldExit(0); + }); + + test("command line args take precedence", () { + d.file("dart_test.yaml", JSON.encode({ + "timeout": "0s" + })).create(); + + d.file("test.dart", """ + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () => new Future.delayed(Duration.ZERO)); + } + """).create(); + + var test = runTest(["--timeout=none", "test.dart"]); + test.stdout.expect(consumeThrough(contains("All tests passed!"))); + test.shouldExit(0); + }); +} diff --git a/test/runner/pub_serve_test.dart b/test/runner/pub_serve_test.dart index b2e6ad99..4d84f622 100644 --- a/test/runner/pub_serve_test.dart +++ b/test/runner/pub_serve_test.dart @@ -31,8 +31,6 @@ transformers: \$include: test/**_test.dart - test/pub_serve: \$include: test/**_test.dart -dependency_overrides: - matcher: '0.12.0-alpha.0' """).create(); d.dir("test", [ diff --git a/test/runner/runner_test.dart b/test/runner/runner_test.dart index 18ecd570..1268b73a 100644 --- a/test/runner/runner_test.dart +++ b/test/runner/runner_test.dart @@ -60,11 +60,11 @@ Usage: pub run test:test [files or directories...] $_browsers -j, --concurrency=<threads> The number of concurrent test suites run. - (defaults to $_defaultConcurrency) + (defaults to "$_defaultConcurrency") --pub-serve=<port> The port of a pub serve instance serving "test/". --timeout The default test timeout. For example: 15s, 2x, none - (defaults to 30s) + (defaults to "30s") --pause-after-load Pauses for debugging before any tests execute. Implies --concurrency=1 and --timeout=none. -- GitLab