diff --git a/lib/src/backend/platform_selector/ast.dart b/lib/src/backend/platform_selector/ast.dart new file mode 100644 index 0000000000000000000000000000000000000000..d1eb0aac16f50b30f33a9256e171cb48dd405313 --- /dev/null +++ b/lib/src/backend/platform_selector/ast.dart @@ -0,0 +1,124 @@ +// 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.ast; + +import 'package:source_span/source_span.dart'; + +/// The superclass of nodes in the platform selector abstract syntax tree. +abstract class Node { + /// The span indicating where this node came from. + /// + /// This is a [FileSpan] because the nodes are parsed from a single continuous + /// string, but the string itself isn't actually a file. It might come from a + /// statically-parsed annotation or from a parameter. + /// + /// This may be `null` for nodes without source information. + FileSpan get span; +} + +/// A single variable. +class VariableNode implements Node { + final FileSpan span; + + /// The variable name. + final String name; + + VariableNode(this.name, [this.span]); + + String toString() => name; +} + +/// A negation expression. +class NotNode implements Node { + final FileSpan span; + + /// The expression being negated. + final Node child; + + NotNode(this.child, [this.span]); + + String toString() => child is VariableNode || child is NotNode + ? "!$child" + : "!($child)"; +} + +/// An or expression. +class OrNode implements Node { + FileSpan get span => _expandSafe(left.span, right.span); + + /// The left-hand branch of the expression. + final Node left; + + /// The right-hand branch of the expression. + final Node right; + + OrNode(this.left, this.right); + + String toString() { + var string1 = left is AndNode || left is ConditionalNode + ? "($left)" + : left; + var string2 = right is AndNode || right is ConditionalNode + ? "($right)" + : right; + + return "$string1 || $string2"; + } +} + +/// An and expression. +class AndNode implements Node { + FileSpan get span => _expandSafe(left.span, right.span); + + /// The left-hand branch of the expression. + final Node left; + + /// The right-hand branch of the expression. + final Node right; + + AndNode(this.left, this.right); + + String toString() { + var string1 = left is OrNode || left is ConditionalNode + ? "($left)" + : left; + var string2 = right is OrNode || right is ConditionalNode + ? "($right)" + : right; + + return "$string1 && $string2"; + } +} + +/// A ternary conditional expression. +class ConditionalNode implements Node { + FileSpan get span => _expandSafe(condition.span, whenFalse.span); + + /// The condition expression to check. + final Node condition; + + /// The branch to run if the condition is true. + final Node whenTrue; + + /// The branch to run if the condition is false. + final Node whenFalse; + + ConditionalNode(this.condition, this.whenTrue, this.whenFalse); + + String toString() { + var conditionString = + condition is ConditionalNode ? "($condition)" : condition; + var trueString = whenTrue is ConditionalNode ? "($whenTrue)" : whenTrue; + return "$conditionString ? $trueString : $whenFalse"; + } +} + +/// Like [FileSpan.expand], except if [start] and [end] are `null` or from +/// different files it returns `null` rather than throwing an error. +FileSpan _expandSafe(FileSpan start, FileSpan end) { + if (start == null || end == null) return null; + if (start.file != end.file) return null; + return start.expand(end); +} diff --git a/lib/src/backend/platform_selector/parser.dart b/lib/src/backend/platform_selector/parser.dart new file mode 100644 index 0000000000000000000000000000000000000000..2761e8c0b2804207aee7682938d94a8e22a807b1 --- /dev/null +++ b/lib/src/backend/platform_selector/parser.dart @@ -0,0 +1,107 @@ +// 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.parser; + +import 'package:source_span/source_span.dart'; + +import 'ast.dart'; +import 'scanner.dart'; +import 'token.dart'; + +/// A class for parsing a platform selector. +/// +/// Platform selectors use a stripped-down version of the Dart expression +/// syntax that only contains variables, parentheses, and boolean operators. +/// Variables may also contain dashes, contrary to Dart's syntax; this allows +/// consistency with command-line arguments. +class Parser { + /// The scanner that tokenizes the selector. + final Scanner _scanner; + + Parser(String selector) + : _scanner = new Scanner(selector); + + /// Parses the selector. + /// + /// This must only be called once per parser. + Node parse() { + var selector = _conditional(); + + if (_scanner.peek().type != TokenType.endOfFile) { + throw new SourceSpanFormatException( + "Expected end of input.", _scanner.peek().span); + } + + return selector; + } + + /// Parses a conditional: + /// + /// conditionalExpression: + /// logicalOrExpression ("?" conditionalExpression ":" + /// conditionalExpression)? + Node _conditional() { + var condition = _or(); + if (!_scanner.scan(TokenType.questionMark)) return condition; + + var whenTrue = _conditional(); + if (!_scanner.scan(TokenType.colon)) { + throw new SourceSpanFormatException( + 'Expected ":".', _scanner.peek().span); + } + + var whenFalse = _conditional(); + return new ConditionalNode(condition, whenTrue, whenFalse); + } + + /// Parses a logical or: + /// + /// logicalOrExpression: + /// logicalAndExpression ("||" logicalOrExpression)? + Node _or() { + var left = _and(); + if (!_scanner.scan(TokenType.or)) return left; + return new OrNode(left, _or()); + } + + /// Parses a logical and: + /// + /// logicalAndExpression: + /// simpleExpression ("&&" logicalAndExpression)? + Node _and() { + var left = _simpleExpression(); + if (!_scanner.scan(TokenType.and)) return left; + return new AndNode(left, _and()); + } + + /// Parses a simple expression: + /// + /// simpleExpression: + /// "!" simpleExpression | + /// "(" conditionalExpression ")" | + /// IDENTIFIER + Node _simpleExpression() { + var token = _scanner.next(); + switch (token.type) { + case TokenType.not: + var child = _simpleExpression(); + return new NotNode(child, token.span.expand(child.span)); + + case TokenType.leftParen: + var child = _conditional(); + if (!_scanner.scan(TokenType.rightParen)) { + throw new SourceSpanFormatException( + 'Expected ")".', _scanner.peek().span); + } + return child; + + case TokenType.identifier: + return new VariableNode(token.name, token.span); + + default: + throw new SourceSpanFormatException("Expected expression.", token.span); + } + } +} diff --git a/lib/src/backend/platform_selector/scanner.dart b/lib/src/backend/platform_selector/scanner.dart new file mode 100644 index 0000000000000000000000000000000000000000..3c2763c9b8a45393ce8ebd6bba05e198d49ee123 --- /dev/null +++ b/lib/src/backend/platform_selector/scanner.dart @@ -0,0 +1,153 @@ +// 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.scanner; + +import 'package:string_scanner/string_scanner.dart'; + +import 'token.dart'; + +/// A regular expression matching both whitespace and single-line comments. +/// +/// This will only match if consumes at least one character. +final _whitespaceAndSingleLineComments = + new RegExp(r"([ \t\n]+|//[^\n]*(\n|$))+"); + +/// A regular expression matching the body of a multi-line comment, after `/*` +/// but before `*/` or a nested `/*`. +/// +/// This will only match if it consumes at least one character. +final _multiLineCommentBody = new RegExp(r"([^/*]|/[^*]|\*[^/])+"); + +/// A regular expression matching an identifier. +/// +/// Unlike standard Dart identifiers, platform selector identifiers may +/// contain dashes for consistency with command-line arguments. +final _identifier = new RegExp(r"[a-zA-Z_-][a-zA-Z0-9_-]*"); + +/// A scanner that converts a platform selector string into a stream of +/// tokens. +class Scanner { + /// The underlying string scanner. + final SpanScanner _scanner; + + /// The next token to emit. + Token _next; + + /// Whether the scanner has emitted a [TokenType.endOfFile] token. + bool _endOfFileEmitted = false; + + Scanner(String selector) + : _scanner = new SpanScanner(selector); + + /// Returns the next token that will be returned by [next]. + /// + /// Throws a [StateError] if a [TokenType.endOfFile] token has already been + /// consumed. + Token peek() { + if (_next == null) _next = _getNext(); + return _next; + } + + /// Consumes and returns the next token in the stream. + /// + /// Throws a [StateError] if a [TokenType.endOfFile] token has already been + /// consumed. + Token next() { + var token = _next == null ? _getNext() : _next; + _endOfFileEmitted = token.type == TokenType.endOfFile; + _next = null; + return token; + } + + /// If the next token matches [type], consumes it and returns `true`; + /// otherwise, returns `false`. + /// + /// Throws a [StateError] if a [TokenType.endOfFile] token has already been + /// consumed. + bool scan(TokenType type) { + if (peek().type != type) return false; + next(); + return true; + } + + /// Scan and return the next token in the stream. + Token _getNext() { + if (_endOfFileEmitted) throw new StateError("No more tokens."); + + _consumeWhitespace(); + if (_scanner.isDone) { + return new Token( + TokenType.endOfFile, _scanner.spanFrom(_scanner.state)); + } + + switch (_scanner.peekChar()) { + case 0x28 /* ( */: return _scanOperator(TokenType.leftParen); + case 0x29 /* ) */: return _scanOperator(TokenType.rightParen); + case 0x3F /* ? */: return _scanOperator(TokenType.questionMark); + case 0x3A /* : */: return _scanOperator(TokenType.colon); + case 0x21 /* ! */: return _scanOperator(TokenType.not); + case 0x7C /* | */: return _scanOr(); + case 0x26 /* & */: return _scanAnd(); + default: return _scanIdentifier(); + } + } + + /// Scans a single-character operator and returns a token of type [type]. + /// + /// This assumes that the caller has already verified that the next character + /// is correct for the given operator. + Token _scanOperator(TokenType type) { + var start = _scanner.state; + _scanner.readChar(); + return new Token(type, _scanner.spanFrom(start)); + } + + /// Scans a `||` operator and returns the appropriate token. + /// + /// This validates that the next two characters are `||`. + Token _scanOr() { + var start = _scanner.state; + _scanner.expect("||"); + return new Token(TokenType.or, _scanner.spanFrom(start)); + } + + /// Scans a `&&` operator and returns the appropriate token. + /// + /// This validates that the next two characters are `&&`. + Token _scanAnd() { + var start = _scanner.state; + _scanner.expect("&&"); + return new Token(TokenType.and, _scanner.spanFrom(start)); + } + + /// Scans and returns an identifier token. + Token _scanIdentifier() { + _scanner.expect(_identifier, name: "expression"); + return new IdentifierToken(_scanner.lastMatch[0], _scanner.lastSpan); + } + + /// Consumes all whitespace and comments immediately following the cursor's + /// current position. + void _consumeWhitespace() { + while (_scanner.scan(_whitespaceAndSingleLineComments) || + _multiLineComment()) { + // Do nothing. + } + } + + /// Consumes a single multi-line comment. + /// + /// Returns whether or not a comment was consumed. + bool _multiLineComment() { + if (!_scanner.scan("/*")) return false; + + while (_scanner.scan(_multiLineCommentBody) || _multiLineComment()) { + // Do nothing. + } + _scanner.expect("*/"); + + return true; + } +} diff --git a/lib/src/backend/platform_selector/token.dart b/lib/src/backend/platform_selector/token.dart new file mode 100644 index 0000000000000000000000000000000000000000..fc6828a76cb48ef5e50e50bd1dfecc3a44bface8 --- /dev/null +++ b/lib/src/backend/platform_selector/token.dart @@ -0,0 +1,72 @@ +// 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.token; + +import 'package:source_span/source_span.dart'; + +/// A token in a platform selector. +class Token { + /// The type of the token. + final TokenType type; + + /// The span indicating where this token came from. + /// + /// This is a [FileSpan] because the tokens are parsed from a single + /// continuous string, but the string itself isn't actually a file. It might + /// come from a statically-parsed annotation or from a parameter. + final FileSpan span; + + Token(this.type, this.span); +} + +/// A token representing an identifier. +class IdentifierToken implements Token { + final type = TokenType.identifier; + final FileSpan span; + + /// The name of the identifier. + final String name; + + IdentifierToken(this.name, this.span); + + String toString() => 'identifier "$name"'; +} + +/// An enumeration of types of tokens. +class TokenType { + /// A `(` character. + static const leftParen = const TokenType._("left paren"); + + /// A `)` character. + static const rightParen = const TokenType._("right paren"); + + /// A `||` sequence. + static const or = const TokenType._("or"); + + /// A `&&` sequence. + static const and = const TokenType._("and"); + + /// A `!` character. + static const not = const TokenType._("not"); + + /// A `?` character. + static const questionMark = const TokenType._("question mark"); + + /// A `:` character. + static const colon = const TokenType._("colon"); + + /// A named identifier. + static const identifier = const TokenType._("identifier"); + + /// The end of the selector. + static const endOfFile = const TokenType._("end of file"); + + /// The name of the token type. + final String name; + + const TokenType._(this.name); + + String toString() => name; +} diff --git a/test/backend/platform_selector/ast_test.dart b/test/backend/platform_selector/ast_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..1362141dd5da0869513c38ed11c9e344187554ce --- /dev/null +++ b/test/backend/platform_selector/ast_test.dart @@ -0,0 +1,83 @@ +// 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:unittest/unittest.dart'; +import 'package:unittest/src/backend/platform_selector/ast.dart'; +import 'package:unittest/src/backend/platform_selector/parser.dart'; + +void main() { + group("toString() for", () { + test("a variable is its name", () { + _expectToString("foo"); + _expectToString("a-b"); + }); + + group("not", () { + test("doesn't parenthesize a variable", () => _expectToString("!a")); + test("doesn't parenthesize a nested not", () => _expectToString("!!a")); + test("parenthesizes an or", () => _expectToString("!(a || b)")); + test("parenthesizes an and", () => _expectToString("!(a && b)")); + test("parenthesizes a condition", () => _expectToString("!(a ? b : c)")); + }); + + group("or", () { + test("doesn't parenthesize variables", () => _expectToString("a || b")); + test("doesn't parenthesize nots", () => _expectToString("!a || !b")); + + test("doesn't parenthesize ors", () { + _expectToString("a || b || c || d"); + _expectToString("((a || b) || c) || d", "a || b || c || d"); + }); + + test("parenthesizes ands", () => + _expectToString("a && b || c && d", "(a && b) || (c && d)")); + + test("parenthesizes conditions", () => + _expectToString("(a ? b : c) || (e ? f : g)")); + }); + + group("and", () { + test("doesn't parenthesize variables", () => _expectToString("a && b")); + test("doesn't parenthesize nots", () => _expectToString("!a && !b")); + + test("parenthesizes ors", () => + _expectToString("(a || b) && (c || d)", "(a || b) && (c || d)")); + + test("doesn't parenthesize ands", () { + _expectToString("a && b && c && d"); + _expectToString("((a && b) && c) && d", "a && b && c && d"); + }); + + test("parenthesizes conditions", () => + _expectToString("(a ? b : c) && (e ? f : g)")); + }); + + group("conditional", () { + test("doesn't parenthesize variables", () => + _expectToString("a ? b : c")); + + test("doesn't parenthesize nots", () => _expectToString("!a ? !b : !c")); + + test("doesn't parenthesize ors", () => + _expectToString("a || b ? c || d : e || f")); + + test("doesn't parenthesize ands", () => + _expectToString("a && b ? c && d : e && f")); + + test("parenthesizes non-trailing conditions", () { + _expectToString("(a ? b : c) ? (e ? f : g) : h ? i : j"); + _expectToString("(a ? b : c) ? (e ? f : g) : (h ? i : j)", + "(a ? b : c) ? (e ? f : g) : h ? i : j"); + }); + }); + }); +} + +void _expectToString(String selector, [String result]) { + if (result == null) result = selector; + expect(_toString(selector), equals(result), + reason: 'Expected toString of "$selector" to be "$result".'); +} + +String _toString(String selector) => new Parser(selector).parse().toString(); diff --git a/test/backend/platform_selector/parser_test.dart b/test/backend/platform_selector/parser_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d53020b923fdae7a7387682d9cb252fcde62232d --- /dev/null +++ b/test/backend/platform_selector/parser_test.dart @@ -0,0 +1,267 @@ +// 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:unittest/unittest.dart'; +import 'package:unittest/src/backend/platform_selector/ast.dart'; +import 'package:unittest/src/backend/platform_selector/parser.dart'; + +/// A matcher that asserts that a value is a [ConditionalNode]. +Matcher _isConditionalNode = + new isInstanceOf<ConditionalNode>("ConditionalNode"); + +/// A matcher that asserts that a value is an [OrNode]. +Matcher _isOrNode = new isInstanceOf<OrNode>("OrNode"); + +/// A matcher that asserts that a value is an [AndNode]. +Matcher _isAndNode = new isInstanceOf<AndNode>("AndNode"); + +/// A matcher that asserts that a value is a [NotNode]. +Matcher _isNotNode = new isInstanceOf<NotNode>("NotNode"); + +void main() { + group("parses a conditional expression", () { + test("with identifiers", () { + var node = _parse(" a ? b : c "); + expect(node.toString(), equals("a ? b : c")); + + expect(node.span.text, equals("a ? b : c")); + expect(node.span.start.offset, equals(2)); + expect(node.span.end.offset, equals(11)); + }); + + test("with nested ors", () { + // Should parse as "(a || b) ? (c || d) : (e || f)". + // Should not parse as "a || (b ? (c || d) : (e || f))". + // Should not parse as "((a || b) ? (c || d) : e) || f". + // Should not parse as "a || (b ? (c || d) : e) || f". + _expectToString("a || b ? c || d : e || f", + "a || b ? c || d : e || f"); + }); + + test("with a conditional expression as branch 1", () { + // Should parse as "a ? (b ? c : d) : e". + var node = _parse("a ? b ? c : d : e"); + expect(node, _isConditionalNode); + expect(node.condition, _isVar("a")); + expect(node.whenFalse, _isVar("e")); + + expect(node.whenTrue, _isConditionalNode); + expect(node.whenTrue.condition, _isVar("b")); + expect(node.whenTrue.whenTrue, _isVar("c")); + expect(node.whenTrue.whenFalse, _isVar("d")); + }); + + test("with a conditional expression as branch 2", () { + // Should parse as "a ? b : (c ? d : e)". + // Should not parse as "(a ? b : c) ? d : e". + var node = _parse("a ? b : c ? d : e"); + expect(node, _isConditionalNode); + expect(node.condition, _isVar("a")); + expect(node.whenTrue, _isVar("b")); + + expect(node.whenFalse, _isConditionalNode); + expect(node.whenFalse.condition, _isVar("c")); + expect(node.whenFalse.whenTrue, _isVar("d")); + expect(node.whenFalse.whenFalse, _isVar("e")); + }); + + group("which must have", () { + test("an expression after the ?", () { + expect(() => _parse("a ?"), throwsFormatException); + expect(() => _parse("a ? && b"), throwsFormatException); + }); + + test("a :", () { + expect(() => _parse("a ? b"), throwsFormatException); + expect(() => _parse("a ? b && c"), throwsFormatException); + }); + + test("an expression after the :", () { + expect(() => _parse("a ? b :"), throwsFormatException); + expect(() => _parse("a ? b : && c"), throwsFormatException); + }); + }); + }); + + group("parses an or expression", () { + test("with identifiers", () { + var node = _parse(" a || b "); + expect(node, _isOrNode); + expect(node.left, _isVar("a")); + expect(node.right, _isVar("b")); + + expect(node.span.text, equals("a || b")); + expect(node.span.start.offset, equals(2)); + expect(node.span.end.offset, equals(8)); + }); + + test("with nested ands", () { + // Should parse as "(a && b) || (c && d)". + // Should not parse as "a && (b || c) && d". + var node = _parse("a && b || c && d"); + expect(node, _isOrNode); + + expect(node.left, _isAndNode); + expect(node.left.left, _isVar("a")); + expect(node.left.right, _isVar("b")); + + expect(node.right, _isAndNode); + expect(node.right.left, _isVar("c")); + expect(node.right.right, _isVar("d")); + }); + + test("with trailing ors", () { + // Should parse as "a || (b || (c || d))", although it doesn't affect the + // semantics. + var node = _parse("a || b || c || d"); + + for (var variable in ["a", "b", "c"]) { + expect(node, _isOrNode); + expect(node.left, _isVar(variable)); + node = node.right; + } + expect(node, _isVar("d")); + }); + + test("which must have an expression after the ||", () { + expect(() => _parse("a ||"), throwsFormatException); + expect(() => _parse("a || && b"), throwsFormatException); + }); + }); + + group("parses an and expression", () { + test("with identifiers", () { + var node = _parse(" a && b "); + expect(node, _isAndNode); + expect(node.left, _isVar("a")); + expect(node.right, _isVar("b")); + + expect(node.span.text, equals("a && b")); + expect(node.span.start.offset, equals(2)); + expect(node.span.end.offset, equals(8)); + }); + + test("with nested nots", () { + // Should parse as "(!a) && (!b)", obviously. + // Should not parse as "!(a && (!b))". + var node = _parse("!a && !b"); + expect(node, _isAndNode); + + expect(node.left, _isNotNode); + expect(node.left.child, _isVar("a")); + + expect(node.right, _isNotNode); + expect(node.right.child, _isVar("b")); + }); + + test("with trailing ands", () { + // Should parse as "a && (b && (c && d))", although it doesn't affect the + // semantics since . + var node = _parse("a && b && c && d"); + + for (var variable in ["a", "b", "c"]) { + expect(node, _isAndNode); + expect(node.left, _isVar(variable)); + node = node.right; + } + expect(node, _isVar("d")); + }); + + test("which must have an expression after the &&", () { + expect(() => _parse("a &&"), throwsFormatException); + expect(() => _parse("a && && b"), throwsFormatException); + }); + }); + + group("parses a not expression", () { + test("with an identifier", () { + var node = _parse(" ! a "); + expect(node, _isNotNode); + expect(node.child, _isVar("a")); + + expect(node.span.text, equals("! a")); + expect(node.span.start.offset, equals(2)); + expect(node.span.end.offset, equals(5)); + }); + + test("with a parenthesized expression", () { + var node = _parse("!(a || b)"); + expect(node, _isNotNode); + + expect(node.child, _isOrNode); + expect(node.child.left, _isVar("a")); + expect(node.child.right, _isVar("b")); + }); + + test("with a nested not", () { + var node = _parse("!!a"); + expect(node, _isNotNode); + expect(node.child, _isNotNode); + expect(node.child.child, _isVar("a")); + }); + + test("which must have an expression after the !", () { + expect(() => _parse("!"), throwsFormatException); + expect(() => _parse("! && a"), throwsFormatException); + }); + }); + + group("parses a parenthesized expression", () { + test("with an identifier", () { + var node = _parse("(a)"); + expect(node, _isVar("a")); + }); + + test("controls precedence", () { + // Without parentheses, this would parse as "(a || b) ? c : d". + var node = _parse("a || (b ? c : d)"); + + expect(node, _isOrNode); + expect(node.left, _isVar("a")); + + expect(node.right, _isConditionalNode); + expect(node.right.condition, _isVar("b")); + expect(node.right.whenTrue, _isVar("c")); + expect(node.right.whenFalse, _isVar("d")); + }); + + group("which must have", () { + test("an expression within the ()", () { + expect(() => _parse("()"), throwsFormatException); + expect(() => _parse("( && a )"), throwsFormatException); + }); + + test("a matching )", () { + expect(() => _parse("( a"), throwsFormatException); + }); + }); + }); + + group("disallows", () { + test("an empty selector", () { + expect(() => _parse(""), throwsFormatException); + }); + + test("too many expressions", () { + expect(() => _parse("a b"), throwsFormatException); + }); + }); +} + +/// Parses [selector] and returns its root node. +Node _parse(String selector) => new Parser(selector).parse(); + +/// A matcher that asserts that a value is a [VariableNode] with the given +/// [name]. +Matcher _isVar(String name) => predicate( + (value) => value is VariableNode && value.name == name, + 'is a variable named "$name"'); + +void _expectToString(String selector, [String result]) { + if (result == null) result = selector; + expect(_toString(selector), equals(result), + reason: 'Expected toString of "$selector" to be "$result".'); +} + +String _toString(String selector) => new Parser(selector).parse().toString(); diff --git a/test/backend/platform_selector/scanner_test.dart b/test/backend/platform_selector/scanner_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..e2ecdda6d67b1b452768444d8f1eac54bb6e39cb --- /dev/null +++ b/test/backend/platform_selector/scanner_test.dart @@ -0,0 +1,266 @@ +// 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:unittest/unittest.dart'; +import 'package:unittest/src/backend/platform_selector/scanner.dart'; +import 'package:unittest/src/backend/platform_selector/token.dart'; + +void main() { + group("peek()", () { + test("returns the next token without consuming it", () { + var scanner = new Scanner("( )"); + expect(scanner.peek().type, equals(TokenType.leftParen)); + expect(scanner.peek().type, equals(TokenType.leftParen)); + expect(scanner.peek().type, equals(TokenType.leftParen)); + }); + + test("returns an end-of-file token at the end of a file", () { + var scanner = new Scanner("( )"); + scanner.next(); + scanner.next(); + + var token = scanner.peek(); + expect(token.type, equals(TokenType.endOfFile)); + expect(token.span.start.offset, equals(3)); + expect(token.span.end.offset, equals(3)); + }); + + test("throws a StateError if called after end-of-file was consumed", () { + var scanner = new Scanner("( )"); + scanner.next(); + scanner.next(); + scanner.next(); + expect(() => scanner.peek(), throwsStateError); + }); + }); + + group("next()", () { + test("consumes and returns the next token", () { + var scanner = new Scanner("( )"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.peek().type, equals(TokenType.rightParen)); + expect(scanner.next().type, equals(TokenType.rightParen)); + }); + + test("returns an end-of-file token at the end of a file", () { + var scanner = new Scanner("( )"); + scanner.next(); + scanner.next(); + + var token = scanner.next(); + expect(token.type, equals(TokenType.endOfFile)); + expect(token.span.start.offset, equals(3)); + expect(token.span.end.offset, equals(3)); + }); + + test("throws a StateError if called after end-of-file was consumed", () { + var scanner = new Scanner("( )"); + scanner.next(); + scanner.next(); + scanner.next(); + expect(() => scanner.next(), throwsStateError); + }); + }); + + group("scan()", () { + test("consumes a matching token and returns true", () { + var scanner = new Scanner("( )"); + expect(scanner.scan(TokenType.leftParen), isTrue); + expect(scanner.peek().type, equals(TokenType.rightParen)); + }); + + test("doesn't consume a matching token and returns false", () { + var scanner = new Scanner("( )"); + expect(scanner.scan(TokenType.questionMark), isFalse); + expect(scanner.peek().type, equals(TokenType.leftParen)); + }); + + test("throws a StateError called after end-of-file was consumed", () { + var scanner = new Scanner("( )"); + scanner.next(); + scanner.next(); + scanner.next(); + expect(() => scanner.scan(TokenType.endOfFile), throwsStateError); + }); + }); + + group("scans a simple token:", () { + test("left paren", () => _expectSimpleScan("(", TokenType.leftParen)); + test("right paren", () => _expectSimpleScan(")", TokenType.rightParen)); + test("or", () => _expectSimpleScan("||", TokenType.or)); + test("and", () => _expectSimpleScan("&&", TokenType.and)); + test("not", () => _expectSimpleScan("!", TokenType.not)); + test("question mark", () => _expectSimpleScan("?", TokenType.questionMark)); + test("colon", () => _expectSimpleScan(":", TokenType.colon)); + }); + + group("scans an identifier that", () { + test("is simple", () { + var token = _scan(" foo "); + expect(token.name, equals("foo")); + expect(token.span.text, equals("foo")); + expect(token.span.start.offset, equals(3)); + expect(token.span.end.offset, equals(6)); + }); + + test("is a single character", () { + var token = _scan("f"); + expect(token.name, equals("f")); + }); + + test("has a leading underscore", () { + var token = _scan("_foo"); + expect(token.name, equals("_foo")); + }); + + test("has a leading dash", () { + var token = _scan("-foo"); + expect(token.name, equals("-foo")); + }); + + test("contains an underscore", () { + var token = _scan("foo_bar"); + expect(token.name, equals("foo_bar")); + }); + + test("contains a dash", () { + var token = _scan("foo-bar"); + expect(token.name, equals("foo-bar")); + }); + + test("is capitalized", () { + var token = _scan("FOO"); + expect(token.name, equals("FOO")); + }); + + test("contains numbers", () { + var token = _scan("foo123"); + expect(token.name, equals("foo123")); + }); + }); + + test("scans an empty selector", () { + expect(_scan("").type, equals(TokenType.endOfFile)); + }); + + test("scans multiple tokens", () { + var scanner = new Scanner("(foo && bar)"); + + var token = scanner.next(); + expect(token.type, equals(TokenType.leftParen)); + expect(token.span.start.offset, equals(0)); + expect(token.span.end.offset, equals(1)); + + token = scanner.next(); + expect(token.type, equals(TokenType.identifier)); + expect(token.name, equals("foo")); + expect(token.span.start.offset, equals(1)); + expect(token.span.end.offset, equals(4)); + + token = scanner.next(); + expect(token.type, equals(TokenType.and)); + expect(token.span.start.offset, equals(5)); + expect(token.span.end.offset, equals(7)); + + token = scanner.next(); + expect(token.type, equals(TokenType.identifier)); + expect(token.name, equals("bar")); + expect(token.span.start.offset, equals(8)); + expect(token.span.end.offset, equals(11)); + + token = scanner.next(); + expect(token.type, equals(TokenType.rightParen)); + expect(token.span.start.offset, equals(11)); + expect(token.span.end.offset, equals(12)); + + token = scanner.next(); + expect(token.type, equals(TokenType.endOfFile)); + expect(token.span.start.offset, equals(12)); + expect(token.span.end.offset, equals(12)); + }); + + group("ignores", () { + test("a single-line comment", () { + var scanner = new Scanner("( // &&\n// ||\n)"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.next().type, equals(TokenType.rightParen)); + expect(scanner.next().type, equals(TokenType.endOfFile)); + }); + + test("a single-line comment without a trailing newline", () { + var scanner = new Scanner("( // &&"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.next().type, equals(TokenType.endOfFile)); + }); + + test("a multi-line comment", () { + var scanner = new Scanner("( /* && * /\n|| */\n)"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.next().type, equals(TokenType.rightParen)); + expect(scanner.next().type, equals(TokenType.endOfFile)); + }); + + test("a multi-line nested comment", () { + var scanner = new Scanner("(/* && /* ? /* || */ : */ ! */)"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.next().type, equals(TokenType.rightParen)); + expect(scanner.next().type, equals(TokenType.endOfFile)); + }); + + test("Dart's notion of whitespace", () { + var scanner = new Scanner("( \t \n)"); + expect(scanner.next().type, equals(TokenType.leftParen)); + expect(scanner.next().type, equals(TokenType.rightParen)); + expect(scanner.next().type, equals(TokenType.endOfFile)); + }); + }); + + group("disallows", () { + test("a single |", () { + expect(() => _scan("|"), throwsFormatException); + }); + + test('"| |"', () { + expect(() => _scan("| |"), throwsFormatException); + }); + + test("a single &", () { + expect(() => _scan("&"), throwsFormatException); + }); + + test('"& &"', () { + expect(() => _scan("& &"), throwsFormatException); + }); + + test("an unknown operator", () { + expect(() => _scan("=="), throwsFormatException); + }); + + test("unicode", () { + expect(() => _scan("öh"), throwsFormatException); + }); + + test("an unclosed multi-line comment", () { + expect(() => _scan("/*"), throwsFormatException); + }); + + test("an unopened multi-line comment", () { + expect(() => _scan("*/"), throwsFormatException); + }); + }); +} + +/// Asserts that the first token scanned from [selector] has type [type], +/// and that that token's span is exactly [selector]. +void _expectSimpleScan(String selector, TokenType type) { + // Complicate the selector to test that the span covers it correctly. + var token = _scan(" $selector "); + expect(token.type, equals(type)); + expect(token.span.text, equals(selector)); + expect(token.span.start.offset, equals(3)); + expect(token.span.end.offset, equals(3 + selector.length)); +} + +/// Scans a single token from [selector]. +Token _scan(String selector) => new Scanner(selector).next();