| 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;
|
| + }
|
| +}
|
|
|