9f11b2fd45
Unlike my previous couple of CLs, I went ahead here and also migrated most of the classes to use primary constructors since so many of them were small and well suited for it. In the process, I found and fixed one bug in the assist to convert to a primary constructor: https://dart-review.googlesource.com/c/sdk/+/506760 So I guess this CL has already paid its way. :) Change-Id: I9b7d08e27f4ddce3f19127aaf4f14fe627b75dab Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/506743 Reviewed-by: Paul Berry <paulberry@google.com> Commit-Queue: Bob Nystrom <rnystrom@google.com> Commit-Queue: Paul Berry <paulberry@google.com> Auto-Submit: Bob Nystrom <rnystrom@google.com>
383 lines
12 KiB
Dart
383 lines
12 KiB
Dart
// 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 'status_file.dart';
|
|
import 'src/expression.dart';
|
|
|
|
/// Matches the header that begins a new section, like:
|
|
///
|
|
/// [ $compiler == dart2js && $minified ]
|
|
final RegExp _sectionPattern = 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 RegExp _entryPattern = RegExp(r"^([^:#]+):([^#]+)(#.*)?");
|
|
|
|
/// Matches an issue number in a comment, like:
|
|
///
|
|
/// blah_test: Fail # Issue 1234
|
|
/// ^^^^
|
|
final RegExp _issuePattern = RegExp(r"[Ii]ssue (\d+)");
|
|
|
|
/// Matches a comment and indented comment, like:
|
|
///
|
|
/// < white space > #
|
|
final RegExp _commentPattern = RegExp(r"^(\s*)#");
|
|
|
|
/// 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 is a regular expression
|
|
/// which may match one or more file or directory paths. If it matches a
|
|
/// directory path, it is considered to match all files in that directory or
|
|
/// (recursively) its subdirectories.
|
|
///
|
|
/// The intent is that status files will not have stand-alone comment lines. All
|
|
/// comments should be at the end of a single entry, and apply to that entry
|
|
/// only.
|
|
///
|
|
/// Until this is true for all status files, this program handles stand-alone
|
|
/// comment lines as follows:
|
|
///
|
|
/// 1) One or more comment lines immediately preceding a section header if there
|
|
/// is a linebreak from the previous section. Comment sections are added
|
|
/// directly to a section and are not entries.
|
|
/// 2) Comment lines anywhere else. These should be carefully removed when
|
|
/// found.
|
|
///
|
|
/// The reason for this distinction is to allow comments to be above sections,
|
|
/// without including these when lexicographically ordering section entries.
|
|
///
|
|
/// Entries may also appear before any section header, are considered to belong
|
|
/// to a default section, which always applies.
|
|
class StatusFile {
|
|
final String path;
|
|
final List<StatusSection> sections = [];
|
|
|
|
int _lineCount = 1;
|
|
|
|
/// Constructor for creating a new [StatusFile]. Will not create the default
|
|
/// section that status files have.
|
|
new(this.path);
|
|
|
|
/// Reads and parses the status file at [path].
|
|
///
|
|
/// Throws a [SyntaxError] if the file could not be parsed.
|
|
new read(this.path) {
|
|
_parse(File(path).readAsLinesSync());
|
|
}
|
|
|
|
/// Parses lines of strings coming from a status file at [path].
|
|
///
|
|
/// Throws a [SyntaxError] if the file could not be parsed.
|
|
new parse(this.path, List<String> lines) {
|
|
_parse(lines.map((line) => line.trim()).toList());
|
|
}
|
|
|
|
void _parse(List<String> lines) {
|
|
// We define a few helper functions that are used when parsing.
|
|
|
|
/// Checks if [currentLine] is a comment and returns the first regular
|
|
/// expression match, or null otherwise.
|
|
Match? commentEntryMatch(int currentLine) {
|
|
if (currentLine < 1 || currentLine > lines.length) {
|
|
return null;
|
|
}
|
|
return _commentPattern.firstMatch(lines[currentLine - 1]);
|
|
}
|
|
|
|
/// Finds a section header on [currentLine] if the line is in range of
|
|
/// [lines].
|
|
Match? sectionHeaderMatch(int currentLine) {
|
|
if (currentLine < 1 || currentLine > lines.length) {
|
|
return null;
|
|
}
|
|
return _sectionPattern.firstMatch(lines[currentLine - 1]);
|
|
}
|
|
|
|
/// Checks if a line has a break from the previous section. A break is an
|
|
/// empty line. It searches recursively until it find a break or a test
|
|
/// entry.
|
|
bool hasBreakFromPreviousSection(int currentLine) {
|
|
if (currentLine <= 1) {
|
|
return true;
|
|
}
|
|
|
|
var line = lines[currentLine - 1];
|
|
if (line.isEmpty) {
|
|
return true;
|
|
}
|
|
|
|
if (line.startsWith("#")) {
|
|
return hasBreakFromPreviousSection(currentLine - 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Checks if comment on [currentLine] belongs to the next section.
|
|
bool commentBelongsToNextSectionHeader(int currentLine) {
|
|
if (currentLine >= lines.length ||
|
|
commentEntryMatch(currentLine) == null) {
|
|
return false;
|
|
}
|
|
return sectionHeaderMatch(currentLine + 1) != null ||
|
|
commentBelongsToNextSectionHeader(currentLine + 1);
|
|
}
|
|
|
|
// List of comments added before the next section's header.
|
|
List<Entry> sectionHeaderComments = [];
|
|
|
|
// Parse file comments
|
|
var lastEmptyLine = 0;
|
|
for (; _lineCount <= lines.length; _lineCount++) {
|
|
var line = lines[_lineCount - 1];
|
|
if (!line.startsWith("#") && line.isNotEmpty) {
|
|
break;
|
|
}
|
|
if (line.isEmpty) {
|
|
sectionHeaderComments.add(EmptyEntry(_lineCount));
|
|
lastEmptyLine = _lineCount;
|
|
} else {
|
|
sectionHeaderComments.add(CommentEntry(_lineCount, Comment(line)));
|
|
}
|
|
}
|
|
|
|
var implicitSectionHeaderComments = sectionHeaderComments;
|
|
var entries = <Entry>[];
|
|
if (lastEmptyLine > 0 && sectionHeaderMatch(_lineCount) != null) {
|
|
// Comments after the last empty line belong to the next section's header.
|
|
// The empty line is not added to the section header, because it will be
|
|
// added to the section's entries.
|
|
implicitSectionHeaderComments = implicitSectionHeaderComments.sublist(
|
|
0,
|
|
lastEmptyLine - 1,
|
|
);
|
|
entries.add(sectionHeaderComments[lastEmptyLine - 1]);
|
|
sectionHeaderComments = sectionHeaderComments.sublist(lastEmptyLine);
|
|
} else {
|
|
// Reset section header comments.
|
|
sectionHeaderComments = [];
|
|
}
|
|
|
|
// The current section whose rules are being parsed. Initialized to an
|
|
// implicit section that matches everything.
|
|
StatusSection section = StatusSection(
|
|
Expression.always,
|
|
-1,
|
|
implicitSectionHeaderComments,
|
|
);
|
|
section.entries.addAll(entries);
|
|
sections.add(section);
|
|
|
|
for (; _lineCount <= lines.length; _lineCount++) {
|
|
var line = lines[_lineCount - 1];
|
|
|
|
fail(String message, [List<String>? errors]) {
|
|
throw SyntaxError(_shortPath, _lineCount, line, message, errors);
|
|
}
|
|
|
|
// If it is an empty line
|
|
if (line.isEmpty) {
|
|
section.entries.add(EmptyEntry(_lineCount));
|
|
continue;
|
|
}
|
|
|
|
// See if we are starting a new section.
|
|
var match = _sectionPattern.firstMatch(line);
|
|
if (match != null) {
|
|
try {
|
|
var condition = Expression.parse(match[1]!.trim());
|
|
section = StatusSection(condition, _lineCount, sectionHeaderComments);
|
|
sections.add(section);
|
|
// Reset section header comments.
|
|
sectionHeaderComments = [];
|
|
} on FormatException {
|
|
fail("Status expression syntax error");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If it is in a new entry we should add to the current section.
|
|
match = _entryPattern.firstMatch(line);
|
|
if (match != null) {
|
|
var path = match[1]!.trim();
|
|
var expectations = <Expectation>[];
|
|
// split expectations
|
|
match[2]!.split(",").forEach((name) {
|
|
try {
|
|
expectations.add(Expectation.find(name.trim()));
|
|
} on ArgumentError {
|
|
fail('Unrecognized test expectation "${name.trim()}"');
|
|
}
|
|
});
|
|
if (match[3] == null) {
|
|
section.entries.add(
|
|
StatusEntry(path, _lineCount, expectations, null),
|
|
);
|
|
} else {
|
|
section.entries.add(
|
|
StatusEntry(path, _lineCount, expectations, Comment(match[3]!)),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If it is a comment, we have to find if it belongs with the current
|
|
// section or the next section
|
|
match = _commentPattern.firstMatch(line);
|
|
if (match != null) {
|
|
var commentEntry = CommentEntry(_lineCount, Comment(line));
|
|
if (hasBreakFromPreviousSection(_lineCount) &&
|
|
commentBelongsToNextSectionHeader(_lineCount)) {
|
|
sectionHeaderComments.add(commentEntry);
|
|
} else {
|
|
section.entries.add(commentEntry);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
fail("Unrecognized input");
|
|
}
|
|
|
|
// There are no comment entries in [sectionHeaderComments], because of the
|
|
// check for [commentBelongsToSectionHeader].
|
|
assert(sectionHeaderComments.isEmpty);
|
|
}
|
|
|
|
bool get isEmpty => sections.length == 1 && sections[0].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) {
|
|
for (var section in sections) {
|
|
var errors = <String>[];
|
|
section.condition.validate(environment, errors);
|
|
|
|
if (errors.isNotEmpty) {
|
|
var s = errors.length > 1 ? "s" : "";
|
|
throw 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.fromUri(Platform.script.resolve('../../../'));
|
|
return p.normalize(p.relative(path, from: repoRoot));
|
|
}
|
|
|
|
/// Returns the status file as a string. This preserves comments and gives a
|
|
/// "canonical" rendering of the status file that can be saved back to disc.
|
|
@override
|
|
String toString() {
|
|
var buffer = StringBuffer();
|
|
sections.forEach(buffer.write);
|
|
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.
|
|
///
|
|
/// Will be [Expression.always] 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 section appears in the file.
|
|
final int lineNumber,
|
|
|
|
final List<Entry> sectionHeaderComments,
|
|
) {
|
|
/// Collection of all comment and status line entries.
|
|
final List<Entry> entries = [];
|
|
|
|
/// Returns true if this section should apply in the given [environment].
|
|
bool isEnabled(Environment environment) => condition.evaluate(environment);
|
|
|
|
bool isEmpty() => !entries.any((entry) => entry is StatusEntry);
|
|
|
|
@override
|
|
String toString() {
|
|
var buffer = StringBuffer();
|
|
sectionHeaderComments.forEach(buffer.writeln);
|
|
if (condition != Expression.always) {
|
|
buffer.writeln("[ $condition ]");
|
|
}
|
|
entries.forEach(buffer.writeln);
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
class Comment(final String _comment) {
|
|
/// 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]!);
|
|
}
|
|
|
|
@override
|
|
String toString() => _comment;
|
|
}
|
|
|
|
abstract class Entry(
|
|
/// The one-based line number where the entry appears in the file.
|
|
final int lineNumber,
|
|
);
|
|
|
|
class EmptyEntry(super.lineNumber) extends Entry {
|
|
@override
|
|
String toString() => '';
|
|
}
|
|
|
|
class CommentEntry(super.lineNumber, final Comment comment) extends Entry {
|
|
@override
|
|
String toString() {
|
|
return comment.toString();
|
|
}
|
|
}
|
|
|
|
/// Describes the test status of the file or files at a given path.
|
|
class StatusEntry(
|
|
final String path,
|
|
super.lineNumber,
|
|
final List<Expectation> expectations,
|
|
final Comment? comment,
|
|
) extends Entry {
|
|
@override
|
|
String toString() {
|
|
return comment == null
|
|
? "$path: ${expectations.join(', ')}"
|
|
: "$path: ${expectations.join(', ')} $comment";
|
|
}
|
|
}
|