| Index: pkg/scheduled_test/test/metatest.dart
|
| diff --git a/pkg/scheduled_test/test/metatest.dart b/pkg/scheduled_test/test/metatest.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..2dd50be98b879dae083a2f954d307f5a7e47aa71
|
| --- /dev/null
|
| +++ b/pkg/scheduled_test/test/metatest.dart
|
| @@ -0,0 +1,204 @@
|
| +// Copyright (c) 2013, 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.
|
| +
|
| +/// A test library for testing test libraries? We must go deeper.
|
| +///
|
| +/// Since unit testing code tends to use a lot of global state, it can be tough
|
| +/// to test. This library manages it by running each test case in a child
|
| +/// isolate, then reporting the results back to the parent isolate.
|
| +library metatest;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:isolate';
|
| +
|
| +import '../../../pkg/path/lib/path.dart' as path;
|
| +import 'package:scheduled_test/src/utils.dart';
|
| +import 'package:unittest/unittest.dart';
|
| +
|
| +/// Declares a test with the given [description] and [body]. [body] corresponds
|
| +/// to the `main` method of a test file, and will be run in an isolate. By
|
| +/// default, this expects that all tests defined in [body] pass, but if
|
| +/// [passing] is passed, only tests listed there are expected to pass.
|
| +void expectTestsPass(String description, void body(), {List<String> passing}) {
|
| + _setUpTest(description, body, (results) {
|
| + if (_hasError(results)) {
|
| + throw 'Expected all tests to pass, but got error(s):\n'
|
| + '${_summarizeTests(results)}';
|
| + } else if (passing == null) {
|
| + if (results['failed'] != 0) {
|
| + throw 'Expected all tests to pass, but some failed:\n'
|
| + '${_summarizeTests(results)}';
|
| + }
|
| + } else {
|
| + var shouldPass = new Set.from(passing);
|
| + var didPass = new Set.from(results['results']
|
| + .where((t) => t['result'] == 'pass')
|
| + .map((t) => t['description']));
|
| +
|
| + if (!shouldPass.containsAll(didPass) ||
|
| + !didPass.containsAll(shouldPass)) {
|
| + String stringify(Set<String> tests) =>
|
| + '{${tests.map((t) => '"$t"').join(', ')}}';
|
| +
|
| + fail('Expected exactly ${stringify(shouldPass)} to pass, but '
|
| + '${stringify(didPass)} passed.\n'
|
| + '${_summarizeTests(results)}');
|
| + }
|
| + }
|
| + });
|
| +}
|
| +
|
| +/// Declares a test with the given [description] and [body]. [body] corresponds
|
| +/// to the `main` method of a test file, and will be run in an isolate. Expects
|
| +/// all tests defined by [body] to fail.
|
| +void expectTestsFail(String description, void body()) {
|
| + _setUpTest(description, body, (results) {
|
| + if (_hasError(results)) {
|
| + throw 'Expected all tests to fail, but got error(s):\n'
|
| + '${_summarizeTests(results)}';
|
| + } else if (results['passed'] != 0) {
|
| + throw 'Expected all tests to fail, but some passed:\n'
|
| + '${_summarizeTests(results)}';
|
| + }
|
| + });
|
| +}
|
| +
|
| +/// Sets up a test with the given [description] and [body]. After the test runs,
|
| +/// calls [validate] with the result map.
|
| +void _setUpTest(String description, void body(), void validate(Map)) {
|
| + _inChildIsolate.then((inIsolate) {
|
| + if (inIsolate) {
|
| + _ensureInitialized();
|
| + if (_testToRun == description) body();
|
| + } else {
|
| + test(description, () {
|
| + expect(_runInIsolate(description).then(validate), completes);
|
| + });
|
| + }
|
| + });
|
| +}
|
| +
|
| +/// The description of the test to run in the child isolate. `null` in the
|
| +/// parent isolate. Not set until [_inChildIsolate] completes.
|
| +String _testToRun;
|
| +
|
| +/// The port with which the child isolate should communicate with the parent
|
| +/// isolate. `null` in the parent isolate. Not set until [_inChildIsolate]
|
| +/// completes.
|
| +SendPort _replyTo;
|
| +
|
| +/// The cached [Future] for [_inChildIsolate].
|
| +Future<bool> _inChildIsolateFuture;
|
| +
|
| +/// Returns whether or not we're running in a child isolate that's supposed to
|
| +/// run a test.
|
| +Future<bool> get _inChildIsolate {
|
| + if (_inChildIsolateFuture != null) return _inChildIsolateFuture;
|
| +
|
| + var completer = new Completer();
|
| + port.receive((message, replyTo) {
|
| + _testToRun = message;
|
| + _replyTo = replyTo;
|
| + port.close();
|
| + completer.complete(true);
|
| + });
|
| +
|
| + // TODO(nweiz): don't use a timeout here once issue 8416 is fixed.
|
| + _inChildIsolateFuture = timeout(completer.future, 500, () {
|
| + port.close();
|
| + return false;
|
| + });
|
| + return _inChildIsolateFuture;
|
| +}
|
| +
|
| +/// Runs the test described by [description] in its own isolate. Returns a map
|
| +/// describing the results of that test run.
|
| +Future<Map> _runInIsolate(String description) {
|
| + var future = spawnUri(path.join(path.current, new Options().script))
|
| + .call(description);
|
| + // TODO(nweiz): Remove this timeout once issue 8417 is fixed and we can
|
| + // capture top-level exceptions.
|
| + return timeout(future, 30 * 1000, () {
|
| + throw 'Timed out waiting for test to complete.';
|
| + });
|
| +}
|
| +
|
| +/// Returns whether [results] (a test result map) describes a test run in which
|
| +/// an error occurred.
|
| +bool _hasError(Map results) {
|
| + return results['errors'] > 0 || results['uncaughtError'] != null ||
|
| + (results['passed'] == 0 && results['failed'] == 0);
|
| +}
|
| +
|
| +/// Returns a string description of the test run descibed by [results].
|
| +String _summarizeTests(Map results) {
|
| + var buffer = new StringBuffer();
|
| + for (var t in results["results"]) {
|
| + buffer.add("${t['result'].toUpperCase()}: ${t['description']}\n");
|
| + if (t['message'] != '') buffer.add("${_indent(t['message'])}\n");
|
| + if (t['stackTrace'] != null && t['stackTrace'] != '') {
|
| + buffer.add("${_indent(t['stackTrace'])}\n");
|
| + }
|
| + }
|
| +
|
| + buffer.add("\n");
|
| +
|
| + var success = false;
|
| + if (results['passed'] == 0 && results['failed'] == 0 &&
|
| + results['errors'] == 0 && results['uncaughtError'] == null) {
|
| + buffer.add('No tests found.');
|
| + // This is considered a failure too.
|
| + } else if (results['failed'] == 0 && results['errors'] == 0 &&
|
| + results['uncaughtError'] == null) {
|
| + buffer.add('All ${results['passed']} tests passed.');
|
| + success = true;
|
| + } else {
|
| + if (results['uncaughtError'] != null) {
|
| + buffer.add('Top-level uncaught error: ${results['uncaughtError']}');
|
| + }
|
| + buffer.add('${results['passed']} PASSED, ${results['failed']} FAILED, '
|
| + '${results['errors']} ERRORS');
|
| + }
|
| + return prefixLines(buffer.toString());
|
| +}
|
| +
|
| +/// Indents each line of [str] by two spaces.
|
| +String _indent(String str) {
|
| + // TODO(nweiz): Use this simpler code once issue 2980 is fixed.
|
| + // return str.replaceAll(new RegExp("^", multiLine: true), " ");
|
| +
|
| + return Strings.join(str.split("\n").map((line) => " $line"), "\n");
|
| +}
|
| +
|
| +/// Ensure that the metatest configuration is loaded.
|
| +void _ensureInitialized() {
|
| + if (config is! _MetaConfiguration) configure(new _MetaConfiguration());
|
| +}
|
| +
|
| +/// Special test configuration for use within the child isolates. This hides all
|
| +/// output and reports data back to the parent isolate.
|
| +class _MetaConfiguration extends Configuration {
|
| + final name = "MetaConfiguration";
|
| +
|
| + void logTestCaseMesssage(TestCase testCase, String message) {}
|
| +
|
| + void onSummary(int passed, int failed, int errors, List<TestCase> results,
|
| + String uncaughtError) {
|
| + _replyTo.send({
|
| + "passed": passed,
|
| + "failed": failed,
|
| + "errors": errors,
|
| + "uncaughtError": uncaughtError,
|
| + "results": results.map((testCase) => {
|
| + "description": testCase.description,
|
| + "message": testCase.message,
|
| + "result": testCase.result,
|
| + "stackTrace": testCase.stackTrace
|
| + }).toList()
|
| + });
|
| + }
|
| +
|
| + void onInit() {}
|
| + void onDone(bool success) {}
|
| +}
|
|
|