diff --git a/bin/unittest.dart b/bin/unittest.dart index 53cfcde65c72ed1620efd288366ab1e2bc3b636a..b86cf2f1d14270005bc41c6eb2ddb4898b6b601c 100644 --- a/bin/unittest.dart +++ b/bin/unittest.dart @@ -11,8 +11,8 @@ import 'dart:isolate'; import 'package:args/args.dart'; import 'package:stack_trace/stack_trace.dart'; +import 'package:unittest/src/backend/test_platform.dart'; import 'package:unittest/src/runner/reporter/compact.dart'; -import 'package:unittest/src/runner/test_platform.dart'; import 'package:unittest/src/runner/load_exception.dart'; import 'package:unittest/src/runner/loader.dart'; import 'package:unittest/src/util/exit_codes.dart' as exit_codes; diff --git a/lib/src/backend/operating_system.dart b/lib/src/backend/operating_system.dart new file mode 100644 index 0000000000000000000000000000000000000000..cf964306931337c534faf6ed9e22bfcd521fea47 --- /dev/null +++ b/lib/src/backend/operating_system.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. + +library unittest.backend.operating_system; + +/// An enum of all operating systems supported by Dart. +/// +/// This is used for selecting which operating systems a test can run on. Even +/// for browser tests, this indicates the operating system of the machine +/// running the test runner. +class OperatingSystem { + /// Microsoft Windows. + static const windows = const OperatingSystem._("windows"); + + /// Mac OS X. + static const macOS = const OperatingSystem._("mac-os"); + + /// GNU/Linux. + static const linux = const OperatingSystem._("linux"); + + /// Android. + /// + /// Since this is the operating system the test runner is running on, this + /// won't be true when testing remotely on an Android browser. + static const android = const OperatingSystem._("android"); + + /// No operating system. + /// + /// This is used when running in the browser, or if an unrecognized operating + /// system is used. It can't be referenced by name in platform selectors. + static const none = const OperatingSystem._("none"); + + /// A list of all instances of [OperatingSystem] other than [none]. + static const all = const [windows, macOS, linux, android]; + + /// Finds an operating system by its name. + /// + /// If no operating system is found, returns [none]. + static OperatingSystem find(String name) => + all.firstWhere((platform) => platform.name == name, orElse: () => null); + + /// Finds an operating system by the return value from `dart:io`'s + /// `Platform.operatingSystem`. + /// + /// If no operating system is found, returns [none]. + static OperatingSystem findByIoName(String name) { + switch (name) { + case "windows": return windows; + case "macos": return macOS; + case "linux": return linux; + case "android": return android; + default: return none; + } + } + + /// The name of the operating system. + final String name; + + /// Whether this is a POSIX-ish operating system. + bool get isPosix => this != windows && this != none; + + const OperatingSystem._(this.name); + + String toString() => name; +} diff --git a/lib/src/backend/platform_selector.dart b/lib/src/backend/platform_selector.dart new file mode 100644 index 0000000000000000000000000000000000000000..46e4db27aabc796340893b94f8db261585d2ac9e --- /dev/null +++ b/lib/src/backend/platform_selector.dart @@ -0,0 +1,59 @@ +// 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. + +library unittest.backend.platform_selector; + +import 'package:source_span/source_span.dart'; + +import 'operating_system.dart'; +import 'platform_selector/ast.dart'; +import 'platform_selector/evaluator.dart'; +import 'platform_selector/parser.dart'; +import 'platform_selector/visitor.dart'; +import 'test_platform.dart'; + +/// The set of all valid variable names. +final _validVariables = + new Set<String>.from(["posix", "dart-vm", "browser", "js", "blink"]) + ..addAll(TestPlatform.all.map((platform) => platform.identifier)) + ..addAll(OperatingSystem.all.map((os) => os.name)); + +/// An expression for selecting certain platforms, including operating systems +/// and browsers. +/// +/// The syntax is mostly Dart's expression syntax restricted to boolean +/// operations. See the README for full details. +class PlatformSelector { + /// The parsed AST. + final Node _selector; + + /// Parses [selector]. + /// + /// This will throw a [SourceSpanFormatException] if the selector is + /// malformed or if it uses an undefined variable. + PlatformSelector.parse(String selector) + : _selector = new Parser(selector).parse() { + _selector.accept(const _VariableValidator()); + } + + /// Returns whether the selector matches the given [platform] and [os]. + /// + /// [os] defaults to [OperatingSystem.none]. + bool evaluate(TestPlatform platform, {OperatingSystem os}) => + _selector.accept(new Evaluator(platform, os: os)); +} + +/// An AST visitor that ensures that all variables are valid. +/// +/// This isn't done when evaluating to ensure that errors are eagerly detected, +/// and it isn't done when parsing to avoid coupling the syntax too tightly to +/// the semantics. +class _VariableValidator extends RecursiveVisitor { + const _VariableValidator(); + + void visitVariable(VariableNode node) { + if (_validVariables.contains(node.name)) return; + throw new SourceSpanFormatException("Undefined variable.", node.span); + } +} diff --git a/lib/src/backend/platform_selector/ast.dart b/lib/src/backend/platform_selector/ast.dart index d1eb0aac16f50b30f33a9256e171cb48dd405313..c76fe18096598fa13056f6b41525607b6b3e461f 100644 --- a/lib/src/backend/platform_selector/ast.dart +++ b/lib/src/backend/platform_selector/ast.dart @@ -6,6 +6,8 @@ library unittest.backend.platform_selector.ast; import 'package:source_span/source_span.dart'; +import 'visitor.dart'; + /// The superclass of nodes in the platform selector abstract syntax tree. abstract class Node { /// The span indicating where this node came from. @@ -16,6 +18,9 @@ abstract class Node { /// /// This may be `null` for nodes without source information. FileSpan get span; + + /// Calls the appropriate [Visitor] method on [this] and returns the result. + accept(Visitor visitor); } /// A single variable. @@ -27,6 +32,8 @@ class VariableNode implements Node { VariableNode(this.name, [this.span]); + accept(Visitor visitor) => visitor.visitVariable(this); + String toString() => name; } @@ -39,6 +46,8 @@ class NotNode implements Node { NotNode(this.child, [this.span]); + accept(Visitor visitor) => visitor.visitNot(this); + String toString() => child is VariableNode || child is NotNode ? "!$child" : "!($child)"; @@ -56,6 +65,8 @@ class OrNode implements Node { OrNode(this.left, this.right); + accept(Visitor visitor) => visitor.visitOr(this); + String toString() { var string1 = left is AndNode || left is ConditionalNode ? "($left)" @@ -80,6 +91,8 @@ class AndNode implements Node { AndNode(this.left, this.right); + accept(Visitor visitor) => visitor.visitAnd(this); + String toString() { var string1 = left is OrNode || left is ConditionalNode ? "($left)" @@ -107,6 +120,8 @@ class ConditionalNode implements Node { ConditionalNode(this.condition, this.whenTrue, this.whenFalse); + accept(Visitor visitor) => visitor.visitConditional(this); + String toString() { var conditionString = condition is ConditionalNode ? "($condition)" : condition; diff --git a/lib/src/backend/platform_selector/evaluator.dart b/lib/src/backend/platform_selector/evaluator.dart new file mode 100644 index 0000000000000000000000000000000000000000..da035a3deed401778ee9a7228e6bba5891fdc0ac --- /dev/null +++ b/lib/src/backend/platform_selector/evaluator.dart @@ -0,0 +1,51 @@ +// 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. + +library unittest.backend.platform_selector.evaluator; + +import 'package:source_span/source_span.dart'; + +import '../operating_system.dart'; +import '../test_platform.dart'; +import 'ast.dart'; +import 'visitor.dart'; + +/// A visitor for evaluating platform selectors against a specific +/// [TestPlatform] and [OperatingSystem]. +class Evaluator implements Visitor<bool> { + /// The platform to test against. + final TestPlatform _platform; + + /// The operating system to test against. + final OperatingSystem _os; + + Evaluator(this._platform, {OperatingSystem os}) + : _os = os == null ? OperatingSystem.none : os; + + bool visitVariable(VariableNode node) { + if (node.name == _platform.identifier) return true; + if (node.name == _os.name) return true; + + switch (node.name) { + case "dart-vm": return _platform.isDartVm; + case "browser": return _platform.isBrowser; + case "js": return _platform.isJS; + case "blink": return _platform.isBlink; + case "posix": return _os.isPosix; + default: return false; + } + } + + bool visitNot(NotNode node) => !node.child.accept(this); + + bool visitOr(OrNode node) => + node.left.accept(this) || node.right.accept(this); + + bool visitAnd(AndNode node) => + node.left.accept(this) && node.right.accept(this); + + bool visitConditional(ConditionalNode node) => node.condition.accept(this) + ? node.whenTrue.accept(this) + : node.whenFalse.accept(this); +} diff --git a/lib/src/backend/platform_selector/visitor.dart b/lib/src/backend/platform_selector/visitor.dart new file mode 100644 index 0000000000000000000000000000000000000000..09951e49fc1b22f5d860cecdd8c766e4f07d466b --- /dev/null +++ b/lib/src/backend/platform_selector/visitor.dart @@ -0,0 +1,46 @@ +// 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. + +library unittest.backend.platform_selector.visitor; + +import 'ast.dart'; + +/// The interface for visitors of the platform selector AST. +abstract class Visitor<T> { + T visitVariable(VariableNode node); + T visitNot(NotNode node); + T visitOr(OrNode node); + T visitAnd(AndNode node); + T visitConditional(ConditionalNode node); +} + +/// An abstract superclass for side-effect-based visitors. +/// +/// The default implementations of this visitor's methods just traverse the AST +/// and do nothing with it. +abstract class RecursiveVisitor implements Visitor { + const RecursiveVisitor(); + + void visitVariable(VariableNode node) {} + + void visitNot(NotNode node) { + node.child.accept(this); + } + + void visitOr(OrNode node) { + node.left.accept(this); + node.right.accept(this); + } + + void visitAnd(AndNode node) { + node.left.accept(this); + node.right.accept(this); + } + + void visitConditional(ConditionalNode node) { + node.condition.accept(this); + node.whenTrue.accept(this); + node.whenFalse.accept(this); + } +} diff --git a/lib/src/runner/test_platform.dart b/lib/src/backend/test_platform.dart similarity index 52% rename from lib/src/runner/test_platform.dart rename to lib/src/backend/test_platform.dart index 406dad0cefc4ebef5222cde9bb661282007de23d..a52d3075fa91011cd0e8b2891cc76c96c8c5b3e8 100644 --- a/lib/src/runner/test_platform.dart +++ b/lib/src/backend/test_platform.dart @@ -2,14 +2,20 @@ // 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. +library unittest.backend.test_platform; + // TODO(nweiz): support pluggable platforms. /// An enum of all platforms on which tests can run. class TestPlatform { + // When adding new platforms, be sure to update the baseline and derived + // variable tests in test/backend/platform_selector/evaluate_test. + /// The command-line Dart VM. - static const vm = const TestPlatform._("VM", "vm"); + static const vm = const TestPlatform._("VM", "vm", isDartVm: true); /// Google Chrome. - static const chrome = const TestPlatform._("Chrome", "chrome"); + static const chrome = const TestPlatform._("Chrome", "chrome", + isBrowser: true, isJS: true, isBlink: true); /// A list of all instances of [TestPlatform]. static const all = const [vm, chrome]; @@ -27,7 +33,20 @@ class TestPlatform { /// The identifier used to look up the platform. final String identifier; - const TestPlatform._(this.name, this.identifier); + /// Whether this platform runs the Dart VM in any capacity. + final bool isDartVm; + + /// Whether this platform is a browser. + final bool isBrowser; + + /// Whether this platform runs Dart compiled to JavaScript. + final bool isJS; + + /// Whether this platform uses the Blink rendering engine. + final bool isBlink; + + const TestPlatform._(this.name, this.identifier, {this.isDartVm: false, + this.isBrowser: false, this.isJS: false, this.isBlink: false}); String toString() => name; } diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart index f9517124b06751d8a36881b03232187ffb0e192e..6188cd5bd47cafa2c962b1a0a1cf590b60cb7461 100644 --- a/lib/src/runner/loader.dart +++ b/lib/src/runner/loader.dart @@ -11,7 +11,7 @@ import 'dart:isolate'; import 'package:path/path.dart' as p; import '../backend/suite.dart'; -import '../runner/test_platform.dart'; +import '../backend/test_platform.dart'; import '../util/dart.dart'; import '../util/io.dart'; import '../util/remote_exception.dart'; diff --git a/test/backend/platform_selector/evaluate_test.dart b/test/backend/platform_selector/evaluate_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..cca156008a2b9fe8274397875cb8f6a7e3db4cad --- /dev/null +++ b/test/backend/platform_selector/evaluate_test.dart @@ -0,0 +1,140 @@ +// 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 'dart:io'; + +import 'package:unittest/unittest.dart'; +import 'package:unittest/src/backend/operating_system.dart'; +import 'package:unittest/src/backend/platform_selector.dart'; +import 'package:unittest/src/backend/test_platform.dart'; + +void main() { + test("new PlatformSelector.parse() disallows invalid variables", () { + expect(() => new PlatformSelector.parse("undefined"), + throwsFormatException); + }); + + group("operator:", () { + test("conditional", () { + _expectEval("vm ? vm : browser", true); + _expectEval("vm ? browser : vm", false); + _expectEval("browser ? vm : browser", false); + _expectEval("browser ? browser : vm", true); + }); + + test("or", () { + _expectEval("vm || vm", true); + _expectEval("vm || browser", true); + _expectEval("browser || vm", true); + _expectEval("browser || browser", false); + }); + + test("and", () { + _expectEval("vm && vm", true); + _expectEval("vm && browser", false); + _expectEval("browser && vm", false); + _expectEval("browser && browser", false); + }); + + test("not", () { + _expectEval("!vm", false); + _expectEval("!browser", true); + }); + }); + + group("baseline variable:", () { + test("vm", () { + _expectEval("vm", true, platform: TestPlatform.vm); + _expectEval("vm", false, platform: TestPlatform.chrome); + }); + + test("chrome", () { + _expectEval("chrome", true, platform: TestPlatform.chrome); + _expectEval("chrome", false, platform: TestPlatform.vm); + }); + + test("windows", () { + _expectEval("windows", true, os: OperatingSystem.windows); + _expectEval("windows", false, os: OperatingSystem.linux); + _expectEval("windows", false, os: OperatingSystem.none); + }); + + test("mac-os", () { + _expectEval("mac-os", true, os: OperatingSystem.macOS); + _expectEval("mac-os", false, os: OperatingSystem.linux); + _expectEval("mac-os", false, os: OperatingSystem.none); + }); + + test("linux", () { + _expectEval("linux", true, os: OperatingSystem.linux); + _expectEval("linux", false, os: OperatingSystem.android); + _expectEval("linux", false, os: OperatingSystem.none); + }); + + test("android", () { + _expectEval("android", true, os: OperatingSystem.android); + _expectEval("android", false, os: OperatingSystem.linux); + _expectEval("android", false, os: OperatingSystem.none); + }); + }); + + group("derived variable:", () { + test("dart-vm", () { + _expectEval("dart-vm", true, platform: TestPlatform.vm); + _expectEval("dart-vm", false, platform: TestPlatform.chrome); + }); + + test("browser", () { + _expectEval("browser", true, platform: TestPlatform.chrome); + _expectEval("browser", false, platform: TestPlatform.vm); + }); + + test("js", () { + _expectEval("js", true, platform: TestPlatform.chrome); + _expectEval("js", false, platform: TestPlatform.vm); + }); + + test("blink", () { + _expectEval("blink", true, platform: TestPlatform.chrome); + _expectEval("blink", false, platform: TestPlatform.vm); + }); + + test("posix", () { + _expectEval("posix", false, os: OperatingSystem.windows); + _expectEval("posix", true, os: OperatingSystem.macOS); + _expectEval("posix", true, os: OperatingSystem.linux); + _expectEval("posix", true, os: OperatingSystem.android); + _expectEval("posix", false, os: OperatingSystem.none); + }); + }); +} + +/// Asserts that [expression] evaluates to [result] on [platform] and [os]. +/// +/// [platform] defaults to [TestPlatform.vm]; [os] defaults to the current +/// operating system. +void _expectEval(String expression, bool result, {TestPlatform platform, + OperatingSystem os}) { + + var reason = 'Expected "$expression" to evaluate to $result'; + if (platform != null && os != null) { + reason += ' on $platform and $os.'; + } else if (platform != null || os != null) { + reason += ' on ${platform == null ? os : platform}'; + } + + expect(_eval(expression, platform: platform, os: os), equals(result), + reason: '$reason.'); +} + +/// Returns the result of evaluating [expression] on [platform] and [os]. +/// +/// [platform] defaults to [TestPlatform.vm]; [os] defaults to the current +/// operating system. +bool _eval(String expression, {TestPlatform platform, OperatingSystem os}) { + if (platform == null) platform = TestPlatform.vm; + if (os == null) os = OperatingSystem.findByIoName(Platform.operatingSystem); + var selector = new PlatformSelector.parse(expression); + return selector.evaluate(platform, os: os); +} diff --git a/test/runner/browser/loader_test.dart b/test/runner/browser/loader_test.dart index 796b0c2318557194e35d43cb3f7eeeb8d15abe53..e2ff2688b4167772de5a2fcedc0263499410660b 100644 --- a/test/runner/browser/loader_test.dart +++ b/test/runner/browser/loader_test.dart @@ -6,7 +6,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'package:unittest/src/backend/state.dart'; -import 'package:unittest/src/runner/test_platform.dart'; +import 'package:unittest/src/backend/test_platform.dart'; import 'package:unittest/src/runner/loader.dart'; import 'package:unittest/unittest.dart';