blob: 62fb46bda337345b85ad9aaa1ed74c1cee9ed196 [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.loader;
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:analyzer/analyzer.dart';
import 'package:async/async.dart';
import 'package:path/path.dart' as p;
import 'package:stack_trace/stack_trace.dart';
import '../backend/group.dart';
import '../backend/metadata.dart';
import '../backend/test.dart';
import '../backend/test_platform.dart';
import '../util/dart.dart' as dart;
import '../util/io.dart';
import '../util/remote_exception.dart';
import '../utils.dart';
import 'browser/server.dart';
import 'configuration.dart';
import 'hack_load_vm_file_hook.dart';
import 'load_exception.dart';
import 'load_suite.dart';
import 'parse_metadata.dart';
import 'runner_suite.dart';
import 'vm/environment.dart';
import 'vm/isolate_test.dart';
/// A class for finding test files and loading them into a runnable form.
class Loader {
/// The test runner configuration.
final Configuration _config;
/// The root directory that will be served for browser tests.
final String _root;
/// All suites that have been created by the loader.
final _suites = new Set<RunnerSuite>();
/// The server that serves browser test pages.
///
/// This is lazily initialized the first time it's accessed.
Future<BrowserServer> get _browserServer {
return _browserServerMemo.runOnce(() {
return BrowserServer.start(_config, root: _root);
});
}
final _browserServerMemo = new AsyncMemoizer<BrowserServer>();
/// The memoizer for running [close] exactly once.
final _closeMemo = new AsyncMemoizer();
/// Creates a new loader that loads tests on platforms defined in [_config].
///
/// [root] is the root directory that will be served for browser tests. It
/// defaults to the working directory.
Loader(this._config, {String root})
: _root = root == null ? p.current : root;
/// Loads all test suites in [dir].
///
/// This will load tests from files that end in "_test.dart". Any tests that
/// fail to load will be emitted as [LoadException]s.
///
/// This emits [LoadSuite]s that must then be run to emit the actual
/// [RunnerSuite]s defined in the file.
Stream<LoadSuite> loadDir(String dir) {
return mergeStreams(new Directory(dir).listSync(recursive: true)
.map((entry) {
if (entry is! File) return new Stream.fromIterable([]);
if (!entry.path.endsWith("_test.dart")) {
return new Stream.fromIterable([]);
}
if (p.split(entry.path).contains('packages')) {
return new Stream.fromIterable([]);
}
return loadFile(entry.path);
}));
}
/// Loads a test suite from the file at [path].
///
/// This emits [LoadSuite]s that must then be run to emit the actual
/// [RunnerSuite]s defined in the file.
///
/// This will emit a [LoadException] if the file fails to load.
Stream<LoadSuite> loadFile(String path) async* {
var suiteMetadata;
try {
suiteMetadata = parseMetadata(path);
} on AnalyzerErrorGroup catch (_) {
// Ignore the analyzer's error, since its formatting is much worse than
// the VM's or dart2js's.
suiteMetadata = new Metadata();
} on FormatException catch (error, stackTrace) {
yield new LoadSuite.forLoadException(
new LoadException(path, error), stackTrace: stackTrace);
return;
}
suiteMetadata = _config.metadata.merge(suiteMetadata);
if (_config.pubServeUrl != null && !p.isWithin('test', path)) {
yield new LoadSuite.forLoadException(new LoadException(
path, 'When using "pub serve", all test files must be in test/.'));
return;
}
for (var platform in _config.platforms) {
if (!suiteMetadata.testOn.evaluate(platform, os: currentOS)) continue;
var metadata = suiteMetadata.forPlatform(platform, os: currentOS);
// Don't load a skipped suite.
if (metadata.skip) {
yield new LoadSuite.forSuite(new RunnerSuite(
const VMEnvironment(),
new Group.root([], metadata: metadata),
path: path, platform: platform));
continue;
}
var name = (platform.isJS ? "compiling " : "loading ") + path;
yield new LoadSuite(name, () {
return platform == TestPlatform.vm
? _loadVmFile(path, metadata)
: _loadBrowserFile(path, platform, metadata);
}, platform: platform);
}
}
/// Load the test suite at [path] in [platform].
///
/// [metadata] is the suite-level metadata for the test.
Future<RunnerSuite> _loadBrowserFile(String path, TestPlatform platform,
Metadata metadata) async =>
(await _browserServer).loadSuite(path, platform, metadata);
/// Load the test suite at [path] in VM isolate.
///
/// [metadata] is the suite-level metadata for the test.
Future<RunnerSuite> _loadVmFile(String path, Metadata metadata) async {
if (loadVMFileHook != null) {
var suite = await loadVMFileHook(path, metadata, _config);
_suites.add(suite);
return suite;
}
var receivePort = new ReceivePort();
var isolate;
try {
if (_config.pubServeUrl != null) {
var url = _config.pubServeUrl.resolveUri(
p.toUri(p.relative(path, from: 'test') + '.vm_test.dart'));
try {
isolate = await Isolate.spawnUri(url, [], {
'reply': receivePort.sendPort,
'metadata': metadata.serialize()
}, checked: true);
} on IsolateSpawnException catch (error) {
if (error.message.contains("OS Error: Connection refused") ||
error.message.contains("The remote computer refused")) {
throw new LoadException(path,
"Error getting $url: Connection refused\n"
'Make sure "pub serve" is running.');
} else if (error.message.contains("404 Not Found")) {
throw new LoadException(path,
"Error getting $url: 404 Not Found\n"
'Make sure "pub serve" is serving the test/ directory.');
}
throw new LoadException(path, error);
}
} else {
isolate = await dart.runInIsolate('''
import "package:test/src/backend/metadata.dart";
import "package:test/src/runner/vm/isolate_listener.dart";
import "${p.toUri(p.absolute(path))}" as test;
void main(_, Map message) {
var sendPort = message['reply'];
var metadata = new Metadata.deserialize(message['metadata']);
IsolateListener.start(sendPort, metadata, () => test.main);
}
''', {
'reply': receivePort.sendPort,
'metadata': metadata.serialize()
}, packageRoot: p.toUri(_config.packageRoot), checked: true);
}
} catch (error, stackTrace) {
receivePort.close();
if (error is LoadException) rethrow;
await new Future.error(new LoadException(path, error), stackTrace);
}
var completer = new Completer();
var subscription = receivePort.listen((response) {
if (response["type"] == "print") {
print(response["line"]);
} else if (response["type"] == "loadException") {
isolate.kill();
completer.completeError(
new LoadException(path, response["message"]),
new Trace.current());
} else if (response["type"] == "error") {
isolate.kill();
var asyncError = RemoteException.deserialize(response["error"]);
completer.completeError(
new LoadException(path, asyncError.error),
asyncError.stackTrace);
} else {
assert(response["type"] == "success");
completer.complete(response["root"]);
}
});
try {
var suite = new RunnerSuite(
const VMEnvironment(),
_deserializeGroup(await completer.future),
path: path,
platform: TestPlatform.vm,
os: currentOS,
onClose: isolate.kill);
_suites.add(suite);
return suite;
} finally {
subscription.cancel();
}
}
/// Deserializes [group] into a concrete [Group] class.
Group _deserializeGroup(Map group) {
var metadata = new Metadata.deserialize(group['metadata']);
return new Group(group['name'], group['entries'].map((entry) {
if (entry['type'] == 'group') return _deserializeGroup(entry);
return _deserializeTest(entry);
}),
metadata: metadata,
setUpAll: _deserializeTest(group['setUpAll']),
tearDownAll: _deserializeTest(group['tearDownAll']));
}
/// Deserializes [test] into a concrete [Test] class.
///
/// Returns `null` if [test] is `null`.
Test _deserializeTest(Map test) {
if (test == null) return null;
var metadata = new Metadata.deserialize(test['metadata']);
return new IsolateTest(test['name'], metadata, test['sendPort']);
}
/// Closes the loader and releases all resources allocated by it.
Future close() {
return _closeMemo.runOnce(() async {
await Future.wait(_suites.map((suite) => suite.close()));
_suites.clear();
if (!_browserServerMemo.hasRun) return;
await (await _browserServer).close();
});
}
}