Index: lib/src/runner/parse_metadata.dart |
diff --git a/lib/src/runner/parse_metadata.dart b/lib/src/runner/parse_metadata.dart |
index 1fcbaf072f067d7ed0103337af11dc889e22b76f..5cc4cb61039b3fa19277c68dc193ba882eff2a7f 100644 |
--- a/lib/src/runner/parse_metadata.dart |
+++ b/lib/src/runner/parse_metadata.dart |
@@ -12,13 +12,25 @@ import 'package:path/path.dart' as p; |
import 'package:source_span/source_span.dart'; |
import '../backend/metadata.dart'; |
+import '../frontend/timeout.dart'; |
import '../util/dart.dart'; |
+/// The valid argument names for [new Duration]. |
+const _durationArgs = const [ |
+ "days", |
+ "hours", |
+ "minutes", |
+ "seconds", |
+ "milliseconds", |
+ "microseconds" |
+]; |
+ |
/// Parse the test metadata for the test file at [path]. |
/// |
/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the |
/// test annotations are incorrect. |
Metadata parseMetadata(String path) { |
+ var timeout; |
var testOn; |
var contents = new File(path).readAsStringSync(); |
@@ -56,59 +68,202 @@ Metadata parseMetadata(String path) { |
} |
} |
- if (name != 'TestOn') continue; |
- if (constructorName != null) { |
- throw new SourceSpanFormatException( |
- 'TestOn doesn\'t have a constructor named "$constructorName".', |
- _spanFor(identifier.identifier, path)); |
+ if (name == 'TestOn') { |
+ if (testOn != null) { |
+ throw new SourceSpanFormatException( |
+ "Only a single TestOn annotation may be used for a given test file.", |
+ _spanFor(annotation, path)); |
+ } |
+ testOn = _parseTestOn(annotation, constructorName, path); |
+ } else if (name == 'Timeout') { |
+ if (timeout != null) { |
+ throw new SourceSpanFormatException( |
+ "Only a single Timeout annotation may be used for a given test file.", |
+ _spanFor(annotation, path)); |
+ } |
+ timeout = _parseTimeout(annotation, constructorName, path); |
} |
+ } |
- if (annotation.arguments == null) { |
- throw new SourceSpanFormatException( |
- 'TestOn takes one argument.', _spanFor(annotation, path)); |
- } |
+ try { |
+ return new Metadata.parse( |
+ testOn: testOn == null ? null : testOn.stringValue, |
+ timeout: timeout); |
+ } 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); |
+ } |
+} |
- var args = annotation.arguments.arguments; |
- if (args.isEmpty) { |
- throw new SourceSpanFormatException( |
- 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); |
- } |
+/// Parses a `@TestOn` annotation. |
+/// |
+/// [annotation] is the annotation. [constructorName] is the name of the named |
+/// constructor for the annotation, if any. [path] is the path to the file from |
+/// which the annotation was parsed. |
+StringLiteral _parseTestOn(Annotation annotation, String constructorName, |
+ String path) { |
+ if (constructorName != null) { |
+ throw new SourceSpanFormatException( |
+ 'TestOn doesn\'t have a constructor named "$constructorName".', |
+ _spanFor(annotation, path)); |
+ } |
- if (args.first is NamedExpression) { |
- throw new SourceSpanFormatException( |
- "TestOn doesn't take named parameters.", _spanFor(args.first, path)); |
- } |
+ if (annotation.arguments == null) { |
+ throw new SourceSpanFormatException( |
+ 'TestOn takes one argument.', _spanFor(annotation, path)); |
+ } |
+ |
+ var args = annotation.arguments.arguments; |
+ if (args.isEmpty) { |
+ throw new SourceSpanFormatException( |
+ 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); |
+ } |
+ |
+ if (args.first is NamedExpression) { |
+ throw new SourceSpanFormatException( |
+ "TestOn doesn't take named parameters.", _spanFor(args.first, path)); |
+ } |
+ |
+ if (args.length > 1) { |
+ throw new SourceSpanFormatException( |
+ "TestOn takes only one argument.", |
+ _spanFor(annotation.arguments, path)); |
+ } |
+ |
+ if (args.first is! StringLiteral) { |
+ throw new SourceSpanFormatException( |
+ "TestOn takes a String.", _spanFor(args.first, path)); |
+ } |
+ |
+ return args.first; |
+} |
+ |
+/// Parses a `@Timeout` annotation. |
+/// |
+/// [annotation] is the annotation. [constructorName] is the name of the named |
+/// constructor for the annotation, if any. [path] is the path to the file from |
+/// which the annotation was parsed. |
+Timeout _parseTimeout(Annotation annotation, String constructorName, |
+ String path) { |
+ if (constructorName != null && constructorName != 'factor') { |
+ throw new SourceSpanFormatException( |
+ 'Timeout doesn\'t have a constructor named "$constructorName".', |
+ _spanFor(annotation, path)); |
+ } |
+ |
+ var description = 'Timeout'; |
+ if (constructorName != null) description += '.$constructorName'; |
+ |
+ if (annotation.arguments == null) { |
+ throw new SourceSpanFormatException( |
+ '$description takes one argument.', _spanFor(annotation, path)); |
+ } |
+ |
+ var args = annotation.arguments.arguments; |
+ if (args.isEmpty) { |
+ throw new SourceSpanFormatException( |
+ '$description takes one argument.', |
+ _spanFor(annotation.arguments, path)); |
+ } |
+ |
+ if (args.first is NamedExpression) { |
+ throw new SourceSpanFormatException( |
+ "$description doesn't take named parameters.", |
+ _spanFor(args.first, path)); |
+ } |
+ |
+ if (args.length > 1) { |
+ throw new SourceSpanFormatException( |
+ "$description takes only one argument.", |
+ _spanFor(annotation.arguments, path)); |
+ } |
+ |
+ if (constructorName == null) { |
+ return new Timeout(_parseDuration(args.first, path)); |
+ } else { |
+ return new Timeout.factor(_parseNum(args.first, path)); |
+ } |
+} |
+ |
+/// Parses a `const Duration` expression. |
+Duration _parseDuration(Expression expression, String path) { |
+ if (expression is! InstanceCreationExpression) { |
+ throw new SourceSpanFormatException( |
+ "Expected a Duration.", |
+ _spanFor(expression, path)); |
+ } |
+ |
+ var constructor = expression as InstanceCreationExpression; |
+ if (constructor.constructorName.type.name.name != 'Duration') { |
+ throw new SourceSpanFormatException( |
+ "Expected a Duration.", |
+ _spanFor(constructor, path)); |
+ } |
- if (args.length > 1) { |
+ if (constructor.keyword.lexeme != "const") { |
+ throw new SourceSpanFormatException( |
+ "Duration must use a const constructor.", |
+ _spanFor(constructor, path)); |
+ } |
+ |
+ if (constructor.constructorName.name != null) { |
+ throw new SourceSpanFormatException( |
+ "Duration doesn't have a constructor named " |
+ '"${constructor.constructorName}".', |
+ _spanFor(constructor.constructorName, path)); |
+ } |
+ |
+ var values = {}; |
+ var args = constructor.argumentList.arguments; |
+ for (var argument in args) { |
+ if (argument is! NamedExpression) { |
throw new SourceSpanFormatException( |
- "TestOn takes only one argument.", |
- _spanFor(annotation.arguments, path)); |
+ "Duration doesn't take positional arguments.", |
+ _spanFor(argument, path)); |
} |
- if (args.first is! StringLiteral) { |
+ var name = argument.name.label.name; |
+ if (!_durationArgs.contains(name)) { |
throw new SourceSpanFormatException( |
- "TestOn takes a String.", _spanFor(args.first, path)); |
+ 'Duration doesn\'t take an argument named "$name".', |
+ _spanFor(argument, path)); |
} |
- if (testOn != null) { |
+ if (values.containsKey(name)) { |
throw new SourceSpanFormatException( |
- "Only a single TestOn annotation may be used for a given test file.", |
- _spanFor(annotation, path)); |
+ 'An argument named "$name" was already passed.', |
+ _spanFor(argument, path)); |
} |
- testOn = args.first; |
+ values[name] = _parseInt(argument.expression, path); |
} |
- try { |
- return new Metadata.parse( |
- testOn: testOn == null ? null : testOn.stringValue); |
- } 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 Duration( |
+ days: values["days"] == null ? 0 : values["days"], |
+ hours: values["hours"] == null ? 0 : values["hours"], |
+ minutes: values["minutes"] == null ? 0 : values["minutes"], |
+ seconds: values["seconds"] == null ? 0 : values["seconds"], |
+ milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"], |
+ microseconds: |
+ values["microseconds"] == null ? 0 : values["microseconds"]); |
+} |
+ |
+/// Parses a constant number literal. |
+num _parseNum(Expression expression, String path) { |
+ if (expression is IntegerLiteral) return expression.value; |
+ if (expression is DoubleLiteral) return expression.value; |
+ throw new SourceSpanFormatException( |
+ "Expected a number.", _spanFor(expression, path)); |
+} |
+ |
+/// Parses a constant int literal. |
+int _parseInt(Expression expression, String path) { |
+ if (expression is IntegerLiteral) return expression.value; |
+ throw new SourceSpanFormatException( |
+ "Expected an integer.", _spanFor(expression, path)); |
} |
/// Creates a [SourceSpan] for [node]. |