blob: 3143df6d5e9f29115daeaae186cbbda2af3e59d6 [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:dds/dds.dart';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'service_test_common.dart';
export 'service_test_common.dart' show DDSTest, IsolateTest, VMTest;
/// Determines whether DDS is enabled for this test run.
const bool useDds = const bool.fromEnvironment('USE_DDS');
/// The extra arguments to use
const List<String> extraDebuggingArgs = ['--lazy-async-stacks'];
/// Will be set to the http address of the VM's service protocol before
/// any tests are invoked.
late String serviceHttpAddress;
late String serviceWebsocketAddress;
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(
{Function? testeeBefore: null,
Function? testeeConcurrent: null,
bool pause_on_start: false,
bool pause_on_exit: false}) async {
if (!pause_on_start) {
if (testeeBefore != null) {
final result = testeeBefore();
if (result is Future) {
await result;
}
}
print(''); // Print blank line to signal that testeeBefore has run.
}
if (testeeConcurrent != null) {
final 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(
{Function? testeeBeforeSync: null,
Function? 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;
Future<void> get exited => _processCompleter.future;
final _processCompleter = Completer<void>();
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 enable_service_port_fallback,
bool testeeControlsServer,
Uri serviceInfoUri,
int port,
List<String>? extraArgs,
List<String>? executableArgs) {
assert(pause_on_start != null);
assert(pause_on_exit != null);
assert(pause_on_unhandled_exceptions != null);
assert(testeeControlsServer != null);
if (_shouldLaunchSkyShell()) {
return _spawnSkyProcess(
pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
testeeControlsServer,
extraArgs,
executableArgs);
} else {
return _spawnDartProcess(
pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
enable_service_port_fallback,
testeeControlsServer,
serviceInfoUri,
port,
extraArgs,
executableArgs);
}
}
Future<Process> _spawnDartProcess(
bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool enable_service_port_fallback,
bool testeeControlsServer,
Uri serviceInfoUri,
int port,
List<String>? extraArgs,
List<String>? executableArgs) {
assert(!_shouldLaunchSkyShell());
final String dartExecutable = Platform.executable;
final fullArgs = <String>[
'--disable-dart-dev',
];
if (pause_on_start) {
fullArgs.add('--pause-isolates-on-start');
}
if (pause_on_exit) {
fullArgs.add('--pause-isolates-on-exit');
}
if (enable_service_port_fallback) {
fullArgs.add('--enable_service_port_fallback');
}
fullArgs.add('--write-service-info=$serviceInfoUri');
if (pause_on_unhandled_exceptions) {
fullArgs.add('--pause-isolates-on-unhandled-exceptions');
}
fullArgs.add('--profiler');
if (extraArgs != null) {
fullArgs.addAll(extraArgs);
}
if (executableArgs != null) {
fullArgs.addAll(executableArgs);
}
if (!testeeControlsServer) {
fullArgs.add('--enable-vm-service:$port');
}
fullArgs.addAll(args);
return _spawnCommon(dartExecutable, fullArgs, <String, String>{});
}
Future<Process> _spawnSkyProcess(
bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool testeeControlsServer,
List<String>? extraArgs,
List<String>? executableArgs) {
assert(_shouldLaunchSkyShell());
final String dartExecutable = _skyShellPath()!;
final dartFlags = <String>[];
final fullArgs = <String>[];
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');
}
dartFlags.add('--profiler');
// Override mirrors.
dartFlags.add('--enable_mirrors=true');
if (extraArgs != null) {
fullArgs.addAll(extraArgs);
}
if (executableArgs != null) {
fullArgs.addAll(executableArgs);
}
if (!testeeControlsServer) {
fullArgs.add('--observatory-port=0');
}
fullArgs.add('--dart-flags=${dartFlags.join(' ')}');
fullArgs.addAll(args);
return _spawnCommon(dartExecutable, fullArgs, <String, String>{});
}
Future<Process> _spawnCommon(String executable, List<String> arguments,
Map<String, String> dartEnvironment) {
final environment = _TESTEE_SPAWN_ENV;
final bashEnvironment = new StringBuffer();
environment.forEach((k, v) => bashEnvironment.write("$k=$v "));
if (dartEnvironment != null) {
dartEnvironment.forEach((k, v) {
arguments.insert(0, '-D$k=$v');
});
}
print('** Launching $bashEnvironment$executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: environment);
}
Future<Uri> launch(
bool pause_on_start,
bool pause_on_exit,
bool pause_on_unhandled_exceptions,
bool enable_service_port_fallback,
bool testeeControlsServer,
int port,
List<String>? extraArgs,
List<String>? executableArgs) async {
final completer = new Completer<Uri>();
final serviceInfoDir =
await Directory.systemTemp.createTemp('dart_service');
final serviceInfoUri = serviceInfoDir.uri.resolve('service_info.json');
final serviceInfoFile = await File.fromUri(serviceInfoUri).create();
_spawnProcess(
pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
enable_service_port_fallback,
testeeControlsServer,
serviceInfoUri,
port,
extraArgs,
executableArgs)
.then((p) async {
process = p;
Uri uri;
final blankCompleter = Completer();
bool blankLineReceived = false;
p.stdout
.transform(utf8.decoder)
.transform(new LineSplitter())
.listen((line) {
if (!blankLineReceived && (pause_on_start || line == '')) {
// Received blank line.
blankLineReceived = true;
blankCompleter.complete();
}
print('>testee>out> $line');
});
p.stderr
.transform(utf8.decoder)
.transform(new LineSplitter())
.listen((line) {
print('>testee>err> ${line.trim()}');
});
p.exitCode.then((exitCode) async {
await serviceInfoDir.delete(recursive: true);
if ((exitCode != 0) && !killedByTester) {
throw "Testee exited with $exitCode";
}
print("** Process exited");
_processCompleter.complete();
});
// Wait for the blank line which signals that we're ready to run.
await blankCompleter.future;
while ((await serviceInfoFile.length()) <= 5) {
await Future.delayed(Duration(milliseconds: 50));
}
final content = await serviceInfoFile.readAsString();
final infoJson = json.decode(content);
String rawUri = infoJson['uri'];
// If rawUri ends with a /, Uri.parse will include an empty string as the
// last path segment. Make sure it's not there to ensure we have a
// consistent Uri.
if (rawUri.endsWith('/')) {
rawUri = rawUri.substring(0, rawUri.length - 1);
}
uri = Uri.parse(rawUri);
completer.complete(uri);
});
return completer.future;
}
void requestExit() {
if (process != null) {
print('** Killing script');
if (process!.kill()) {
killedByTester = true;
}
}
}
}
void setupAddresses(Uri serverAddress) {
serviceWebsocketAddress =
'ws://${serverAddress.authority}${serverAddress.path}/ws';
serviceHttpAddress = 'http://${serverAddress.authority}${serverAddress.path}';
}
class _ServiceTesterRunner {
void run({
List<String>? mainArgs,
List<String>? extraArgs,
List<String>? executableArgs,
List<DDSTest>? ddsTests,
List<IsolateTest>? isolateTests,
List<VMTest>? vmTests,
bool pause_on_start: false,
bool pause_on_exit: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false,
bool enable_service_port_fallback: false,
bool testeeControlsServer: false,
bool enableDds: true,
bool enableService: true,
int port = 0,
}) {
if (executableArgs == null) {
executableArgs = Platform.executableArguments;
}
late DartDevelopmentService dds;
late WebSocketVM vm;
late _ServiceTesteeLauncher process;
bool testsDone = false;
ignoreLateException(Function f) async {
try {
await f();
} catch (error, stackTrace) {
if (testsDone) {
print('Ignoring late exception during process exit:\n'
'$error\n$stackTrace');
} else {
rethrow;
}
}
}
setUp(
() => ignoreLateException(
() async {
process = _ServiceTesteeLauncher();
await process
.launch(
pause_on_start,
pause_on_exit,
pause_on_unhandled_exceptions,
enable_service_port_fallback,
testeeControlsServer,
port,
extraArgs,
executableArgs)
.then((Uri serverAddress) async {
if (mainArgs != null && mainArgs.contains("--gdb")) {
final pid = process.process!.pid;
final wait = new Duration(seconds: 10);
print("Testee has pid $pid, waiting $wait before continuing");
sleep(wait);
}
if (useDds) {
dds = await DartDevelopmentService.startDartDevelopmentService(
serverAddress);
setupAddresses(dds.uri);
} else {
setupAddresses(serverAddress);
}
print('** Signaled to run test queries on $serviceHttpAddress'
' (${useDds ? "DDS" : "VM Service"})');
vm =
new WebSocketVM(new WebSocketVMTarget(serviceWebsocketAddress));
print('Loading VM...');
await vm.load();
print('Done loading VM');
});
},
),
);
tearDown(
() => ignoreLateException(
() async {
if (useDds) {
await dds.shutdown();
}
process.requestExit();
},
),
);
final name = Platform.script.pathSegments.last;
runTest(String name) {
test(
'$name (${useDds ? 'DDS' : 'VM Service'})',
() => ignoreLateException(
() async {
// Run vm tests.
if (vmTests != null) {
int testIndex = 1;
final totalTests = vmTests.length;
for (var test in vmTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(vm);
}
}
// Run dds tests.
if (ddsTests != null) {
int testIndex = 1;
final totalTests = ddsTests.length;
for (var test in ddsTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(vm, dds);
}
}
// Run isolate tests.
if (isolateTests != null) {
final isolate = await getFirstIsolate(vm);
int testIndex = 1;
final totalTests = isolateTests.length;
for (var test in isolateTests) {
vm.verbose = verbose_vm;
print('Running $name [$testIndex/$totalTests]');
testIndex++;
await test(isolate);
}
}
print('All service tests completed successfully.');
testsDone = true;
},
),
// Some service tests run fairly long (e.g., valid_source_locations_test).
timeout: Timeout.none,
);
}
if ((useDds && !enableDds) || (!useDds && ddsTests != null)) {
print('Skipping DDS run for $name');
return;
}
if (!useDds && !enableService) {
print('Skipping VM Service run for $name');
return;
}
runTest(name);
}
Future<Isolate> getFirstIsolate(WebSocketVM vm) async {
if (vm.isolates.isNotEmpty) {
final isolate = await vm.isolates.first.load();
if (isolate is Isolate) {
return isolate;
}
}
Completer<Isolate>? completer = new Completer<Isolate>();
vm.getEventStream(VM.kIsolateStream).then((stream) {
var subscription;
subscription = stream.listen((ServiceEvent event) async {
if (completer == null) {
subscription.cancel();
return;
}
if (event.kind == ServiceEvent.kIsolateRunnable) {
if (vm.isolates.isNotEmpty) {
vm.isolates.first.load().then((result) {
if (result is Isolate) {
subscription.cancel();
completer!.complete(result);
completer = null;
}
});
}
}
});
});
// The isolate may have started before we subscribed.
if (vm.isolates.isNotEmpty) {
vm.isolates.first.reload().then((result) async {
if (completer != null && result is Isolate) {
completer!.complete(result);
completer = null;
}
});
}
return await completer!.future;
}
}
/// 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 invocation of the script.
Future runIsolateTests(List<String> mainArgs, List<IsolateTest> tests,
{Function? testeeBefore,
Function? testeeConcurrent,
bool pause_on_start: false,
bool pause_on_exit: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false,
bool testeeControlsServer: false,
bool enableDds: true,
bool enableService: true,
List<String>? extraArgs}) 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,
extraArgs: extraArgs,
isolateTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions,
testeeControlsServer: testeeControlsServer,
enableDds: enableDds,
enableService: enableService,
);
}
}
/// 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 invocation 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,
{Function? testeeBefore,
Function? testeeConcurrent,
bool pause_on_start: false,
bool pause_on_exit: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false,
List<String>? extraArgs}) {
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,
extraArgs: extraArgs,
isolateTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions);
}
}
/// Runs [tests] in sequence, each of which should take a [VM] 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 invocation of the script.
Future runVMTests(List<String> mainArgs, List<VMTest> tests,
{Function? testeeBefore,
Function? testeeConcurrent,
bool pause_on_start: false,
bool pause_on_exit: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false,
bool enable_service_port_fallback: false,
bool enableDds: true,
bool enableService: true,
int port = 0,
List<String>? extraArgs,
List<String>? executableArgs}) 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,
extraArgs: extraArgs,
executableArgs: executableArgs,
vmTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions,
enable_service_port_fallback: enable_service_port_fallback,
enableDds: enableDds,
enableService: enableService,
port: port,
);
}
}
/// Runs [tests] in sequence, each of which should take a [VM] and
/// [DartDevelopmentService] 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 invocation of the
/// script.
Future runDDSTests(List<String> mainArgs, List<DDSTest> tests,
{Function? testeeBefore,
Function? testeeConcurrent,
bool pause_on_start: false,
bool pause_on_exit: false,
bool verbose_vm: false,
bool pause_on_unhandled_exceptions: false,
bool enable_service_port_fallback: false,
int port = 0,
List<String>? extraArgs,
List<String>? executableArgs}) 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,
extraArgs: extraArgs,
executableArgs: executableArgs,
ddsTests: tests,
pause_on_start: pause_on_start,
pause_on_exit: pause_on_exit,
verbose_vm: verbose_vm,
pause_on_unhandled_exceptions: pause_on_unhandled_exceptions,
enable_service_port_fallback: enable_service_port_fallback,
enableDds: true,
enableService: false,
port: port,
);
}
}