diff --git a/CHANGELOG.md b/CHANGELOG.md index a3427cf937c4f96c777d614d8bb534f99ee73852..0f9a687d473dcebce8013d5e611e421852b2af97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.12.25 + +* Add a `override_platforms` configuration field which allows test platforms' + settings (such as browsers' executables) to be overridden by the user. + +* Add a `define_platforms` configuration field which makes it possible to define + new platforms that use the same logic as existing ones but have different + settings. + ## 0.12.24+8 * `spawnHybridUri()` now interprets relative URIs correctly in browser tests. diff --git a/doc/configuration.md b/doc/configuration.md index 24cf7c038e8ad638c55b2e5687fbc56567160cbf..800cc5ddaee879085775553df7b48755980553a2 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -52,6 +52,10 @@ tags: * [Configuring Platforms](#configuring-platforms) * [`on_os`](#on_os) * [`on_platform`](#on_platform) + * [`override_platforms`](#override_platforms) + * [`define_platforms`](#define_platforms) + * [Browser/Node.js Settings](#browser-and-node-js-settings) + * [`executable`](#executable) * [Configuration Presets](#configuration-presets) * [`presets`](#presets) * [`add_preset`](#add_preset) @@ -567,6 +571,126 @@ when running on a particular operating system, use [`on_os`](#on_os) instead. This field counts as [test configuration](#test-configuration). +### `override_platforms` + +This field allows you to customize the settings for built-in test platforms. It +takes a map from platform identifiers to settings for those platforms. For example: + +```yaml +override_platforms: + chrome: + # The settings to override for this platform. + settings: + executable: chromium +``` + +This tells the test runner to use the `chromium` executable for Chrome tests. It +calls that executable with the same logic and flags it normally uses for Chrome. + +Each platform can define exactly which settings it supports. All browsers and +Node.js support [the same settings](#browser-and-node-js-settings), but the VM +doesn't support any settings and so can't be overridden. + +### `define_platforms` + +You can define new platforms in terms of old ones using the `define_platforms` +field. This lets you define variants of existing platforms without overriding +the old ones. This field takes a map from the new platform identifiers to +definitions for those platforms. For example: + +```yaml +define_platforms: + # This identifier is used to select the platform with the --platform flag. + chromium: + # A human-friendly name for the platform. + name: Chromium + + # The identifier for the platform that this is based on. + extends: chrome + + # Settings for the new child platform. + settings: + executable: chromium +``` + +Once this is defined, you can run `pub run test -p chromium` and it will run +those tests in the Chromium browser, using the same logic it normally uses for +Chrome. You can even use `chromium` in platform selectors; for example, you +might pass `testOn: "chromium"` to declare that a test is Chromium-specific. +User-defined platforms also count as their parents, so Chromium will run tests +that say `testOn: "chrome"` as well. + +Each platform can define exactly which settings it supports. All browsers and +Node.js support [the same settings](#browser-and-node-js-settings), but the VM +doesn't support any settings and so can't be extended. + +This field is not supported in the +[global configuration file](#global-configuration). + +### Browser and Node.js Settings + +All built-in browser platforms, as well as the built-in Node.js platform, +provide the same settings that can be set using +[`define_platforms`](#define_platforms), which control how their executables are +invoked. + +#### `arguments` + +The `arguments` field provides extra arguments to the executable. It takes a +string, and parses it in the same way as the POSIX shell: + +```yaml +override_platforms: + firefox: + settings: + arguments: -headless +``` + +#### `executable` + +The `executable` field tells the test runner where to look for the executable to +use to start the subprocess. It has three sub-keys, one for each supported +operating system, which each take a path or an executable name: + +```yaml +define_platforms: + chromium: + name: Chromium + extends: chrome + + settings: + executable: + linux: chromium + mac_os: /Applications/Chromium.app/Contents/MacOS/Chromium + windows: Chromium\Application\chrome.exe +``` + +Executables can be defined in three ways: + +* As a plain basename, with no path separators. These executables are passed + directly to the OS, which looks them up using the `PATH` environment variable. + +* As an absolute path, which is used as-is. + +* **Only on Windows**, as a relative path. The test runner will look up this + path relative to the `LOCALAPPATA`, `PROGRAMFILES`, and `PROGRAMFILES(X86)` + environment variables, in that order. + +If a platform is omitted, it defaults to using the built-in executable location. + +As a shorthand, you can also define the same executable for all operating +systems: + +```yaml +define_platforms: + chromium: + name: Chromium + extends: chrome + + settings: + executable: chromium +``` + ## Configuration Presets *Presets* are collections of configuration that can be explicitly selected on diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart index 5432e9f128f1badd108485bac0ab075a121716f9..4e8851d4fde8c731e9ba7ac636bc2f387269bb14 100644 --- a/lib/src/backend/declarer.dart +++ b/lib/src/backend/declarer.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:stack_trace/stack_trace.dart'; import '../frontend/timeout.dart'; @@ -35,6 +36,10 @@ class Declarer { /// and of the test suite. final Metadata _metadata; + /// The set of variables that are valid for platform selectors, in addition to + /// the built-in variables that are allowed everywhere. + final Set<String> _platformVariables; + /// The stack trace for this group. final Trace _trace; @@ -84,17 +89,31 @@ class Declarer { /// If [metadata] is passed, it's used as the metadata for the implicit root /// group. /// + /// The [platformVariables] are the set of variables that are valid for + /// platform selectors in test and group metadata, in addition to the built-in + /// variables that are allowed everywhere. + /// /// If [collectTraces] is `true`, this will set [GroupEntry.trace] for all /// entries built by the declarer. Note that this can be noticeably slow when /// thousands of tests are being declared (see #457). /// /// If [noRetry] is `true` tests will be run at most once. - Declarer({Metadata metadata, bool collectTraces: false, bool noRetry: false}) - : this._(null, null, metadata ?? new Metadata(), collectTraces, null, + Declarer( + {Metadata metadata, + Set<String> platformVariables, + bool collectTraces: false, + bool noRetry: false}) + : this._( + null, + null, + metadata ?? new Metadata(), + platformVariables ?? const UnmodifiableSetView.empty(), + collectTraces, + null, noRetry); - Declarer._(this._parent, this._name, this._metadata, this._collectTraces, - this._trace, this._noRetry); + Declarer._(this._parent, this._name, this._metadata, this._platformVariables, + this._collectTraces, this._trace, this._noRetry); /// Runs [body] with this declarer as [Declarer.current]. /// @@ -111,13 +130,15 @@ class Declarer { int retry}) { _checkNotBuilt("test"); - var metadata = _metadata.merge(new Metadata.parse( + var newMetadata = new Metadata.parse( testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform, tags: tags, - retry: _noRetry ? 0 : retry)); + retry: _noRetry ? 0 : retry); + newMetadata.validatePlatformSelectors(_platformVariables); + var metadata = _metadata.merge(newMetadata); _entries.add(new LocalTest(_prefix(name), metadata, () async { var parents = <Declarer>[]; @@ -151,17 +172,19 @@ class Declarer { int retry}) { _checkNotBuilt("group"); - var metadata = _metadata.merge(new Metadata.parse( + var newMetadata = new Metadata.parse( testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform, tags: tags, - retry: retry)); + retry: _noRetry ? 0 : retry); + newMetadata.validatePlatformSelectors(_platformVariables); + var metadata = _metadata.merge(newMetadata); var trace = _collectTraces ? new Trace.current(2) : null; - var declarer = new Declarer._( - this, _prefix(name), metadata, _collectTraces, trace, _noRetry); + var declarer = new Declarer._(this, _prefix(name), metadata, + _platformVariables, _collectTraces, trace, _noRetry); declarer.declare(() { // Cast to dynamic to avoid the analyzer complaining about us using the // result of a void method. diff --git a/lib/src/backend/metadata.dart b/lib/src/backend/metadata.dart index 92a8ac81114b149df9a53ae7bce4bd34b0824571..7ff1a86246831b02377cbbb075d6bc8176493e2b 100644 --- a/lib/src/backend/metadata.dart +++ b/lib/src/backend/metadata.dart @@ -286,6 +286,17 @@ class Metadata { "Dart identifiers."); } + /// Throws a [FormatException] if any [PlatformSelector]s use any variables + /// that don't appear either in [validVariables] or in the set of variables + /// that are known to be valid for all selectors. + void validatePlatformSelectors(Set<String> validVariables) { + testOn.validate(validVariables); + onPlatform.forEach((selector, metadata) { + selector.validate(validVariables); + metadata.validatePlatformSelectors(validVariables); + }); + } + /// Return a new [Metadata] that merges [this] with [other]. /// /// If the two [Metadata]s have conflicting properties, [other] wins. If diff --git a/lib/src/backend/platform_selector.dart b/lib/src/backend/platform_selector.dart index 590fc4e6addd1601713320330bfa697baad50492..d6da9f4246e678bfb474f40e1158a4b32e081609 100644 --- a/lib/src/backend/platform_selector.dart +++ b/lib/src/backend/platform_selector.dart @@ -8,10 +8,10 @@ import 'package:source_span/source_span.dart'; import 'operating_system.dart'; import 'test_platform.dart'; -/// The set of all valid variable names. -final _validVariables = +/// The set of variable names that are valid for all platform selectors. +final _universalValidVariables = new Set<String>.from(["posix", "dart-vm", "browser", "js", "blink"]) - ..addAll(TestPlatform.all.map((platform) => platform.identifier)) + ..addAll(TestPlatform.builtIn.map((platform) => platform.identifier)) ..addAll(OperatingSystem.all.map((os) => os.identifier)); /// An expression for selecting certain platforms, including operating systems @@ -27,16 +27,46 @@ class PlatformSelector { /// The boolean selector used to implement this selector. final BooleanSelector _inner; + /// The source span from which this selector was parsed. + final SourceSpan _span; + /// Parses [selector]. /// - /// This will throw a [SourceSpanFormatException] if the selector is - /// malformed or if it uses an undefined variable. - PlatformSelector.parse(String selector) - : _inner = new BooleanSelector.parse(selector) { - _inner.validate(_validVariables.contains); + /// If [span] is passed, it indicates the location of the text for [selector] + /// in a larger document. It's used for error reporting. + PlatformSelector.parse(String selector, [SourceSpan span]) + : _inner = _wrapFormatException( + () => new BooleanSelector.parse(selector), span), + _span = span; + + const PlatformSelector._(this._inner) : _span = null; + + /// Runs [body] and wraps any [FormatException] it throws in a + /// [SourceSpanFormatException] using [span]. + /// + /// If [span] is `null`, runs [body] as-is. + static T _wrapFormatException<T>(T body(), SourceSpan span) { + if (span == null) return body(); + + try { + return body(); + } on FormatException catch (error) { + throw new SourceSpanFormatException(error.message, span); + } } - const PlatformSelector._(this._inner); + /// Throws a [FormatException] if this selector uses any variables that don't + /// appear either in [validVariables] or in the set of variables that are + /// known to be valid for all selectors. + void validate(Set<String> validVariables) { + if (identical(this, all)) return; + + _wrapFormatException( + () => _inner.validate((name) => + _universalValidVariables.contains(name) || + validVariables.contains(name)), + _span); + } /// Returns whether the selector matches the given [platform] and [os]. /// @@ -46,6 +76,7 @@ class PlatformSelector { return _inner.evaluate((variable) { if (variable == platform.identifier) return true; + if (variable == platform.parent?.identifier) return true; if (variable == os.identifier) return true; switch (variable) { case "dart-vm": diff --git a/lib/src/backend/test_platform.dart b/lib/src/backend/test_platform.dart index d387eba4987630015003a6f10390febca85b9398..e8ead67735b064bb90af5b106607218e33041fda 100644 --- a/lib/src/backend/test_platform.dart +++ b/lib/src/backend/test_platform.dart @@ -2,8 +2,6 @@ // 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:collection'; - // TODO(nweiz): support pluggable platforms. /// An enum of all platforms on which tests can run. class TestPlatform { @@ -11,54 +9,55 @@ class TestPlatform { // variable tests in test/backend/platform_selector/evaluate_test. /// The command-line Dart VM. - static const TestPlatform vm = - const TestPlatform._("VM", "vm", isDartVM: true); + static const TestPlatform vm = const TestPlatform("VM", "vm", isDartVM: true); /// Dartium. - static const TestPlatform dartium = const TestPlatform._("Dartium", "dartium", + static const TestPlatform dartium = const TestPlatform("Dartium", "dartium", isBrowser: true, isBlink: true, isDartVM: true); /// Dartium content shell. - static const TestPlatform contentShell = const TestPlatform._( + static const TestPlatform contentShell = const TestPlatform( "Dartium Content Shell", "content-shell", isBrowser: true, isBlink: true, isDartVM: true, isHeadless: true); /// Google Chrome. - static const TestPlatform chrome = const TestPlatform._("Chrome", "chrome", + static const TestPlatform chrome = const TestPlatform("Chrome", "chrome", isBrowser: true, isJS: true, isBlink: true); /// PhantomJS. - static const TestPlatform phantomJS = const TestPlatform._( + static const TestPlatform phantomJS = const TestPlatform( "PhantomJS", "phantomjs", isBrowser: true, isJS: true, isBlink: true, isHeadless: true); /// Mozilla Firefox. static const TestPlatform firefox = - const TestPlatform._("Firefox", "firefox", isBrowser: true, isJS: true); + const TestPlatform("Firefox", "firefox", isBrowser: true, isJS: true); /// Apple Safari. static const TestPlatform safari = - const TestPlatform._("Safari", "safari", isBrowser: true, isJS: true); + const TestPlatform("Safari", "safari", isBrowser: true, isJS: true); /// Microsoft Internet Explorer. - static const TestPlatform internetExplorer = const TestPlatform._( + static const TestPlatform internetExplorer = const TestPlatform( "Internet Explorer", "ie", isBrowser: true, isJS: true); /// The command-line Node.js VM. static const TestPlatform nodeJS = - const TestPlatform._("Node.js", "node", isJS: true); - - /// A list of all instances of [TestPlatform]. - static final UnmodifiableListView<TestPlatform> all = - new UnmodifiableListView<TestPlatform>(_allPlatforms); - - /// Finds a platform by its identifier string. - /// - /// If no platform is found, returns `null`. - static TestPlatform find(String identifier) => - all.firstWhere((platform) => platform.identifier == identifier, - orElse: () => null); + const TestPlatform("Node.js", "node", isJS: true); + + /// The platforms that are supported by the test runner by default. + static const List<TestPlatform> builtIn = const [ + TestPlatform.vm, + TestPlatform.dartium, + TestPlatform.contentShell, + TestPlatform.chrome, + TestPlatform.phantomJS, + TestPlatform.firefox, + TestPlatform.safari, + TestPlatform.internetExplorer, + TestPlatform.nodeJS + ]; /// The human-friendly name of the platform. final String name; @@ -66,6 +65,13 @@ class TestPlatform { /// The identifier used to look up the platform. final String identifier; + /// The parent platform that this is based on, or `null` if there is no + /// parent. + final TestPlatform parent; + + /// Returns whether this is a child of another platform. + bool get isChild => parent != null; + /// Whether this platform runs the Dart VM in any capacity. final bool isDartVM; @@ -81,45 +87,86 @@ class TestPlatform { /// Whether this platform has no visible window. final bool isHeadless; - const TestPlatform._(this.name, this.identifier, + /// Returns the platform this is based on, or [this] if it's not based on + /// anything. + /// + /// That is, returns [parent] if it's non-`null` or [this] if it's `null`. + TestPlatform get root => parent ?? this; + + const TestPlatform(this.name, this.identifier, {this.isDartVM: false, this.isBrowser: false, this.isJS: false, this.isBlink: false, - this.isHeadless: false}); + this.isHeadless: false}) + : parent = null; + + TestPlatform._child(this.name, this.identifier, this.parent) + : isDartVM = parent.isDartVM, + isBrowser = parent.isBrowser, + isJS = parent.isJS, + isBlink = parent.isBlink, + isHeadless = parent.isHeadless; + + /// Converts a JSON-safe representation generated by [serialize] back into a + /// [TestPlatform]. + factory TestPlatform.deserialize(Object serialized) { + if (serialized is String) { + return builtIn + .firstWhere((platform) => platform.identifier == serialized); + } + + var map = serialized as Map; + var parent = map["parent"]; + if (parent != null) { + // Note that the returned platform's [parent] won't necessarily be `==` to + // a separately-deserialized parent platform. This should be fine, though, + // since we only deserialize platforms in the remote execution context + // where they're only used to evaluate platform selectors. + return new TestPlatform._child( + map["name"], map["identifier"], new TestPlatform.deserialize(parent)); + } + + return new TestPlatform(map["name"], map["identifier"], + isDartVM: map["isDartVM"], + isBrowser: map["isBrowser"], + isJS: map["isJS"], + isBlink: map["isBlink"], + isHeadless: map["isHeadless"]); + } + + /// Converts [this] into a JSON-safe object that can be converted back to a + /// [TestPlatform] using [new TestPlatform.deserialize]. + Object serialize() { + if (builtIn.contains(this)) return identifier; + + if (parent != null) { + return { + "name": name, + "identifier": identifier, + "parent": parent.serialize() + }; + } + + return { + "name": name, + "identifier": identifier, + "isDartVM": isDartVM, + "isBrowser": isBrowser, + "isJS": isJS, + "isBlink": isBlink, + "isHeadless": isHeadless + }; + } + + /// Returns a child of [this] that counts as both this platform's identifier + /// and the new [identifier]. + /// + /// This may not be called on a platform that's already a child. + TestPlatform extend(String name, String identifier) { + if (parent == null) return new TestPlatform._child(name, identifier, this); + throw new StateError("A child platform may not be extended."); + } String toString() => name; } - -final List<TestPlatform> _allPlatforms = [ - TestPlatform.vm, - TestPlatform.dartium, - TestPlatform.contentShell, - TestPlatform.chrome, - TestPlatform.phantomJS, - TestPlatform.firefox, - TestPlatform.safari, - TestPlatform.internetExplorer, - TestPlatform.nodeJS -]; - -/// **Do not call this function without express permission from the test package -/// authors**. -/// -/// This constructs and globally registers a new TestPlatform with the provided -/// details. -TestPlatform registerTestPlatform(String name, String identifier, - {bool isDartVM: false, - bool isBrowser: false, - bool isJS: false, - bool isBlink: false, - bool isHeadless: false}) { - var platform = new TestPlatform._(name, identifier, - isDartVM: isDartVM, - isBrowser: isBrowser, - isJS: isJS, - isBlink: isBlink, - isHeadless: isHeadless); - _allPlatforms.add(platform); - return platform; -} diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 1b2b1d772a2c3d12a18286dfbc1be55590a57f44..5fd756f6afa108d3a6920f958fce8ab87110800d 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -154,24 +154,30 @@ transformers: return; } - var runner = new Runner(configuration); - + Runner runner; var signalSubscription; close() async { if (signalSubscription == null) return; signalSubscription.cancel(); signalSubscription = null; stdinLines.cancel(immediate: true); - await runner.close(); + await runner?.close(); } signalSubscription = _signals.listen((_) => close()); try { + runner = new Runner(configuration); exitCode = (await runner.run()) ? 0 : 1; } on ApplicationException catch (error) { stderr.writeln(error.message); exitCode = exit_codes.data; + } on SourceSpanFormatException catch (error) { + stderr.writeln(error.toString(color: configuration.color)); + exitCode = exit_codes.data; + } on FormatException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.data; } catch (error, stackTrace) { stderr.writeln(getErrorMessage(error)); stderr.writeln(new Trace.from(stackTrace).terse); diff --git a/lib/src/runner.dart b/lib/src/runner.dart index 0b63e5ceae6f26d9c3c9c6ec910b5dedcf65e10e..91a444e971097a27504c65fe336f8435ec9aa231 100644 --- a/lib/src/runner.dart +++ b/lib/src/runner.dart @@ -128,7 +128,9 @@ class Runner { if (testOn == PlatformSelector.all) return; var unsupportedPlatforms = _config.suiteDefaults.platforms - .where((platform) => !testOn.evaluate(platform, os: currentOS)) + .map(_loader.findTestPlatform) + .where((platform) => + platform != null && !testOn.evaluate(platform, os: currentOS)) .toList(); if (unsupportedPlatforms.isEmpty) return; @@ -141,7 +143,7 @@ class Runner { var unsupportedBrowsers = unsupportedPlatforms.where((platform) => platform.isBrowser); if (unsupportedBrowsers.isNotEmpty) { - var supportsAnyBrowser = TestPlatform.all + var supportsAnyBrowser = _loader.allPlatforms .where((platform) => platform.isBrowser) .any((platform) => testOn.evaluate(platform)); @@ -351,7 +353,7 @@ class Runner { /// Loads each suite in [suites] in order, pausing after load for platforms /// that support debugging. Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { - if (_config.suiteDefaults.platforms.contains(TestPlatform.vm)) { + if (_config.suiteDefaults.platforms.contains(TestPlatform.vm.identifier)) { warn("Debugging is currently unsupported on the Dart VM.", color: _config.color); } diff --git a/lib/src/runner/browser/browser.dart b/lib/src/runner/browser/browser.dart index 79583b6b2f11590192286b8c7b67eb0729239809..e1602317e5976ebf3787acf3ecfb52e277b7e483 100644 --- a/lib/src/runner/browser/browser.dart +++ b/lib/src/runner/browser/browser.dart @@ -105,6 +105,7 @@ abstract class Browser { _process.then((process) => process.kill()); if (stackTrace == null) stackTrace = new Trace.current(); + if (_onExitCompleter.isCompleted) return; _onExitCompleter.completeError( new ApplicationException( "Failed to run $name: ${getErrorMessage(error)}."), diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart index 896672036d6c17be38c4f046070d14646463e393..9f9e3c9ad1595438bd164a6d7f6c5c9c63454081 100644 --- a/lib/src/runner/browser/browser_manager.dart +++ b/lib/src/runner/browser/browser_manager.dart @@ -15,6 +15,7 @@ import '../../util/stack_trace_mapper.dart'; import '../application_exception.dart'; import '../configuration/suite.dart'; import '../environment.dart'; +import '../executable_settings.dart'; import '../plugin/platform_helpers.dart'; import '../runner_suite.dart'; import 'browser.dart'; @@ -93,12 +94,14 @@ class BrowserManager { /// [future]. If [debug] is true, starts the browser in debug mode, with its /// debugger interfaces on and detected. /// + /// The [settings] indicate how to invoke this browser's executable. + /// /// Returns the browser manager, or throws an [ApplicationException] if a /// connection fails to be established. - static Future<BrowserManager> start( - TestPlatform platform, Uri url, Future<WebSocketChannel> future, + static Future<BrowserManager> start(TestPlatform platform, Uri url, + Future<WebSocketChannel> future, ExecutableSettings settings, {bool debug: false}) { - var browser = _newBrowser(url, platform, debug: debug); + var browser = _newBrowser(url, platform, settings, debug: debug); var completer = new Completer<BrowserManager>(); @@ -128,26 +131,27 @@ class BrowserManager { }); } - /// Starts the browser identified by [browser] and has it load [url]. + /// Starts the browser identified by [browser] using [settings] and has it load [url]. /// /// If [debug] is true, starts the browser in debug mode. - static Browser _newBrowser(Uri url, TestPlatform browser, + static Browser _newBrowser( + Uri url, TestPlatform browser, ExecutableSettings settings, {bool debug: false}) { - switch (browser) { + switch (browser.root) { case TestPlatform.dartium: - return new Dartium(url, debug: debug); + return new Dartium(url, settings: settings, debug: debug); case TestPlatform.contentShell: - return new ContentShell(url, debug: debug); + return new ContentShell(url, settings: settings, debug: debug); case TestPlatform.chrome: - return new Chrome(url, debug: debug); + return new Chrome(url, settings: settings, debug: debug); case TestPlatform.phantomJS: - return new PhantomJS(url, debug: debug); + return new PhantomJS(url, settings: settings, debug: debug); case TestPlatform.firefox: - return new Firefox(url); + return new Firefox(url, settings: settings); case TestPlatform.safari: - return new Safari(url); + return new Safari(url, settings: settings); case TestPlatform.internetExplorer: - return new InternetExplorer(url); + return new InternetExplorer(url, settings: settings); default: throw new ArgumentError("$browser is not a browser."); } @@ -201,7 +205,8 @@ class BrowserManager { /// /// If [mapper] is passed, it's used to map stack traces for errors coming /// from this test suite. - Future<RunnerSuite> load(String path, Uri url, SuiteConfiguration suiteConfig, + Future<RunnerSuite> load( + String path, Uri url, SuiteConfiguration suiteConfig, Object message, {StackTraceMapper mapper}) async { url = url.replace( fragment: Uri.encodeFull(JSON.encode({ @@ -236,8 +241,8 @@ class BrowserManager { }); try { - controller = await deserializeSuite( - path, _platform, suiteConfig, await _environment, suiteChannel, + controller = await deserializeSuite(path, _platform, suiteConfig, + await _environment, suiteChannel, message, mapper: mapper); _controllers.add(controller); return controller.suite; diff --git a/lib/src/runner/browser/chrome.dart b/lib/src/runner/browser/chrome.dart index 7c5f481731980d9c0f1c8a0d2b26aa8c192b4f8f..e2150e240189c86762ca4c18c78e5fe0a2150136 100644 --- a/lib/src/runner/browser/chrome.dart +++ b/lib/src/runner/browser/chrome.dart @@ -5,10 +5,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:path/path.dart' as p; - +import '../../backend/test_platform.dart'; import '../../util/io.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; // TODO(nweiz): move this into its own package? /// A class for running an instance of Chrome. @@ -25,14 +26,10 @@ class Chrome extends Browser { /// Starts a new instance of Chrome open to the given [url], which may be a /// [Uri] or a [String]. - /// - /// If [executable] is passed, it's used as the Chrome executable. Otherwise - /// the default executable name for the current OS will be used. - factory Chrome(url, {String executable, bool debug: false}) { + factory Chrome(url, {ExecutableSettings settings, bool debug: false}) { + settings ??= defaultSettings[TestPlatform.chrome]; var remoteDebuggerCompleter = new Completer<Uri>.sync(); return new Chrome._(() async { - if (executable == null) executable = _defaultExecutable(); - var tryPort = ([int port]) async { var dir = createTempDir(); var args = [ @@ -45,7 +42,7 @@ class Chrome extends Browser { "--no-default-browser-check", "--disable-default-apps", "--disable-translate", - ]; + ]..addAll(settings.arguments); // Currently, Chrome doesn't provide any way of ensuring that this port // was successfully bound. It produces an error if the binding fails, @@ -54,7 +51,7 @@ class Chrome extends Browser { // though. if (port != null) args.add("--remote-debugging-port=$port"); - var process = await Process.start(executable, args); + var process = await Process.start(settings.executable, args); if (port != null) { remoteDebuggerCompleter.complete( @@ -76,32 +73,4 @@ class Chrome extends Browser { Chrome._(Future<Process> startBrowser(), this.remoteDebuggerUrl) : super(startBrowser); - - /// Return the default executable for the current operating system. - static String _defaultExecutable() { - if (Platform.isMacOS) { - return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; - } - if (!Platform.isWindows) return 'google-chrome'; - - // Chrome could be installed in several places on Windows. The only way to - // find it is to check. - var prefixes = [ - Platform.environment['LOCALAPPDATA'], - Platform.environment['PROGRAMFILES'], - Platform.environment['PROGRAMFILES(X86)'] - ]; - var suffix = r'Google\Chrome\Application\chrome.exe'; - - for (var prefix in prefixes) { - if (prefix == null) continue; - - var path = p.join(prefix, suffix); - if (new File(p.join(prefix, suffix)).existsSync()) return path; - } - - // Fall back on looking it up on the path. This probably won't work, but at - // least it will fail with a useful error message. - return "chrome.exe"; - } } diff --git a/lib/src/runner/browser/content_shell.dart b/lib/src/runner/browser/content_shell.dart index 5599a8dd5900af69535b75a4e1b691f2e9f54da6..e52bf1f20f0aafe5c357af30ffc73014f746f62b 100644 --- a/lib/src/runner/browser/content_shell.dart +++ b/lib/src/runner/browser/content_shell.dart @@ -5,10 +5,13 @@ import 'dart:async'; import 'dart:io'; +import '../../backend/test_platform.dart'; import '../../util/io.dart'; import '../../utils.dart'; import '../application_exception.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; final _observatoryRegExp = new RegExp(r"^Observatory listening on ([^ ]+)"); final _errorTimeout = const Duration(seconds: 10); @@ -27,18 +30,18 @@ class ContentShell extends Browser { final Future<Uri> remoteDebuggerUrl; - factory ContentShell(url, {String executable, bool debug: false}) { + factory ContentShell(url, {ExecutableSettings settings, bool debug: false}) { + settings ??= defaultSettings[TestPlatform.contentShell]; var observatoryCompleter = new Completer<Uri>.sync(); var remoteDebuggerCompleter = new Completer<Uri>.sync(); return new ContentShell._(() { - if (executable == null) executable = _defaultExecutable(); - var tryPort = ([int port]) async { - var args = ["--dump-render-tree", url.toString()]; + var args = ["--dump-render-tree", url.toString()] + ..addAll(settings.arguments); if (port != null) args.add("--remote-debugging-port=$port"); - var process = await Process - .start(executable, args, environment: {"DART_FLAGS": "--checked"}); + var process = await Process.start(settings.executable, args, + environment: {"DART_FLAGS": "--checked"}); if (debug) { observatoryCompleter.complete(lineSplitter @@ -111,8 +114,4 @@ class ContentShell extends Browser { ContentShell._(Future<Process> startBrowser(), this.observatoryUrl, this.remoteDebuggerUrl) : super(startBrowser); - - /// Return the default executable for the current operating system. - static String _defaultExecutable() => - Platform.isWindows ? "content_shell.exe" : "content_shell"; } diff --git a/lib/src/runner/browser/dartium.dart b/lib/src/runner/browser/dartium.dart index aacf4140cbd79e82a3be605c58be9a47adde7e7e..e785231c0fd1c1d7921a65fa9d5343db93a28840 100644 --- a/lib/src/runner/browser/dartium.dart +++ b/lib/src/runner/browser/dartium.dart @@ -7,11 +7,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:async/async.dart'; -import 'package:path/path.dart' as p; +import '../../backend/test_platform.dart'; import '../../util/io.dart'; import '../../utils.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; final _observatoryRegExp = new RegExp(r'Observatory listening (?:on|at) ([^ "]+)'); @@ -30,12 +32,11 @@ class Dartium extends Browser { final Future<Uri> remoteDebuggerUrl; - factory Dartium(url, {String executable, bool debug: false}) { + factory Dartium(url, {ExecutableSettings settings, bool debug: false}) { + settings ??= defaultSettings[TestPlatform.dartium]; var observatoryCompleter = new Completer<Uri>.sync(); var remoteDebuggerCompleter = new Completer<Uri>.sync(); return new Dartium._(() async { - if (executable == null) executable = _defaultExecutable(); - var tryPort = ([int port]) async { var dir = createTempDir(); var args = [ @@ -48,7 +49,7 @@ class Dartium extends Browser { "--no-default-browser-check", "--disable-default-apps", "--disable-translate" - ]; + ]..addAll(settings.arguments); if (port != null) { args.add("--remote-debugging-port=$port"); @@ -62,8 +63,8 @@ class Dartium extends Browser { args.add("--vmodule=startup_browser_creator_impl=1"); } - var process = await Process - .start(executable, args, environment: {"DART_FLAGS": "--checked"}); + var process = await Process.start(settings.executable, args, + environment: {"DART_FLAGS": "--checked"}); if (port != null) { // Dartium on Windows prints all standard IO to stderr, so we need to @@ -124,47 +125,6 @@ class Dartium extends Browser { this.remoteDebuggerUrl) : super(startBrowser); - /// Starts a new instance of Dartium open to the given [url], which may be a - /// [Uri] or a [String]. - /// - /// If [executable] is passed, it's used as the Dartium executable. Otherwise - /// the default executable name for the current OS will be used. - - /// Return the default executable for the current operating system. - static String _defaultExecutable() { - var dartium = _executableInEditor(); - if (dartium != null) return dartium; - return Platform.isWindows ? "dartium.exe" : "dartium"; - } - - static String _executableInEditor() { - var dir = p.dirname(sdkDir); - - if (Platform.isWindows) { - if (!new File(p.join(dir, "DartEditor.exe")).existsSync()) return null; - - var dartium = p.join(dir, "chromium\\chrome.exe"); - return new File(dartium).existsSync() ? dartium : null; - } - - if (Platform.isMacOS) { - if (!new File(p.join(dir, "DartEditor.app/Contents/MacOS/DartEditor")) - .existsSync()) { - return null; - } - - var dartium = - p.join(dir, "chromium/Chromium.app/Contents/MacOS/Chromium"); - return new File(dartium).existsSync() ? dartium : null; - } - - assert(Platform.isLinux); - if (!new File(p.join(dir, "DartEditor")).existsSync()) return null; - - var dartium = p.join(dir, "chromium", "chrome"); - return new File(dartium).existsSync() ? dartium : null; - } - // TODO(nweiz): simplify this when sdk#23923 is fixed. /// Returns the Observatory URL for the Dartium executable with the given /// [stdout] stream, or `null` if the correct one couldn't be found. diff --git a/lib/src/runner/browser/default_settings.dart b/lib/src/runner/browser/default_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..600092694a64711660ea0d0c4e492ba8538ab90f --- /dev/null +++ b/lib/src/runner/browser/default_settings.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2017, 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:collection'; + +import '../../backend/test_platform.dart'; +import '../executable_settings.dart'; + +/// Default settings for starting browser executables. +final defaultSettings = new UnmodifiableMapView({ + TestPlatform.chrome: new ExecutableSettings( + linuxExecutable: 'google-chrome', + macOSExecutable: + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + windowsExecutable: r'Google\Chrome\Application\chrome.exe'), + TestPlatform.contentShell: new ExecutableSettings( + linuxExecutable: 'content_shell', + macOSExecutable: 'content_shell', + windowsExecutable: 'content_shell.exe'), + TestPlatform.dartium: new ExecutableSettings( + linuxExecutable: 'dartium', + macOSExecutable: 'dartium', + windowsExecutable: 'dartium.exe'), + TestPlatform.firefox: new ExecutableSettings( + linuxExecutable: 'firefox', + macOSExecutable: '/Applications/Firefox.app/Contents/MacOS/firefox-bin', + windowsExecutable: r'Mozilla Firefox\firefox.exe'), + TestPlatform.internetExplorer: new ExecutableSettings( + windowsExecutable: r'Internet Explorer\iexplore.exe'), + TestPlatform.phantomJS: new ExecutableSettings( + linuxExecutable: 'phantomjs', + macOSExecutable: 'phantomjs', + windowsExecutable: 'phantomjs.exe'), + TestPlatform.safari: new ExecutableSettings( + macOSExecutable: '/Applications/Safari.app/Contents/MacOS/Safari') +}); diff --git a/lib/src/runner/browser/firefox.dart b/lib/src/runner/browser/firefox.dart index 5c9a016a3f7285e22e867ba113082a3b9aaca660..4e3ee8e1706e5f5ef3dd3ab100d9a56ad0dfedd3 100644 --- a/lib/src/runner/browser/firefox.dart +++ b/lib/src/runner/browser/firefox.dart @@ -7,8 +7,11 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import '../../backend/test_platform.dart'; import '../../util/io.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; final _preferences = ''' user_pref("browser.shell.checkDefaultBrowser", false); @@ -26,22 +29,20 @@ user_pref("dom.max_script_run_time", 0); class Firefox extends Browser { final name = "Firefox"; - Firefox(url, {String executable}) - : super(() => _startBrowser(url, executable)); + Firefox(url, {ExecutableSettings settings}) + : super(() => _startBrowser(url, settings)); /// Starts a new instance of Firefox open to the given [url], which may be a /// [Uri] or a [String]. - /// - /// If [executable] is passed, it's used as the Firefox executable. - /// Otherwise the default executable name for the current OS will be used. - static Future<Process> _startBrowser(url, [String executable]) async { - if (executable == null) executable = _defaultExecutable(); - + static Future<Process> _startBrowser(url, ExecutableSettings settings) async { + settings ??= defaultSettings[TestPlatform.dartium]; var dir = createTempDir(); new File(p.join(dir, 'prefs.js')).writeAsStringSync(_preferences); var process = await Process.start( - executable, ["--profile", "$dir", url.toString(), "--no-remote"], + settings.executable, + ["--profile", "$dir", url.toString(), "--no-remote"] + ..addAll(settings.arguments), environment: {"MOZ_CRASHREPORTER_DISABLE": "1"}); process.exitCode @@ -49,31 +50,4 @@ class Firefox extends Browser { return process; } - - /// Return the default executable for the current operating system. - static String _defaultExecutable() { - if (Platform.isMacOS) { - return '/Applications/Firefox.app/Contents/MacOS/firefox-bin'; - } - if (!Platform.isWindows) return 'firefox'; - - // Firefox could be installed in several places on Windows. The only way to - // find it is to check. - var prefixes = [ - Platform.environment['PROGRAMFILES'], - Platform.environment['PROGRAMFILES(X86)'] - ]; - var suffix = r'Mozilla Firefox\firefox.exe'; - - for (var prefix in prefixes) { - if (prefix == null) continue; - - var path = p.join(prefix, suffix); - if (new File(p.join(prefix, suffix)).existsSync()) return path; - } - - // Fall back on looking it up on the path. This probably won't work, but at - // least it will fail with a useful error message. - return "firefox.exe"; - } } diff --git a/lib/src/runner/browser/internet_explorer.dart b/lib/src/runner/browser/internet_explorer.dart index 8d20e29c6be1d1aa976079f67ad687ec1fb31cda..eee452478647b4cf61f1f7368b612c57da24d40c 100644 --- a/lib/src/runner/browser/internet_explorer.dart +++ b/lib/src/runner/browser/internet_explorer.dart @@ -5,9 +5,10 @@ import 'dart:async'; import 'dart:io'; -import 'package:path/path.dart' as p; - +import '../../backend/test_platform.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; /// A class for running an instance of Internet Explorer. /// @@ -15,39 +16,15 @@ import 'browser.dart'; class InternetExplorer extends Browser { final name = "Internet Explorer"; - InternetExplorer(url, {String executable}) - : super(() => _startBrowser(url, executable)); + InternetExplorer(url, {ExecutableSettings settings}) + : super(() => _startBrowser(url, settings)); /// Starts a new instance of Internet Explorer open to the given [url], which /// may be a [Uri] or a [String]. - /// - /// If [executable] is passed, it's used as the Internet Explorer executable. - /// Otherwise the default executable name for the current OS will be used. - static Future<Process> _startBrowser(url, [String executable]) { - if (executable == null) executable = _defaultExecutable(); - return Process.start(executable, ['-extoff', url.toString()]); - } - - /// Return the default executable for the current operating system. - static String _defaultExecutable() { - // Internet Explorer could be installed in several places on Windows. The - // only way to find it is to check. - var prefixes = [ - Platform.environment['PROGRAMW6432'], - Platform.environment['PROGRAMFILES'], - Platform.environment['PROGRAMFILES(X86)'] - ]; - var suffix = r'Internet Explorer\iexplore.exe'; - - for (var prefix in prefixes) { - if (prefix == null) continue; - - var path = p.join(prefix, suffix); - if (new File(p.join(prefix, suffix)).existsSync()) return path; - } + static Future<Process> _startBrowser(url, ExecutableSettings settings) { + settings ??= defaultSettings[TestPlatform.dartium]; - // Fall back on looking it up on the path. This probably won't work, but at - // least it will fail with a useful error message. - return "iexplore.exe"; + return Process.start(settings.executable, + ['-extoff', url.toString()]..addAll(settings.arguments)); } } diff --git a/lib/src/runner/browser/phantom_js.dart b/lib/src/runner/browser/phantom_js.dart index 640841de2aba488acb951ca07093198a395d7f17..25e9b99ede14041d0307e060309cc3ce9e112039 100644 --- a/lib/src/runner/browser/phantom_js.dart +++ b/lib/src/runner/browser/phantom_js.dart @@ -7,10 +7,13 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import '../../backend/test_platform.dart'; import '../../util/exit_codes.dart' as exit_codes; import '../../util/io.dart'; import '../application_exception.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; /// The PhantomJS script that opens the host page. final _script = """ @@ -39,26 +42,23 @@ class PhantomJS extends Browser { final Future<Uri> remoteDebuggerUrl; - factory PhantomJS(url, {String executable, bool debug: false}) { + factory PhantomJS(url, {ExecutableSettings settings, bool debug: false}) { + settings ??= defaultSettings[TestPlatform.phantomJS]; var remoteDebuggerCompleter = new Completer<Uri>.sync(); return new PhantomJS._(() async { - if (executable == null) { - executable = Platform.isWindows ? "phantomjs.exe" : "phantomjs"; - } - var dir = createTempDir(); var script = p.join(dir, "script.js"); new File(script).writeAsStringSync(_script); var port = debug ? await getUnsafeUnusedPort() : null; - var args = <String>[]; + var args = settings.arguments.toList(); if (debug) { args.addAll( ["--remote-debugger-port=$port", "--remote-debugger-autorun=yes"]); } args.addAll([script, url.toString()]); - var process = await Process.start(executable, args); + var process = await Process.start(settings.executable, args); // PhantomJS synchronously emits standard output, which means that if we // don't drain its stdout stream it can deadlock. diff --git a/lib/src/runner/browser/platform.dart b/lib/src/runner/browser/platform.dart index 2268d2e4a61fa5f62c66decb537f885596d46e39..4b040c48e7bc67cee9e670253fbc4cf1a990ce86 100644 --- a/lib/src/runner/browser/platform.dart +++ b/lib/src/runner/browser/platform.dart @@ -19,6 +19,7 @@ import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:shelf_packages_handler/shelf_packages_handler.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:yaml/yaml.dart'; import '../../backend/test_platform.dart'; import '../../util/io.dart'; @@ -29,13 +30,17 @@ import '../../utils.dart'; import '../compiler_pool.dart'; import '../configuration.dart'; import '../configuration/suite.dart'; +import '../executable_settings.dart'; import '../load_exception.dart'; +import '../plugin/customizable_platform.dart'; import '../plugin/platform.dart'; import '../runner_suite.dart'; import 'browser_manager.dart'; +import 'default_settings.dart'; import 'polymer.dart'; -class BrowserPlatform extends PlatformPlugin { +class BrowserPlatform extends PlatformPlugin + implements CustomizablePlatform<ExecutableSettings> { /// Starts the server. /// /// [root] is the root directory that the server should serve. It defaults to @@ -100,7 +105,13 @@ class BrowserPlatform extends PlatformPlugin { /// [BrowserManager]s for those browsers, or `null` if they failed to load. /// /// This should only be accessed through [_browserManagerFor]. - final _browserManagers = new Map<TestPlatform, Future<BrowserManager>>(); + final _browserManagers = <TestPlatform, Future<BrowserManager>>{}; + + /// Settings for invoking each browser. + /// + /// This starts out with the default settings, which may be overridden by user settings. + final _browserSettings = + new Map<TestPlatform, ExecutableSettings>.from(defaultSettings); /// A cascade of handlers for suites' precompiled paths. /// @@ -198,13 +209,27 @@ class BrowserPlatform extends PlatformPlugin { return new shelf.Response.notFound('Not found.'); } + ExecutableSettings parsePlatformSettings(YamlMap settings) => + new ExecutableSettings.parse(settings); + + ExecutableSettings mergePlatformSettings( + ExecutableSettings settings1, ExecutableSettings settings2) => + settings1.merge(settings2); + + void customizePlatform(TestPlatform platform, ExecutableSettings settings) { + var oldSettings = + _browserSettings[platform] ?? _browserSettings[platform.root]; + if (oldSettings != null) settings = oldSettings.merge(settings); + _browserSettings[platform] = settings; + } + /// Loads the test suite at [path] on the browser [browser]. /// /// This will start a browser to load the suite if one isn't already running. /// Throws an [ArgumentError] if [browser] isn't a browser platform. - Future<RunnerSuite> load( - String path, TestPlatform browser, SuiteConfiguration suiteConfig) async { - assert(suiteConfig.platforms.contains(browser)); + Future<RunnerSuite> load(String path, TestPlatform browser, + SuiteConfiguration suiteConfig, Object message) async { + assert(suiteConfig.platforms.contains(browser.identifier)); if (!browser.isBrowser) { throw new ArgumentError("$browser is not a browser."); @@ -281,7 +306,7 @@ class BrowserPlatform extends PlatformPlugin { var browserManager = await _browserManagerFor(browser); if (_closed || browserManager == null) return null; - var suite = await browserManager.load(path, suiteUrl, suiteConfig, + var suite = await browserManager.load(path, suiteUrl, suiteConfig, message, mapper: browser.isJS ? _mappers[path] : null); if (_closed) return null; return suite; @@ -427,7 +452,8 @@ class BrowserPlatform extends PlatformPlugin { 'debug': _config.pauseAfterLoad.toString() }); - var future = BrowserManager.start(platform, hostUrl, completer.future, + var future = BrowserManager.start( + platform, hostUrl, completer.future, _browserSettings[platform], debug: _config.pauseAfterLoad); // Store null values for browsers that error out so we know not to load them diff --git a/lib/src/runner/browser/safari.dart b/lib/src/runner/browser/safari.dart index ac1b7d5e7324ae66abd6ef0371cd43d0e994ed0b..2b35749c0df844bbdc651be9f004a6b70e9e76b2 100644 --- a/lib/src/runner/browser/safari.dart +++ b/lib/src/runner/browser/safari.dart @@ -8,8 +8,11 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import '../../backend/test_platform.dart'; import '../../util/io.dart'; +import '../executable_settings.dart'; import 'browser.dart'; +import 'default_settings.dart'; /// A class for running an instance of Safari. /// @@ -17,19 +20,13 @@ import 'browser.dart'; class Safari extends Browser { final name = "Safari"; - Safari(url, {String executable}) - : super(() => _startBrowser(url, executable)); + Safari(url, {ExecutableSettings settings}) + : super(() => _startBrowser(url, settings)); /// Starts a new instance of Safari open to the given [url], which may be a /// [Uri] or a [String]. - /// - /// If [executable] is passed, it's used as the content shell executable. - /// Otherwise the default executable name for the current OS will be used. - static Future<Process> _startBrowser(url, [String executable]) async { - if (executable == null) { - executable = '/Applications/Safari.app/Contents/MacOS/Safari'; - } - + static Future<Process> _startBrowser(url, ExecutableSettings settings) async { + settings ??= defaultSettings[TestPlatform.safari]; var dir = createTempDir(); // Safari will only open files (not general URLs) via the command-line @@ -39,7 +36,8 @@ class Safari extends Browser { new File(redirect).writeAsStringSync( "<script>location = " + JSON.encode(url.toString()) + "</script>"); - var process = await Process.start(executable, [redirect]); + var process = await Process.start( + settings.executable, settings.arguments.toList()..add(redirect)); process.exitCode .then((_) => new Directory(dir).deleteSync(recursive: true)); diff --git a/lib/src/runner/configuration.dart b/lib/src/runner/configuration.dart index e6a02b985da17b65d817bc199cecfb2293febdbd..873ef6a6159ccb719d2dcb2cbe3b50fb4c5572e5 100644 --- a/lib/src/runner/configuration.dart +++ b/lib/src/runner/configuration.dart @@ -8,13 +8,18 @@ import 'package:boolean_selector/boolean_selector.dart'; import 'package:collection/collection.dart'; import 'package:glob/glob.dart'; import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; import '../backend/platform_selector.dart'; import '../backend/test_platform.dart'; import '../frontend/timeout.dart'; import '../util/io.dart'; +import '../utils.dart'; import 'configuration/args.dart' as args; +import 'configuration/custom_platform.dart'; import 'configuration/load.dart'; +import 'configuration/platform_selection.dart'; +import 'configuration/platform_settings.dart'; import 'configuration/reporters.dart'; import 'configuration/suite.dart'; import 'configuration/values.dart'; @@ -169,6 +174,12 @@ class Configuration { Set<String> _knownPresets; + /// Built-in platforms whose settings are overridden by the user. + final Map<String, PlatformSettings> overridePlatforms; + + /// Platforms defined by the user in terms of existing platforms. + final Map<String, CustomPlatform> definePlatforms; + /// The default suite-level configuration. final SuiteConfiguration suiteDefaults; @@ -212,6 +223,8 @@ class Configuration { Glob filename, Iterable<String> chosenPresets, Map<String, Configuration> presets, + Map<String, PlatformSettings> overridePlatforms, + Map<String, CustomPlatform> definePlatforms, bool noRetry, // Suite-level configuration @@ -220,7 +233,7 @@ class Configuration { Iterable<String> dart2jsArgs, String precompiledPath, Iterable<Pattern> patterns, - Iterable<TestPlatform> platforms, + Iterable<PlatformSelection> platforms, BooleanSelector includeTags, BooleanSelector excludeTags, Map<BooleanSelector, SuiteConfiguration> tags, @@ -254,6 +267,8 @@ class Configuration { filename: filename, chosenPresets: chosenPresetSet, presets: _withChosenPresets(presets, chosenPresetSet), + overridePlatforms: overridePlatforms, + definePlatforms: definePlatforms, noRetry: noRetry, suiteDefaults: new SuiteConfiguration( jsTrace: jsTrace, @@ -308,6 +323,8 @@ class Configuration { Glob filename, Iterable<String> chosenPresets, Map<String, Configuration> presets, + Map<String, PlatformSettings> overridePlatforms, + Map<String, CustomPlatform> definePlatforms, bool noRetry, SuiteConfiguration suiteDefaults}) : _help = help, @@ -328,6 +345,8 @@ class Configuration { chosenPresets = new UnmodifiableSetView(chosenPresets?.toSet() ?? new Set()), presets = _map(presets), + overridePlatforms = _map(overridePlatforms), + definePlatforms = _map(definePlatforms), _noRetry = noRetry, suiteDefaults = pauseAfterLoad == true ? suiteDefaults?.change(timeout: Timeout.none) ?? @@ -384,6 +403,26 @@ class Configuration { /// asynchronous callbacks transitively created by [body]. T asCurrent<T>(T body()) => runZoned(body, zoneValues: {_currentKey: this}); + /// Throws a [FormatException] if [this] refers to any undefined platforms. + void validatePlatforms(List<TestPlatform> allPlatforms) { + // We don't need to verify [customPlatforms] here because those platforms + // already need to be verified and resolved to create [allPlatforms]. + + for (var settings in overridePlatforms.values) { + if (!allPlatforms + .any((platform) => platform.identifier == settings.identifier)) { + throw new SourceSpanFormatException( + 'Unknown platform "${settings.identifier}".', + settings.identifierSpan); + } + } + + suiteDefaults.validatePlatforms(allPlatforms); + for (var config in presets.values) { + config.validatePlatforms(allPlatforms); + } + } + /// Merges this with [other]. /// /// For most fields, if both configurations have values set, [other]'s value @@ -427,6 +466,14 @@ class Configuration { filename: other._filename ?? _filename, chosenPresets: chosenPresets.union(other.chosenPresets), presets: _mergeConfigMaps(presets, other.presets), + overridePlatforms: mergeUnmodifiableMaps( + overridePlatforms, other.overridePlatforms, + value: (settings1, settings2) => new PlatformSettings( + settings1.identifier, + settings1.identifierSpan, + settings1.settings.toList()..addAll(settings2.settings))), + definePlatforms: + mergeUnmodifiableMaps(definePlatforms, other.definePlatforms), noRetry: other._noRetry ?? _noRetry, suiteDefaults: suiteDefaults.merge(other.suiteDefaults)); result = result._resolvePresets(); @@ -459,6 +506,8 @@ class Configuration { Glob filename, Iterable<String> chosenPresets, Map<String, Configuration> presets, + Map<String, PlatformSettings> overridePlatforms, + Map<String, CustomPlatform> definePlatforms, bool noRetry, // Suite-level configuration @@ -467,7 +516,7 @@ class Configuration { Iterable<String> dart2jsArgs, String precompiledPath, Iterable<Pattern> patterns, - Iterable<TestPlatform> platforms, + Iterable<PlatformSelection> platforms, BooleanSelector includeTags, BooleanSelector excludeTags, Map<BooleanSelector, SuiteConfiguration> tags, @@ -499,6 +548,8 @@ class Configuration { filename: filename ?? _filename, chosenPresets: chosenPresets ?? this.chosenPresets, presets: presets ?? this.presets, + overridePlatforms: overridePlatforms ?? this.overridePlatforms, + definePlatforms: definePlatforms ?? this.definePlatforms, noRetry: noRetry ?? _noRetry, suiteDefaults: suiteDefaults.change( jsTrace: jsTrace, diff --git a/lib/src/runner/configuration/args.dart b/lib/src/runner/configuration/args.dart index 83b0b05f261283467e64ddfffab0f750677df116..a3a84e7f8e1a28d79c3827654c553cd62b0283e6 100644 --- a/lib/src/runner/configuration/args.dart +++ b/lib/src/runner/configuration/args.dart @@ -10,6 +10,7 @@ import 'package:boolean_selector/boolean_selector.dart'; import '../../backend/test_platform.dart'; import '../../frontend/timeout.dart'; import '../configuration.dart'; +import 'platform_selection.dart'; import 'reporters.dart'; import 'values.dart'; @@ -17,7 +18,7 @@ import 'values.dart'; final ArgParser _parser = (() { var parser = new ArgParser(allowTrailingOptions: true); - var allPlatforms = TestPlatform.all.toList(); + var allPlatforms = TestPlatform.builtIn.toList()..remove(TestPlatform.vm); if (!Platform.isMacOS) allPlatforms.remove(TestPlatform.safari); if (!Platform.isWindows) allPlatforms.remove(TestPlatform.internetExplorer); @@ -62,9 +63,9 @@ final ArgParser _parser = (() { 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(), + help: 'The platform(s) on which to run the tests.\n' + '[vm (default), ' + '${allPlatforms.map((platform) => platform.identifier).join(", ")}]', allowMultiple: true); parser.addOption("preset", abbr: 'P', @@ -220,8 +221,9 @@ class _Parser { totalShards: totalShards, timeout: _parseOption('timeout', (value) => new Timeout.parse(value)), patterns: patterns, - platforms: - (_ifParsed('platform') as List<String>)?.map(TestPlatform.find), + platforms: (_ifParsed('platform') as List<String>) + ?.map((platform) => new PlatformSelection(platform)) + ?.toList(), runSkipped: _ifParsed('run-skipped'), chosenPresets: _ifParsed('preset') as List<String>, paths: _options.rest.isEmpty ? null : _options.rest, diff --git a/lib/src/runner/configuration/custom_platform.dart b/lib/src/runner/configuration/custom_platform.dart new file mode 100644 index 0000000000000000000000000000000000000000..c4372de1414642e70b61f2fc6dc9667489a5d434 --- /dev/null +++ b/lib/src/runner/configuration/custom_platform.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2017, 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 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +/// A user-defined test platform, based on an existing platform but with +/// different configuration. +class CustomPlatform { + /// The human-friendly name of the platform. + final String name; + + /// The location that [name] was defined in the configuration file. + final SourceSpan nameSpan; + + /// The identifier used to look up the platform. + final String identifier; + + /// The location that [identifier] was defined in the configuration file. + final SourceSpan identifierSpan; + + /// The identifier of the platform that this extends. + final String parent; + + /// The location that [parent] was defined in the configuration file. + final SourceSpan parentSpan; + + /// The user's settings for this platform. + final YamlMap settings; + + CustomPlatform(this.name, this.nameSpan, this.identifier, this.identifierSpan, + this.parent, this.parentSpan, this.settings); +} diff --git a/lib/src/runner/configuration/load.dart b/lib/src/runner/configuration/load.dart index 1fdfc002b1d64b1ef3ee5a67ee392eee7cbab945..cd9acf99c9ed9667514e5ac63450bf4ea2985c6e 100644 --- a/lib/src/runner/configuration/load.dart +++ b/lib/src/runner/configuration/load.dart @@ -13,12 +13,14 @@ import 'package:yaml/yaml.dart'; import '../../backend/operating_system.dart'; import '../../backend/platform_selector.dart'; -import '../../backend/test_platform.dart'; import '../../frontend/timeout.dart'; import '../../util/io.dart'; import '../../utils.dart'; import '../configuration.dart'; import '../configuration/suite.dart'; +import 'custom_platform.dart'; +import 'platform_selection.dart'; +import 'platform_settings.dart'; import 'reporters.dart'; /// A regular expression matching a Dart identifier. @@ -94,7 +96,7 @@ class _ConfigurationLoader { var onPlatform = _getMap("on_platform", key: (keyNode) => _parseNode(keyNode, "on_platform key", - (value) => new PlatformSelector.parse(value)), + (value) => new PlatformSelector.parse(value, keyNode.span)), value: (valueNode) => _nestedConfig(valueNode, "on_platform value", runnerConfig: false)); @@ -155,8 +157,7 @@ class _ConfigurationLoader { skip = true; } - var testOn = - _parseValue("test_on", (value) => new PlatformSelector.parse(value)); + var testOn = _parsePlatformSelector("test_on"); var addTags = _getList( "add_tags", (tagNode) => _parseIdentifierLike(tagNode, "Tag name")); @@ -193,6 +194,7 @@ class _ConfigurationLoader { _disallow("plain_names"); _disallow("platforms"); _disallow("add_presets"); + _disallow("override_platforms"); return Configuration.empty; } @@ -206,27 +208,50 @@ class _ConfigurationLoader { var concurrency = _getInt("concurrency"); - 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); - }); + var platforms = _getList( + "platforms", + (platformNode) => new PlatformSelection( + _parseIdentifierLike(platformNode, "Platform name"), + platformNode.span)); var chosenPresets = _getList("add_presets", (presetNode) => _parseIdentifierLike(presetNode, "Preset name")); + var overridePlatforms = _loadOverridePlatforms(); + return new Configuration( pauseAfterLoad: pauseAfterLoad, runSkipped: runSkipped, reporter: reporter, concurrency: concurrency, platforms: platforms, - chosenPresets: chosenPresets); + chosenPresets: chosenPresets, + overridePlatforms: overridePlatforms); + } + + /// Loads the `override_platforms` field. + Map<String, PlatformSettings> _loadOverridePlatforms() { + var platformsNode = + _getNode("override_platforms", "map", (value) => value is Map) + as YamlMap; + if (platformsNode == null) return const {}; + + var platforms = <String, PlatformSettings>{}; + platformsNode.nodes.forEach((identifierNode, valueNode) { + var identifier = + _parseIdentifierLike(identifierNode, "Platform identifier"); + + _validate(valueNode, "Platform definition must be a map.", + (value) => value is Map); + var map = valueNode as YamlMap; + + var settings = _expect(map, "settings"); + _validate(settings, "Must be a map.", (value) => value is Map); + + platforms[identifier] = new PlatformSettings( + identifier, identifierNode.span, [settings as YamlMap]); + }); + return platforms; } /// Loads runner configuration that's not allowed in the global configuration @@ -243,6 +268,7 @@ class _ConfigurationLoader { _disallow("filename"); _disallow("include_tags"); _disallow("exclude_tags"); + _disallow("define_platforms"); return Configuration.empty; } @@ -270,13 +296,16 @@ class _ConfigurationLoader { var includeTags = _parseBooleanSelector("include_tags"); var excludeTags = _parseBooleanSelector("exclude_tags"); + var definePlatforms = _loadDefinePlatforms(); + return new Configuration( pubServePort: pubServePort, patterns: patterns, paths: paths, filename: filename, includeTags: includeTags, - excludeTags: excludeTags); + excludeTags: excludeTags, + definePlatforms: definePlatforms); } /// Returns a map representation of the `fold_stack_frames` configuration. @@ -317,6 +346,43 @@ class _ConfigurationLoader { }); } + /// Loads the `define_platforms` field. + Map<String, CustomPlatform> _loadDefinePlatforms() { + var platformsNode = + _getNode("define_platforms", "map", (value) => value is Map) as YamlMap; + if (platformsNode == null) return const {}; + + var platforms = <String, CustomPlatform>{}; + platformsNode.nodes.forEach((identifierNode, valueNode) { + var identifier = + _parseIdentifierLike(identifierNode, "Platform identifier"); + + _validate(valueNode, "Platform definition must be a map.", + (value) => value is Map); + var map = valueNode as YamlMap; + + var nameNode = _expect(map, "name"); + _validate(nameNode, "Must be a string.", (value) => value is String); + var name = nameNode.value as String; + + var parentNode = _expect(map, "extends"); + var parent = _parseIdentifierLike(parentNode, "Platform parent"); + + var settings = _expect(map, "settings"); + _validate(settings, "Must be a map.", (value) => value is Map); + + platforms[identifier] = new CustomPlatform( + name, + nameNode.span, + identifier, + identifierNode.span, + parent, + parentNode.span, + settings as YamlMap); + }); + return platforms; + } + /// Throws an exception with [message] if [test] returns `false` when passed /// [node]'s value. void _validate(YamlNode node, String message, bool test(value)) { @@ -411,6 +477,14 @@ class _ConfigurationLoader { BooleanSelector _parseBooleanSelector(String name) => _parseValue(name, (value) => new BooleanSelector.parse(value)); + /// Parses [node]'s value as a platform selector. + PlatformSelector _parsePlatformSelector(String field) { + var node = _document.nodes[field]; + if (node == null) return null; + return _parseNode( + node, field, (value) => new PlatformSelector.parse(value, node.span)); + } + /// Asserts that [node] is a string, passes its value to [parse], and returns /// the result. /// @@ -485,6 +559,15 @@ class _ConfigurationLoader { } } + /// Asserts that [map] has a field named [field] and returns it. + YamlNode _expect(YamlMap map, String field) { + var node = map.nodes[field]; + if (node != null) return node; + + throw new SourceSpanFormatException( + 'Missing required field "$field".', map.span, _source); + } + /// Throws an error if a field named [field] exists at this level. void _disallow(String field) { if (!_document.containsKey(field)) return; diff --git a/lib/src/runner/configuration/platform_selection.dart b/lib/src/runner/configuration/platform_selection.dart new file mode 100644 index 0000000000000000000000000000000000000000..386ecfdd990e300726262207ff28f87b8e1dd6f3 --- /dev/null +++ b/lib/src/runner/configuration/platform_selection.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2017, 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 'package:source_span/source_span.dart'; + +/// A platform on which the user has chosen to run tests. +class PlatformSelection { + /// The name of the platform. + final String name; + + /// The location in the configuration file of this platform string, or `null` + /// if it was defined outside a configuration file (for example, on the + /// command line). + final SourceSpan span; + + PlatformSelection(this.name, [this.span]); + + bool operator ==(other) => other is PlatformSelection && other.name == name; + + int get hashCode => name.hashCode; +} diff --git a/lib/src/runner/configuration/platform_settings.dart b/lib/src/runner/configuration/platform_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..20390156467e5354bd553d6959441e24a01feec9 --- /dev/null +++ b/lib/src/runner/configuration/platform_settings.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2017, 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 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import '../plugin/customizable_platform.dart'; + +/// User-defined settings for a built-in test platform. +class PlatformSettings { + /// The identifier used to look up the platform being overridden. + final String identifier; + + /// The location that [identifier] was defined in the configuration file. + final SourceSpan identifierSpan; + + /// The user's settings for this platform. + /// + /// This is a list of settings, from most global to most specific, that will + /// eventually be merged using [CustomizablePlatform.mergePlatformSettings]. + final List<YamlMap> settings; + + PlatformSettings(this.identifier, this.identifierSpan, List<YamlMap> settings) + : settings = new List.unmodifiable(settings); +} diff --git a/lib/src/runner/configuration/suite.dart b/lib/src/runner/configuration/suite.dart index c41f88c459c3404dd4403900f0f8aab984b53fec..6ed34586c9d6e45f31a35e354fc78a0aaaa9eb55 100644 --- a/lib/src/runner/configuration/suite.dart +++ b/lib/src/runner/configuration/suite.dart @@ -4,12 +4,14 @@ import 'package:boolean_selector/boolean_selector.dart'; import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; import '../../backend/metadata.dart'; import '../../backend/operating_system.dart'; import '../../backend/platform_selector.dart'; import '../../backend/test_platform.dart'; import '../../frontend/timeout.dart'; +import 'platform_selection.dart'; /// Suite-level configuration. /// @@ -51,8 +53,10 @@ class SuiteConfiguration { final Set<Pattern> patterns; /// The set of platforms on which to run tests. - List<TestPlatform> get platforms => _platforms ?? const [TestPlatform.vm]; - final List<TestPlatform> _platforms; + List<String> get platforms => _platforms == null + ? const ["vm"] + : new List.unmodifiable(_platforms.map((platform) => platform.name)); + final List<PlatformSelection> _platforms; /// Only run tests whose tags match this selector. /// @@ -124,7 +128,7 @@ class SuiteConfiguration { Iterable<String> dart2jsArgs, String precompiledPath, Iterable<Pattern> patterns, - Iterable<TestPlatform> platforms, + Iterable<PlatformSelection> platforms, BooleanSelector includeTags, BooleanSelector excludeTags, Map<BooleanSelector, SuiteConfiguration> tags, @@ -172,7 +176,7 @@ class SuiteConfiguration { Iterable<String> dart2jsArgs, this.precompiledPath, Iterable<Pattern> patterns, - Iterable<TestPlatform> platforms, + Iterable<PlatformSelection> platforms, BooleanSelector includeTags, BooleanSelector excludeTags, Map<BooleanSelector, SuiteConfiguration> tags, @@ -249,7 +253,7 @@ class SuiteConfiguration { Iterable<String> dart2jsArgs, String precompiledPath, Iterable<Pattern> patterns, - Iterable<TestPlatform> platforms, + Iterable<PlatformSelection> platforms, BooleanSelector includeTags, BooleanSelector excludeTags, Map<BooleanSelector, SuiteConfiguration> tags, @@ -287,6 +291,32 @@ class SuiteConfiguration { return config._resolveTags(); } + /// Throws a [FormatException] if [this] refers to any undefined platforms. + void validatePlatforms(List<TestPlatform> allPlatforms) { + var validVariables = + allPlatforms.map((platform) => platform.identifier).toSet(); + _metadata.validatePlatformSelectors(validVariables); + + if (_platforms != null) { + for (var selection in _platforms) { + if (!allPlatforms + .any((platform) => platform.identifier == selection.name)) { + if (selection.span != null) { + throw new SourceSpanFormatException( + 'Unknown platform "${selection.name}".', selection.span); + } else { + throw new FormatException('Unknown platform "${selection.name}".'); + } + } + } + } + + onPlatform.forEach((selector, config) { + selector.validate(validVariables); + config.validatePlatforms(allPlatforms); + }); + } + /// Returns a copy of [this] with all platform-specific configuration from /// [onPlatform] resolved. SuiteConfiguration forPlatform(TestPlatform platform, {OperatingSystem os}) { diff --git a/lib/src/runner/executable_settings.dart b/lib/src/runner/executable_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..069fa39af745f99e424446a1498ade16fb4cb414 --- /dev/null +++ b/lib/src/runner/executable_settings.dart @@ -0,0 +1,162 @@ +// Copyright (c) 2017, 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:io/io.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +/// User-provided settings for invoking an executable. +class ExecutableSettings { + /// Additional arguments to pass to the executable. + final List<String> arguments; + + /// The path to the executable on Linux. + /// + /// This may be an absolute path or a basename, in which case it will be + /// looked up on the system path. It may not be relative. + final String _linuxExecutable; + + /// The path to the executable on Mac OS. + /// + /// This may be an absolute path or a basename, in which case it will be + /// looked up on the system path. It may not be relative. + final String _macOSExecutable; + + /// The path to the executable on Windows. + /// + /// This may be an absolute path; a basename, in which case it will be looked + /// up on the system path; or a relative path, in which case it will be looked + /// up relative to the paths in the `LOCALAPPDATA`, `PROGRAMFILES`, and + /// `PROGRAMFILES(X64)` environment variables. + final String _windowsExecutable; + + /// The path to the executable for the current operating system. + String get executable { + if (Platform.isMacOS) return _macOSExecutable; + if (!Platform.isWindows) return _linuxExecutable; + if (p.isAbsolute(_windowsExecutable)) return _windowsExecutable; + if (p.basename(_windowsExecutable) == _windowsExecutable) { + return _windowsExecutable; + } + + var prefixes = [ + Platform.environment['LOCALAPPDATA'], + Platform.environment['PROGRAMFILES'], + Platform.environment['PROGRAMFILES(X86)'] + ]; + + for (var prefix in prefixes) { + if (prefix == null) continue; + + var path = p.join(prefix, _windowsExecutable); + if (new File(path).existsSync()) return path; + } + + // If we can't find a path that works, return one that doesn't. This will + // cause an "executable not found" error to surface. + return p.join( + prefixes.firstWhere((prefix) => prefix != null, orElse: () => '.'), + _windowsExecutable); + } + + /// Parses settings from a user-provided YAML mapping. + factory ExecutableSettings.parse(YamlMap settings) { + List<String> arguments; + var argumentsNode = settings.nodes["arguments"]; + if (argumentsNode != null) { + if (argumentsNode.value is String) { + try { + arguments = shellSplit(argumentsNode.value); + } on FormatException catch (error) { + throw new SourceSpanFormatException( + error.message, argumentsNode.span); + } + } else { + throw new SourceSpanFormatException( + "Must be a string.", argumentsNode.span); + } + } + + String linuxExecutable; + String macOSExecutable; + String windowsExecutable; + var executableNode = settings.nodes["executable"]; + if (executableNode != null) { + if (executableNode.value is String) { + // Don't check this on Windows because people may want to set relative + // paths in their global config. + if (!Platform.isWindows) _assertNotRelative(executableNode); + + linuxExecutable = executableNode.value; + macOSExecutable = executableNode.value; + windowsExecutable = executableNode.value; + } else if (executableNode is YamlMap) { + linuxExecutable = _getExecutable(executableNode.nodes["linux"]); + macOSExecutable = _getExecutable(executableNode.nodes["mac_os"]); + windowsExecutable = _getExecutable(executableNode.nodes["windows"], + allowRelative: true); + } else { + throw new SourceSpanFormatException( + "Must be a map or a string.", executableNode.span); + } + } + + return new ExecutableSettings( + arguments: arguments, + linuxExecutable: linuxExecutable, + macOSExecutable: macOSExecutable, + windowsExecutable: windowsExecutable); + } + + /// Asserts that [executableNode] is a string or `null` and returns it. + /// + /// If [allowRelative] is `false` (the default), asserts that the value isn't + /// a relative path. + static String _getExecutable(YamlNode executableNode, + {bool allowRelative: false}) { + if (executableNode == null || executableNode.value == null) return null; + if (executableNode.value is! String) { + throw new SourceSpanFormatException( + "Must be a string.", executableNode.span); + } + if (!allowRelative) _assertNotRelative(executableNode); + return executableNode.value; + } + + /// Throws a [SourceSpanFormatException] if [executableNode]'s value is a + /// relative POSIX path that's not just a plain basename. + /// + /// We loop up basenames on the PATH and we can resolve absolute paths, but we + /// have no way of interpreting relative paths. + static void _assertNotRelative(YamlScalar executableNode) { + var executable = executableNode.value as String; + if (!p.posix.isRelative(executable)) return; + if (p.posix.basename(executable) == executable) return; + + throw new SourceSpanFormatException( + "Linux and Mac OS executables may not be relative paths.", + executableNode.span); + } + + ExecutableSettings( + {Iterable<String> arguments, + String linuxExecutable, + String macOSExecutable, + String windowsExecutable}) + : arguments = + arguments == null ? const [] : new List.unmodifiable(arguments), + _linuxExecutable = linuxExecutable, + _macOSExecutable = macOSExecutable, + _windowsExecutable = windowsExecutable; + + /// Merges [this] with [other], with [other]'s settings taking priority. + ExecutableSettings merge(ExecutableSettings other) => new ExecutableSettings( + arguments: arguments.toList()..addAll(other.arguments), + linuxExecutable: other._linuxExecutable ?? _linuxExecutable, + macOSExecutable: other._macOSExecutable ?? _macOSExecutable, + windowsExecutable: other._windowsExecutable ?? _windowsExecutable); +} diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart index dc654f6295a6ca39e2fe9c7abd83ebaa5b650606..5f0e8ae530806a4421160f222af732aa93587677 100644 --- a/lib/src/runner/loader.dart +++ b/lib/src/runner/loader.dart @@ -8,6 +8,8 @@ import 'dart:io'; import 'package:analyzer/analyzer.dart' hide Configuration; import 'package:async/async.dart'; import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; import '../backend/group.dart'; import '../backend/invoker.dart'; @@ -21,12 +23,16 @@ import 'load_exception.dart'; import 'load_suite.dart'; import 'node/platform.dart'; import 'parse_metadata.dart'; +import 'plugin/customizable_platform.dart'; import 'plugin/environment.dart'; import 'plugin/hack_register_platform.dart'; import 'plugin/platform.dart'; import 'runner_suite.dart'; import 'vm/platform.dart'; +// TODO(nweiz): Use inline function types when sdk#30858 is fixed. +typedef FutureOr<PlatformPlugin> _PlatformPluginFunction(); + /// A class for finding test files and loading them into a runnable form. class Loader { /// The test runner configuration. @@ -41,17 +47,46 @@ class Loader { /// The functions to use to load [_platformPlugins]. /// /// These are passed to the plugins' async memoizers when a plugin is needed. - final _platformCallbacks = <TestPlatform, AsyncFunction>{}; + final _platformCallbacks = <TestPlatform, _PlatformPluginFunction>{}; + + /// A map of all platforms registered in [_platformCallbacks], indexed by + /// their string identifiers. + final _platformsByIdentifier = <String, TestPlatform>{}; + + /// The user-provided settings for platforms, as a list of settings that will + /// be merged together using [CustomizablePlatform.mergePlatformSettings]. + final _platformSettings = <TestPlatform, List<YamlMap>>{}; + + /// The user-provided settings for platforms. + final _parsedPlatformSettings = <TestPlatform, Object>{}; + + /// All plaforms supported by this [Loader]. + List<TestPlatform> get allPlatforms => + new List.unmodifiable(_platformCallbacks.keys); + + /// The platform variables supported by this loader, in addition the default + /// variables that are always supported. + Iterable<String> get _platformVariables => + _platformCallbacks.keys.map((platform) => platform.identifier); /// Creates a new loader that loads tests on platforms defined in /// [Configuration.current]. /// /// [root] is the root directory that will be served for browser tests. It /// defaults to the working directory. - Loader({String root}) { - registerPlatformPlugin([TestPlatform.vm], () => new VMPlatform()); - registerPlatformPlugin([TestPlatform.nodeJS], () => new NodePlatform()); - registerPlatformPlugin([ + /// + /// The [plugins] register [PlatformPlugin]s that are associated with the + /// provided platforms. When the runner first requests that a suite be loaded + /// for one of the given platforms, the lodaer will call the associated + /// callback to load the platform plugin. That plugin is then preserved and + /// used to load all suites for all matching platforms. Platform plugins may + /// override built-in platforms. + Loader( + {String root, + Map<Iterable<TestPlatform>, _PlatformPluginFunction> plugins}) { + _registerPlatformPlugin([TestPlatform.vm], () => new VMPlatform()); + _registerPlatformPlugin([TestPlatform.nodeJS], () => new NodePlatform()); + _registerPlatformPlugin([ TestPlatform.dartium, TestPlatform.contentShell, TestPlatform.chrome, @@ -62,27 +97,75 @@ class Loader { ], () => BrowserPlatform.start(root: root)); platformCallbacks.forEach((platform, plugin) { - registerPlatformPlugin([platform], plugin); + _registerPlatformPlugin([platform], plugin); }); + + plugins?.forEach(_registerPlatformPlugin); + + _registerCustomPlatforms(); + + _config.validatePlatforms(allPlatforms); + + _registerPlatformOverrides(); } /// Registers a [PlatformPlugin] for [platforms]. - /// - /// When the runner first requests that a suite be loaded for one of the given - /// platforms, this will call [getPlugin] to load the platform plugin. It may - /// return either a [PlatformPlugin] or a [Future<PlatformPlugin>]. That - /// plugin is then preserved and used to load all suites for all matching - /// platforms. - /// - /// This overwrites previous plugins for those platforms. - void registerPlatformPlugin(Iterable<TestPlatform> platforms, getPlugin()) { + void _registerPlatformPlugin( + Iterable<TestPlatform> platforms, FutureOr<PlatformPlugin> getPlugin()) { var memoizer = new AsyncMemoizer<PlatformPlugin>(); for (var platform in platforms) { _platformPlugins[platform] = memoizer; _platformCallbacks[platform] = getPlugin; + _platformsByIdentifier[platform.identifier] = platform; } } + /// Registers user-defined platforms from [Configuration.definePlatforms]. + void _registerCustomPlatforms() { + for (var customPlatform in _config.definePlatforms.values) { + if (_platformsByIdentifier.containsKey(customPlatform.identifier)) { + throw new SourceSpanFormatException( + wordWrap( + 'The platform "${customPlatform.identifier}" already exists. ' + 'Use override_platforms to override it.'), + customPlatform.identifierSpan); + } + + var parent = _platformsByIdentifier[customPlatform.parent]; + if (parent == null) { + throw new SourceSpanFormatException( + 'Unknown platform.', customPlatform.parentSpan); + } + + var platform = + parent.extend(customPlatform.name, customPlatform.identifier); + _platformPlugins[platform] = _platformPlugins[parent]; + _platformCallbacks[platform] = _platformCallbacks[parent]; + _platformsByIdentifier[platform.identifier] = platform; + + _platformSettings[platform] = [customPlatform.settings]; + } + } + + /// Registers users' platform settings from [Configuration.overridePlatforms]. + void _registerPlatformOverrides() { + for (var settings in _config.overridePlatforms.values) { + var platform = _platformsByIdentifier[settings.identifier]; + + // This is officially validated in [Configuration.validatePlatforms]. + assert(platform != null); + + _platformSettings + .putIfAbsent(platform, () => []) + .addAll(settings.settings); + } + } + + /// Returns the [TestPlatform] registered with this loader that's identified + /// by [identifier], or `null` if none can be found. + TestPlatform findTestPlatform(String identifier) => + _platformsByIdentifier[identifier]; + /// Loads all test suites in [dir] according to [suiteConfig]. /// /// This will load tests from files that match the global configuration's @@ -117,8 +200,8 @@ class Loader { Stream<LoadSuite> loadFile( String path, SuiteConfiguration suiteConfig) async* { try { - suiteConfig = suiteConfig - .merge(new SuiteConfiguration.fromMetadata(parseMetadata(path))); + suiteConfig = suiteConfig.merge(new SuiteConfiguration.fromMetadata( + parseMetadata(path, _platformVariables.toSet()))); } on AnalyzerErrorGroup catch (_) { // Ignore the analyzer's error, since its formatting is much worse than // the VM's or dart2js's. @@ -137,7 +220,10 @@ class Loader { return; } - for (var platform in suiteConfig.platforms) { + for (var platformName in suiteConfig.platforms) { + var platform = findTestPlatform(platformName); + assert(platform != null, 'Unknown platform "$platformName".'); + if (!suiteConfig.metadata.testOn.evaluate(platform, os: currentOS)) { continue; } @@ -163,7 +249,9 @@ class Loader { try { var plugin = await memo.runOnce(_platformCallbacks[platform]); - var suite = await plugin.load(path, platform, platformConfig); + _customizePlatform(plugin, platform); + var suite = await plugin.load(path, platform, platformConfig, + {"platformVariables": _platformVariables.toList()}); if (suite != null) _suites.add(suite); return suite; } catch (error, stackTrace) { @@ -175,6 +263,39 @@ class Loader { } } + /// Passes user-defined settings to [plugin] if necessary. + void _customizePlatform(PlatformPlugin plugin, TestPlatform platform) { + var parsed = _parsedPlatformSettings[platform]; + if (parsed != null) { + (plugin as CustomizablePlatform).customizePlatform(platform, parsed); + return; + } + + var settings = _platformSettings[platform]; + if (settings == null) return; + + if (plugin is CustomizablePlatform) { + parsed = settings + .map(plugin.parsePlatformSettings) + .reduce(plugin.mergePlatformSettings); + plugin.customizePlatform(platform, parsed); + _parsedPlatformSettings[platform] = parsed; + } else { + String identifier; + SourceSpan span; + if (platform.isChild) { + identifier = platform.parent.identifier; + span = _config.definePlatforms[platform.identifier].parentSpan; + } else { + identifier = platform.identifier; + span = _config.overridePlatforms[platform.identifier].identifierSpan; + } + + throw new SourceSpanFormatException( + 'The "$identifier" platform can\'t be customized.', span); + } + } + Future closeEphemeral() async { await Future.wait(_platformPlugins.values.map((memo) async { if (!memo.hasRun) return; diff --git a/lib/src/runner/node/platform.dart b/lib/src/runner/node/platform.dart index 179edc41d7728abf683f94093f5e7aeb37bb45d8..c6e76334cc5b20913b688989a29f57859baac688 100644 --- a/lib/src/runner/node/platform.dart +++ b/lib/src/runner/node/platform.dart @@ -11,22 +11,27 @@ import 'package:node_preamble/preamble.dart' as preamble; import 'package:package_resolver/package_resolver.dart'; import 'package:path/path.dart' as p; import 'package:stream_channel/stream_channel.dart'; +import 'package:yaml/yaml.dart'; import '../../backend/test_platform.dart'; import '../../util/io.dart'; import '../../util/stack_trace_mapper.dart'; import '../../utils.dart'; +import '../application_exception.dart'; import '../compiler_pool.dart'; import '../configuration.dart'; import '../configuration/suite.dart'; +import '../executable_settings.dart'; import '../load_exception.dart'; +import '../plugin/customizable_platform.dart'; import '../plugin/environment.dart'; import '../plugin/platform.dart'; import '../plugin/platform_helpers.dart'; import '../runner_suite.dart'; /// A platform that loads tests in Node.js processes. -class NodePlatform extends PlatformPlugin { +class NodePlatform extends PlatformPlugin + implements CustomizablePlatform<ExecutableSettings> { /// The test runner configuration. final Configuration _config; @@ -39,24 +44,43 @@ class NodePlatform extends PlatformPlugin { /// The HTTP client to use when fetching JS files for `pub serve`. final HttpClient _http; - /// The Node executable to use. - String get _executable => Platform.isWindows ? "node.exe" : "node"; + /// Executable settings for [TestPlatform.nodeJS] and platforms that extend + /// it. + final _settings = { + TestPlatform.nodeJS: new ExecutableSettings( + linuxExecutable: "node", + macOSExecutable: "node", + windowsExecutable: "node.exe") + }; NodePlatform() : _config = Configuration.current, _http = Configuration.current.pubServeUrl == null ? null : new HttpClient(); + ExecutableSettings parsePlatformSettings(YamlMap settings) => + new ExecutableSettings.parse(settings); + + ExecutableSettings mergePlatformSettings( + ExecutableSettings settings1, ExecutableSettings settings2) => + settings1.merge(settings2); + + void customizePlatform(TestPlatform platform, ExecutableSettings settings) { + var oldSettings = _settings[platform] ?? _settings[platform.root]; + if (oldSettings != null) settings = oldSettings.merge(settings); + _settings[platform] = settings; + } + StreamChannel loadChannel(String path, TestPlatform platform) => throw new UnimplementedError(); Future<RunnerSuite> load(String path, TestPlatform platform, - SuiteConfiguration suiteConfig) async { + SuiteConfiguration suiteConfig, Object message) async { assert(platform == TestPlatform.nodeJS); - var pair = await _loadChannel(path, suiteConfig); - var controller = await deserializeSuite( - path, platform, suiteConfig, new PluginEnvironment(), pair.first, + var pair = await _loadChannel(path, platform, suiteConfig); + var controller = await deserializeSuite(path, platform, suiteConfig, + new PluginEnvironment(), pair.first, message, mapper: pair.last); return controller.suite; } @@ -65,9 +89,9 @@ class NodePlatform extends PlatformPlugin { /// /// Returns that channel along with a [StackTraceMapper] representing the /// source map for the compiled suite. - Future<Pair<StreamChannel, StackTraceMapper>> _loadChannel( - String path, SuiteConfiguration suiteConfig) async { - var pair = await _spawnProcess(path, suiteConfig); + Future<Pair<StreamChannel, StackTraceMapper>> _loadChannel(String path, + TestPlatform platform, SuiteConfiguration suiteConfig) async { + var pair = await _spawnProcess(path, platform, suiteConfig); var process = pair.first; // Node normally doesn't emit any standard error, but if it does we forward @@ -91,8 +115,8 @@ class NodePlatform extends PlatformPlugin { /// /// Returns that channel along with a [StackTraceMapper] representing the /// source map for the compiled suite. - Future<Pair<Process, StackTraceMapper>> _spawnProcess( - String path, SuiteConfiguration suiteConfig) async { + Future<Pair<Process, StackTraceMapper>> _spawnProcess(String path, + TestPlatform platform, SuiteConfiguration suiteConfig) async { var dir = new Directory(_compiledDir).createTempSync('test_').path; var jsPath = p.join(dir, p.basename(path) + ".node_test.dart.js"); @@ -122,7 +146,7 @@ class NodePlatform extends PlatformPlugin { sdkRoot: p.toUri(sdkDir)); } - return new Pair(await Process.start(_executable, [jsPath]), mapper); + return new Pair(await _startProcess(platform, jsPath), mapper); } var url = _config.pubServeUrl.resolveUri( @@ -141,7 +165,22 @@ class NodePlatform extends PlatformPlugin { sdkRoot: p.toUri('packages/\$sdk')); } - return new Pair(await Process.start(_executable, [jsPath]), mapper); + return new Pair(await _startProcess(platform, jsPath), mapper); + } + + /// Starts the Node.js process for [platform] with [jsPath]. + Future<Process> _startProcess(TestPlatform platform, String jsPath) async { + var settings = _settings[platform]; + try { + return await Process.start( + settings.executable, settings.arguments.toList()..add(jsPath)); + } catch (error, stackTrace) { + await new Future.error( + new ApplicationException( + "Failed to run ${platform.name}: ${getErrorMessage(error)}"), + stackTrace); + return null; + } } /// Runs an HTTP GET on [url]. diff --git a/lib/src/runner/parse_metadata.dart b/lib/src/runner/parse_metadata.dart index 1ab4c30b842f53eedcd2d5cc43b3259c630d727a..e8aa1ae22417770e204531464816423cf91252b6 100644 --- a/lib/src/runner/parse_metadata.dart +++ b/lib/src/runner/parse_metadata.dart @@ -18,22 +18,31 @@ import '../utils.dart'; /// Parse the test metadata for the test file at [path]. /// +/// The [platformVariables] are the set of variables that are valid for platform +/// selectors in suite metadata, in addition to the built-in variables that are +/// allowed everywhere. +/// /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the /// test annotations are incorrect. -Metadata parseMetadata(String path) => new _Parser(path).parse(); +Metadata parseMetadata(String path, Set<String> platformVariables) => + new _Parser(path, platformVariables).parse(); /// A parser for test suite metadata. class _Parser { /// The path to the test suite. final String _path; + /// The set of variables that are valid for platform selectors, in addition to + /// the built-in variables that are allowed everywhere. + final Set<String> _platformVariables; + /// All annotations at the top of the file. List<Annotation> _annotations; /// All prefixes defined by imports in this file. Set<String> _prefixes; - _Parser(String path) : _path = path { + _Parser(String path, this._platformVariables) : _path = path { var contents = new File(path).readAsStringSync(); var directives = parseDirectives(contents, name: path).directives; _annotations = directives.isEmpty ? [] : directives.first.metadata; @@ -105,9 +114,17 @@ class _Parser { PlatformSelector _parseTestOn(Annotation annotation, String constructorName) { _assertConstructorName(constructorName, 'TestOn', annotation); _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1); - var literal = _parseString(annotation.arguments.arguments.first); + return _parsePlatformSelector(annotation.arguments.arguments.first); + } + + /// Parses an [expression] that should contain a string representing a + /// [PlatformSelector]. + PlatformSelector _parsePlatformSelector(Expression expression) { + var literal = _parseString(expression); return _contextualize( - literal, () => new PlatformSelector.parse(literal.stringValue)); + literal, + () => new PlatformSelector.parse(literal.stringValue) + ..validate(_platformVariables)); } /// Parses a `@Retry` annotation. @@ -217,9 +234,7 @@ class _Parser { positional: 1); return _parseMap(annotation.arguments.arguments.first, key: (key) { - var selector = _parseString(key); - return _contextualize( - selector, () => new PlatformSelector.parse(selector.stringValue)); + return _parsePlatformSelector(key); }, value: (value) { var expressions = []; if (value is ListLiteral) { diff --git a/lib/src/runner/plugin/customizable_platform.dart b/lib/src/runner/plugin/customizable_platform.dart new file mode 100644 index 0000000000000000000000000000000000000000..e1ebd57e2e2e9bc3bae1cffd88f7860cc8aec198 --- /dev/null +++ b/lib/src/runner/plugin/customizable_platform.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2017, 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 'package:yaml/yaml.dart'; + +import '../../backend/test_platform.dart'; +import 'platform.dart'; + +/// An interface for [PlatformPlugin]s that support per-platform customization. +/// +/// If a [PlatformPlugin] implements this, the user will be able to override the +/// [TestPlatform]s it supports using the +/// [`override_platforms`][override_platforms] configuration field, and define +/// new platforms based on them using the [`define_platforms`][define_platforms] +/// field. The custom settings will be passed to the plugin using +/// [customizePlatform]. +/// +/// [override_platforms]: https://github.com/dart-lang/test/blob/master/doc/configuration.md#override_platforms +/// [define_platforms]: https://github.com/dart-lang/test/blob/master/doc/configuration.md#define_platforms +/// +/// Plugins that implement this **must** support children of recognized +/// platforms (created by [TestPlatform.extend]) in their [loadChannel] or +/// [load] methods. +abstract class CustomizablePlatform<T> extends PlatformPlugin { + /// Parses user-provided [settings] for a custom platform into a + /// plugin-defined format. + /// + /// The [settings] come from a user's configuration file. The parsed output + /// will be passed to [customizePlatform]. + /// + /// Subclasses should throw [SourceSpanFormatException]s if [settings] + /// contains invalid configuration. Unrecognized fields should be ignored if + /// possible. + T parsePlatformSettings(YamlMap settings); + + /// Merges [settings1] with [settings2] and returns a new settings object that + /// includes the configuration of both. + /// + /// When the settings conflict, [settings2] should take priority. + /// + /// This is used to merge global settings with local settings, or a custom + /// platform's settings with its parent's. + T mergePlatformSettings(T settings1, T settings2); + + /// Defines user-provided [settings] for [platform]. + /// + /// The [platform] is a platform this plugin was declared to accept when + /// registered with [Loader.registerPlatformPlugin], or a platform whose + /// [TestPlatform.parent] is one of those platforms. Subclasses should + /// customize the behavior for these platforms when [loadChannel] or [load] is + /// called with the given [platform], using the [settings] which are parsed by + /// [parsePlatformSettings]. This is guaranteed to be called before either + /// `load` method. + void customizePlatform(TestPlatform platform, T settings); +} diff --git a/lib/src/runner/plugin/platform.dart b/lib/src/runner/plugin/platform.dart index f3e7eec008ba6cfd0d50d0216fa9d4dd8000d3e6..744c46347a644d2b03218ba9bc27cfb4725da101 100644 --- a/lib/src/runner/plugin/platform.dart +++ b/lib/src/runner/plugin/platform.dart @@ -23,8 +23,8 @@ import 'platform_helpers.dart'; /// In order to support interactive debugging, a plugin must override [load] as /// well, which returns a [RunnerSuite] that can contain a custom [Environment] /// and control debugging metadata such as [RunnerSuite.isDebugging] and -/// [RunnerSuite.onDebugging]. To make this easier, implementations can call -/// [deserializeSuite] in `platform_helpers.dart`. +/// [RunnerSuite.onDebugging]. The plugin must create this suite by calling the +/// [deserializeSuite] helper function. /// /// A platform plugin can be registered with [Loader.registerPlatformPlugin]. abstract class PlatformPlugin { @@ -52,16 +52,16 @@ abstract class PlatformPlugin { /// fine-grained control over the [RunnerSuite], including providing a custom /// implementation of [Environment]. /// - /// It's recommended that subclasses overriding this method call - /// [deserializeSuite] in `platform_helpers.dart` to obtain a - /// [RunnerSuiteController]. + /// Subclasses overriding this method must call [deserializeSuite] in + /// `platform_helpers.dart` to obtain a [RunnerSuiteController]. They must + /// pass the opaque [message] parameter to the [deserializeSuite] call. Future<RunnerSuite> load(String path, TestPlatform platform, - SuiteConfiguration suiteConfig) async { + SuiteConfiguration suiteConfig, Object message) async { // loadChannel may throw an exception. That's fine; it will cause the // LoadSuite to emit an error, which will be presented to the user. var channel = loadChannel(path, platform); var controller = await deserializeSuite( - path, platform, suiteConfig, new PluginEnvironment(), channel); + path, platform, suiteConfig, new PluginEnvironment(), channel, message); return controller.suite; } diff --git a/lib/src/runner/plugin/platform_helpers.dart b/lib/src/runner/plugin/platform_helpers.dart index 786acd69921fba21de1a235266ef4ca05ca1457a..b41e5ba8491486ad0953618911a87ac0b65a2c43 100644 --- a/lib/src/runner/plugin/platform_helpers.dart +++ b/lib/src/runner/plugin/platform_helpers.dart @@ -33,24 +33,25 @@ final _deserializeTimeout = new Duration(minutes: 8); /// /// If the suite is closed, this will close [channel]. /// -/// If [mapTrace] is passed, it will be used to adjust stack traces for any -/// errors emitted by tests. +/// The [message] parameter is an opaque object passed from the runner to +/// [PlatformPlugin.load]. Plugins shouldn't interact with it other than to pass +/// it on to [deserializeSuite]. /// -/// If [asciiSymbols] is passed, it controls whether the `symbol` package is -/// configured to use plain ASCII or Unicode symbols. It defaults to `true` on -/// Windows and `false` elsewhere. +/// If [mapper] is passed, it will be used to adjust stack traces for any errors +/// emitted by tests. Future<RunnerSuiteController> deserializeSuite( String path, TestPlatform platform, SuiteConfiguration suiteConfig, Environment environment, StreamChannel channel, + Object message, {StackTraceMapper mapper}) async { var disconnector = new Disconnector(); var suiteChannel = new MultiChannel(channel.transform(disconnector)); suiteChannel.sink.add({ - 'platform': platform.identifier, + 'platform': platform.serialize(), 'metadata': suiteConfig.metadata.serialize(), 'os': platform == TestPlatform.vm ? currentOS.identifier : null, 'asciiGlyphs': Platform.isWindows, @@ -60,7 +61,7 @@ Future<RunnerSuiteController> deserializeSuite( 'stackTraceMapper': mapper?.serialize(), 'foldTraceExcept': Configuration.current.foldTraceExcept.toList(), 'foldTraceOnly': Configuration.current.foldTraceOnly.toList(), - }); + }..addAll(message as Map)); var completer = new Completer(); @@ -74,7 +75,7 @@ Future<RunnerSuiteController> deserializeSuite( // notify the user of the error. loadSuiteZone.handleUncaughtError(error, stackTrace); } else { - completer.completeError(error); + completer.completeError(error, stackTrace); } } diff --git a/lib/src/runner/remote_listener.dart b/lib/src/runner/remote_listener.dart index a61fd0edf9dfdeab246fbb3bd9ccd458cc9fead3..883101fa498e14023139f42058bdb887e449d2de 100644 --- a/lib/src/runner/remote_listener.dart +++ b/lib/src/runner/remote_listener.dart @@ -79,6 +79,7 @@ class RemoteListener { verboseChain = metadata.verboseTrace; var declarer = new Declarer( metadata: metadata, + platformVariables: message['platformVariables'].toSet(), collectTraces: message['collectTraces'], noRetry: message['noRetry']); @@ -89,11 +90,13 @@ class RemoteListener { await declarer.declare(main); - var os = - message['os'] == null ? null : OperatingSystem.find(message['os']); - var platform = TestPlatform.find(message['platform']); var suite = new Suite(declarer.build(), - platform: platform, os: os, path: message['path']); + platform: new TestPlatform.deserialize(message['platform']), + os: message['os'] == null + ? null + : OperatingSystem.find(message['os']), + path: message['path']); + new RemoteListener._(suite, printZone)._listen(channel); }, onError: (error, stackTrace) { _sendError(channel, error, stackTrace, verboseChain); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index f545e742563e81d522c447a6f3d7a75f2347dcab..2e06ef1c1a633fea41b31d20266c04b1472cabf4 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'package:async/async.dart'; +import 'package:collection/collection.dart'; import 'package:matcher/matcher.dart'; import 'package:path/path.dart' as p; import 'package:stream_channel/stream_channel.dart'; @@ -194,6 +195,17 @@ List flatten(Iterable nested) { return result; } +/// Like [mergeMaps], but assumes both maps are unmodifiable and so avoids +/// creating a new map unnecessarily. +/// +/// The return value *may or may not* be unmodifiable. +Map<K, V> mergeUnmodifiableMaps<K, V>(Map<K, V> map1, Map<K, V> map2, + {V value(V value1, V value2)}) { + if (map1.isEmpty) return map2; + if (map2.isEmpty) return map1; + return mergeMaps(map1, map2, value: value); +} + /// Truncates [text] to fit within [maxLength]. /// /// This will try to truncate along word boundaries and preserve words both at diff --git a/pubspec.yaml b/pubspec.yaml index 375ca446b2438f570624c61edfd676995c8a32e8..c1c0441f885b229518f359734da6aab3255d014f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: test -version: 0.12.24+8 +version: 0.12.25-dev author: Dart Team <misc@dartlang.org> description: A library for writing dart unit tests. homepage: https://github.com/dart-lang/test @@ -14,6 +14,7 @@ dependencies: collection: '^1.8.0' glob: '^1.0.0' http_multi_server: '>=1.0.0 <3.0.0' + io: '^0.3.0' js: '^0.6.0' meta: '^1.0.0' node_preamble: '^1.3.0' diff --git a/test/backend/metadata_test.dart b/test/backend/metadata_test.dart index b32dfaf98895e6277571fbb2a28583403c08261a..35fc9ef183cb1cef4008ae6b4e090ba8f4b890eb 100644 --- a/test/backend/metadata_test.dart +++ b/test/backend/metadata_test.dart @@ -173,7 +173,7 @@ void main() { test("refuses an invalid platform selector", () { expect(() { - new Metadata.parse(onPlatform: {"invalid": new Skip()}); + new Metadata.parse(onPlatform: {"vm &&": new Skip()}); }, throwsFormatException); }); @@ -194,6 +194,32 @@ void main() { }); }); + group("validatePlatformSelectors", () { + test("succeeds if onPlatform uses valid platforms", () { + new Metadata.parse(onPlatform: {"vm || browser": new Skip()}) + .validatePlatformSelectors(new Set.from(["vm"])); + }); + + test("succeeds if testOn uses valid platforms", () { + new Metadata.parse(testOn: "vm || browser") + .validatePlatformSelectors(new Set.from(["vm"])); + }); + + test("fails if onPlatform uses an invalid platform", () { + expect(() { + new Metadata.parse(onPlatform: {"unknown": new Skip()}) + .validatePlatformSelectors(new Set.from(["vm"])); + }, throwsFormatException); + }); + + test("fails if testOn uses an invalid platform", () { + expect(() { + new Metadata.parse(testOn: "unknown") + .validatePlatformSelectors(new Set.from(["vm"])); + }, throwsFormatException); + }); + }); + group("change", () { test("preserves all fields if no parameters are passed", () { var metadata = new Metadata( diff --git a/test/runner/browser/chrome_test.dart b/test/runner/browser/chrome_test.dart index 5f34c064c9b5994fe96f989767454fe21f5d179f..4ff242f356810c27b7fd75f23076d06e26c4e147 100644 --- a/test/runner/browser/chrome_test.dart +++ b/test/runner/browser/chrome_test.dart @@ -7,6 +7,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/src/runner/browser/chrome.dart'; import 'package:test/test.dart'; @@ -42,8 +43,11 @@ webSocket.addEventListener("open", function() { }); test("reports an error in onExit", () { - var chrome = - new Chrome("http://dart-lang.org", executable: "_does_not_exist"); + var chrome = new Chrome("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( chrome.onExit, throwsA(isApplicationException( diff --git a/test/runner/browser/content_shell_test.dart b/test/runner/browser/content_shell_test.dart index 2370cd472eedf2cbbda4fe55976d25f0d3329fda..f506f5d3d6418e4e17878f44146094e14730231e 100644 --- a/test/runner/browser/content_shell_test.dart +++ b/test/runner/browser/content_shell_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/content_shell.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -40,8 +41,11 @@ webSocket.send("loaded!"); }); test("reports an error in onExit", () { - var contentShell = - new ContentShell("http://dart-lang.org", executable: "_does_not_exist"); + var contentShell = new ContentShell("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( contentShell.onExit, throwsA(isApplicationException( diff --git a/test/runner/browser/dartium_test.dart b/test/runner/browser/dartium_test.dart index 9991b9141a29106e799053ce34802c0b0af53693..0aac2a37f25199398993f2a205b32cef7c6cfff4 100644 --- a/test/runner/browser/dartium_test.dart +++ b/test/runner/browser/dartium_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/dartium.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -40,8 +41,11 @@ webSocket.send("loaded!"); }); test("reports an error in onExit", () { - var dartium = - new Dartium("http://dart-lang.org", executable: "_does_not_exist"); + var dartium = new Dartium("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( dartium.onExit, throwsA(isApplicationException( diff --git a/test/runner/browser/firefox_test.dart b/test/runner/browser/firefox_test.dart index d18ac0b3ab9f719cf3fb6828932c3c1209819a3d..ccb7647751aadd42a73227867671395a6fd28672 100644 --- a/test/runner/browser/firefox_test.dart +++ b/test/runner/browser/firefox_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/firefox.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -40,8 +41,11 @@ webSocket.addEventListener("open", function() { }); test("reports an error in onExit", () { - var firefox = - new Firefox("http://dart-lang.org", executable: "_does_not_exist"); + var firefox = new Firefox("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( firefox.onExit, throwsA(isApplicationException( diff --git a/test/runner/browser/internet_explorer_test.dart b/test/runner/browser/internet_explorer_test.dart index 61a0248cb831990a68647a362bd92af872907d01..36f3c968a328d66861664f8a97aae3548179402e 100644 --- a/test/runner/browser/internet_explorer_test.dart +++ b/test/runner/browser/internet_explorer_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/internet_explorer.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -41,7 +42,10 @@ webSocket.addEventListener("open", function() { test("reports an error in onExit", () { var ie = new InternetExplorer("http://dart-lang.org", - executable: "_does_not_exist"); + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( ie.onExit, throwsA(isApplicationException(startsWith( diff --git a/test/runner/browser/loader_test.dart b/test/runner/browser/loader_test.dart index b245176d2637ee7fd6b460cde9c1bb6c1ac0d43e..a2739c83d11713ca47145f38bf1b6a4382a36153 100644 --- a/test/runner/browser/loader_test.dart +++ b/test/runner/browser/loader_test.dart @@ -12,6 +12,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/backend/state.dart'; import 'package:test/src/backend/test.dart'; import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/configuration/platform_selection.dart'; import 'package:test/src/runner/configuration/suite.dart'; import 'package:test/src/runner/loader.dart'; import 'package:test/test.dart'; @@ -21,7 +22,8 @@ import '../../utils.dart'; Loader _loader; /// A configuration that loads suites on Chrome. -final _chrome = new SuiteConfiguration(platforms: [TestPlatform.chrome]); +final _chrome = new SuiteConfiguration( + platforms: [new PlatformSelection(TestPlatform.chrome.identifier)]); void main() { setUp(() async { @@ -124,8 +126,10 @@ Future main() { var suites = await _loader .loadFile( path, - new SuiteConfiguration( - platforms: [TestPlatform.vm, TestPlatform.chrome])) + new SuiteConfiguration(platforms: [ + new PlatformSelection(TestPlatform.vm.identifier), + new PlatformSelection(TestPlatform.chrome.identifier) + ])) .asyncMap((loadSuite) => loadSuite.getSuite()) .toList(); expect(suites[0].platform, equals(TestPlatform.vm)); diff --git a/test/runner/browser/phantom_js_test.dart b/test/runner/browser/phantom_js_test.dart index fc1311bdde956a63e25b4445747bca75352c4a83..7441006f80e75e26ff4553a6b64e95598e8c3301 100644 --- a/test/runner/browser/phantom_js_test.dart +++ b/test/runner/browser/phantom_js_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/phantom_js.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -40,8 +41,11 @@ webSocket.addEventListener("open", function() { }); test("reports an error in onExit", () { - var phantomJS = - new PhantomJS("http://dart-lang.org", executable: "_does_not_exist"); + var phantomJS = new PhantomJS("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( phantomJS.onExit, throwsA(isApplicationException( diff --git a/test/runner/browser/safari_test.dart b/test/runner/browser/safari_test.dart index 82b475613458fd99a739aa25e68aaa4dfa32afe7..ac86d75dddc2fba0421c16418d31e3158fd577b7 100644 --- a/test/runner/browser/safari_test.dart +++ b/test/runner/browser/safari_test.dart @@ -8,6 +8,7 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test/src/runner/browser/safari.dart'; +import 'package:test/src/runner/executable_settings.dart'; import 'package:test/test.dart'; import '../../io.dart'; @@ -40,8 +41,11 @@ webSocket.addEventListener("open", function() { }); test("reports an error in onExit", () { - var safari = - new Safari("http://dart-lang.org", executable: "_does_not_exist"); + var safari = new Safari("http://dart-lang.org", + settings: new ExecutableSettings( + linuxExecutable: "_does_not_exist", + macOSExecutable: "_does_not_exist", + windowsExecutable: "_does_not_exist")); expect( safari.onExit, throwsA(isApplicationException( diff --git a/test/runner/configuration/custom_platform_test.dart b/test/runner/configuration/custom_platform_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..6977e6d391e7478f2cdfb51b6757109c74f56b9c --- /dev/null +++ b/test/runner/configuration/custom_platform_test.dart @@ -0,0 +1,869 @@ +// Copyright (c) 2017, 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:io'; + +import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:test_process/test_process.dart'; + +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/browser/default_settings.dart'; +import 'package:test/src/util/exit_codes.dart' as exit_codes; +import 'package:test/test.dart'; + +import '../../io.dart'; + +void main() { + setUp(() async { + await d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + """).create(); + }); + + group("override_platforms", () { + group("can override a browser", () { + test("without any changes", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: {} + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + + test("that's user-defined", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: {} + + override_platforms: + chromium: + settings: {} + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + + test("with a basename-only executable", () async { + await d.file("dart_test.yaml", """ + override_platforms: + dartium: + settings: + executable: + linux: dartium + mac_os: dartium + windows: dartium.exe + """).create(); + + var test = await runTest(["-p", "dartium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "dartium"); + + test("with an absolute-path executable", () async { + String path; + if (Platform.isLinux) { + var process = await TestProcess.start("which", ["google-chrome"]); + path = await process.stdout.next; + await process.shouldExit(0); + } else { + path = defaultSettings[TestPlatform.chrome].executable; + } + + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: $path + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + }); + + test("can override Node.js without any changes", () async { + await d.file("dart_test.yaml", """ + override_platforms: + node: + settings: {} + """).create(); + + var test = await runTest(["-p", "node", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "node"); + + group("errors", () { + test("rejects a non-map value", () async { + await d.file("dart_test.yaml", "override_platforms: 12").create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["override_platforms must be a map.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-string key", () async { + await d + .file("dart_test.yaml", "override_platforms: {12: null}") + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["Platform identifier must be a string.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-identifier-like key", () async { + await d + .file("dart_test.yaml", "override_platforms: {foo bar: null}") + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + "Platform identifier must be an (optionally hyphenated) Dart " + "identifier.", + "^^^^^^^" + ])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-map definition", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: 12 + """).create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["Platform definition must be a map.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("requires a settings key", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: {} + """).create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Missing required field "settings".', "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("settings must be a map", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: null + """).create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a map.', "^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("the overridden platform must exist", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chromium: + settings: {} + """).create(); + + var test = await runTest(["test.dart"]); + expect(test.stderr, + containsInOrder(['Unknown platform "chromium".', "^^^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("uncustomizable platforms can't be overridden", () async { + await d.file("dart_test.yaml", """ + override_platforms: + vm: + settings: {} + """).create(); + + var test = await runTest(["-p", "vm", "test.dart"]); + expect(test.stdout, + containsInOrder(['The "vm" platform can\'t be customized.', "^^"])); + await test.shouldExit(1); + }); + + group("when overriding browsers", () { + test("executable must be a string or map", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: 12 + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, + containsInOrder(['Must be a map or a string.', "^^"])); + await test.shouldExit(1); + }); + + test("executable string may not be relative on POSIX", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: foo/bar + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }, + // We allow relative executables for Windows so that Windows users + // can set a global executable without having to explicitly write + // `windows:`. + testOn: "!windows"); + + test("Linux executable must be a string", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: + linux: 12 + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("Linux executable may not be relative", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: + linux: foo/bar + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }); + + test("Mac OS executable must be a string", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: + mac_os: 12 + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("Mac OS executable may not be relative", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: + mac_os: foo/bar + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }); + + test("Windows executable must be a string", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: + windows: 12 + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("executable must exist", () async { + await d.file("dart_test.yaml", """ + override_platforms: + chrome: + settings: + executable: _does_not_exist + """).create(); + + var test = await runTest(["-p", "chrome", "test.dart"]); + expect( + test.stdout, + emitsThrough( + contains("Failed to run Chrome: $noSuchFileMessage"))); + await test.shouldExit(1); + }); + + test("executable must exist for Node.js", () async { + await d.file("dart_test.yaml", """ + override_platforms: + node: + settings: + executable: _does_not_exist + """).create(); + + var test = await runTest(["-p", "node", "test.dart"]); + expect( + test.stdout, + emitsThrough( + contains("Failed to run Node.js: $noSuchFileMessage"))); + await test.shouldExit(1); + }, tags: "node"); + }); + }); + }); + + group("define_platforms", () { + group("can define a new browser", () { + group("without any changes", () { + setUp(() async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: {} + """).create(); + }); + + test("that can be used to run tests", () async { + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + + test("that can be used in platform selectors", () async { + await d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}, testOn: "chromium"); + } + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + + test = await runTest(["-p", "chrome", "test.dart"]); + expect(test.stdout, emitsThrough(contains("No tests ran."))); + await test.shouldExit(0); + }, tags: "chrome"); + + test("that counts as its parent", () async { + await d.file("test.dart", """ + import 'package:test/test.dart'; + + void main() { + test("success", () {}, testOn: "chrome"); + } + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + }); + + test("with a basename-only executable", () async { + await d.file("dart_test.yaml", """ + define_platforms: + my-dartium: + name: My Dartium + extends: dartium + settings: + executable: + linux: dartium + mac_os: dartium + windows: dartium.exe + """).create(); + + var test = await runTest(["-p", "my-dartium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "dartium"); + + test("with an absolute-path executable", () async { + String path; + if (Platform.isLinux) { + var process = await TestProcess.start("which", ["google-chrome"]); + path = await process.stdout.next; + await process.shouldExit(0); + } else { + path = defaultSettings[TestPlatform.chrome].executable; + } + + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: $path + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, emitsThrough(contains("All tests passed!"))); + await test.shouldExit(0); + }, tags: "chrome"); + }); + + group("errors", () { + test("rejects a non-map value", () async { + await d.file("dart_test.yaml", "define_platforms: 12").create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["define_platforms must be a map.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-string key", () async { + await d.file("dart_test.yaml", "define_platforms: {12: null}").create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["Platform identifier must be a string.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-identifier-like key", () async { + await d + .file("dart_test.yaml", "define_platforms: {foo bar: null}") + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + "Platform identifier must be an (optionally hyphenated) Dart " + "identifier.", + "^^^^^^^" + ])); + await test.shouldExit(exit_codes.data); + }); + + test("rejects a non-map definition", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: 12 + """).create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["Platform definition must be a map.", "^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("requires a name key", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + extends: chrome + settings: {} + """).create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "name".', "^^^^^^^^^^^^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("name must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: null + extends: chrome + settings: {} + """).create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a string.', "^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("requires an extends key", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + settings: {} + """).create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "extends".', "^^^^^^^^^^^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("extends must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: null + settings: {} + """).create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform parent must be a string.', "^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("extends must be identifier-like", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: foo bar + settings: {} + """).create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + "Platform parent must be an (optionally hyphenated) Dart " + "identifier.", + "^^^^^^^" + ])); + await test.shouldExit(exit_codes.data); + }); + + test("requires a settings key", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + """).create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "settings".', "^^^^^^^^^^^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("settings must be a map", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: null + """).create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a map.', "^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("the new platform may not override an existing platform", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chrome: + name: Chromium + extends: firefox + settings: {} + """).create(); + + await d.dir("test").create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'The platform "chrome" already exists. Use override_platforms to ' + 'override it.', + "^^^^^^" + ])); + await test.shouldExit(exit_codes.data); + }); + + test("the new platform must extend an existing platform", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: foobar + settings: {} + """).create(); + + await d.dir("test").create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Unknown platform.', "^^^^^^"])); + await test.shouldExit(exit_codes.data); + }); + + test("the new platform can't extend an uncustomizable platform", + () async { + await d.file("dart_test.yaml", """ + define_platforms: + myvm: + name: My VM + extends: vm + settings: {} + """).create(); + + var test = await runTest(["-p", "myvm", "test.dart"]); + expect(test.stdout, + containsInOrder(['The "vm" platform can\'t be customized.', "^^"])); + await test.shouldExit(1); + }); + + group("when overriding browsers", () { + test("executable must be a string or map", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: 12 + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, + containsInOrder(['Must be a map or a string.', "^^"])); + await test.shouldExit(1); + }); + + test("executable string may not be relative on POSIX", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: foo/bar + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }, + // We allow relative executables for Windows so that Windows users + // can set a global executable without having to explicitly write + // `windows:`. + testOn: "!windows"); + + test("Linux executable must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + linux: 12 + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("Linux executable may not be relative", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + linux: foo/bar + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }); + + test("Mac OS executable must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + mac_os: 12 + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("Mac OS executable may not be relative", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + mac_os: foo/bar + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + "^^^^^^^" + ])); + await test.shouldExit(1); + }); + + test("Windows executable must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + windows: 12 + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("executable must exist", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: _does_not_exist + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect( + test.stdout, + emitsThrough( + contains("Failed to run Chrome: $noSuchFileMessage"))); + await test.shouldExit(1); + }); + + test("arguments must be a string", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + arguments: 12 + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, containsInOrder(['Must be a string.', "^^"])); + await test.shouldExit(1); + }); + + test("arguments must be shell parseable", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + arguments: --foo 'bar + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, + containsInOrder(['Unmatched single quote.', "^^^^^^^^^^"])); + await test.shouldExit(1); + }); + + test("with an argument that causes the browser to quit", () async { + await d.file("dart_test.yaml", """ + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + arguments: --version + """).create(); + + var test = await runTest(["-p", "chromium", "test.dart"]); + expect(test.stdout, + emitsThrough(contains("Chromium exited before connecting."))); + await test.shouldExit(1); + }, tags: "chrome"); + }); + }); + }); +} diff --git a/test/runner/configuration/global_test.dart b/test/runner/configuration/global_test.dart index 6590438a0d0f7b753598da89ece2e0e9ac5ee7af..67fc7245f841084a6ae2f9a28755d08b6825df64 100644 --- a/test/runner/configuration/global_test.dart +++ b/test/runner/configuration/global_test.dart @@ -94,9 +94,10 @@ void main() { group("disallows local-only configuration:", () { for (var field in [ "skip", "retry", "test_on", "paths", "filename", "names", "tags", // - "plain_names", "include_tags", "exclude_tags", "pub_serve", "add_tags" + "plain_names", "include_tags", "exclude_tags", "pub_serve", "add_tags", + "define_platforms" ]) { - test("rejects local-only configuration", () async { + test("for $field", () async { await d.file("global_test.yaml", JSON.encode({field: null})).create(); await d.file("test.dart", """ diff --git a/test/runner/configuration/platform_test.dart b/test/runner/configuration/platform_test.dart index 2bf1d6989789012572e95e3ca7c9ac4850f4429f..2d5c59dde19cb4f094f821ea59f3a68bd03e6b9e 100644 --- a/test/runner/configuration/platform_test.dart +++ b/test/runner/configuration/platform_test.dart @@ -116,11 +116,10 @@ void main() { })) .create(); + await d.dir("test").create(); + var test = await runTest([]); - expect( - test.stderr, - containsInOrder( - ["Invalid on_platform key: Undefined variable.", "^^^^^"])); + expect(test.stderr, containsInOrder(["Undefined variable.", "^^^^^"])); await test.shouldExit(exit_codes.data); }); diff --git a/test/runner/configuration/suite_test.dart b/test/runner/configuration/suite_test.dart index 44cfd59369e02649d534b40b60ff8629234a09c0..a6ec33f9df1dd7dc9c61fa63cfbf439527c82d6f 100644 --- a/test/runner/configuration/suite_test.dart +++ b/test/runner/configuration/suite_test.dart @@ -8,6 +8,7 @@ import 'package:test/test.dart'; import 'package:test/src/backend/platform_selector.dart'; import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/runner/configuration/platform_selection.dart'; import 'package:test/src/runner/configuration/suite.dart'; void main() { @@ -18,7 +19,7 @@ void main() { expect(merged.jsTrace, isFalse); expect(merged.runSkipped, isFalse); expect(merged.precompiledPath, isNull); - expect(merged.platforms, equals([TestPlatform.vm])); + expect(merged.platforms, equals([TestPlatform.vm.identifier])); }); test("if only the old configuration's is defined, uses it", () { @@ -26,12 +27,14 @@ void main() { jsTrace: true, runSkipped: true, precompiledPath: "/tmp/js", - platforms: [TestPlatform.chrome]).merge(new SuiteConfiguration()); + platforms: [ + new PlatformSelection(TestPlatform.chrome.identifier) + ]).merge(new SuiteConfiguration()); expect(merged.jsTrace, isTrue); expect(merged.runSkipped, isTrue); expect(merged.precompiledPath, equals("/tmp/js")); - expect(merged.platforms, equals([TestPlatform.chrome])); + expect(merged.platforms, equals([TestPlatform.chrome.identifier])); }); test("if only the new configuration's is defined, uses it", () { @@ -39,12 +42,14 @@ void main() { jsTrace: true, runSkipped: true, precompiledPath: "/tmp/js", - platforms: [TestPlatform.chrome])); + platforms: [ + new PlatformSelection(TestPlatform.chrome.identifier) + ])); expect(merged.jsTrace, isTrue); expect(merged.runSkipped, isTrue); expect(merged.precompiledPath, equals("/tmp/js")); - expect(merged.platforms, equals([TestPlatform.chrome])); + expect(merged.platforms, equals([TestPlatform.chrome.identifier])); }); test( @@ -54,18 +59,20 @@ void main() { jsTrace: false, runSkipped: true, precompiledPath: "/tmp/js", - platforms: [TestPlatform.chrome]); + platforms: [new PlatformSelection(TestPlatform.chrome.identifier)]); var newer = new SuiteConfiguration( jsTrace: true, runSkipped: false, precompiledPath: "../js", - platforms: [TestPlatform.dartium]); + platforms: [ + new PlatformSelection(TestPlatform.dartium.identifier) + ]); var merged = older.merge(newer); expect(merged.jsTrace, isTrue); expect(merged.runSkipped, isFalse); expect(merged.precompiledPath, equals("../js")); - expect(merged.platforms, equals([TestPlatform.dartium])); + expect(merged.platforms, equals([TestPlatform.dartium.identifier])); }); }); diff --git a/test/runner/configuration/top_level_error_test.dart b/test/runner/configuration/top_level_error_test.dart index 712cf5ec9e3e7720c2bbc338b2626fcd0174b57c..0eb4a4449b2b35048d0461afb994dad6e28206af 100644 --- a/test/runner/configuration/top_level_error_test.dart +++ b/test/runner/configuration/top_level_error_test.dart @@ -281,7 +281,8 @@ void main() { .create(); var test = await runTest(["test.dart"]); - expect(test.stderr, containsInOrder(["Platforms must be strings", "^^"])); + expect(test.stderr, + containsInOrder(["Platform name must be a string", "^^"])); await test.shouldExit(exit_codes.data); }); @@ -294,7 +295,9 @@ void main() { })) .create(); - var test = await runTest(["test.dart"]); + await d.dir("test").create(); + + var test = await runTest([]); expect(test.stderr, containsInOrder(['Unknown platform "foo"', "^^^^^"])); await test.shouldExit(exit_codes.data); }); diff --git a/test/runner/parse_metadata_test.dart b/test/runner/parse_metadata_test.dart index 4da4ddcd9d4e58856fbab7214670a5087dfd7dd0..343307ec9b5f6bdc7ef9f6285ae12e6e858a423e 100644 --- a/test/runner/parse_metadata_test.dart +++ b/test/runner/parse_metadata_test.dart @@ -27,21 +27,21 @@ void main() { test("returns empty metadata for an empty file", () { new File(_path).writeAsStringSync(""); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn, equals(PlatformSelector.all)); expect(metadata.timeout.scaleFactor, equals(1)); }); test("ignores irrelevant annotations", () { new File(_path).writeAsStringSync("@Fblthp\n@Fblthp.foo\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn, equals(PlatformSelector.all)); }); test("parses a prefixed annotation", () { new File(_path).writeAsStringSync("@foo.TestOn('vm')\n" "import 'package:test/test.dart' as foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn.evaluate(TestPlatform.vm), isTrue); expect(metadata.testOn.evaluate(TestPlatform.chrome), isFalse); }); @@ -49,54 +49,54 @@ void main() { group("@TestOn:", () { test("parses a valid annotation", () { new File(_path).writeAsStringSync("@TestOn('vm')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn.evaluate(TestPlatform.vm), isTrue); expect(metadata.testOn.evaluate(TestPlatform.chrome), isFalse); }); test("ignores a constructor named TestOn", () { new File(_path).writeAsStringSync("@foo.TestOn('foo')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn, equals(PlatformSelector.all)); }); group("throws an error for", () { test("a named constructor", () { new File(_path).writeAsStringSync("@TestOn.name('foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("no argument list", () { new File(_path).writeAsStringSync("@TestOn\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("an empty argument list", () { new File(_path).writeAsStringSync("@TestOn()\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named argument", () { new File(_path) .writeAsStringSync("@TestOn(expression: 'foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple arguments", () { new File(_path) .writeAsStringSync("@TestOn('foo', 'bar')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-string argument", () { new File(_path).writeAsStringSync("@TestOn(123)\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple @TestOns", () { new File(_path) .writeAsStringSync("@TestOn('foo')\n@TestOn('bar')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); }); }); @@ -113,7 +113,7 @@ void main() { library foo; """); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect( metadata.timeout.duration, equals(new Duration( @@ -130,7 +130,7 @@ library foo; library foo; """); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.timeout.scaleFactor, equals(1)); }); @@ -140,7 +140,7 @@ library foo; library foo; """); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.timeout.scaleFactor, equals(0.5)); }); @@ -150,95 +150,95 @@ library foo; library foo; """); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.timeout, same(Timeout.none)); }); test("ignores a constructor named Timeout", () { new File(_path).writeAsStringSync("@foo.Timeout('foo')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.timeout.scaleFactor, equals(1)); }); group("throws an error for", () { test("an unknown named constructor", () { new File(_path).writeAsStringSync("@Timeout.name('foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("no argument list", () { new File(_path).writeAsStringSync("@Timeout\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("an empty argument list", () { new File(_path).writeAsStringSync("@Timeout()\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("an argument list for Timeout.none", () { new File(_path).writeAsStringSync("@Timeout.none()\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named argument", () { new File(_path).writeAsStringSync( "@Timeout(duration: const Duration(seconds: 1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple arguments", () { new File(_path) .writeAsStringSync("@Timeout.factor(1, 2)\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-Duration argument", () { new File(_path).writeAsStringSync("@Timeout(10)\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-num argument", () { new File(_path) .writeAsStringSync("@Timeout.factor('foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple @Timeouts", () { new File(_path).writeAsStringSync( "@Timeout.factor(1)\n@Timeout.factor(2)\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); group("a Duration with", () { test("a non-const constructor", () { new File(_path) .writeAsStringSync("@Timeout(new Duration(1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named constructor", () { new File(_path).writeAsStringSync( "@Timeout(const Duration.name(seconds: 1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a positional argument", () { new File(_path) .writeAsStringSync("@Timeout(const Duration(1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("an unknown named argument", () { new File(_path).writeAsStringSync( "@Timeout(const Duration(name: 1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a duplicate named argument", () { new File(_path).writeAsStringSync( "@Timeout(const Duration(seconds: 1, seconds: 1))\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); }); }); @@ -247,54 +247,54 @@ library foo; group("@Skip:", () { test("parses a valid annotation", () { new File(_path).writeAsStringSync("@Skip()\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.skip, isTrue); expect(metadata.skipReason, isNull); }); test("parses a valid annotation with a reason", () { new File(_path).writeAsStringSync("@Skip('reason')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.skip, isTrue); expect(metadata.skipReason, equals('reason')); }); test("ignores a constructor named Skip", () { new File(_path).writeAsStringSync("@foo.Skip('foo')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.skip, isFalse); }); group("throws an error for", () { test("a named constructor", () { new File(_path).writeAsStringSync("@Skip.name('foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("no argument list", () { new File(_path).writeAsStringSync("@Skip\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named argument", () { new File(_path).writeAsStringSync("@Skip(reason: 'foo')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple arguments", () { new File(_path).writeAsStringSync("@Skip('foo', 'bar')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-string argument", () { new File(_path).writeAsStringSync("@Skip(123)\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple @Skips", () { new File(_path) .writeAsStringSync("@Skip('foo')\n@Skip('bar')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); }); }); @@ -302,13 +302,13 @@ library foo; group("@Tags:", () { test("parses a valid annotation", () { new File(_path).writeAsStringSync("@Tags(const ['a'])\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.tags, equals(["a"])); }); test("ignores a constructor named Tags", () { new File(_path).writeAsStringSync("@foo.Tags(const ['a'])\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.tags, isEmpty); }); @@ -316,34 +316,34 @@ library foo; test("a named constructor", () { new File(_path) .writeAsStringSync("@Tags.name(const ['a'])\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("no argument list", () { new File(_path).writeAsStringSync("@Tags\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named argument", () { new File(_path).writeAsStringSync("@Tags(tags: ['a'])\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple arguments", () { new File(_path) .writeAsStringSync("@Tags(const ['a'], ['b'])\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-list argument", () { new File(_path).writeAsStringSync("@Tags('a')\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple @Tags", () { new File(_path).writeAsStringSync( "@Tags(const ['a'])\n@Tags(const ['b'])\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); }); }); @@ -356,7 +356,7 @@ library foo; 'vm': const [const Skip(), const Timeout.factor(3)] }) library foo;"""); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); var key = metadata.onPlatform.keys.first; expect(key.evaluate(TestPlatform.chrome), isTrue); @@ -374,7 +374,7 @@ library foo;"""); test("ignores a constructor named OnPlatform", () { new File(_path).writeAsStringSync("@foo.OnPlatform('foo')\nlibrary foo;"); - var metadata = parseMetadata(_path); + var metadata = parseMetadata(_path, new Set()); expect(metadata.testOn, equals(PlatformSelector.all)); }); @@ -382,70 +382,70 @@ library foo;"""); test("a named constructor", () { new File(_path) .writeAsStringSync("@OnPlatform.name(const {})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("no argument list", () { new File(_path).writeAsStringSync("@OnPlatform\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("an empty argument list", () { new File(_path).writeAsStringSync("@OnPlatform()\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a named argument", () { new File(_path) .writeAsStringSync("@OnPlatform(map: const {})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple arguments", () { new File(_path) .writeAsStringSync("@OnPlatform(const {}, const {})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-map argument", () { new File(_path) .writeAsStringSync("@OnPlatform(const Skip())\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a non-const map", () { new File(_path).writeAsStringSync("@OnPlatform({})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a map with a non-String key", () { new File(_path).writeAsStringSync( "@OnPlatform(const {1: const Skip()})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a map with a unparseable key", () { new File(_path).writeAsStringSync( "@OnPlatform(const {'invalid': const Skip()})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a map with an invalid value", () { new File(_path).writeAsStringSync( "@OnPlatform(const {'vm': const TestOn('vm')})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("a map with an invalid value in a list", () { new File(_path).writeAsStringSync( "@OnPlatform(const {'vm': [const TestOn('vm')]})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); test("multiple @OnPlatforms", () { new File(_path).writeAsStringSync( "@OnPlatform(const {})\n@OnPlatform(const {})\nlibrary foo;"); - expect(() => parseMetadata(_path), throwsFormatException); + expect(() => parseMetadata(_path, new Set()), throwsFormatException); }); }); });