blob: cd2d65f2f9511fadf5a356abb930d42fae257000 [file] [log] [blame]
// 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 'expectation.dart';
import 'status_expression.dart';
/// Splits out a trailing line comment
final _commentPattern = new RegExp("^([^#]*)(#.*)?\$");
/// 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"\s*([^: ]*)\s*:(.*)");
/// Matches an issue number in a comment, like:
///
/// blah_test: Fail # Issue 1234
/// ^^^^
final _issuePattern = new RegExp("[Ii]ssue ([0-9]+)");
/// 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 List<StatusSection> sections = [];
/// Parses the status file at [path].
StatusFile.read(String path) {
var lines = new File(path).readAsLinesSync();
// The current section whose rules are being parsed.
StatusSection section;
var lineNumber = 0;
for (var line in lines) {
lineNumber++;
// Strip off the comment and whitespace.
var match = _commentPattern.firstMatch(line);
var source = "";
var comment = "";
if (match != null) {
source = match[1].trim();
comment = match[2] ?? "";
}
// Ignore empty (or comment-only) lines.
if (source.isEmpty) continue;
// See if we are starting a new section.
match = _sectionPattern.firstMatch(source);
if (match != null) {
var condition = Expression.parse(match[1].trim());
section = new StatusSection(condition);
sections.add(section);
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 = _parseExpectations(match[2]);
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);
sections.add(section);
}
section.entries.add(new StatusEntry(path, expectations, issue));
continue;
}
throw new FormatException(
"Could not parse line $lineNumber of status file '$path':\n$line");
}
}
/// Parses a comma-separated list of expectation names from [text].
List<Expectation> _parseExpectations(String text) {
return text
.split(",")
.map((name) => Expectation.find(name.trim()))
.toList();
}
/// 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], onError: (_) => null);
}
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();
}
}
/// 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;
final List<StatusEntry> entries = [];
/// Returns true if this section should apply in the given [environment].
bool isEnabled(Map<String, dynamic> environment) =>
_condition == null || _condition.evaluate(environment);
StatusSection(this._condition);
}
/// Describes the test status of the file or files at a given path.
class StatusEntry {
final String path;
final List<Expectation> expectations;
final int issue;
StatusEntry(this.path, this.expectations, this.issue);
}