blob: 53256aebfaa47cb23f144d442079598c1b40e3c8 [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.md file.
library testing.chain;
import 'dart:async' show Future, Stream;
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 'test_dart/status_file_parser.dart'
show ReadTestExpectations, TestExpectations;
import 'zone_helper.dart' show runGuarded;
import 'error_handling.dart' show withErrorHandling;
import 'log.dart'
show
logMessage,
logStepComplete,
logStepStart,
logSuiteComplete,
logTestComplete,
logUnexpectedResult,
splitLines;
import 'multitest.dart' show MultitestTransformer, isError;
import 'expectation.dart' show Expectation, ExpectationSet;
typedef Future<ChainContext> CreateContext(
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 uri;
final List<RegExp> pattern;
final List<RegExp> exclude;
final bool processMultitests;
Chain(String name, String kind, this.source, this.uri, Uri statusFile,
this.pattern, this.exclude, this.processMultitests)
: super(name, kind, statusFile);
factory Chain.fromJsonMap(Uri base, Map json, String name, String kind) {
Uri source = base.resolve(json["source"]);
String path = json["path"];
if (!path.endsWith("/")) {
path += "/";
}
Uri uri = base.resolve(path);
Uri statusFile = base.resolve(json["status"]);
List<RegExp> pattern =
json["pattern"].map<RegExp>((p) => new RegExp(p)).toList();
List<RegExp> exclude =
json["exclude"].map<RegExp>((p) => new RegExp(p)).toList();
bool processMultitests = json["process-multitests"] ?? false;
return new Chain(name, kind, source, uri, statusFile, pattern, exclude,
processMultitests);
}
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(new JsonEncoder.withIndent(" ").convert(this)),
jsonExtraIndent);
sink.writeln("''');");
}
Map toJson() {
return {
"name": name,
"kind": kind,
"source": "$source",
"path": "$uri",
"status": "$statusFile",
"process-multitests": processMultitests,
"pattern": []..addAll(pattern.map((RegExp r) => r.pattern)),
"exclude": []..addAll(exclude.map((RegExp r) => r.pattern)),
};
}
}
abstract class ChainContext {
const ChainContext();
List<Step> get steps;
ExpectationSet get expectationSet => ExpectationSet.Default;
Future<Null> run(Chain suite, Set<String> selectors) async {
List<String> partialSelectors = selectors
.where((s) => s.endsWith('...'))
.map((s) => s.substring(0, s.length - 3))
.toList();
TestExpectations expectations = await ReadTestExpectations(
<String>[suite.statusFile.toFilePath()], {}, expectationSet);
Stream<TestDescription> stream = list(suite);
if (suite.processMultitests) {
stream = stream.transform(new MultitestTransformer());
}
List<TestDescription> descriptions = await stream.toList();
descriptions.sort();
Map<TestDescription, Result> unexpectedResults =
<TestDescription, Result>{};
Map<TestDescription, Set<Expectation>> unexpectedOutcomes =
<TestDescription, Set<Expectation>>{};
int completed = 0;
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) &&
!partialSelectors.any((s) => selector.startsWith(s))) {
continue;
}
final Set<Expectation> expectedOutcomes =
expectations.expectations(description.shortName);
final StringBuffer sb = new StringBuffer();
final Step lastStep = steps.isNotEmpty ? steps.last : null;
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;
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 = new Future.value(null);
}
future = future.then((_currentResult) async {
Result currentResult = _currentResult;
if (currentResult != null) {
logStepComplete(completed, unexpectedResults.length,
descriptions.length, suite, description, lastStepRun);
result = currentResult;
if (currentResult.outcome == Expectation.Pass) {
// The input to the next step is the output of this step.
return doStep(result.output);
}
}
await cleanUp(description, result);
result =
processTestResult(description, result, lastStep == lastStepRun);
if (!expectedOutcomes.contains(result.outcome) &&
!expectedOutcomes.contains(result.outcome.canonical)) {
result.addLog("$sb");
unexpectedResults[description] = result;
unexpectedOutcomes[description] = expectedOutcomes;
logUnexpectedResult(suite, description, result, expectedOutcomes);
exitCode = 1;
} else {
logMessage(sb);
}
logTestComplete(++completed, unexpectedResults.length,
descriptions.length, suite, description);
});
if (isAsync) {
futures.add(future);
return null;
} else {
return future;
}
}
// The input of the first step is [description].
await doStep(description);
}
await Future.wait(futures);
logSuiteComplete();
if (unexpectedResults.isNotEmpty) {
unexpectedResults.forEach((TestDescription description, Result result) {
logUnexpectedResult(
suite, description, result, unexpectedOutcomes[description]);
});
print("${unexpectedResults.length} failed:");
unexpectedResults.forEach((TestDescription description, Result result) {
print("${suite.name}/${description.shortName}: ${result.outcome}");
});
}
}
Stream<TestDescription> list(Chain suite) async* {
Directory testRoot = new Directory.fromUri(suite.uri);
if (await testRoot.exists()) {
Stream<FileSystemEntity> files =
testRoot.list(recursive: true, followLinks: false);
await for (FileSystemEntity entity in files) {
if (entity is! File) continue;
String path = entity.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
if (suite.pattern.any((RegExp r) => path.contains(r))) {
yield new FileBasedTestDescription(suite.uri, entity);
}
}
} else {
throw "${suite.uri} isn't a directory";
}
}
Result processTestResult(
TestDescription description, Result result, bool last) {
if (description is FileBasedTestDescription &&
description.multitestExpectations != null) {
if (isError(description.multitestExpectations)) {
result =
toNegativeTestResult(result, description.multitestExpectations);
}
} else if (last && description.shortName.endsWith("negative_test")) {
if (result.outcome == Expectation.Pass) {
result.addLog("Negative test didn't report an error.\n");
} else if (result.outcome == Expectation.Fail) {
result.addLog("Negative test reported an error as expeceted.\n");
}
result = toNegativeTestResult(result);
}
return result;
}
Result toNegativeTestResult(Result result, [Set<String> expectations]) {
Expectation outcome = result.outcome;
if (outcome == Expectation.Pass) {
if (expectations == null) {
outcome = Expectation.Fail;
} else if (expectations.contains("compile-time error")) {
outcome = expectationSet["MissingCompileTimeError"];
} else if (expectations.contains("runtime error") ||
expectations.contains("dynamic type error")) {
outcome = expectationSet["MissingRuntimeError"];
} else {
outcome = Expectation.Fail;
}
} else if (outcome == Expectation.Fail) {
outcome = Expectation.Pass;
}
return result.copyWithOutcome(outcome);
}
Future<void> cleanUp(TestDescription description, Result result) => null;
}
abstract class Step<I, O, C extends ChainContext> {
const Step();
String get name;
bool get isAsync => false;
bool get isCompiler => false;
bool get isRuntime => false;
Future<Result<O>> run(I input, C context);
Result<O> unhandledError(error, StackTrace trace) {
return new Result<O>.crash(error, trace);
}
Result<O> pass(O output) => new Result<O>.pass(output);
Result<O> crash(error, StackTrace trace) => new Result<O>.crash(error, trace);
Result<O> fail(O output, [error, StackTrace trace]) {
return new Result<O>.fail(output, error, trace);
}
}
class Result<O> {
final O output;
final Expectation outcome;
final error;
final StackTrace trace;
final List<String> logs = <String>[];
Result(this.output, this.outcome, this.error, this.trace);
Result.pass(O output) : this(output, Expectation.Pass, null, null);
Result.crash(error, StackTrace trace)
: this(null, Expectation.Crash, error, trace);
Result.fail(O output, [error, StackTrace trace])
: this(output, Expectation.Fail, error, trace);
String get log => logs.join();
void addLog(String log) {
logs.add(log);
}
Result<O> copyWithOutcome(Expectation outcome) {
return new Result<O>(output, outcome, error, trace)..logs.addAll(logs);
}
}
/// This is called from generated code.
Future<Null> runChain(CreateContext f, Map<String, String> environment,
Set<String> selectors, String jsonText) {
return withErrorHandling(() async {
Chain suite = new Suite.fromJsonMap(Uri.base, json.decode(jsonText));
print("Running ${suite.name}");
ChainContext context = await f(suite, environment);
return context.run(suite, selectors);
});
}