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