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); |
+ } |
+ } |
} |