blob: e5831e58a8d29d6d44921f6ec68b54ea8f9253df [file] [log] [blame]
// Copyright (c) 2015, 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 test.runner;
import 'dart:async';
import 'dart:io';
import 'package:async/async.dart';
import 'backend/test_platform.dart';
import 'runner/application_exception.dart';
import 'runner/configuration.dart';
import 'runner/engine.dart';
import 'runner/load_exception.dart';
import 'runner/load_suite.dart';
import 'runner/loader.dart';
import 'runner/reporter.dart';
import 'runner/reporter/compact.dart';
import 'runner/reporter/expanded.dart';
import 'runner/runner_suite.dart';
import 'util/io.dart';
import 'utils.dart';
/// A class that loads and runs tests based on a [Configuration].
///
/// This maintains a [Loader] and an [Engine] and passes test suites from one to
/// the other, as well as printing out tests with a [CompactReporter] or an
/// [ExpandedReporter].
class Runner {
/// The configuration for the runner.
final Configuration _config;
/// The loader that loads the test suites from the filesystem.
final Loader _loader;
/// The engine that runs the test suites.
final Engine _engine;
/// The reporter that's emitting the test runner's results.
final Reporter _reporter;
/// The subscription to the stream returned by [_loadSuites].
StreamSubscription _suiteSubscription;
/// The memoizer for ensuring [close] only runs once.
final _closeMemo = new AsyncMemoizer();
bool get _closed => _closeMemo.hasRun;
/// Creates a new runner based on [configuration].
factory Runner(Configuration config) {
var loader = new Loader(config);
var engine = new Engine(concurrency: config.concurrency);
var watch = config.reporter == "compact"
? CompactReporter.watch
: ExpandedReporter.watch;
var reporter = watch(
engine,
color: config.color,
verboseTrace: config.verboseTrace,
printPath: config.paths.length > 1 ||
new Directory(config.paths.single).existsSync(),
printPlatform: config.platforms.length > 1);
return new Runner._(config, loader, engine, reporter);
}
Runner._(this._config, this._loader, this._engine, this._reporter);
/// Starts the runner.
///
/// This starts running tests and printing their progress. It returns whether
/// or not they ran successfully.
Future<bool> run() async {
if (_closed) {
throw new StateError("run() may not be called on a closed Runner.");
}
var suites = _loadSuites();
var success;
if (_config.pauseAfterLoad) {
success = await _loadThenPause(suites);
} else {
_suiteSubscription = suites.listen(_engine.suiteSink.add);
var results = await Future.wait([
_suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()),
_engine.run()
], eagerError: true);
success = results.last;
}
if (_closed) return false;
if (_engine.passed.length == 0 && _engine.failed.length == 0 &&
_engine.skipped.length == 0 && _config.pattern != null) {
var message = 'No tests match ';
if (_config.pattern is RegExp) {
var pattern = (_config.pattern as RegExp).pattern;
message += 'regular expression "$pattern".';
} else {
message += '"${_config.pattern}".';
}
throw new ApplicationException(message);
}
// Explicitly check "== true" here because [Engine.run] can return `null`
// if the engine was closed prematurely.
return success == true;
}
/// Closes the runner.
///
/// This stops any future test suites from running. It will wait for any
/// currently-running VM tests, in case they have stuff to clean up on the
/// filesystem.
Future close() => _closeMemo.runOnce(() async {
var timer;
if (!_engine.isIdle) {
// Wait a bit to print this message, since printing it eagerly looks weird
// if the tests then finish immediately.
timer = new Timer(new Duration(seconds: 1), () {
// Pause the reporter while we print to ensure that we don't interfere
// with its output.
_reporter.pause();
print("Waiting for current test(s) to finish.");
print("Press Control-C again to terminate immediately.");
_reporter.resume();
});
}
if (_suiteSubscription != null) _suiteSubscription.cancel();
_suiteSubscription = null;
// Make sure we close the engine *before* the loader. Otherwise,
// LoadSuites provided by the loader may get into bad states.
await _engine.close();
if (timer != null) timer.cancel();
await _loader.close();
});
/// Return a stream of [LoadSuite]s in [_config.paths].
///
/// Only tests that match [_config.pattern] will be included in the
/// suites once they're loaded.
Stream<LoadSuite> _loadSuites() {
return mergeStreams(_config.paths.map((path) {
if (new Directory(path).existsSync()) return _loader.loadDir(path);
if (new File(path).existsSync()) return _loader.loadFile(path);
return new Stream.fromIterable([
new LoadSuite("loading $path", () =>
throw new LoadException(path, 'Does not exist.'))
]);
})).map((loadSuite) {
return loadSuite.changeSuite((suite) {
if (_config.pattern == null) return suite;
return suite.filter((test) => test.name.contains(_config.pattern));
});
});
}
/// Loads each suite in [suites] in order, pausing after load for platforms
/// that support debugging.
Future<bool> _loadThenPause(Stream<LoadSuite> suites) async {
if (_config.platforms.contains(TestPlatform.vm)) {
warn("Debugging is currently unsupported on the Dart VM.",
color: _config.color);
}
_suiteSubscription = suites.asyncMap((loadSuite) async {
// Make the underlying suite null so that the engine doesn't start running
// it immediately.
_engine.suiteSink.add(loadSuite.changeSuite((_) => null));
var suite = await loadSuite.suite;
if (suite == null) return;
await _pause(suite);
if (_closed) return;
_engine.suiteSink.add(suite);
await _engine.onIdle.first;
}).listen(null);
var results = await Future.wait([
_suiteSubscription.asFuture().then((_) => _engine.suiteSink.close()),
_engine.run()
]);
return results.last;
}
/// Pauses the engine and the reporter so that the user can set breakpoints as
/// necessary.
///
/// This is a no-op for test suites that aren't on platforms where debugging
/// is supported.
Future _pause(RunnerSuite suite) async {
if (suite.platform == null) return;
if (suite.platform == TestPlatform.vm) return;
try {
_reporter.pause();
var bold = _config.color ? '\u001b[1m' : '';
var yellow = _config.color ? '\u001b[33m' : '';
var noColor = _config.color ? '\u001b[0m' : '';
print('');
if (suite.platform.isDartVM) {
var url = suite.environment.observatoryUrl;
if (url == null) {
print("${yellow}Observatory URL not found. Make sure you're using "
"${suite.platform.name} 1.11 or later.$noColor");
} else {
print("Observatory URL: $bold$url$noColor");
}
}
if (suite.platform.isHeadless) {
var url = suite.environment.remoteDebuggerUrl;
if (url == null) {
print("${yellow}Remote debugger URL not found.$noColor");
} else {
print("Remote debugger URL: $bold$url$noColor");
}
}
var buffer = new StringBuffer(
"${bold}The test runner is paused.${noColor} ");
if (!suite.platform.isHeadless) {
buffer.write("Open the dev console in ${suite.platform} ");
} else {
buffer.write("Open the remote debugger ");
}
if (suite.platform.isDartVM) buffer.write("or the Observatory ");
buffer.write("and set breakpoints. Once you're finished, return to this "
"terminal and press Enter.");
print(wordWrap(buffer.toString()));
await inCompletionOrder([
suite.environment.displayPause(),
cancelableNext(stdinLines)
]).first;
} finally {
_reporter.resume();
}
}
}