OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
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. | |
4 | |
5 library status_file_parser; | |
6 | |
7 import "dart:async"; | |
8 import "dart:convert" show LineSplitter, UTF8; | |
9 import "dart:io"; | |
10 | |
11 import "path.dart"; | |
12 import "status_expression.dart"; | |
13 | |
14 typedef void Action(); | |
15 | |
16 class Expectation { | |
17 // Possible outcomes of running a test. | |
18 static Expectation PASS = byName('Pass'); | |
19 static Expectation CRASH = byName('Crash'); | |
20 static Expectation TIMEOUT = byName('Timeout'); | |
21 static Expectation FAIL = byName('Fail'); | |
22 | |
23 // Special 'FAIL' cases | |
24 static Expectation RUNTIME_ERROR = byName('RuntimeError'); | |
25 static Expectation COMPILETIME_ERROR = byName('CompileTimeError'); | |
26 static Expectation MISSING_RUNTIME_ERROR = byName('MissingRuntimeError'); | |
27 static Expectation MISSING_COMPILETIME_ERROR = | |
28 byName('MissingCompileTimeError'); | |
29 static Expectation STATIC_WARNING = byName('StaticWarning'); | |
30 static Expectation MISSING_STATIC_WARNING = byName('MissingStaticWarning'); | |
31 static Expectation PUB_GET_ERROR = byName('PubGetError'); | |
32 static Expectation NON_UTF8_ERROR = byName('NonUtf8Output'); | |
33 | |
34 // Special 'CRASH' cases | |
35 static Expectation DARTK_CRASH = byName('DartkCrash'); | |
36 | |
37 // Special 'TIMEOUT' cases | |
38 static Expectation DARTK_TIMEOUT = byName('DartkTimeout'); | |
39 | |
40 // Special 'COMPILETIME_ERROR' | |
41 static Expectation DARTK_COMPILETIME_ERROR = byName('DartkCompileTimeError'); | |
42 | |
43 // "meta expectations" | |
44 static Expectation OK = byName('Ok'); | |
45 static Expectation SLOW = byName('Slow'); | |
46 static Expectation SKIP = byName('Skip'); | |
47 static Expectation SKIP_SLOW = byName('SkipSlow'); | |
48 static Expectation SKIP_BY_DESIGN = byName('SkipByDesign'); | |
49 | |
50 // Can be returned by the test runner to say the result should be ignored, | |
51 // and assumed to meet the expectations, due to an infrastructure failure. | |
52 // Do not place in status files. | |
53 static Expectation IGNORE = byName('Ignore'); | |
54 | |
55 static Expectation byName(String name) { | |
56 _initialize(); | |
57 name = name.toLowerCase(); | |
58 if (!_AllExpectations.containsKey(name)) { | |
59 throw new Exception("Expectation.byName(name='$name'): Invalid name."); | |
60 } | |
61 return _AllExpectations[name]; | |
62 } | |
63 | |
64 // Keep a map of all possible Expectation objects, initialized lazily. | |
65 static Map<String, Expectation> _AllExpectations; | |
66 static void _initialize() { | |
67 if (_AllExpectations == null) { | |
68 _AllExpectations = new Map<String, Expectation>(); | |
69 | |
70 Expectation build(String prettyName, | |
71 {Expectation group, bool isMetaExpectation: false}) { | |
72 var expectation = new Expectation._(prettyName, | |
73 group: group, isMetaExpectation: isMetaExpectation); | |
74 assert(!_AllExpectations.containsKey(expectation.name)); | |
75 return _AllExpectations[expectation.name] = expectation; | |
76 } | |
77 | |
78 var fail = build("Fail"); | |
79 var crash = build("Crash"); | |
80 var timeout = build("Timeout"); | |
81 build("Pass"); | |
82 | |
83 var compileError = build("CompileTimeError", group: fail); | |
84 build("MissingCompileTimeError", group: fail); | |
85 build("MissingRuntimeError", group: fail); | |
86 build("RuntimeError", group: fail); | |
87 build("NonUtf8Output", group: fail); | |
88 | |
89 // Dartk sub expectations | |
90 build("DartkCrash", group: crash); | |
91 build("DartkTimeout", group: timeout); | |
92 build("DartkCompileTimeError", group: compileError); | |
93 | |
94 build("MissingStaticWarning", group: fail); | |
95 build("StaticWarning", group: fail); | |
96 | |
97 build("PubGetError", group: fail); | |
98 | |
99 var skip = build("Skip", isMetaExpectation: true); | |
100 build("SkipByDesign", isMetaExpectation: true); | |
101 build("SkipSlow", group: skip, isMetaExpectation: true); | |
102 build("Ok", isMetaExpectation: true); | |
103 build("Slow", isMetaExpectation: true); | |
104 build("Ignore"); | |
105 } | |
106 } | |
107 | |
108 final String prettyName; | |
109 final String name; | |
110 final Expectation group; | |
111 // Indicates whether this expectation cannot be a test outcome (i.e. it is a | |
112 // "meta marker"). | |
113 final bool isMetaExpectation; | |
114 | |
115 Expectation._(this.prettyName, | |
116 {Expectation this.group: null, bool this.isMetaExpectation: false}) | |
117 : name = prettyName.toLowerCase(); | |
118 | |
119 bool canBeOutcomeOf(Expectation expectation) { | |
120 Expectation outcome = this; | |
121 if (outcome == IGNORE) return true; | |
122 while (outcome != null) { | |
123 if (outcome == expectation) { | |
124 return true; | |
125 } | |
126 outcome = outcome.group; | |
127 } | |
128 return false; | |
129 } | |
130 | |
131 String toString() => prettyName; | |
132 } | |
133 | |
134 final RegExp SplitComment = new RegExp("^([^#]*)(#.*)?\$"); | |
135 final RegExp HeaderPattern = new RegExp(r"^\[([^\]]+)\]"); | |
136 final RegExp RulePattern = new RegExp(r"\s*([^: ]*)\s*:(.*)"); | |
137 final RegExp IssueNumberPattern = new RegExp("[Ii]ssue ([0-9]+)"); | |
138 | |
139 class StatusFile { | |
140 final Path location; | |
141 | |
142 StatusFile(this.location); | |
143 } | |
144 | |
145 // TODO(whesse): Implement configuration_info library that contains data | |
146 // structures for test configuration, including Section. | |
147 class Section { | |
148 final StatusFile statusFile; | |
149 | |
150 final BooleanExpression condition; | |
151 final List<TestRule> testRules; | |
152 final int lineNumber; | |
153 | |
154 Section.always(this.statusFile, this.lineNumber) | |
155 : condition = null, | |
156 testRules = new List<TestRule>(); | |
157 Section(this.statusFile, this.condition, this.lineNumber) | |
158 : testRules = new List<TestRule>(); | |
159 | |
160 bool isEnabled(environment) => | |
161 condition == null || condition.evaluate(environment); | |
162 | |
163 String toString() { | |
164 return "Section: $condition"; | |
165 } | |
166 } | |
167 | |
168 Future<TestExpectations> ReadTestExpectations( | |
169 List<String> statusFilePaths, Map<String, String> environment) { | |
170 var testExpectations = new TestExpectations(); | |
171 return Future.wait(statusFilePaths.map((String statusFile) { | |
172 return ReadTestExpectationsInto(testExpectations, statusFile, environment); | |
173 })).then((_) => testExpectations); | |
174 } | |
175 | |
176 Future ReadTestExpectationsInto(TestExpectations expectations, | |
177 String statusFilePath, Map<String, String> environment) { | |
178 var completer = new Completer<Null>(); | |
179 var sections = <Section>[]; | |
180 | |
181 void sectionsRead() { | |
182 for (Section section in sections) { | |
183 if (section.isEnabled(environment)) { | |
184 for (var rule in section.testRules) { | |
185 expectations.addRule(rule, environment); | |
186 } | |
187 } | |
188 } | |
189 completer.complete(); | |
190 } | |
191 | |
192 ReadConfigurationInto(new Path(statusFilePath), sections, sectionsRead); | |
193 return completer.future; | |
194 } | |
195 | |
196 void ReadConfigurationInto(Path path, List<Section> sections, Action onDone) { | |
197 StatusFile statusFile = new StatusFile(path); | |
198 File file = new File(path.toNativePath()); | |
199 if (!file.existsSync()) { | |
200 throw new Exception('Cannot find test status file $path'); | |
201 } | |
202 int lineNumber = 0; | |
203 Stream<String> lines = | |
204 file.openRead().transform(UTF8.decoder).transform(new LineSplitter()); | |
205 | |
206 Section currentSection = new Section.always(statusFile, -1); | |
207 sections.add(currentSection); | |
208 | |
209 lines.listen((String line) { | |
210 lineNumber++; | |
211 Match match = SplitComment.firstMatch(line); | |
212 line = (match == null) ? "" : match[1]; | |
213 line = line.trim(); | |
214 if (line.isEmpty) return; | |
215 | |
216 // Extract the comment to get the issue number if needed. | |
217 String comment = (match == null || match[2] == null) ? "" : match[2]; | |
218 | |
219 match = HeaderPattern.firstMatch(line); | |
220 if (match != null) { | |
221 String condition_string = match[1].trim(); | |
222 List<String> tokens = new Tokenizer(condition_string).tokenize(); | |
223 ExpressionParser parser = new ExpressionParser(new Scanner(tokens)); | |
224 currentSection = | |
225 new Section(statusFile, parser.parseBooleanExpression(), lineNumber); | |
226 sections.add(currentSection); | |
227 return; | |
228 } | |
229 | |
230 match = RulePattern.firstMatch(line); | |
231 if (match != null) { | |
232 String name = match[1].trim(); | |
233 // TODO(whesse): Handle test names ending in a wildcard (*). | |
234 String expression_string = match[2].trim(); | |
235 List<String> tokens = new Tokenizer(expression_string).tokenize(); | |
236 SetExpression expression = | |
237 new ExpressionParser(new Scanner(tokens)).parseSetExpression(); | |
238 | |
239 // Look for issue number in comment. | |
240 String issueString = null; | |
241 match = IssueNumberPattern.firstMatch(comment); | |
242 if (match != null) { | |
243 issueString = match[1]; | |
244 if (issueString == null) issueString = match[2]; | |
245 } | |
246 int issue = issueString != null ? int.parse(issueString) : null; | |
247 currentSection.testRules | |
248 .add(new TestRule(name, expression, issue, lineNumber)); | |
249 return; | |
250 } | |
251 | |
252 print("unmatched line: $line"); | |
253 }, onDone: onDone); | |
254 } | |
255 | |
256 class TestRule { | |
257 String name; | |
258 SetExpression expression; | |
259 int issue; | |
260 int lineNumber; | |
261 | |
262 TestRule(this.name, this.expression, this.issue, this.lineNumber); | |
263 | |
264 bool get hasIssue => issue != null; | |
265 | |
266 String toString() => 'TestRule($name, $expression, $issue)'; | |
267 } | |
268 | |
269 class TestExpectations { | |
270 // Only create one copy of each Set<Expectation>. | |
271 // We just use .toString as a key, so we may make a few | |
272 // sets that only differ in their toString element order. | |
273 static Map<String, Set<Expectation>> _cachedSets = {}; | |
274 | |
275 Map<String, Set<Expectation>> _map; | |
276 bool _preprocessed = false; | |
277 Map<String, RegExp> _regExpCache; | |
278 Map<String, List<RegExp>> _keyToRegExps; | |
279 | |
280 /** | |
281 * Create a TestExpectations object. See the [expectations] method | |
282 * for an explanation of matching. | |
283 */ | |
284 TestExpectations() : _map = {}; | |
285 | |
286 /** | |
287 * Add a rule to the expectations. | |
288 */ | |
289 void addRule(TestRule testRule, environment) { | |
290 // Once we have started using the expectations we cannot add more | |
291 // rules. | |
292 if (_preprocessed) { | |
293 throw "TestExpectations.addRule: cannot add more rules"; | |
294 } | |
295 var names = testRule.expression.evaluate(environment); | |
296 var expectations = names.map((name) => Expectation.byName(name)); | |
297 _map | |
298 .putIfAbsent(testRule.name, () => new Set<Expectation>()) | |
299 .addAll(expectations); | |
300 } | |
301 | |
302 /** | |
303 * Compute the expectations for a test based on the filename. | |
304 * | |
305 * For every (key, expectation) pair. Match the key with the file | |
306 * name. Return the union of the expectations for all the keys | |
307 * that match. | |
308 * | |
309 * Normal matching splits the key and the filename into path | |
310 * components and checks that the anchored regular expression | |
311 * "^$keyComponent\$" matches the corresponding filename component. | |
312 */ | |
313 Set<Expectation> expectations(String filename) { | |
314 var result = new Set<Expectation>(); | |
315 var splitFilename = filename.split('/'); | |
316 | |
317 // Create mapping from keys to list of RegExps once and for all. | |
318 _preprocessForMatching(); | |
319 | |
320 _map.forEach((key, Set<Expectation> expectations) { | |
321 List<RegExp> regExps = _keyToRegExps[key]; | |
322 if (regExps.length > splitFilename.length) return; | |
323 for (var i = 0; i < regExps.length; i++) { | |
324 if (!regExps[i].hasMatch(splitFilename[i])) return; | |
325 } | |
326 // If all components of the status file key matches the filename | |
327 // add the expectations to the result. | |
328 result.addAll(expectations); | |
329 }); | |
330 | |
331 // If no expectations were found the expectation is that the test | |
332 // passes. | |
333 if (result.isEmpty) { | |
334 result.add(Expectation.PASS); | |
335 } | |
336 return _cachedSets.putIfAbsent(result.toString(), () => result); | |
337 } | |
338 | |
339 // Preprocess the expectations for matching against | |
340 // filenames. Generate lists of regular expressions once and for all | |
341 // for each key. | |
342 void _preprocessForMatching() { | |
343 if (_preprocessed) return; | |
344 | |
345 _keyToRegExps = {}; | |
346 _regExpCache = {}; | |
347 | |
348 _map.forEach((key, expectations) { | |
349 if (_keyToRegExps[key] != null) return; | |
350 var splitKey = key.split('/'); | |
351 var regExps = new List<RegExp>(splitKey.length); | |
352 for (var i = 0; i < splitKey.length; i++) { | |
353 var component = splitKey[i]; | |
354 var regExp = _regExpCache[component]; | |
355 if (regExp == null) { | |
356 var pattern = "^${splitKey[i]}\$".replaceAll('*', '.*'); | |
357 regExp = new RegExp(pattern); | |
358 _regExpCache[component] = regExp; | |
359 } | |
360 regExps[i] = regExp; | |
361 } | |
362 _keyToRegExps[key] = regExps; | |
363 }); | |
364 | |
365 _regExpCache = null; | |
366 _preprocessed = true; | |
367 } | |
368 } | |
OLD | NEW |