blob: df140c525009ca782ffa65b57fbef5b261fd61fe [file] [log] [blame]
// 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.
library test_helper;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:observatory/service_io.dart';
import 'service_test_common.dart';
/// Will be set to the http address of the VM's service protocol before
/// any tests are invoked.
String serviceHttpAddress;
String serviceWebsocketAddress;
bool _isWebSocketDisconnect(e) {
return e is NetworkRpcException;
}
const String _TESTEE_ENV_KEY = 'SERVICE_TEST_TESTEE';
const Map<String, String> _TESTEE_SPAWN_ENV = const {
_TESTEE_ENV_KEY: 'true'
};
bool _isTestee() {
return Platform.environment.containsKey(_TESTEE_ENV_KEY);
}
const String _SKY_SHELL_ENV_KEY = 'SERVICE_TEST_SKY_SHELL';
bool _shouldLaunchSkyShell() {
return Platform.environment.containsKey(_SKY_SHELL_ENV_KEY);
}
String _skyShellPath() {
return Platform.environment[_SKY_SHELL_ENV_KEY];
}
class _ServiceTesteeRunner {
Future run({testeeBefore(): null,
testeeConcurrent(): null,
bool pause_on_start: false,
bool pause_on_exit: false}) async {
if (!pause_on_start) {
if (testeeBefore != null) {
var result = testeeBefore();
if (result is Future) {
await result;
}
}
print(''); // Print blank line to signal that testeeBefore has run.
}
if (testeeConcurrent != null) {
var result = testeeConcurrent();
if (result is Future) {
await result;
}
}
if (!pause_on_exit) {
// Wait around for the process to be killed.
stdin.first.then((_) => exit(0));
}
}
void runSync({void testeeBeforeSync(): null,
void testeeConcurrentSync(): null,
bool pause_on_start: false,
bool pause_on_exit: false}) {
if (!pause_on_start) {
if (testeeBeforeSync != null) {
testeeBeforeSync();
}
print(''); // Print blank line to signal that testeeBefore has run.
}
if (testeeConcurrentSync != null) {
testeeConcurrentSync();
}
if (!pause_on_exit) {
// Wait around for the process to be killed.
stdin.first.then((_) => exit(0));
}
}
}
class _ServiceTesteeLauncher {
Process process;
final List<String> args;
bool killedByTester = false;
_ServiceTesteeLauncher() :
args = [Platform.script.toFilePath()] {}
// Spawn the testee process.
Future<Process> _spawnProcess(bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool trace_service,
bool trace_compiler) {
assert(pause_on_start != null);
assert(pause_on_exit != null);
assert(pause_on_unhandled_exceptions != null);
assert(trace_service != null);
assert(trace_compiler != null);
if (_shouldLaunchSkyShell()) {
return _spawnSkyProcess(pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
trace_service,
trace_compiler);
} else {
return _spawnDartProcess(pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
trace_service,
trace_compiler);
}
}
Future<Process> _spawnDartProcess(bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool trace_service,
bool trace_compiler) {
assert(!_shouldLaunchSkyShell());
String dartExecutable = Platform.executable;
var fullArgs = [];
if (trace_service) {
fullArgs.add('--trace-service');
fullArgs.add('--trace-service-verbose');
}
if (trace_compiler) {
fullArgs.add('--trace-compiler');
}
if (pause_on_start) {
fullArgs.add('--pause-isolates-on-start');
}
if (pause_on_exit) {
fullArgs.add('--pause-isolates-on-exit');
}
if (pause_on_unhandled_exceptions) {
fullArgs.add('--pause-isolates-on-unhandled-exceptions');
}
fullArgs.addAll(Platform.executableArguments);
fullArgs.add('--enable-vm-service:0');
fullArgs.addAll(args);
return _spawnCommon(dartExecutable, fullArgs);
}
Future<Process> _spawnSkyProcess(bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool trace_service,
bool trace_compiler) {
assert(_shouldLaunchSkyShell());
String dartExecutable = _skyShellPath();
var dartFlags = [];
var fullArgs = [];
if (trace_service) {
dartFlags.add('--trace_service');
dartFlags.add('--trace_service_verbose');
}
if (trace_compiler) {
dartFlags.add('--trace_compiler');
}
if (pause_on_start) {
dartFlags.add('--pause_isolates_on_start');
fullArgs.add('--start-paused');
}
if (pause_on_exit) {
dartFlags.add('--pause_isolates_on_exit');
}
if (pause_on_unhandled_exceptions) {
dartFlags.add('--pause_isolates_on_unhandled_exceptions');
}
// Override mirrors.
dartFlags.add('--enable_mirrors=true');
fullArgs.addAll(Platform.executableArguments);
fullArgs.add('--observatory-port=0');
fullArgs.add('--dart-flags=${dartFlags.join(' ')}');
fullArgs.addAll(args);
return _spawnCommon(dartExecutable, fullArgs);
}
Future<Process> _spawnCommon(String executable, List<String> arguments) {
print('** Launching $executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: _TESTEE_SPAWN_ENV);
}
Future<int> launch(bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool trace_service,
bool trace_compiler) {
return _spawnProcess(pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
trace_service,
trace_compiler).then((p) {
Completer completer = new Completer();
process = p;
var portNumber;
var blank;
var first = true;
process.stdout.transform(UTF8.decoder)
.transform(new LineSplitter()).listen((line) {
if (line.startsWith('Observatory listening on http://')) {
RegExp portExp = new RegExp(r"\d+.\d+.\d+.\d+:(\d+)");
var port = portExp.firstMatch(line).group(1);
portNumber = int.parse(port);
}
if (pause_on_start || line == '') {
// Received blank line.
blank = true;
}
if (portNumber != null && blank == true && first == true) {
completer.complete(portNumber);
// Stop repeat completions.
first = false;
print('** Signaled to run test queries on $portNumber');
}
print(line);
});
process.stderr.transform(UTF8.decoder)
.transform(new LineSplitter()).listen((line) {
print(line);
});
process.exitCode.then((exitCode) {
if ((exitCode != 0) && !killedByTester) {
throw "Testee exited with $exitCode";
}
print("** Process exited");
});
return completer.future;
});
}
void requestExit() {
print('** Killing script');
if (process.kill()) {
killedByTester = true;
}
}
}
// A tester runner that doesn't spawn a process but instead connects to
// an already running flutter application running on a device. Assumes
// port 8100. This is only useful for debugging.
class _FlutterDeviceServiceTesterRunner {
void run({List<String> mainArgs,
List<VMTest> vmTests,
List<IsolateTest> isolateTests,
bool pause_on_start: false,
bool pause_on_exit: false,
bool trace_service: false,
bool trace_compiler: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false}) {
var port = 8100;
serviceWebsocketAddress = 'ws://localhost:$port/ws';
serviceHttpAddress = 'http://localhost:$port';
var name = Platform.script.pathSegments.last;
runZoned(() async {
var vm =
new WebSocketVM(new WebSocketVMTarget(serviceWebsocketAddress));
print('Loading VM...');
await vm.load();
print('Done loading VM');
// Run vm tests.
if (vmTests != null) {
var testIndex = 1;
var totalTests = vmTests.length;
for (var test in vmTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(vm);
}
}
// Run isolate tests.
if (isolateTests != null) {
var isolate = await vm.isolates.first.load();
var testIndex = 1;
var totalTests = isolateTests.length;
for (var test in isolateTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(isolate);
}
}
}, onError: (e, st) {
if (!_isWebSocketDisconnect(e)) {
print('Unexpected exception in service tests: $e $st');
throw e;
}
});
}
}
void suppressWarning() {
new _FlutterDeviceServiceTesterRunner();
}
class _ServiceTesterRunner {
void run({List<String> mainArgs,
List<VMTest> vmTests,
List<IsolateTest> isolateTests,
bool pause_on_start: false,
bool pause_on_exit: false,
bool trace_service: false,
bool trace_compiler: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false}) {
var process = new _ServiceTesteeLauncher();
process.launch(pause_on_start, pause_on_exit,
pause_on_unhandled_exceptions,
trace_service, trace_compiler).then((port) async {
if (mainArgs.contains("--gdb")) {
port = 8181;
}
serviceWebsocketAddress = 'ws://localhost:$port/ws';
serviceHttpAddress = 'http://localhost:$port';
var name = Platform.script.pathSegments.last;
runZoned(() async {
var vm =
new WebSocketVM(new WebSocketVMTarget(serviceWebsocketAddress));
print('Loading VM...');
await vm.load();
print('Done loading VM');
// Run vm tests.
if (vmTests != null) {
var testIndex = 1;
var totalTests = vmTests.length;
for (var test in vmTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(vm);
}
}
// Run isolate tests.
if (isolateTests != null) {
var isolate = await vm.isolates.first.load();
var testIndex = 1;
var totalTests = isolateTests.length;
for (var test in isolateTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(isolate);
}
}
await process.requestExit();
}, onError: (e, st) {
process.requestExit();
// TODO: remove this workaround.
// This is necessary due to non awaited operations.
// E.G. object.dart (398~402)
// When an exception is thrown inside a test (directly or via await) the
// stacktrace is non-null and shows where the exception has been thrown.
// If vice versa the exception is due to an error in a non-awaited
// Future the stacktrace is null.
if (st != null || !_isWebSocketDisconnect(e)) {
print('Unexpected exception in service tests: $e $st');
throw e;
}
});
});
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invokation of the script.
Future runIsolateTests(List<String> mainArgs,
List<IsolateTest> tests,
{testeeBefore(),
testeeConcurrent(),
bool pause_on_start: false,
bool pause_on_exit: false,
bool trace_service: false,
bool trace_compiler: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false}) async {
assert(!pause_on_start || testeeBefore == null);
if (_isTestee()) {
new _ServiceTesteeRunner().run(testeeBefore: testeeBefore,
testeeConcurrent: testeeConcurrent,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit);
} else {
new _ServiceTesterRunner().run(
mainArgs: mainArgs,
isolateTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
trace_service: trace_service,
trace_compiler: trace_compiler,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions);
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invokation of the script.
///
/// This is a special version of this test harness specifically for the
/// pause_on_unhandled_exceptions_test, which cannot properly function
/// in an async context (because exceptions are *always* handled in async
/// functions).
void runIsolateTestsSynchronous(List<String> mainArgs,
List<IsolateTest> tests,
{void testeeBefore(),
void testeeConcurrent(),
bool pause_on_start: false,
bool pause_on_exit: false,
bool trace_service: false,
bool trace_compiler: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false}) {
assert(!pause_on_start || testeeBefore == null);
if (_isTestee()) {
new _ServiceTesteeRunner().runSync(testeeBeforeSync: testeeBefore,
testeeConcurrentSync: testeeConcurrent,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit);
} else {
new _ServiceTesterRunner().run(
mainArgs: mainArgs,
isolateTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
trace_service: trace_service,
trace_compiler: trace_compiler,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions);
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invokation of the script.
Future runVMTests(List<String> mainArgs,
List<VMTest> tests,
{testeeBefore(),
testeeConcurrent(),
bool pause_on_start: false,
bool pause_on_exit: false,
bool trace_service: false,
bool trace_compiler: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false}) async {
if (_isTestee()) {
new _ServiceTesteeRunner().run(testeeBefore: testeeBefore,
testeeConcurrent: testeeConcurrent,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit);
} else {
new _ServiceTesterRunner().run(
mainArgs: mainArgs,
vmTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
trace_service: trace_service,
trace_compiler: trace_compiler,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions);
}
}