Index: pkg/testing/lib/src/chain.dart |
diff --git a/pkg/testing/lib/src/chain.dart b/pkg/testing/lib/src/chain.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8925b9dc9f482ba0d3418602bb26c4b8713ba89b |
--- /dev/null |
+++ b/pkg/testing/lib/src/chain.dart |
@@ -0,0 +1,330 @@ |
+// 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 |
+ TestDescription; |
+ |
+import 'test_dart/status_file_parser.dart' show |
+ Expectation, |
+ 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; |
+ |
+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"]); |
+ Uri uri = base.resolve(json["path"]); |
+ Uri statusFile = base.resolve(json["status"]); |
+ List<RegExp> pattern = new List<RegExp>.from( |
+ json["pattern"].map((String p) => new RegExp(p))); |
+ List<RegExp> exclude = new List<RegExp>.from( |
+ json["exclude"].map((String p) => new RegExp(p))); |
+ 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; |
+ |
+ Future<Null> run(Chain suite, Set<String> selectors) async { |
+ TestExpectations expectations = await ReadTestExpectations( |
+ <String>[suite.statusFile.toFilePath()], {}); |
+ 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)) { |
+ continue; |
+ } |
+ Set<Expectation> expectedOutcomes = |
+ expectations.expectations(description.shortName); |
+ Result result; |
+ StringBuffer sb = new StringBuffer(); |
+ // Records the outcome of the last step that was run. |
+ Step lastStep = null; |
+ Iterator<Step> iterator = steps.iterator; |
+ |
+ /// Performs one step of [iterator]. |
+ /// |
+ /// If `step.isAsync` is true, the corresponding step is said to be |
+ /// asynchronous. |
+ /// |
+ /// If a step is asynchrouns the future returned from this function will |
+ /// complete after the 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; |
+ lastStep = step; |
+ isAsync = step.isAsync; |
+ logStepStart(completed, unexpectedResults.length, descriptions.length, |
+ suite, description, step); |
+ 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((Result currentResult) { |
+ if (currentResult != null) { |
+ logStepComplete(completed, unexpectedResults.length, |
+ descriptions.length, suite, description, lastStep); |
+ result = currentResult; |
+ if (currentResult.outcome == Expectation.PASS) { |
+ // The input to the next step is the output of this step. |
+ return doStep(result.output); |
+ } |
+ } |
+ if (steps.isNotEmpty && steps.last == lastStep && |
+ 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 = result.toNegativeTestResult(); |
+ } |
+ if (!expectedOutcomes.contains(result.outcome)) { |
+ result.addLog("$sb"); |
+ unexpectedResults[description] = result; |
+ unexpectedOutcomes[description] = expectedOutcomes; |
+ logUnexpectedResult(suite, description, result, expectedOutcomes); |
+ } 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) { |
+ exitCode = 1; |
+ 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 TestDescription(suite.uri, entity); |
+ } |
+ } |
+ } else { |
+ throw "${suite.uri} isn't a directory"; |
+ } |
+ } |
+} |
+ |
+abstract class Step<I, O, C extends ChainContext> { |
+ const Step(); |
+ |
+ String get name; |
+ |
+ bool get isAsync => 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> toNegativeTestResult() { |
+ Expectation outcome = this.outcome; |
+ if (outcome == Expectation.PASS) { |
+ outcome = Expectation.FAIL; |
+ } else if (outcome == Expectation.FAIL) { |
+ outcome = Expectation.PASS; |
+ } |
+ 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 json) { |
+ return withErrorHandling(() async { |
+ Chain suite = new Suite.fromJsonMap(Uri.base, JSON.decode(json)); |
+ print("Running ${suite.name}"); |
+ ChainContext context = await f(suite, environment); |
+ return context.run(suite, selectors); |
+ }); |
+} |