From 16f8247a8e5f84a78a354bf158cd86dff42ecacb Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum <nweiz@google.com> Date: Mon, 20 Apr 2015 19:19:18 -0700 Subject: [PATCH] Add an "onPlatform" parameter to test() and group(). This allows platform-specific metadata to be declared. See #75 R=kevmoo@google.com Review URL: https://codereview.chromium.org//1097183003 --- lib/src/backend/declarer.dart | 56 +------- lib/src/backend/invoker.dart | 6 + lib/src/backend/metadata.dart | 151 ++++++++++++++++---- lib/src/backend/suite.dart | 11 +- lib/src/backend/test.dart | 4 + lib/src/runner/browser/browser_manager.dart | 18 ++- lib/src/runner/browser/iframe_listener.dart | 12 +- lib/src/runner/browser/iframe_test.dart | 6 + lib/src/runner/browser/server.dart | 2 +- lib/src/runner/loader.dart | 4 +- lib/src/runner/vm/isolate_listener.dart | 7 +- lib/src/runner/vm/isolate_test.dart | 8 +- lib/src/utils.dart | 14 ++ lib/test.dart | 54 ++++++- test/backend/metadata_test.dart | 66 +++++++++ test/runner/browser/runner_test.dart | 94 ++++++++++++ test/runner/runner_test.dart | 94 ++++++++++++ 17 files changed, 502 insertions(+), 105 deletions(-) create mode 100644 test/backend/metadata_test.dart diff --git a/lib/src/backend/declarer.dart b/lib/src/backend/declarer.dart index 9912274c..46e88629 100644 --- a/lib/src/backend/declarer.dart +++ b/lib/src/backend/declarer.dart @@ -27,30 +27,16 @@ class Declarer { Declarer(); /// Defines a test case with the given description and body. - /// - /// The description will be added to the descriptions of any surrounding - /// [group]s. - /// - /// If [testOn] is passed, it's parsed as a [PlatformSelector], and the test - /// will only be run on matching platforms. - /// - /// If [timeout] is passed, it's used to modify or replace the default timeout - /// of 30 seconds. Timeout modifications take precedence in suite-group-test - /// order, so [timeout] will also modify any timeouts set on the group or - /// suite. - /// - /// If [skip] is a String or `true`, the test is skipped. If it's a String, it - /// should explain why the test is skipped; this reason will be printed - /// instead of running the test. void test(String description, body(), {String testOn, Timeout timeout, - skip}) { + skip, Map<String, dynamic> onPlatform}) { // TODO(nweiz): Once tests have begun running, throw an error if [test] is // called. var prefix = _group.description; if (prefix != null) description = "$prefix $description"; - var metadata = _group.metadata.merge( - new Metadata.parse(testOn: testOn, timeout: timeout, skip: skip)); + var metadata = _group.metadata.merge(new Metadata.parse( + testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform)); + var group = _group; _tests.add(new LocalTest(description, metadata, () { // TODO(nweiz): It might be useful to throw an error here if a test starts @@ -61,28 +47,12 @@ class Declarer { } /// Creates a group of tests. - /// - /// A group's description is included in the descriptions of any tests or - /// sub-groups it contains. [setUp] and [tearDown] are also scoped to the - /// containing group. - /// - /// If [testOn] is passed, it's parsed as a [PlatformSelector], and any tests - /// in the group will only be run on matching platforms. - /// - /// If [timeout] is passed, it's used to modify or replace the default timeout - /// of 30 seconds. Timeout modifications take precedence in suite-group-test - /// order, so [timeout] will also modify any timeouts set on the group or - /// suite. - /// - /// If [skip] is a String or `true`, the group is skipped. If it's a String, - /// it should explain why the group is skipped; this reason will be printed - /// instead of running the group's tests. void group(String description, void body(), {String testOn, - Timeout timeout, skip}) { + Timeout timeout, skip, Map<String, dynamic> onPlatform}) { var oldGroup = _group; var metadata = new Metadata.parse( - testOn: testOn, timeout: timeout, skip: skip); + testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform); // Don' load the tests for a skipped group. if (metadata.skip) { @@ -99,13 +69,6 @@ class Declarer { } /// Registers a function to be run before tests. - /// - /// This function will be called before each test is run. [callback] may be - /// asynchronous; if so, it must return a [Future]. - /// - /// If this is called within a [group], it applies only to tests in that - /// group. [callback] will be run after any set-up callbacks in parent groups - /// or at the top level. void setUp(callback()) { if (_group.setUp != null) { throw new StateError("setUp() may not be called multiple times for the " @@ -116,13 +79,6 @@ class Declarer { } /// Registers a function to be run after tests. - /// - /// This function will be called after each test is run. [callback] may be - /// asynchronous; if so, it must return a [Future]. - /// - /// If this is called within a [group], it applies only to tests in that - /// group. [callback] will be run before any tear-down callbacks in parent - /// groups or at the top level. void tearDown(callback()) { if (_group.tearDown != null) { throw new StateError("tearDown() may not be called multiple times for " diff --git a/lib/src/backend/invoker.dart b/lib/src/backend/invoker.dart index 2ff17725..81a931e4 100644 --- a/lib/src/backend/invoker.dart +++ b/lib/src/backend/invoker.dart @@ -42,6 +42,12 @@ class LocalTest implements Test { var invoker = new Invoker._(suite, this); return invoker.liveTest; } + + Test change({String name, Metadata metadata}) { + if (name == null) name = this.name; + if (metadata == null) metadata = this.metadata; + return new LocalTest(name, metadata, _body, tearDown: _tearDown); + } } /// The class responsible for managing the lifecycle of a single local test. diff --git a/lib/src/backend/metadata.dart b/lib/src/backend/metadata.dart index ecce4404..0bcf5f88 100644 --- a/lib/src/backend/metadata.dart +++ b/lib/src/backend/metadata.dart @@ -4,7 +4,13 @@ library test.backend.metadata; +import 'dart:collection'; + +import '../backend/operating_system.dart'; +import '../backend/test_platform.dart'; +import '../frontend/skip.dart'; import '../frontend/timeout.dart'; +import '../utils.dart'; import 'platform_selector.dart'; /// Metadata for a test or test suite. @@ -24,26 +30,81 @@ class Metadata { /// The reason the test or suite should be skipped, if given. final String skipReason; + /// Platform-specific metadata. + /// + /// Each key identifies a platform, and its value identifies the specific + /// metadata for that platform. These can be applied by calling [forPlatform]. + final Map<PlatformSelector, Metadata> onPlatform; + + /// Parses a user-provided map into the value for [onPlatform]. + static Map<PlatformSelector, Metadata> _parseOnPlatform( + Map<String, dynamic> onPlatform) { + if (onPlatform == null) return {}; + + var result = {}; + onPlatform.forEach((platform, metadata) { + if (metadata is Timeout || metadata is Skip) { + metadata = [metadata]; + } else if (metadata is! List) { + throw new ArgumentError('Metadata for platform "$platform" must be a ' + 'Timeout, Skip, or List of those; was "$metadata".'); + } + + var selector = new PlatformSelector.parse(platform); + + var timeout; + var skip; + for (var metadatum in metadata) { + if (metadatum is Timeout) { + if (timeout != null) { + throw new ArgumentError('Only a single Timeout may be declared for ' + '"$platform".'); + } + + timeout = metadatum; + } else if (metadatum is Skip) { + if (skip != null) { + throw new ArgumentError('Only a single Skip may be declared for ' + '"$platform".'); + } + + skip = metadatum.reason == null ? true : metadatum.reason; + } else { + throw new ArgumentError('Metadata for platform "$platform" must be a ' + 'Timeout, Skip, or List of those; was "$metadata".'); + } + } + + result[selector] = new Metadata.parse(timeout: timeout, skip: skip); + }); + return result; + } + /// Creates new Metadata. /// /// [testOn] defaults to [PlatformSelector.all]. Metadata({PlatformSelector testOn, Timeout timeout, bool skip: false, - this.skipReason}) + this.skipReason, Map<PlatformSelector, Metadata> onPlatform}) : testOn = testOn == null ? PlatformSelector.all : testOn, timeout = timeout == null ? const Timeout.factor(1) : timeout, - skip = skip; + skip = skip, + onPlatform = onPlatform == null + ? const {} + : new UnmodifiableMapView(onPlatform); - /// Creates a new Metadata, but with fields parsed from strings where - /// applicable. + /// Creates a new Metadata, but with fields parsed from caller-friendly values + /// where applicable. /// /// Throws a [FormatException] if any field is invalid. - Metadata.parse({String testOn, Timeout timeout, skip}) + Metadata.parse({String testOn, Timeout timeout, skip, + Map<String, dynamic> onPlatform}) : testOn = testOn == null ? PlatformSelector.all : new PlatformSelector.parse(testOn), timeout = timeout == null ? const Timeout.factor(1) : timeout, skip = skip != null && skip != false, - skipReason = skip is String ? skip : null { + skipReason = skip is String ? skip : null, + onPlatform = _parseOnPlatform(onPlatform) { if (skip != null && skip is! String && skip is! bool) { throw new ArgumentError( '"skip" must be a String or a bool, was "$skip".'); @@ -52,15 +113,18 @@ class Metadata { /// Dezerializes the result of [Metadata.serialize] into a new [Metadata]. Metadata.deserialize(serialized) - : this.parse( - testOn: serialized['testOn'], - timeout: serialized['timeout']['duration'] == null - ? new Timeout.factor(serialized['timeout']['scaleFactor']) - : new Timeout(new Duration( - microseconds: serialized['timeout']['duration'])), - skip: serialized['skipReason'] == null - ? serialized['skip'] - : serialized['skipReason']); + : testOn = serialized['testOn'] == null + ? PlatformSelector.all + : new PlatformSelector.parse(serialized['testOn']), + timeout = serialized['timeout']['duration'] == null + ? new Timeout.factor(serialized['timeout']['scaleFactor']) + : new Timeout(new Duration( + microseconds: serialized['timeout']['duration'])), + skip = serialized['skip'], + skipReason = serialized['skipReason'], + onPlatform = new Map.fromIterable(serialized['onPlatform'], + key: (pair) => new PlatformSelector.parse(pair.first), + value: (pair) => new Metadata.deserialize(pair.last)); /// Return a new [Metadata] that merges [this] with [other]. /// @@ -70,19 +134,52 @@ class Metadata { testOn: testOn.intersect(other.testOn), timeout: timeout.merge(other.timeout), skip: skip || other.skip, - skipReason: other.skipReason == null ? skipReason : other.skipReason); + skipReason: other.skipReason == null ? skipReason : other.skipReason, + onPlatform: mergeMaps(onPlatform, other.onPlatform)); + + /// Returns a copy of [this] with the given fields changed. + Metadata change({PlatformSelector testOn, Timeout timeout, bool skip, + String skipReason, Map<PlatformSelector, Metadata> onPlatform}) { + if (testOn == null) testOn = this.testOn; + if (timeout == null) timeout = this.timeout; + if (skip == null) skip = this.skip; + if (skipReason == null) skipReason = this.skipReason; + if (onPlatform == null) onPlatform = this.onPlatform; + return new Metadata(testOn: testOn, timeout: timeout, skip: skip, + skipReason: skipReason, onPlatform: onPlatform); + } + + /// Returns a copy of [this] with all platform-specific metadata from + /// [onPlatform] resolved. + Metadata forPlatform(TestPlatform platform, {OperatingSystem os}) { + var metadata = this; + onPlatform.forEach((platformSelector, platformMetadata) { + if (!platformSelector.evaluate(platform, os: os)) return; + metadata = metadata.merge(platformMetadata); + }); + return metadata.change(onPlatform: {}); + } /// Serializes [this] into a JSON-safe object that can be deserialized using /// [new Metadata.deserialize]. - serialize() => { - 'testOn': testOn == PlatformSelector.all ? null : testOn.toString(), - 'timeout': { - 'duration': timeout.duration == null - ? null - : timeout.duration.inMicroseconds, - 'scaleFactor': timeout.scaleFactor - }, - 'skip': skip, - 'skipReason': skipReason - }; + serialize() { + // Make this a list to guarantee that the order is preserved. + var serializedOnPlatform = []; + onPlatform.forEach((key, value) { + serializedOnPlatform.add([key.toString(), value.serialize()]); + }); + + return { + 'testOn': testOn == PlatformSelector.all ? null : testOn.toString(), + 'timeout': { + 'duration': timeout.duration == null + ? null + : timeout.duration.inMicroseconds, + 'scaleFactor': timeout.scaleFactor + }, + 'skip': skip, + 'skipReason': skipReason, + 'onPlatform': serializedOnPlatform + }; + } } diff --git a/lib/src/backend/suite.dart b/lib/src/backend/suite.dart index 78cb8742..e7596a87 100644 --- a/lib/src/backend/suite.dart +++ b/lib/src/backend/suite.dart @@ -33,14 +33,17 @@ class Suite { : metadata = metadata == null ? new Metadata() : metadata, tests = new UnmodifiableListView<Test>(tests.toList()); - /// Returns a new suite that only contains tests that are valid for the given - /// [platform] and [os]. + /// Returns a view of this suite for the given [platform] and [os]. /// - /// If the suite itself is invalid for [platform] and [os], returns `null`. - Suite filter(TestPlatform platform, {OperatingSystem os}) { + /// This filters out tests that are invalid for [platform] and [os] and + /// resolves platform-specific metadata. If the suite itself is invalid for + /// [platform] and [os], returns `null`. + Suite forPlatform(TestPlatform platform, {OperatingSystem os}) { if (!metadata.testOn.evaluate(platform, os: os)) return null; return change(tests: tests.where((test) { return test.metadata.testOn.evaluate(platform, os: os); + }).map((test) { + return test.change(metadata: test.metadata.forPlatform(platform, os: os)); })); } diff --git a/lib/src/backend/test.dart b/lib/src/backend/test.dart index 85ca2c7d..0334a8f4 100644 --- a/lib/src/backend/test.dart +++ b/lib/src/backend/test.dart @@ -25,4 +25,8 @@ abstract class Test { /// /// [suite] is the suite within which this test is being run. LiveTest load(Suite suite); + + /// Returns a new copy of this Test with the given [name] and [metadata], if + /// passed. + Test change({String name, Metadata metadata}); } diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart index 380be41f..b20da079 100644 --- a/lib/src/runner/browser/browser_manager.dart +++ b/lib/src/runner/browser/browser_manager.dart @@ -11,6 +11,7 @@ import 'package:http_parser/http_parser.dart'; import '../../backend/metadata.dart'; import '../../backend/suite.dart'; +import '../../backend/test_platform.dart'; import '../../util/multi_channel.dart'; import '../../util/remote_exception.dart'; import '../../utils.dart'; @@ -22,14 +23,17 @@ import 'iframe_test.dart'; /// This is in charge of telling the browser which test suites to load and /// converting its responses into [Suite] objects. class BrowserManager { + /// The browser that this is managing. + final TestPlatform browser; + /// The channel used to communicate with the browser. /// /// This is connected to a page running `static/host.dart`. final MultiChannel _channel; - /// Creates a new BrowserManager that communicates with a browser over + /// Creates a new BrowserManager that communicates with [browser] over /// [webSocket]. - BrowserManager(CompatibleWebSocket webSocket) + BrowserManager(this.browser, CompatibleWebSocket webSocket) : _channel = new MultiChannel( webSocket.map(JSON.decode), mapSink(webSocket, JSON.encode)); @@ -40,8 +44,10 @@ class BrowserManager { /// suite. [path] is the path of the original test suite file, which is used /// for reporting. [metadata] is the parsed metadata for the test suite. Future<Suite> loadSuite(String path, Uri url, Metadata metadata) { - url = url.replace( - fragment: Uri.encodeFull(JSON.encode(metadata.serialize()))); + url = url.replace(fragment: Uri.encodeFull(JSON.encode({ + "metadata": metadata.serialize(), + "browser": browser.identifier + }))); var suiteChannel = _channel.virtualChannel(); _channel.sink.add({ @@ -59,7 +65,9 @@ class BrowserManager { return maybeFirst(suiteChannel.stream) .timeout(new Duration(seconds: 7), onTimeout: () { throw new LoadException( - path, "Timed out waiting for the test suite to connect."); + path, + "Timed out waiting for the test suite to connect on " + "${browser.name}."); }).then((response) { if (response == null) return null; diff --git a/lib/src/runner/browser/iframe_listener.dart b/lib/src/runner/browser/iframe_listener.dart index 468e3acc..b345f379 100644 --- a/lib/src/runner/browser/iframe_listener.dart +++ b/lib/src/runner/browser/iframe_listener.dart @@ -12,6 +12,7 @@ import '../../backend/declarer.dart'; import '../../backend/metadata.dart'; import '../../backend/suite.dart'; import '../../backend/test.dart'; +import '../../backend/test_platform.dart'; import '../../util/multi_channel.dart'; import '../../util/remote_exception.dart'; import '../../utils.dart'; @@ -66,12 +67,13 @@ class IframeListener { } var url = Uri.parse(window.location.href); - var metadata = url.hasFragment - ? new Metadata.deserialize(JSON.decode(Uri.decodeFull(url.fragment))) - : new Metadata(); + var message = JSON.decode(Uri.decodeFull(url.fragment)); + var metadata = new Metadata.deserialize(message['metadata']); + var browser = TestPlatform.find(message['browser']); - new IframeListener._(new Suite(declarer.tests, metadata: metadata)) - ._listen(channel); + var suite = new Suite(declarer.tests, metadata: metadata) + .forPlatform(browser); + new IframeListener._(suite)._listen(channel); } /// Constructs a [MultiChannel] wrapping the `postMessage` communication with diff --git a/lib/src/runner/browser/iframe_test.dart b/lib/src/runner/browser/iframe_test.dart index 6b799419..bb54e786 100644 --- a/lib/src/runner/browser/iframe_test.dart +++ b/lib/src/runner/browser/iframe_test.dart @@ -60,4 +60,10 @@ class IframeTest implements Test { }); return controller.liveTest; } + + Test change({String name, Metadata metadata}) { + if (name == null) name = this.name; + if (metadata == null) metadata = this.metadata; + return new IframeTest(name, metadata, _channel); + } } diff --git a/lib/src/runner/browser/server.dart b/lib/src/runner/browser/server.dart index 9c58885e..2a8d08e8 100644 --- a/lib/src/runner/browser/server.dart +++ b/lib/src/runner/browser/server.dart @@ -353,7 +353,7 @@ void main() { // value and [browser.onError]. _browserManagers[platform] = completer.future.catchError((_) {}); var path = _webSocketHandler.create(webSocketHandler((webSocket) { - completer.complete(new BrowserManager(webSocket)); + completer.complete(new BrowserManager(platform, webSocket)); })); var webSocketUrl = url.replace(scheme: 'ws').resolve(path); diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart index 4b1f3d74..a6db18d7 100644 --- a/lib/src/runner/loader.dart +++ b/lib/src/runner/loader.dart @@ -148,9 +148,7 @@ class Loader { assert(platform.isBrowser); return _loadBrowserFile(path, platform, metadata); }).then((suite) { - if (suite == null) return; - - controller.add(suite.filter(platform, os: currentOS)); + if (suite != null) controller.add(suite); }).catchError(controller.addError); }).then((_) => controller.close()); diff --git a/lib/src/runner/vm/isolate_listener.dart b/lib/src/runner/vm/isolate_listener.dart index b40140e7..ed23a1d1 100644 --- a/lib/src/runner/vm/isolate_listener.dart +++ b/lib/src/runner/vm/isolate_listener.dart @@ -11,6 +11,8 @@ import '../../backend/declarer.dart'; import '../../backend/metadata.dart'; import '../../backend/suite.dart'; import '../../backend/test.dart'; +import '../../backend/test_platform.dart'; +import '../../util/io.dart'; import '../../util/remote_exception.dart'; import '../../utils.dart'; @@ -59,8 +61,9 @@ class IsolateListener { return; } - new IsolateListener._(new Suite(declarer.tests, metadata: metadata)) - ._listen(sendPort); + var suite = new Suite(declarer.tests, metadata: metadata) + .forPlatform(TestPlatform.vm, os: currentOS); + new IsolateListener._(suite)._listen(sendPort); } /// Sends a message over [sendPort] indicating that the tests failed to load. diff --git a/lib/src/runner/vm/isolate_test.dart b/lib/src/runner/vm/isolate_test.dart index 61a74cc3..46b2048f 100644 --- a/lib/src/runner/vm/isolate_test.dart +++ b/lib/src/runner/vm/isolate_test.dart @@ -16,7 +16,7 @@ import '../../backend/test.dart'; import '../../util/remote_exception.dart'; /// A test in another isolate. -class IsolateTest implements Test { +class IsolateTest extends Test { final String name; final Metadata metadata; @@ -81,4 +81,10 @@ class IsolateTest implements Test { }); return controller.liveTest; } + + Test change({String name, Metadata metadata}) { + if (name == null) name = this.name; + if (metadata == null) metadata = this.metadata; + return new IsolateTest(name, metadata, _sendPort); + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 59edb665..3b8111d0 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -98,6 +98,20 @@ List flatten(Iterable nested) { return result; } +/// Returns a new map with all values in both [map1] and [map2]. +/// +/// If there are conflicting keys, [map2]'s value wins. +Map mergeMaps(Map map1, Map map2) { + var result = {}; + map1.forEach((key, value) { + result[key] = value; + }); + map2.forEach((key, value) { + result[key] = value; + }); + return result; +} + /// Returns a sink that maps events sent to [original] using [fn]. StreamSink mapSink(StreamSink original, fn(event)) { var controller = new StreamController(sync: true); diff --git a/lib/test.dart b/lib/test.dart index 6abe99ec..ed2f058b 100644 --- a/lib/test.dart +++ b/lib/test.dart @@ -50,10 +50,10 @@ Declarer get _declarer { _globalDeclarer = new Declarer(); scheduleMicrotask(() { var suite = - new Suite(_globalDeclarer.tests, - path: p.prettyUri(Uri.base), - platform: "VM") - .filter(TestPlatform.vm, os: currentOSGuess); + new Suite(_globalDeclarer.tests, + path: p.prettyUri(Uri.base), + platform: "VM") + .forPlatform(TestPlatform.vm, os: currentOSGuess); // TODO(nweiz): Set the exit code on the VM when issue 6943 is fixed. new NoIoCompactReporter([suite], color: true).run(); }); @@ -77,10 +77,30 @@ Declarer get _declarer { /// If [skip] is a String or `true`, the test is skipped. If it's a String, it /// should explain why the test is skipped; this reason will be printed instead /// of running the test. +/// +/// [onPlatform] allows tests to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// test("potentially slow test", () { +/// // ... +/// }, onPlatform: { +/// // This test is especially slow on Windows. +/// "windows": new Timeout.factor(2), +/// "browser": [ +/// new Skip("TODO: add browser support"), +/// // This will be slow on browsers once it works on them. +/// new Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. void test(String description, body(), {String testOn, Timeout timeout, - skip}) => + skip, Map<String, dynamic> onPlatform}) => _declarer.test(description, body, - testOn: testOn, timeout: timeout, skip: skip); + testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform); /// Creates a group of tests. /// @@ -101,8 +121,28 @@ void test(String description, body(), {String testOn, Timeout timeout, /// If [skip] is a String or `true`, the group is skipped. If it's a String, it /// should explain why the group is skipped; this reason will be printed instead /// of running the group's tests. +/// +/// [onPlatform] allows groups to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// group("potentially slow tests", () { +/// // ... +/// }, onPlatform: { +/// // These tests are especially slow on Windows. +/// "windows": new Timeout.factor(2), +/// "browser": [ +/// new Skip("TODO: add browser support"), +/// // They'll be slow on browsers once it works on them. +/// new Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. void group(String description, void body(), {String testOn, Timeout timeout, - skip}) => + skip, Map<String, dynamic> onPlatform}) => _declarer.group(description, body, testOn: testOn, timeout: timeout, skip: skip); diff --git a/test/backend/metadata_test.dart b/test/backend/metadata_test.dart new file mode 100644 index 00000000..9e696e5e --- /dev/null +++ b/test/backend/metadata_test.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/src/backend/metadata.dart'; +import 'package:test/src/backend/test_platform.dart'; +import 'package:test/src/frontend/timeout.dart'; +import 'package:test/src/frontend/skip.dart'; +import 'package:test/test.dart'; + +void main() { + group("onPlatform", () { + test("parses a valid map", () { + var metadata = new Metadata.parse(onPlatform: { + "chrome": new Timeout.factor(2), + "vm": [new Skip(), new Timeout.factor(3)] + }); + + var key = metadata.onPlatform.keys.first; + expect(key.evaluate(TestPlatform.chrome), isTrue); + expect(key.evaluate(TestPlatform.vm), isFalse); + var value = metadata.onPlatform.values.first; + expect(value.timeout.scaleFactor, equals(2)); + + + key = metadata.onPlatform.keys.last; + expect(key.evaluate(TestPlatform.vm), isTrue); + expect(key.evaluate(TestPlatform.chrome), isFalse); + value = metadata.onPlatform.values.last; + expect(value.skip, isTrue); + expect(value.timeout.scaleFactor, equals(3)); + }); + + test("refuses an invalid value", () { + expect(() { + new Metadata.parse(onPlatform: {"chrome": new TestOn("chrome")}); + }, throwsArgumentError); + }); + + test("refuses an invalid value in a list", () { + expect(() { + new Metadata.parse(onPlatform: {"chrome": [new TestOn("chrome")]}); + }, throwsArgumentError); + }); + + test("refuses an invalid platform selector", () { + expect(() { + new Metadata.parse(onPlatform: {"invalid": new Skip()}); + }, throwsFormatException); + }); + + test("refuses multiple Timeouts", () { + expect(() { + new Metadata.parse(onPlatform: { + "chrome": [new Timeout.factor(2), new Timeout.factor(3)] + }); + }, throwsArgumentError); + }); + + test("refuses multiple Skips", () { + expect(() { + new Metadata.parse(onPlatform: {"chrome": [new Skip(), new Skip()]}); + }, throwsArgumentError); + }); + }); +} diff --git a/test/runner/browser/runner_test.dart b/test/runner/browser/runner_test.dart index f2d3c638..146c05fd 100644 --- a/test/runner/browser/runner_test.dart +++ b/test/runner/browser/runner_test.dart @@ -513,6 +513,100 @@ void main() { expect(result.stdout, contains("Test timed out after 0 seconds.")); expect(result.stdout, contains("-1: Some tests failed.")); }); + + group("in onPlatform", () { + test("respects matching Skips", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: {"chrome": new Skip()}); +} +'''); + + var result = _runUnittest(["-p", "chrome", "test.dart"]); + expect(result.stdout, contains("+0 ~1: All tests skipped.")); + }); + + test("ignores non-matching Skips", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: {"vm": new Skip()}); +} +'''); + + var result = _runUnittest(["-p", "chrome", "test.dart"]); + expect(result.stdout, contains("+1: All tests passed!")); + }); + + test("respects matching Timeouts", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: { + "chrome": new Timeout(new Duration(seconds: 0)) + }); +} +'''); + + var result = _runUnittest(["-p", "chrome", "test.dart"]); + expect(result.stdout, contains("Test timed out after 0 seconds.")); + expect(result.stdout, contains("-1: Some tests failed.")); + }); + + test("ignores non-matching Timeouts", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "vm": new Timeout(new Duration(seconds: 0)) + }); +} +'''); + + var result = _runUnittest(["-p", "chrome", "test.dart"]); + expect(result.stdout, contains("+1: All tests passed!")); + }); + + test("applies matching platforms in order", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "chrome": new Skip("first"), + "chrome || windows": new Skip("second"), + "chrome || linux": new Skip("third"), + "chrome || mac-os": new Skip("fourth"), + "chrome || android": new Skip("fifth") + }); +} +'''); + + var result = _runUnittest(["-p", "chrome", "test.dart"]); + expect(result.stdout, contains("Skip: fifth")); + expect(result.stdout, isNot(anyOf([ + contains("Skip: first"), + contains("Skip: second"), + contains("Skip: third"), + contains("Skip: fourth") + ]))); + }); + }); } ProcessResult _runUnittest(List<String> args) => diff --git a/test/runner/runner_test.dart b/test/runner/runner_test.dart index d717b671..0214ab9e 100644 --- a/test/runner/runner_test.dart +++ b/test/runner/runner_test.dart @@ -377,6 +377,100 @@ void main() { expect(result.stdout, contains("+0 ~1: All tests skipped.")); }); + group("in onPlatform", () { + test("respects matching Skips", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: {"vm": new Skip()}); +} +'''); + + var result = _runUnittest(["test.dart"]); + expect(result.stdout, contains("+0 ~1: All tests skipped.")); + }); + + test("ignores non-matching Skips", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: {"chrome": new Skip()}); +} +'''); + + var result = _runUnittest(["test.dart"]); + expect(result.stdout, contains("+1: All tests passed!")); + }); + + test("respects matching Timeouts", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: { + "vm": new Timeout(new Duration(seconds: 0)) + }); +} +'''); + + var result = _runUnittest(["test.dart"]); + expect(result.stdout, contains("Test timed out after 0 seconds.")); + expect(result.stdout, contains("-1: Some tests failed.")); + }); + + test("ignores non-matching Timeouts", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "chrome": new Timeout(new Duration(seconds: 0)) + }); +} +'''); + + var result = _runUnittest(["test.dart"]); + expect(result.stdout, contains("+1: All tests passed!")); + }); + + test("applies matching platforms in order", () { + new File(p.join(_sandbox, "test.dart")).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "vm": new Skip("first"), + "vm || windows": new Skip("second"), + "vm || linux": new Skip("third"), + "vm || mac-os": new Skip("fourth"), + "vm || android": new Skip("fifth") + }); +} +'''); + + var result = _runUnittest(["test.dart"]); + expect(result.stdout, contains("Skip: fifth")); + expect(result.stdout, isNot(anyOf([ + contains("Skip: first"), + contains("Skip: second"), + contains("Skip: third"), + contains("Skip: fourth") + ]))); + }); + }); + group("flags:", () { test("with the --color flag, uses colors", () { new File(p.join(_sandbox, "test.dart")).writeAsStringSync(_failure); -- GitLab