| // Copyright (c) 2017, 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. |
| |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as p; |
| |
| import 'environment.dart'; |
| import 'expectation.dart'; |
| import 'src/expression.dart'; |
| |
| /// Matches the header that begins a new section, like: |
| /// |
| /// [ $compiler == dart2js && $minified ] |
| final _sectionPattern = new RegExp(r"^\[(.+?)\]"); |
| |
| /// Matches an entry that defines the status for a path in the current section, |
| /// like: |
| /// |
| /// some/path/to/some_test: Pass || Fail |
| final _entryPattern = new RegExp(r"^([^:#]+):(.*)"); |
| |
| /// Matches an issue number in a comment, like: |
| /// |
| /// blah_test: Fail # Issue 1234 |
| /// ^^^^ |
| final _issuePattern = new RegExp(r"[Ii]ssue (\d+)"); |
| |
| /// A parsed status file, which describes how a collection of tests are |
| /// expected to behave under various configurations and conditions. |
| /// |
| /// Each status file is made of a series of sections. Each section begins with |
| /// a header, followed by a series of entries. A header is enclosed in square |
| /// brackets and contains a Boolean expression. That expression is evaluated in |
| /// an environment. If it evaluates to true, then the entries after the header |
| /// take effect. |
| /// |
| /// Each entry is a glob-like file path followed by a colon and then a |
| /// comma-separated list of [Expectation]s. The path may point to an individual |
| /// file, or a directory, in which case it applies to all files under that path. |
| /// |
| /// Entries may also appear before any section header, in which case they |
| /// always apply. |
| class StatusFile { |
| final String path; |
| final List<StatusSection> sections = []; |
| final List<String> _comments = []; |
| |
| int _lineCount = 0; |
| |
| StatusFile(this.path); |
| |
| /// Parses the status file at [path]. |
| /// |
| /// Throws a [SyntaxError] if the file could not be parsed. |
| StatusFile.read(this.path) { |
| var lines = new File(path).readAsLinesSync(); |
| _comments.length = lines.length + 1; |
| |
| // The current section whose rules are being parsed. |
| StatusSection section; |
| |
| for (var line in lines) { |
| _lineCount++; |
| |
| fail(String message, [List<String> errors]) { |
| throw new SyntaxError(_shortPath, _lineCount, line, message, errors); |
| } |
| |
| // Strip off the comment and whitespace. |
| var source = line; |
| var comment = ""; |
| var hashIndex = line.indexOf('#'); |
| if (hashIndex >= 0) { |
| source = line.substring(0, hashIndex); |
| comment = line.substring(hashIndex + 1); |
| _comments[_lineCount] = comment; |
| } |
| source = source.trim(); |
| |
| // Ignore empty (or comment-only) lines. |
| if (source.isEmpty) continue; |
| |
| // See if we are starting a new section. |
| var match = _sectionPattern.firstMatch(source); |
| if (match != null) { |
| try { |
| var condition = Expression.parse(match[1].trim()); |
| section = new StatusSection(condition, _lineCount); |
| sections.add(section); |
| } on FormatException { |
| fail("Status expression syntax error"); |
| } |
| continue; |
| } |
| |
| // Otherwise, it should be a new entry under the current section. |
| match = _entryPattern.firstMatch(source); |
| if (match != null) { |
| var path = match[1].trim(); |
| // TODO(whesse): Handle test names ending in a wildcard (*). |
| var expectations = <Expectation>[]; |
| for (var name in match[2].split(",")) { |
| name = name.trim(); |
| try { |
| expectations.add(Expectation.find(name)); |
| } on ArgumentError { |
| fail('Unrecognized test expectation "$name"'); |
| } |
| } |
| |
| var issue = _issueNumber(comment); |
| |
| // If we haven't found a section header yet, create an implicit section |
| // that matches everything. |
| if (section == null) { |
| section = new StatusSection(null, -1); |
| sections.add(section); |
| } |
| |
| section.entries |
| .add(new StatusEntry(path, _lineCount, expectations, issue)); |
| continue; |
| } |
| |
| fail("Unrecognized input"); |
| } |
| } |
| |
| bool get isEmpty => sections.isEmpty; |
| |
| /// Validates that the variables and values used in all of the section |
| /// condition expressions are defined in [environment]. |
| /// |
| /// Throws a [SyntaxError] on the first found error. |
| void validate(Environment environment) { |
| // TODO(rnystrom): It would be more useful if it reported all of the errors |
| // instead of stopping on the first. |
| for (var section in sections) { |
| if (section.condition == null) continue; |
| |
| var errors = <String>[]; |
| section.condition.validate(environment, errors); |
| |
| if (errors.isNotEmpty) { |
| var s = errors.length > 1 ? "s" : ""; |
| throw new SyntaxError(_shortPath, section.lineNumber, |
| "[ ${section.condition} ]", 'Validation error$s', errors); |
| } |
| } |
| } |
| |
| /// Gets the path to this status file relative to the Dart repo root. |
| String get _shortPath { |
| var repoRoot = p.join(p.dirname(p.fromUri(Platform.script)), "../../../"); |
| return p.normalize(p.relative(path, from: repoRoot)); |
| } |
| |
| /// Returns the issue number embedded in [comment] or `null` if there is none. |
| int _issueNumber(String comment) { |
| var match = _issuePattern.firstMatch(comment); |
| if (match == null) return null; |
| |
| return int.parse(match[1]); |
| } |
| |
| String toString() { |
| var buffer = new StringBuffer(); |
| for (var section in sections) { |
| buffer.writeln("[ ${section.condition} ]"); |
| |
| for (var entry in section.entries) { |
| buffer.write("${entry.path}: ${entry.expectations.join(', ')}"); |
| if (entry.issue != null) buffer.write(" # Issue ${entry.issue}"); |
| buffer.writeln(); |
| } |
| |
| buffer.writeln(); |
| } |
| |
| return buffer.toString(); |
| } |
| |
| /// Serialize the status file to a string. |
| /// |
| /// Unlike [toString()], this preserves comments and gives a "canonical" |
| /// rendering of the status file that can be saved back to disc. |
| String serialize() { |
| var buffer = new StringBuffer(); |
| |
| var lastLine = 0; |
| var needBlankLine = false; |
| |
| void writeLine(String text, int line) { |
| var comment = _comments[line]; |
| if (text == null && comment == null) { |
| // There's no comment on this line, so it's blank. |
| needBlankLine = true; |
| return; |
| } |
| |
| if (needBlankLine) buffer.writeln(); |
| needBlankLine = false; |
| |
| if (text != null) { |
| buffer.write(text); |
| } |
| |
| if (comment != null) { |
| if (text != null) buffer.write(" "); |
| buffer.write("#$comment"); |
| } |
| |
| buffer.writeln(); |
| } |
| |
| void writeText(String text, int line) { |
| if (line != null) { |
| while (++lastLine < line) { |
| writeLine(null, lastLine); |
| } |
| } |
| |
| writeLine(text, line); |
| } |
| |
| for (var section in sections) { |
| if (section.condition != null) { |
| writeText("[ ${section.condition} ]", section.lineNumber); |
| } |
| |
| for (var entry in section.entries) { |
| writeText("${entry.path}: ${entry.expectations.join(', ')}", |
| entry.lineNumber); |
| } |
| |
| needBlankLine = true; |
| } |
| |
| // Write any trailing comments. |
| while (++lastLine <= _lineCount) { |
| writeLine(null, lastLine); |
| } |
| |
| return buffer.toString(); |
| } |
| } |
| |
| /// One section in a status file. |
| /// |
| /// Contains the condition from the header that begins the section, then all of |
| /// the entries within the section. |
| class StatusSection { |
| /// The expression that determines when this section is applied. |
| /// |
| /// May be `null` for paths that appear before any section header in the file. |
| /// In that case, the section always applies. |
| final Expression condition; |
| |
| /// The one-based line number where the entry appears in the file. |
| final int lineNumber; |
| |
| final List<StatusEntry> entries = []; |
| |
| /// Returns true if this section should apply in the given [environment]. |
| bool isEnabled(Environment environment) => |
| condition == null || condition.evaluate(environment); |
| |
| StatusSection(this.condition, this.lineNumber); |
| } |
| |
| /// Describes the test status of the file or files at a given path. |
| class StatusEntry { |
| final String path; |
| |
| /// The one-based line number where the entry appears in the file. |
| final int lineNumber; |
| |
| final List<Expectation> expectations; |
| final int issue; |
| |
| StatusEntry(this.path, this.lineNumber, this.expectations, this.issue); |
| } |
| |
| /// Error thrown when a parse or validation error occurs in a [StatusFile]. |
| class SyntaxError implements Exception { |
| final String file; |
| final int lineNumber; |
| final String line; |
| final String message; |
| final List<String> errors; |
| |
| SyntaxError(this.file, this.lineNumber, this.line, this.message, this.errors); |
| |
| String toString() { |
| var buffer = new StringBuffer(); |
| buffer.writeln('$message in "$file" line $lineNumber:'); |
| buffer.writeln(line); |
| |
| if (errors != null) { |
| for (var error in errors) { |
| buffer.writeln("- ${error.replaceAll('\n', '\n ')}"); |
| } |
| } |
| |
| return buffer.toString().trimRight(); |
| } |
| } |