| Index: lib/src/runner/parse_metadata.dart
|
| diff --git a/lib/src/runner/parse_metadata.dart b/lib/src/runner/parse_metadata.dart
|
| index e4e4079608b8ee72a583eb3d7d9ad909a2aaf381..7f735deb037a3706e08678e85f6f1b7f68354d3d 100644
|
| --- a/lib/src/runner/parse_metadata.dart
|
| +++ b/lib/src/runner/parse_metadata.dart
|
| @@ -12,8 +12,10 @@ import 'package:path/path.dart' as p;
|
| import 'package:source_span/source_span.dart';
|
|
|
| import '../backend/metadata.dart';
|
| +import '../backend/platform_selector.dart';
|
| import '../frontend/timeout.dart';
|
| import '../util/dart.dart';
|
| +import '../utils.dart';
|
|
|
| /// Parse the test metadata for the test file at [path].
|
| ///
|
| @@ -52,64 +54,47 @@ class _Parser {
|
| var timeout;
|
| var testOn;
|
| var skip;
|
| + var onPlatform;
|
|
|
| for (var annotation in _annotations) {
|
| - // The annotation syntax is ambiguous between named constructors and
|
| - // prefixed annotations, so we need to resolve that ambiguity using the
|
| - // known prefixes. The analyzer parses "@x.y()" as prefix "x", annotation
|
| - // "y", and named constructor null. It parses "@x.y.z()" as prefix "x",
|
| - // annotation "y", and named constructor "z".
|
| - var name;
|
| - var constructorName;
|
| - var identifier = annotation.name;
|
| - if (identifier is PrefixedIdentifier &&
|
| - !_prefixes.contains(identifier.prefix.name) &&
|
| - annotation.constructorName == null) {
|
| - name = identifier.prefix.name;
|
| - constructorName = identifier.identifier.name;
|
| - } else {
|
| - name = identifier is PrefixedIdentifier
|
| - ? identifier.identifier.name
|
| - : identifier.name;
|
| - if (annotation.constructorName != null) {
|
| - constructorName = annotation.constructorName.name;
|
| - }
|
| - }
|
| + var pair = _resolveConstructor(
|
| + annotation.name, annotation.constructorName);
|
| + var name = pair.first;
|
| + var constructorName = pair.last;
|
|
|
| if (name == 'TestOn') {
|
| - _assertSingleAnnotation(testOn, 'TestOn', annotation);
|
| + _assertSingle(testOn, 'TestOn', annotation);
|
| testOn = _parseTestOn(annotation, constructorName);
|
| } else if (name == 'Timeout') {
|
| - _assertSingleAnnotation(timeout, 'Timeout', annotation);
|
| + _assertSingle(timeout, 'Timeout', annotation);
|
| timeout = _parseTimeout(annotation, constructorName);
|
| } else if (name == 'Skip') {
|
| - _assertSingleAnnotation(skip, 'Skip', annotation);
|
| + _assertSingle(skip, 'Skip', annotation);
|
| skip = _parseSkip(annotation, constructorName);
|
| + } else if (name == 'OnPlatform') {
|
| + _assertSingle(onPlatform, 'OnPlatform', annotation);
|
| + onPlatform = _parseOnPlatform(annotation, constructorName);
|
| }
|
| }
|
|
|
| - try {
|
| - return new Metadata.parse(
|
| - testOn: testOn == null ? null : testOn.stringValue,
|
| - timeout: timeout,
|
| - skip: skip);
|
| - } on SourceSpanFormatException catch (error) {
|
| - var file = new SourceFile(new File(_path).readAsStringSync(),
|
| - url: p.toUri(_path));
|
| - var span = contextualizeSpan(error.span, testOn, file);
|
| - if (span == null) rethrow;
|
| - throw new SourceSpanFormatException(error.message, span);
|
| - }
|
| + return new Metadata(
|
| + testOn: testOn,
|
| + timeout: timeout,
|
| + skip: skip != null,
|
| + skipReason: skip is String ? skip : null,
|
| + onPlatform: onPlatform);
|
| }
|
|
|
| /// Parses a `@TestOn` annotation.
|
| ///
|
| /// [annotation] is the annotation. [constructorName] is the name of the named
|
| /// constructor for the annotation, if any.
|
| - StringLiteral _parseTestOn(Annotation annotation, String constructorName) {
|
| + PlatformSelector _parseTestOn(Annotation annotation, String constructorName) {
|
| _assertConstructorName(constructorName, 'TestOn', annotation);
|
| _assertArguments(annotation.arguments, 'TestOn', annotation, positional: 1);
|
| - return _parseString(annotation.arguments.arguments.first);
|
| + var literal = _parseString(annotation.arguments.arguments.first);
|
| + return _contextualize(literal,
|
| + () => new PlatformSelector.parse(literal.stringValue));
|
| }
|
|
|
| /// Parses a `@Timeout` annotation.
|
| @@ -131,6 +116,22 @@ class _Parser {
|
| return new Timeout.factor(_parseNum(args.first));
|
| }
|
|
|
| + /// Parses a `Timeout` constructor.
|
| + Timeout _parseTimeoutConstructor(InstanceCreationExpression constructor) {
|
| + var name = _parseConstructor(constructor, 'Timeout',
|
| + validNames: [null, 'factor']);
|
| +
|
| + var description = 'Timeout';
|
| + if (name != null) description += '.$name';
|
| +
|
| + _assertArguments(constructor.argumentList, description, constructor,
|
| + positional: 1);
|
| +
|
| + var args = constructor.argumentList.arguments;
|
| + if (name == null) return new Timeout(_parseDuration(args.first));
|
| + return new Timeout.factor(_parseNum(args.first));
|
| + }
|
| +
|
| /// Parses a `@Skip` annotation.
|
| ///
|
| /// [annotation] is the annotation. [constructorName] is the name of the named
|
| @@ -145,6 +146,70 @@ class _Parser {
|
| return args.isEmpty ? true : _parseString(args.first).stringValue;
|
| }
|
|
|
| + /// Parses a `Skip` constructor.
|
| + ///
|
| + /// Returns either `true` or a reason string.
|
| + _parseSkipConstructor(InstanceCreationExpression constructor) {
|
| + _parseConstructor(constructor, 'Skip');
|
| + _assertArguments(constructor.argumentList, 'Skip', constructor,
|
| + optional: 1);
|
| +
|
| + var args = constructor.argumentList.arguments;
|
| + return args.isEmpty ? true : _parseString(args.first).stringValue;
|
| + }
|
| +
|
| + /// Parses an `@OnPlatform` annotation.
|
| + ///
|
| + /// [annotation] is the annotation. [constructorName] is the name of the named
|
| + /// constructor for the annotation, if any.
|
| + Map<PlatformSelector, Metadata> _parseOnPlatform(Annotation annotation,
|
| + String constructorName) {
|
| + _assertConstructorName(constructorName, 'OnPlatform', annotation);
|
| + _assertArguments(annotation.arguments, 'OnPlatform', annotation,
|
| + positional: 1);
|
| +
|
| + return _parseMap(annotation.arguments.arguments.first, key: (key) {
|
| + var selector = _parseString(key);
|
| + return _contextualize(selector,
|
| + () => new PlatformSelector.parse(selector.stringValue));
|
| + }, value: (value) {
|
| + var expressions = [];
|
| + if (value is ListLiteral) {
|
| + expressions = _parseList(value);
|
| + } else if (value is InstanceCreationExpression) {
|
| + expressions = [value];
|
| + } else {
|
| + throw new SourceSpanFormatException(
|
| + 'Expected a Timeout, Skip, or List of those.',
|
| + _spanFor(value));
|
| + }
|
| +
|
| + var timeout;
|
| + var skip;
|
| + for (var expression in expressions) {
|
| + var className = expression is InstanceCreationExpression
|
| + ? _resolveConstructor(
|
| + expression.constructorName.type.name,
|
| + expression.constructorName.name).first
|
| + : null;
|
| +
|
| + if (className == 'Timeout') {
|
| + _assertSingle(timeout, 'Timeout', expression);
|
| + timeout = _parseTimeoutConstructor(expression);
|
| + } else if (className == 'Skip') {
|
| + _assertSingle(skip, 'Skip', expression);
|
| + skip = _parseSkipConstructor(expression);
|
| + } else {
|
| + throw new SourceSpanFormatException(
|
| + 'Expected a Timeout or Skip.',
|
| + _spanFor(expression));
|
| + }
|
| + }
|
| +
|
| + return new Metadata.parse(timeout: timeout, skip: skip);
|
| + });
|
| + }
|
| +
|
| /// Parses a `const Duration` expression.
|
| Duration _parseDuration(Expression expression) {
|
| _parseConstructor(expression, 'Duration');
|
| @@ -173,11 +238,38 @@ class _Parser {
|
| ///
|
| /// [name] is the name of the annotation and [node] is its location, used for
|
| /// error reporting.
|
| - void _assertSingleAnnotation(Object existing, String name, AstNode node) {
|
| + void _assertSingle(Object existing, String name, AstNode node) {
|
| if (existing == null) return;
|
| throw new SourceSpanFormatException(
|
| - "Only a single $name annotation may be used for a given test file.",
|
| - _spanFor(node));
|
| + "Only a single $name may be used.", _spanFor(node));
|
| + }
|
| +
|
| + /// Resolves a constructor name from its type [identifier] and its
|
| + /// [constructorName].
|
| + ///
|
| + /// Since the parsed file isn't fully resolved, this is necessary to
|
| + /// disambiguate between prefixed names and named constructors.
|
| + Pair<String, String> _resolveConstructor(Identifier identifier,
|
| + SimpleIdentifier constructorName) {
|
| + // The syntax is ambiguous between named constructors and prefixed
|
| + // annotations, so we need to resolve that ambiguity using the known
|
| + // prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y",
|
| + // and named constructor null. It parses "new x.y.z()" as prefix "x",
|
| + // annotation "y", and named constructor "z".
|
| + var className;
|
| + var namedConstructor;
|
| + if (identifier is PrefixedIdentifier &&
|
| + !_prefixes.contains(identifier.prefix.name) &&
|
| + constructorName == null) {
|
| + className = identifier.prefix.name;
|
| + namedConstructor = identifier.identifier.name;
|
| + } else {
|
| + className = identifier is PrefixedIdentifier
|
| + ? identifier.identifier.name
|
| + : identifier.name;
|
| + if (constructorName != null) namedConstructor = constructorName.name;
|
| + }
|
| + return new Pair(className, namedConstructor);
|
| }
|
|
|
| /// Asserts that [constructorName] is a valid constructor name for an AST
|
| @@ -220,7 +312,13 @@ class _Parser {
|
| }
|
|
|
| var constructor = expression as InstanceCreationExpression;
|
| - if (constructor.constructorName.type.name.name != className) {
|
| + var pair = _resolveConstructor(
|
| + constructor.constructorName.type.name,
|
| + constructor.constructorName.name);
|
| + var actualClassName = pair.first;
|
| + var constructorName = pair.last;
|
| +
|
| + if (actualClassName != className) {
|
| throw new SourceSpanFormatException(
|
| "Expected a $className.", _spanFor(constructor));
|
| }
|
| @@ -230,12 +328,9 @@ class _Parser {
|
| "$className must use a const constructor.", _spanFor(constructor));
|
| }
|
|
|
| - var name = constructor.constructorName == null
|
| - ? null
|
| - : constructor.constructorName.name;
|
| - _assertConstructorName(name, className, expression,
|
| + _assertConstructorName(constructorName, className, expression,
|
| validNames: validNames);
|
| - return name;
|
| + return constructorName;
|
| }
|
|
|
| /// Assert that [arguments] is a valid argument list.
|
| @@ -312,6 +407,47 @@ class _Parser {
|
| return namedValues;
|
| }
|
|
|
| + /// Parses a Map literal.
|
| + ///
|
| + /// By default, returns [Expression] keys and values. These can be overridden
|
| + /// with the [key] and [value] parameters.
|
| + Map _parseMap(Expression expression, {key(Expression expression),
|
| + value(Expression expression)}) {
|
| + if (key == null) key = (expression) => expression;
|
| + if (value == null) value = (expression) => expression;
|
| +
|
| + if (expression is! MapLiteral) {
|
| + throw new SourceSpanFormatException(
|
| + "Expected a Map.", _spanFor(expression));
|
| + }
|
| +
|
| + var map = expression as MapLiteral;
|
| + if (map.constKeyword == null) {
|
| + throw new SourceSpanFormatException(
|
| + "Map literals must be const.", _spanFor(map));
|
| + }
|
| +
|
| + return new Map.fromIterable(map.entries,
|
| + key: (entry) => key(entry.key),
|
| + value: (entry) => value(entry.value));
|
| + }
|
| +
|
| + /// Parses a List literal.
|
| + List<Expression> _parseList(Expression expression) {
|
| + if (expression is! ListLiteral) {
|
| + throw new SourceSpanFormatException(
|
| + "Expected a List.", _spanFor(expression));
|
| + }
|
| +
|
| + var list = expression as ListLiteral;
|
| + if (list.constKeyword == null) {
|
| + throw new SourceSpanFormatException(
|
| + "List literals must be const.", _spanFor(list));
|
| + }
|
| +
|
| + return list.elements;
|
| + }
|
| +
|
| /// Parses a constant number literal.
|
| num _parseNum(Expression expression) {
|
| if (expression is IntegerLiteral) return expression.value;
|
| @@ -342,4 +478,18 @@ class _Parser {
|
| return new SourceFile(contents, url: p.toUri(_path))
|
| .span(node.offset, node.end);
|
| }
|
| +
|
| + /// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur
|
| + /// in it relative to [literal].
|
| + _contextualize(StringLiteral literal, fn()) {
|
| + try {
|
| + return fn();
|
| + } on SourceSpanFormatException catch (error) {
|
| + var file = new SourceFile(new File(_path).readAsStringSync(),
|
| + url: p.toUri(_path));
|
| + var span = contextualizeSpan(error.span, literal, file);
|
| + if (span == null) rethrow;
|
| + throw new SourceSpanFormatException(error.message, span);
|
| + }
|
| + }
|
| }
|
|
|