Index: tools/testing/dart/status_file_parser.dart |
diff --git a/tools/testing/dart/status_file_parser.dart b/tools/testing/dart/status_file_parser.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..49fdc9bf2420589b36a85d69b631d4bf38546e46 |
--- /dev/null |
+++ b/tools/testing/dart/status_file_parser.dart |
@@ -0,0 +1,368 @@ |
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library status_file_parser; |
+ |
+import "dart:async"; |
+import "dart:convert" show LineSplitter, UTF8; |
+import "dart:io"; |
+ |
+import "path.dart"; |
+import "status_expression.dart"; |
+ |
+typedef void Action(); |
+ |
+class Expectation { |
+ // Possible outcomes of running a test. |
+ static Expectation PASS = byName('Pass'); |
+ static Expectation CRASH = byName('Crash'); |
+ static Expectation TIMEOUT = byName('Timeout'); |
+ static Expectation FAIL = byName('Fail'); |
+ |
+ // Special 'FAIL' cases |
+ static Expectation RUNTIME_ERROR = byName('RuntimeError'); |
+ static Expectation COMPILETIME_ERROR = byName('CompileTimeError'); |
+ static Expectation MISSING_RUNTIME_ERROR = byName('MissingRuntimeError'); |
+ static Expectation MISSING_COMPILETIME_ERROR = |
+ byName('MissingCompileTimeError'); |
+ static Expectation STATIC_WARNING = byName('StaticWarning'); |
+ static Expectation MISSING_STATIC_WARNING = byName('MissingStaticWarning'); |
+ static Expectation PUB_GET_ERROR = byName('PubGetError'); |
+ static Expectation NON_UTF8_ERROR = byName('NonUtf8Output'); |
+ |
+ // Special 'CRASH' cases |
+ static Expectation DARTK_CRASH = byName('DartkCrash'); |
+ |
+ // Special 'TIMEOUT' cases |
+ static Expectation DARTK_TIMEOUT = byName('DartkTimeout'); |
+ |
+ // Special 'COMPILETIME_ERROR' |
+ static Expectation DARTK_COMPILETIME_ERROR = byName('DartkCompileTimeError'); |
+ |
+ // "meta expectations" |
+ static Expectation OK = byName('Ok'); |
+ static Expectation SLOW = byName('Slow'); |
+ static Expectation SKIP = byName('Skip'); |
+ static Expectation SKIP_SLOW = byName('SkipSlow'); |
+ static Expectation SKIP_BY_DESIGN = byName('SkipByDesign'); |
+ |
+ // Can be returned by the test runner to say the result should be ignored, |
+ // and assumed to meet the expectations, due to an infrastructure failure. |
+ // Do not place in status files. |
+ static Expectation IGNORE = byName('Ignore'); |
+ |
+ static Expectation byName(String name) { |
+ _initialize(); |
+ name = name.toLowerCase(); |
+ if (!_AllExpectations.containsKey(name)) { |
+ throw new Exception("Expectation.byName(name='$name'): Invalid name."); |
+ } |
+ return _AllExpectations[name]; |
+ } |
+ |
+ // Keep a map of all possible Expectation objects, initialized lazily. |
+ static Map<String, Expectation> _AllExpectations; |
+ static void _initialize() { |
+ if (_AllExpectations == null) { |
+ _AllExpectations = new Map<String, Expectation>(); |
+ |
+ Expectation build(String prettyName, |
+ {Expectation group, bool isMetaExpectation: false}) { |
+ var expectation = new Expectation._(prettyName, |
+ group: group, isMetaExpectation: isMetaExpectation); |
+ assert(!_AllExpectations.containsKey(expectation.name)); |
+ return _AllExpectations[expectation.name] = expectation; |
+ } |
+ |
+ var fail = build("Fail"); |
+ var crash = build("Crash"); |
+ var timeout = build("Timeout"); |
+ build("Pass"); |
+ |
+ var compileError = build("CompileTimeError", group: fail); |
+ build("MissingCompileTimeError", group: fail); |
+ build("MissingRuntimeError", group: fail); |
+ build("RuntimeError", group: fail); |
+ build("NonUtf8Output", group: fail); |
+ |
+ // Dartk sub expectations |
+ build("DartkCrash", group: crash); |
+ build("DartkTimeout", group: timeout); |
+ build("DartkCompileTimeError", group: compileError); |
+ |
+ build("MissingStaticWarning", group: fail); |
+ build("StaticWarning", group: fail); |
+ |
+ build("PubGetError", group: fail); |
+ |
+ var skip = build("Skip", isMetaExpectation: true); |
+ build("SkipByDesign", isMetaExpectation: true); |
+ build("SkipSlow", group: skip, isMetaExpectation: true); |
+ build("Ok", isMetaExpectation: true); |
+ build("Slow", isMetaExpectation: true); |
+ build("Ignore"); |
+ } |
+ } |
+ |
+ final String prettyName; |
+ final String name; |
+ final Expectation group; |
+ // Indicates whether this expectation cannot be a test outcome (i.e. it is a |
+ // "meta marker"). |
+ final bool isMetaExpectation; |
+ |
+ Expectation._(this.prettyName, |
+ {Expectation this.group: null, bool this.isMetaExpectation: false}) |
+ : name = prettyName.toLowerCase(); |
+ |
+ bool canBeOutcomeOf(Expectation expectation) { |
+ Expectation outcome = this; |
+ if (outcome == IGNORE) return true; |
+ while (outcome != null) { |
+ if (outcome == expectation) { |
+ return true; |
+ } |
+ outcome = outcome.group; |
+ } |
+ return false; |
+ } |
+ |
+ String toString() => prettyName; |
+} |
+ |
+final RegExp SplitComment = new RegExp("^([^#]*)(#.*)?\$"); |
+final RegExp HeaderPattern = new RegExp(r"^\[([^\]]+)\]"); |
+final RegExp RulePattern = new RegExp(r"\s*([^: ]*)\s*:(.*)"); |
+final RegExp IssueNumberPattern = new RegExp("[Ii]ssue ([0-9]+)"); |
+ |
+class StatusFile { |
+ final Path location; |
+ |
+ StatusFile(this.location); |
+} |
+ |
+// TODO(whesse): Implement configuration_info library that contains data |
+// structures for test configuration, including Section. |
+class Section { |
+ final StatusFile statusFile; |
+ |
+ final BooleanExpression condition; |
+ final List<TestRule> testRules; |
+ final int lineNumber; |
+ |
+ Section.always(this.statusFile, this.lineNumber) |
+ : condition = null, |
+ testRules = new List<TestRule>(); |
+ Section(this.statusFile, this.condition, this.lineNumber) |
+ : testRules = new List<TestRule>(); |
+ |
+ bool isEnabled(environment) => |
+ condition == null || condition.evaluate(environment); |
+ |
+ String toString() { |
+ return "Section: $condition"; |
+ } |
+} |
+ |
+Future<TestExpectations> ReadTestExpectations( |
+ List<String> statusFilePaths, Map<String, String> environment) { |
+ var testExpectations = new TestExpectations(); |
+ return Future.wait(statusFilePaths.map((String statusFile) { |
+ return ReadTestExpectationsInto(testExpectations, statusFile, environment); |
+ })).then((_) => testExpectations); |
+} |
+ |
+Future ReadTestExpectationsInto(TestExpectations expectations, |
+ String statusFilePath, Map<String, String> environment) { |
+ var completer = new Completer<Null>(); |
+ var sections = <Section>[]; |
+ |
+ void sectionsRead() { |
+ for (Section section in sections) { |
+ if (section.isEnabled(environment)) { |
+ for (var rule in section.testRules) { |
+ expectations.addRule(rule, environment); |
+ } |
+ } |
+ } |
+ completer.complete(); |
+ } |
+ |
+ ReadConfigurationInto(new Path(statusFilePath), sections, sectionsRead); |
+ return completer.future; |
+} |
+ |
+void ReadConfigurationInto(Path path, List<Section> sections, Action onDone) { |
+ StatusFile statusFile = new StatusFile(path); |
+ File file = new File(path.toNativePath()); |
+ if (!file.existsSync()) { |
+ throw new Exception('Cannot find test status file $path'); |
+ } |
+ int lineNumber = 0; |
+ Stream<String> lines = |
+ file.openRead().transform(UTF8.decoder).transform(new LineSplitter()); |
+ |
+ Section currentSection = new Section.always(statusFile, -1); |
+ sections.add(currentSection); |
+ |
+ lines.listen((String line) { |
+ lineNumber++; |
+ Match match = SplitComment.firstMatch(line); |
+ line = (match == null) ? "" : match[1]; |
+ line = line.trim(); |
+ if (line.isEmpty) return; |
+ |
+ // Extract the comment to get the issue number if needed. |
+ String comment = (match == null || match[2] == null) ? "" : match[2]; |
+ |
+ match = HeaderPattern.firstMatch(line); |
+ if (match != null) { |
+ String condition_string = match[1].trim(); |
+ List<String> tokens = new Tokenizer(condition_string).tokenize(); |
+ ExpressionParser parser = new ExpressionParser(new Scanner(tokens)); |
+ currentSection = |
+ new Section(statusFile, parser.parseBooleanExpression(), lineNumber); |
+ sections.add(currentSection); |
+ return; |
+ } |
+ |
+ match = RulePattern.firstMatch(line); |
+ if (match != null) { |
+ String name = match[1].trim(); |
+ // TODO(whesse): Handle test names ending in a wildcard (*). |
+ String expression_string = match[2].trim(); |
+ List<String> tokens = new Tokenizer(expression_string).tokenize(); |
+ SetExpression expression = |
+ new ExpressionParser(new Scanner(tokens)).parseSetExpression(); |
+ |
+ // Look for issue number in comment. |
+ String issueString = null; |
+ match = IssueNumberPattern.firstMatch(comment); |
+ if (match != null) { |
+ issueString = match[1]; |
+ if (issueString == null) issueString = match[2]; |
+ } |
+ int issue = issueString != null ? int.parse(issueString) : null; |
+ currentSection.testRules |
+ .add(new TestRule(name, expression, issue, lineNumber)); |
+ return; |
+ } |
+ |
+ print("unmatched line: $line"); |
+ }, onDone: onDone); |
+} |
+ |
+class TestRule { |
+ String name; |
+ SetExpression expression; |
+ int issue; |
+ int lineNumber; |
+ |
+ TestRule(this.name, this.expression, this.issue, this.lineNumber); |
+ |
+ bool get hasIssue => issue != null; |
+ |
+ String toString() => 'TestRule($name, $expression, $issue)'; |
+} |
+ |
+class TestExpectations { |
+ // Only create one copy of each Set<Expectation>. |
+ // We just use .toString as a key, so we may make a few |
+ // sets that only differ in their toString element order. |
+ static Map<String, Set<Expectation>> _cachedSets = {}; |
+ |
+ Map<String, Set<Expectation>> _map; |
+ bool _preprocessed = false; |
+ Map<String, RegExp> _regExpCache; |
+ Map<String, List<RegExp>> _keyToRegExps; |
+ |
+ /** |
+ * Create a TestExpectations object. See the [expectations] method |
+ * for an explanation of matching. |
+ */ |
+ TestExpectations() : _map = {}; |
+ |
+ /** |
+ * Add a rule to the expectations. |
+ */ |
+ void addRule(TestRule testRule, environment) { |
+ // Once we have started using the expectations we cannot add more |
+ // rules. |
+ if (_preprocessed) { |
+ throw "TestExpectations.addRule: cannot add more rules"; |
+ } |
+ var names = testRule.expression.evaluate(environment); |
+ var expectations = names.map((name) => Expectation.byName(name)); |
+ _map |
+ .putIfAbsent(testRule.name, () => new Set<Expectation>()) |
+ .addAll(expectations); |
+ } |
+ |
+ /** |
+ * Compute the expectations for a test based on the filename. |
+ * |
+ * For every (key, expectation) pair. Match the key with the file |
+ * name. Return the union of the expectations for all the keys |
+ * that match. |
+ * |
+ * Normal matching splits the key and the filename into path |
+ * components and checks that the anchored regular expression |
+ * "^$keyComponent\$" matches the corresponding filename component. |
+ */ |
+ Set<Expectation> expectations(String filename) { |
+ var result = new Set<Expectation>(); |
+ var splitFilename = filename.split('/'); |
+ |
+ // Create mapping from keys to list of RegExps once and for all. |
+ _preprocessForMatching(); |
+ |
+ _map.forEach((key, Set<Expectation> expectations) { |
+ List<RegExp> regExps = _keyToRegExps[key]; |
+ if (regExps.length > splitFilename.length) return; |
+ for (var i = 0; i < regExps.length; i++) { |
+ if (!regExps[i].hasMatch(splitFilename[i])) return; |
+ } |
+ // If all components of the status file key matches the filename |
+ // add the expectations to the result. |
+ result.addAll(expectations); |
+ }); |
+ |
+ // If no expectations were found the expectation is that the test |
+ // passes. |
+ if (result.isEmpty) { |
+ result.add(Expectation.PASS); |
+ } |
+ return _cachedSets.putIfAbsent(result.toString(), () => result); |
+ } |
+ |
+ // Preprocess the expectations for matching against |
+ // filenames. Generate lists of regular expressions once and for all |
+ // for each key. |
+ void _preprocessForMatching() { |
+ if (_preprocessed) return; |
+ |
+ _keyToRegExps = {}; |
+ _regExpCache = {}; |
+ |
+ _map.forEach((key, expectations) { |
+ if (_keyToRegExps[key] != null) return; |
+ var splitKey = key.split('/'); |
+ var regExps = new List<RegExp>(splitKey.length); |
+ for (var i = 0; i < splitKey.length; i++) { |
+ var component = splitKey[i]; |
+ var regExp = _regExpCache[component]; |
+ if (regExp == null) { |
+ var pattern = "^${splitKey[i]}\$".replaceAll('*', '.*'); |
+ regExp = new RegExp(pattern); |
+ _regExpCache[component] = regExp; |
+ } |
+ regExps[i] = regExp; |
+ } |
+ _keyToRegExps[key] = regExps; |
+ }); |
+ |
+ _regExpCache = null; |
+ _preprocessed = true; |
+ } |
+} |