OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library test.runner.parse_metadata; | 5 library test.runner.parse_metadata; |
6 | 6 |
7 import 'dart:io'; | 7 import 'dart:io'; |
8 | 8 |
9 import 'package:analyzer/analyzer.dart'; | 9 import 'package:analyzer/analyzer.dart'; |
10 import 'package:analyzer/src/generated/ast.dart'; | 10 import 'package:analyzer/src/generated/ast.dart'; |
11 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
12 import 'package:source_span/source_span.dart'; | 12 import 'package:source_span/source_span.dart'; |
13 | 13 |
14 import '../backend/metadata.dart'; | 14 import '../backend/metadata.dart'; |
| 15 import '../frontend/timeout.dart'; |
15 import '../util/dart.dart'; | 16 import '../util/dart.dart'; |
16 | 17 |
| 18 /// The valid argument names for [new Duration]. |
| 19 const _durationArgs = const [ |
| 20 "days", |
| 21 "hours", |
| 22 "minutes", |
| 23 "seconds", |
| 24 "milliseconds", |
| 25 "microseconds" |
| 26 ]; |
| 27 |
17 /// Parse the test metadata for the test file at [path]. | 28 /// Parse the test metadata for the test file at [path]. |
18 /// | 29 /// |
19 /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the | 30 /// Throws an [AnalysisError] if parsing fails or a [FormatException] if the |
20 /// test annotations are incorrect. | 31 /// test annotations are incorrect. |
21 Metadata parseMetadata(String path) { | 32 Metadata parseMetadata(String path) { |
| 33 var timeout; |
22 var testOn; | 34 var testOn; |
23 | 35 |
24 var contents = new File(path).readAsStringSync(); | 36 var contents = new File(path).readAsStringSync(); |
25 var directives = parseDirectives(contents, name: path).directives; | 37 var directives = parseDirectives(contents, name: path).directives; |
26 var annotations = directives.isEmpty ? [] : directives.first.metadata; | 38 var annotations = directives.isEmpty ? [] : directives.first.metadata; |
27 | 39 |
28 // We explicitly *don't* just look for "package:test" imports here, | 40 // We explicitly *don't* just look for "package:test" imports here, |
29 // because it could be re-exported from another library. | 41 // because it could be re-exported from another library. |
30 var prefixes = directives.map((directive) { | 42 var prefixes = directives.map((directive) { |
31 if (directive is! ImportDirective) return null; | 43 if (directive is! ImportDirective) return null; |
(...skipping 17 matching lines...) Expand all Loading... |
49 constructorName = identifier.identifier.name; | 61 constructorName = identifier.identifier.name; |
50 } else { | 62 } else { |
51 name = identifier is PrefixedIdentifier | 63 name = identifier is PrefixedIdentifier |
52 ? identifier.identifier.name | 64 ? identifier.identifier.name |
53 : identifier.name; | 65 : identifier.name; |
54 if (annotation.constructorName != null) { | 66 if (annotation.constructorName != null) { |
55 constructorName = annotation.constructorName.name; | 67 constructorName = annotation.constructorName.name; |
56 } | 68 } |
57 } | 69 } |
58 | 70 |
59 if (name != 'TestOn') continue; | 71 if (name == 'TestOn') { |
60 if (constructorName != null) { | 72 if (testOn != null) { |
61 throw new SourceSpanFormatException( | 73 throw new SourceSpanFormatException( |
62 'TestOn doesn\'t have a constructor named "$constructorName".', | 74 "Only a single TestOn annotation may be used for a given test file."
, |
63 _spanFor(identifier.identifier, path)); | 75 _spanFor(annotation, path)); |
64 } | 76 } |
65 | 77 testOn = _parseTestOn(annotation, constructorName, path); |
66 if (annotation.arguments == null) { | 78 } else if (name == 'Timeout') { |
67 throw new SourceSpanFormatException( | 79 if (timeout != null) { |
68 'TestOn takes one argument.', _spanFor(annotation, path)); | 80 throw new SourceSpanFormatException( |
69 } | 81 "Only a single Timeout annotation may be used for a given test file.
", |
70 | 82 _spanFor(annotation, path)); |
71 var args = annotation.arguments.arguments; | 83 } |
72 if (args.isEmpty) { | 84 timeout = _parseTimeout(annotation, constructorName, path); |
73 throw new SourceSpanFormatException( | 85 } |
74 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); | |
75 } | |
76 | |
77 if (args.first is NamedExpression) { | |
78 throw new SourceSpanFormatException( | |
79 "TestOn doesn't take named parameters.", _spanFor(args.first, path)); | |
80 } | |
81 | |
82 if (args.length > 1) { | |
83 throw new SourceSpanFormatException( | |
84 "TestOn takes only one argument.", | |
85 _spanFor(annotation.arguments, path)); | |
86 } | |
87 | |
88 if (args.first is! StringLiteral) { | |
89 throw new SourceSpanFormatException( | |
90 "TestOn takes a String.", _spanFor(args.first, path)); | |
91 } | |
92 | |
93 if (testOn != null) { | |
94 throw new SourceSpanFormatException( | |
95 "Only a single TestOn annotation may be used for a given test file.", | |
96 _spanFor(annotation, path)); | |
97 } | |
98 | |
99 testOn = args.first; | |
100 } | 86 } |
101 | 87 |
102 try { | 88 try { |
103 return new Metadata.parse( | 89 return new Metadata.parse( |
104 testOn: testOn == null ? null : testOn.stringValue); | 90 testOn: testOn == null ? null : testOn.stringValue, |
| 91 timeout: timeout); |
105 } on SourceSpanFormatException catch (error) { | 92 } on SourceSpanFormatException catch (error) { |
106 var file = new SourceFile(new File(path).readAsStringSync(), | 93 var file = new SourceFile(new File(path).readAsStringSync(), |
107 url: p.toUri(path)); | 94 url: p.toUri(path)); |
108 var span = contextualizeSpan(error.span, testOn, file); | 95 var span = contextualizeSpan(error.span, testOn, file); |
109 if (span == null) rethrow; | 96 if (span == null) rethrow; |
110 throw new SourceSpanFormatException(error.message, span); | 97 throw new SourceSpanFormatException(error.message, span); |
111 } | 98 } |
112 } | 99 } |
113 | 100 |
| 101 /// Parses a `@TestOn` annotation. |
| 102 /// |
| 103 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 104 /// constructor for the annotation, if any. [path] is the path to the file from |
| 105 /// which the annotation was parsed. |
| 106 StringLiteral _parseTestOn(Annotation annotation, String constructorName, |
| 107 String path) { |
| 108 if (constructorName != null) { |
| 109 throw new SourceSpanFormatException( |
| 110 'TestOn doesn\'t have a constructor named "$constructorName".', |
| 111 _spanFor(annotation, path)); |
| 112 } |
| 113 |
| 114 if (annotation.arguments == null) { |
| 115 throw new SourceSpanFormatException( |
| 116 'TestOn takes one argument.', _spanFor(annotation, path)); |
| 117 } |
| 118 |
| 119 var args = annotation.arguments.arguments; |
| 120 if (args.isEmpty) { |
| 121 throw new SourceSpanFormatException( |
| 122 'TestOn takes one argument.', _spanFor(annotation.arguments, path)); |
| 123 } |
| 124 |
| 125 if (args.first is NamedExpression) { |
| 126 throw new SourceSpanFormatException( |
| 127 "TestOn doesn't take named parameters.", _spanFor(args.first, path)); |
| 128 } |
| 129 |
| 130 if (args.length > 1) { |
| 131 throw new SourceSpanFormatException( |
| 132 "TestOn takes only one argument.", |
| 133 _spanFor(annotation.arguments, path)); |
| 134 } |
| 135 |
| 136 if (args.first is! StringLiteral) { |
| 137 throw new SourceSpanFormatException( |
| 138 "TestOn takes a String.", _spanFor(args.first, path)); |
| 139 } |
| 140 |
| 141 return args.first; |
| 142 } |
| 143 |
| 144 /// Parses a `@Timeout` annotation. |
| 145 /// |
| 146 /// [annotation] is the annotation. [constructorName] is the name of the named |
| 147 /// constructor for the annotation, if any. [path] is the path to the file from |
| 148 /// which the annotation was parsed. |
| 149 Timeout _parseTimeout(Annotation annotation, String constructorName, |
| 150 String path) { |
| 151 if (constructorName != null && constructorName != 'factor') { |
| 152 throw new SourceSpanFormatException( |
| 153 'Timeout doesn\'t have a constructor named "$constructorName".', |
| 154 _spanFor(annotation, path)); |
| 155 } |
| 156 |
| 157 var description = 'Timeout'; |
| 158 if (constructorName != null) description += '.$constructorName'; |
| 159 |
| 160 if (annotation.arguments == null) { |
| 161 throw new SourceSpanFormatException( |
| 162 '$description takes one argument.', _spanFor(annotation, path)); |
| 163 } |
| 164 |
| 165 var args = annotation.arguments.arguments; |
| 166 if (args.isEmpty) { |
| 167 throw new SourceSpanFormatException( |
| 168 '$description takes one argument.', |
| 169 _spanFor(annotation.arguments, path)); |
| 170 } |
| 171 |
| 172 if (args.first is NamedExpression) { |
| 173 throw new SourceSpanFormatException( |
| 174 "$description doesn't take named parameters.", |
| 175 _spanFor(args.first, path)); |
| 176 } |
| 177 |
| 178 if (args.length > 1) { |
| 179 throw new SourceSpanFormatException( |
| 180 "$description takes only one argument.", |
| 181 _spanFor(annotation.arguments, path)); |
| 182 } |
| 183 |
| 184 if (constructorName == null) { |
| 185 return new Timeout(_parseDuration(args.first, path)); |
| 186 } else { |
| 187 return new Timeout.factor(_parseNum(args.first, path)); |
| 188 } |
| 189 } |
| 190 |
| 191 /// Parses a `const Duration` expression. |
| 192 Duration _parseDuration(Expression expression, String path) { |
| 193 if (expression is! InstanceCreationExpression) { |
| 194 throw new SourceSpanFormatException( |
| 195 "Expected a Duration.", |
| 196 _spanFor(expression, path)); |
| 197 } |
| 198 |
| 199 var constructor = expression as InstanceCreationExpression; |
| 200 if (constructor.constructorName.type.name.name != 'Duration') { |
| 201 throw new SourceSpanFormatException( |
| 202 "Expected a Duration.", |
| 203 _spanFor(constructor, path)); |
| 204 } |
| 205 |
| 206 if (constructor.keyword.lexeme != "const") { |
| 207 throw new SourceSpanFormatException( |
| 208 "Duration must use a const constructor.", |
| 209 _spanFor(constructor, path)); |
| 210 } |
| 211 |
| 212 if (constructor.constructorName.name != null) { |
| 213 throw new SourceSpanFormatException( |
| 214 "Duration doesn't have a constructor named " |
| 215 '"${constructor.constructorName}".', |
| 216 _spanFor(constructor.constructorName, path)); |
| 217 } |
| 218 |
| 219 var values = {}; |
| 220 var args = constructor.argumentList.arguments; |
| 221 for (var argument in args) { |
| 222 if (argument is! NamedExpression) { |
| 223 throw new SourceSpanFormatException( |
| 224 "Duration doesn't take positional arguments.", |
| 225 _spanFor(argument, path)); |
| 226 } |
| 227 |
| 228 var name = argument.name.label.name; |
| 229 if (!_durationArgs.contains(name)) { |
| 230 throw new SourceSpanFormatException( |
| 231 'Duration doesn\'t take an argument named "$name".', |
| 232 _spanFor(argument, path)); |
| 233 } |
| 234 |
| 235 if (values.containsKey(name)) { |
| 236 throw new SourceSpanFormatException( |
| 237 'An argument named "$name" was already passed.', |
| 238 _spanFor(argument, path)); |
| 239 } |
| 240 |
| 241 values[name] = _parseInt(argument.expression, path); |
| 242 } |
| 243 |
| 244 return new Duration( |
| 245 days: values["days"] == null ? 0 : values["days"], |
| 246 hours: values["hours"] == null ? 0 : values["hours"], |
| 247 minutes: values["minutes"] == null ? 0 : values["minutes"], |
| 248 seconds: values["seconds"] == null ? 0 : values["seconds"], |
| 249 milliseconds: values["milliseconds"] == null ? 0 : values["milliseconds"], |
| 250 microseconds: |
| 251 values["microseconds"] == null ? 0 : values["microseconds"]); |
| 252 } |
| 253 |
| 254 /// Parses a constant number literal. |
| 255 num _parseNum(Expression expression, String path) { |
| 256 if (expression is IntegerLiteral) return expression.value; |
| 257 if (expression is DoubleLiteral) return expression.value; |
| 258 throw new SourceSpanFormatException( |
| 259 "Expected a number.", _spanFor(expression, path)); |
| 260 } |
| 261 |
| 262 /// Parses a constant int literal. |
| 263 int _parseInt(Expression expression, String path) { |
| 264 if (expression is IntegerLiteral) return expression.value; |
| 265 throw new SourceSpanFormatException( |
| 266 "Expected an integer.", _spanFor(expression, path)); |
| 267 } |
| 268 |
114 /// Creates a [SourceSpan] for [node]. | 269 /// Creates a [SourceSpan] for [node]. |
115 SourceSpan _spanFor(AstNode node, String path) => | 270 SourceSpan _spanFor(AstNode node, String path) => |
116 // Load a SourceFile from scratch here since we're only ever going to emit | 271 // Load a SourceFile from scratch here since we're only ever going to emit |
117 // one error per file anyway. | 272 // one error per file anyway. |
118 new SourceFile(new File(path).readAsStringSync(), url: p.toUri(path)) | 273 new SourceFile(new File(path).readAsStringSync(), url: p.toUri(path)) |
119 .span(node.offset, node.end); | 274 .span(node.offset, node.end); |
OLD | NEW |