blob: 77cd2d7eb7fc85768b471e0d06b9226830396799 [file] [log] [blame]
// Copyright (c) 2016, 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 testing.chain;
import 'dart:convert' show json, JsonEncoder;
import 'dart:io' show Directory, File, FileSystemEntity, exitCode;
import 'suite.dart' show Suite;
import '../testing.dart' show FileBasedTestDescription, TestDescription;
import 'status_file_parser.dart' show readTestExpectations, TestExpectations;
import 'zone_helper.dart' show runGuarded;
import 'error_handling.dart' show withErrorHandling;
import 'log.dart' show Logger, StdoutLogger, splitLines;
import 'expectation.dart' show Expectation, ExpectationGroup, ExpectationSet;
typedef CreateContext = Future<ChainContext> Function(
Chain suite, Map<String, String> environment);
/// A test suite for tool chains, for example, a compiler.
class Chain extends Suite {
final Uri source;
final Uri root;
final List<Uri> subRoots;
final List<String> includeEndsWith;
final List<RegExp> pattern;
final List<RegExp> exclude;
Chain(String name, String kind, this.source, this.root, this.subRoots,
Uri statusFile, this.includeEndsWith, this.pattern, this.exclude)
: super(name, kind, statusFile);
factory Chain.fromJsonMap(Uri base, Map json, String name, String kind) {
Uri source = base.resolve(json["source"]);
String root = json["root"];
if (!root.endsWith("/")) {
root += "/";
}
Uri rootUri = base.resolve(root);
List<Uri> subRoots = [];
List? subRootsList = json["subRoots"];
if (subRootsList != null) {
for (String subRoot in subRootsList) {
if (!subRoot.endsWith("/")) {
subRoot += "/";
}
subRoots.add(rootUri.resolve(subRoot));
}
} else {
subRoots.add(rootUri);
}
Uri statusFile = base.resolve(json["status"]);
List<String> includeEndsWith =
List<String>.from(json['includeEndsWith'] ?? const []);
List<RegExp> pattern = [
for (final p in json['pattern'] ?? const []) new RegExp(p)
];
List<RegExp> exclude = [
for (final e in json['exclude'] ?? const []) new RegExp(e)
];
return Chain(name, kind, source, rootUri, subRoots, statusFile,
includeEndsWith, pattern, exclude);
}
void writeImportOn(StringSink sink) {
sink.write("import '");
sink.write(source);
sink.write("' as ");
sink.write(name);
sink.writeln(";");
}
void writeClosureOn(StringSink sink) {
sink.write("await runChain(");
sink.write(name);
sink.writeln(".createContext, {...environment}, selectors, r'''");
const String jsonExtraIndent = " ";
sink.write(jsonExtraIndent);
sink.writeAll(splitLines(JsonEncoder.withIndent(" ").convert(this)),
jsonExtraIndent);
sink.writeln("''');");
}
Map toJson() {
return {
"name": name,
"kind": kind,
"source": "$source",
"root": "$root",
"status": "$statusFile",
"pattern": [for (final r in pattern) r.pattern],
"includeEndsWith": includeEndsWith,
"exclude": [for (final r in exclude) r.pattern],
};
}
}
abstract class ChainContext {
const ChainContext();
List<Step> get steps;
ExpectationSet get expectationSet => ExpectationSet.defaultExpectations;
Future<void> run(Chain suite, Set<String> selectors,
{int shards = 1,
int shard = 0,
Logger logger = const StdoutLogger()}) async {
assert(shards >= 1, "Invalid shards count: $shards");
assert(0 <= shard && shard < shards,
"Invalid shard index: $shard, not in range [0,$shards[.");
List<String> tripleDotSelectors = selectors
.where((s) => s.endsWith('...'))
.map((s) => s.substring(0, s.length - 3))
.toList();
List<RegExp> asteriskSelectors = selectors
.where((s) => s.contains('*'))
.map((s) => _createRegExpForAsterisk(s))
.toList();
TestExpectations expectations = readTestExpectations(
<String>[suite.statusFile!.toFilePath()], expectationSet);
List<TestDescription> descriptions = await list(suite);
descriptions.sort();
if (shards > 1) {
List<TestDescription> shardDescriptions = [];
for (int index = 0; index < descriptions.length; index++) {
if (index % shards == shard) {
shardDescriptions.add(descriptions[index]);
}
}
descriptions = shardDescriptions;
}
Map<TestDescription, Result> unexpectedResults =
<TestDescription, Result>{};
Map<TestDescription, Set<Expectation>> unexpectedOutcomes =
<TestDescription, Set<Expectation>>{};
int completed = 0;
logger.logSuiteStarted(suite);
List<Future> futures = <Future>[];
for (TestDescription description in descriptions) {
String selector = "${suite.name}/${description.shortName}";
if (selectors.isNotEmpty &&
!selectors.contains(selector) &&
!selectors.contains(suite.name) &&
!tripleDotSelectors.any((s) => selector.startsWith(s)) &&
!asteriskSelectors.any((s) => s.hasMatch(selector))) {
continue;
}
final Set<Expectation> expectedOutcomes = processExpectedOutcomes(
expectations.expectations(description.shortName), description);
bool shouldSkip = false;
for (Expectation expectation in expectedOutcomes) {
if (expectation.group == ExpectationGroup.skip) {
shouldSkip = true;
break;
}
}
if (shouldSkip) continue;
final StringBuffer sb = StringBuffer();
final Iterator<Step> iterator = steps.iterator;
Result? result;
// Records the outcome of the last step that was run.
Step? lastStepRun;
/// Performs one step of [iterator].
///
/// If `step.isAsync` is true, the corresponding step is said to be
/// asynchronous.
///
/// If a step is asynchronous the future returned from this function will
/// complete after the first asynchronous step is scheduled. This
/// allows us to start processing the next test while an external process
/// completes as steps can be interleaved. To ensure all steps are
/// completed, wait for [futures].
///
/// Otherwise, the future returned will complete when all steps are
/// completed. This ensures that tests are run in sequence without
/// interleaving steps.
Future doStep(dynamic input) async {
Future future;
bool isAsync = false;
if (iterator.moveNext()) {
Step step = iterator.current;
lastStepRun = step;
isAsync = step.isAsync;
logger.logStepStart(completed, unexpectedResults.length,
descriptions.length, suite, description, step);
// TODO(ahe): It's important to share the zone error reporting zone
// between all the tasks. Otherwise, if a future completes with an
// error in one zone, and gets stored, it becomes an uncaught error
// in other zones (this happened in createPlatform).
future = runGuarded(() async {
try {
return await step.run(input, this);
} catch (error, trace) {
return step.unhandledError(error, trace);
}
}, printLineOnStdout: sb.writeln);
} else {
future = Future.value(null);
}
future = future.then((currentResult) async {
if (currentResult != null) {
logger.logStepComplete(completed, unexpectedResults.length,
descriptions.length, suite, description, lastStepRun!);
result = currentResult;
if ((currentResult as Result).outcome == Expectation.pass) {
// The input to the next step is the output of this step.
return doStep(result!.output);
}
}
await cleanUp(description, result!);
if (!expectedOutcomes.contains(result!.outcome) &&
!expectedOutcomes.contains(result!.outcome.canonical)) {
result!.addLog("$sb");
unexpectedResults[description] = result!;
unexpectedOutcomes[description] = expectedOutcomes;
logger.logUnexpectedResult(
suite, description, result!, expectedOutcomes);
exitCode = 1;
} else {
logger.logExpectedResult(
suite, description, result!, expectedOutcomes);
logger.logMessage(sb);
}
logger.logTestComplete(++completed, unexpectedResults.length,
descriptions.length, suite, description);
});
if (isAsync) {
futures.add(future);
return null;
} else {
return future;
}
}
logger.logTestStart(completed, unexpectedResults.length,
descriptions.length, suite, description);
// The input of the first step is [description].
await doStep(description);
}
await Future.wait(futures);
logger.logSuiteComplete(suite);
if (unexpectedResults.isNotEmpty) {
unexpectedResults.forEach((TestDescription description, Result result) {
logger.logUnexpectedResult(
suite, description, result, unexpectedOutcomes[description]!);
});
print("${unexpectedResults.length} failed:");
unexpectedResults.forEach((TestDescription description, Result result) {
print("${suite.name}/${description.shortName}: ${result.outcome}");
});
}
await postRun();
}
Future<List<TestDescription>> list(Chain suite) async {
List<TestDescription> result = [];
for (Uri subRoot in suite.subRoots) {
Directory testRoot = Directory.fromUri(subRoot);
if (testRoot.existsSync()) {
for (FileSystemEntity entity
in testRoot.listSync(recursive: true, followLinks: false)) {
if (entity is! File) continue;
// Use `.uri.path` instead of just `.path` to ensure forward slashes.
String path = entity.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
bool include = false;
if (suite.includeEndsWith.any((String end) => path.endsWith(end))) {
include = true;
}
if (!include && suite.pattern.any((RegExp r) => path.contains(r))) {
include = true;
}
if (include) {
result.add(new FileBasedTestDescription(suite.root, entity));
}
}
} else {
throw "$subRoot isn't a directory";
}
}
return result;
}
Set<Expectation> processExpectedOutcomes(
Set<Expectation> outcomes, TestDescription description) {
return outcomes;
}
Future<void> cleanUp(TestDescription description, Result result) async {}
Future<void> postRun() async {}
}
abstract class Step<I, O, C extends ChainContext> {
const Step();
String get name;
/// Sets this (*and effectively subsequent*) test step(s) as async.
///
/// TL;DR: Either set to false, or only set to true when this and all
/// subsequent steps can run intertwined with another test.
///
/// Details:
///
/// A single test (TestDescription) can have several steps (Step).
/// When running a test the first step is executed, and when that step is done
/// the next step is executed by the now-ending step.
///
/// When isAsync is false each step returns a future which is awaited,
/// effectively meaning that only a single test is run at a time.
///
/// When isAsync is true that step doesn't return a future (but adds it's
/// future to a list which is awaited before sending an 'entire suite done'
/// message), meaning that the next test can start before the step is
/// finished. As the next step in the test only starts after the current
/// step finishes, that also means that the next test can start - and run
/// intertwined with - a subsequent step even if such a subsequent step has
/// isAsync set to false.
bool get isAsync => false;
Future<Result<O>> run(I input, C context);
Result<O> unhandledError(error, StackTrace trace) {
return Result<O>.crash(error, trace);
}
Result<O> pass(O output) => Result<O>.pass(output);
Result<O> crash(error, StackTrace trace) => Result<O>.crash(error, trace);
Result<O> fail(O output, [error, StackTrace? trace]) {
return Result<O>.fail(output, error, trace);
}
}
class Result<O> {
final O? output;
final Expectation outcome;
final Object? error;
final StackTrace? trace;
final List<String> logs = <String>[];
/// If set, running the test with '-D$autoFixCommand' will automatically
/// update the test to match new expectations.
final String? autoFixCommand;
/// If set, the test can be fixed by running
///
/// dart pkg/front_end/tool/update_expectations.dart
///
final bool canBeFixWithUpdateExpectations;
Result(
this.output,
this.outcome,
this.error, {
this.trace,
this.autoFixCommand,
this.canBeFixWithUpdateExpectations = false,
});
Result.pass(O output) : this(output, Expectation.pass, null);
Result.crash(error, StackTrace trace)
: this(null, Expectation.crash, error, trace: trace);
Result.fail(O output, [error, StackTrace? trace])
: this(output, Expectation.fail, error, trace: trace);
bool get isPass => outcome == Expectation.pass;
String get log => logs.join();
void addLog(String log) {
logs.add(log);
}
Result<O2> copyWithOutput<O2>(O2 output) {
return Result<O2>(output, outcome, error,
trace: trace,
autoFixCommand: autoFixCommand,
canBeFixWithUpdateExpectations: canBeFixWithUpdateExpectations)
..logs.addAll(logs);
}
}
/// This is called from generated code.
Future<void> runChain(CreateContext f, Map<String, String> environment,
Set<String> selectors, String jsonText) {
return withErrorHandling(() async {
Chain suite = Suite.fromJsonMap(Uri.base, json.decode(jsonText)) as Chain;
print("Running ${suite.name}");
ChainContext context = await f(suite, environment);
return context.run(suite, selectors);
});
}
RegExp _createRegExpForAsterisk(String s) {
StringBuffer sb = new StringBuffer("^");
String between = "";
for (String split in s.split("*")) {
sb.write(between);
between = ".*";
sb.write(RegExp.escape(split));
}
sb.write("\$");
return new RegExp(sb.toString());
}