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 |