03301c9bfb
The resulting DLL lacks debugging information / PDB. TEST=ci Bug: https://github.com/dart-lang/sdk/issues/60812 Bug: https://github.com/dart-lang/sdk/issues/60813 Cq-Include-Trybots: luci.dart.try:vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64-try,vm-aot-win-release-arm64-try,vm-aot-win-release-x64-try Change-Id: I305bad0081ec24f27249ad9b75ff8d32fa9c4893 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428200 Reviewed-by: Tess Strickland <sstrickl@google.com> Commit-Queue: Ryan Macnak <rmacnak@google.com>
589 lines
18 KiB
Dart
589 lines
18 KiB
Dart
// Copyright (c) 2019, 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' as io;
|
|
|
|
import 'package:test/test.dart';
|
|
import 'package:vm_service/vm_service.dart';
|
|
import 'package:vm_service/vm_service_io.dart';
|
|
|
|
import 'service_test_common.dart';
|
|
|
|
export 'service_test_common.dart' show IsolateTest, VMTest;
|
|
|
|
/// The extra arguments to use
|
|
const List<String> extraDebuggingArgs = [];
|
|
|
|
/// 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 = {_TESTEE_ENV_KEY: 'true'};
|
|
bool _isTestee() {
|
|
return io.Platform.environment.containsKey(_TESTEE_ENV_KEY);
|
|
}
|
|
|
|
Uri _getTestUri(String script) {
|
|
if (io.Platform.script.isScheme('data')) {
|
|
// If running from pub we can assume that we're in the root of the package
|
|
// directory.
|
|
return Uri.parse('test/$script');
|
|
} else if (!io.Platform.script.toFilePath().endsWith('.dart')) {
|
|
// We're running an AOT test. In this case, we need to use the exact URI we
|
|
// launched with.
|
|
return io.Platform.script;
|
|
} else {
|
|
// Resolve the script to ensure that test will fail if the provided script
|
|
// name doesn't match the actual script.
|
|
return io.Platform.script.resolve(script);
|
|
}
|
|
}
|
|
|
|
class _ServiceTesteeRunner {
|
|
Future<void> run({
|
|
Function()? testeeBefore,
|
|
Function()? testeeConcurrent,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
}) async {
|
|
if (!pauseOnStart) {
|
|
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 (!pauseOnExit) {
|
|
// Wait around for the process to be killed.
|
|
await io.stdin.first.then((_) => io.exit(0));
|
|
}
|
|
}
|
|
|
|
void runSync({
|
|
void Function()? testeeBeforeSync,
|
|
void Function()? testeeConcurrentSync,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
}) {
|
|
if (!pauseOnStart) {
|
|
if (testeeBeforeSync != null) {
|
|
testeeBeforeSync();
|
|
}
|
|
print(''); // Print blank line to signal that testeeBefore has run.
|
|
}
|
|
if (testeeConcurrentSync != null) {
|
|
testeeConcurrentSync();
|
|
}
|
|
if (!pauseOnExit) {
|
|
// Wait around for the process to be killed.
|
|
io.stdin.first.then((_) => io.exit(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
class _ServiceTesteeLauncher {
|
|
io.Process? process;
|
|
List<String> args;
|
|
|
|
bool killedByTester = false;
|
|
final _exitCodeCompleter = Completer<int>();
|
|
|
|
_ServiceTesteeLauncher(String script)
|
|
: args = [_getTestUri(script).toFilePath()];
|
|
|
|
Future<int> get exitCode => _exitCodeCompleter.future;
|
|
|
|
// Spawn the testee process.
|
|
Future<io.Process> _spawnProcess(
|
|
bool pauseOnStart,
|
|
bool pauseOnExit,
|
|
bool pauseOnUnhandledExceptions,
|
|
bool testeeControlsServer,
|
|
bool useAuthToken,
|
|
|
|
/// If non-null, any compilation requests sent from the testee VM will be
|
|
/// sent to the resident frontend compiler associated with
|
|
/// [residentCompilerInfoFilePath] rather than the testee VM's kernel
|
|
/// isolate. If null, then those requests will be sent to the testee VM's
|
|
/// kernel isolate.
|
|
String? residentCompilerInfoFilePath,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
) {
|
|
return _spawnDartProcess(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
residentCompilerInfoFilePath,
|
|
experiments,
|
|
extraArgs,
|
|
);
|
|
}
|
|
|
|
Future<io.Process> _spawnDartProcess(
|
|
bool pauseOnStart,
|
|
bool pauseOnExit,
|
|
bool pauseOnUnhandledExceptions,
|
|
bool testeeControlsServer,
|
|
bool useAuthToken,
|
|
|
|
/// If non-null, any compilation requests sent from the testee VM will be
|
|
/// sent to the resident frontend compiler associated with
|
|
/// [residentCompilerInfoFilePath] rather than the testee VM's kernel
|
|
/// isolate. If null, then those requests will be sent to the testee VM's
|
|
/// kernel isolate.
|
|
String? residentCompilerInfoFilePath,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
) {
|
|
final String dartExecutable = io.Platform.executable;
|
|
|
|
final fullArgs = <String>[];
|
|
if (pauseOnStart) {
|
|
fullArgs.add('--pause-isolates-on-start');
|
|
}
|
|
if (pauseOnExit) {
|
|
fullArgs.add('--pause-isolates-on-exit');
|
|
}
|
|
if (!useAuthToken) {
|
|
fullArgs.add('--disable-service-auth-codes');
|
|
}
|
|
if (pauseOnUnhandledExceptions) {
|
|
fullArgs.add('--pause-isolates-on-unhandled-exceptions');
|
|
}
|
|
fullArgs.add('--profiler');
|
|
if (experiments != null) {
|
|
fullArgs.addAll(experiments.map((e) => '--enable-experiment=$e'));
|
|
}
|
|
if (extraArgs != null) {
|
|
fullArgs.addAll(extraArgs);
|
|
}
|
|
|
|
fullArgs.addAll(io.Platform.executableArguments);
|
|
if (!testeeControlsServer) {
|
|
fullArgs.add('--enable-vm-service:0');
|
|
}
|
|
|
|
if (residentCompilerInfoFilePath != null) {
|
|
fullArgs.addAll([
|
|
'run',
|
|
'--resident',
|
|
'--resident-compiler-info-file=$residentCompilerInfoFilePath',
|
|
]);
|
|
}
|
|
fullArgs.addAll(args);
|
|
|
|
return _spawnCommon(dartExecutable, fullArgs, <String, String>{});
|
|
}
|
|
|
|
Future<io.Process> _spawnCommon(
|
|
String executable,
|
|
List<String> arguments,
|
|
Map<String, String> dartEnvironment,
|
|
) {
|
|
final environment = _TESTEE_SPAWN_ENV;
|
|
final bashEnvironment = StringBuffer();
|
|
environment.forEach((k, v) => bashEnvironment.write('$k=$v '));
|
|
dartEnvironment.forEach((k, v) {
|
|
arguments.insert(0, '-D$k=$v');
|
|
});
|
|
print('** Launching $bashEnvironment$executable ${arguments.join(' ')}');
|
|
return io.Process.start(
|
|
executable,
|
|
arguments,
|
|
environment: environment,
|
|
);
|
|
}
|
|
|
|
Future<Uri> launch(
|
|
bool pauseOnStart,
|
|
bool pauseOnExit,
|
|
bool pauseOnUnhandledExceptions,
|
|
bool testeeControlsServer,
|
|
bool useAuthToken,
|
|
|
|
/// If non-null, any compilation requests sent from the testee VM will be
|
|
/// sent to the resident frontend compiler associated with
|
|
/// [residentCompilerInfoFilePath] rather than the testee VM's kernel
|
|
/// isolate. If null, then those requests will be sent to the testee VM's
|
|
/// kernel isolate.
|
|
String? residentCompilerInfoFilePath,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
) {
|
|
return _spawnProcess(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
residentCompilerInfoFilePath,
|
|
experiments,
|
|
extraArgs,
|
|
).then((p) {
|
|
final Completer<Uri> completer = Completer<Uri>();
|
|
process = p;
|
|
Uri? uri;
|
|
bool blank = false;
|
|
var first = true;
|
|
process!.stdout
|
|
.transform(utf8.decoder)
|
|
.transform(LineSplitter())
|
|
.listen((line) {
|
|
const kDartVMServiceListening = 'The Dart VM service is listening on ';
|
|
if (line.startsWith(kDartVMServiceListening)) {
|
|
uri = Uri.parse(line.substring(kDartVMServiceListening.length));
|
|
}
|
|
if (pauseOnStart || line == '') {
|
|
// Received blank line.
|
|
blank = true;
|
|
}
|
|
if ((uri != null) && (blank == true) && (first == true)) {
|
|
completer.complete(uri!);
|
|
// Stop repeat completions.
|
|
first = false;
|
|
print('** Signaled to run test queries on $uri');
|
|
}
|
|
io.stdout.write('>testee>out> $line\n');
|
|
});
|
|
process!.stderr
|
|
.transform(utf8.decoder)
|
|
.transform(LineSplitter())
|
|
.listen((line) {
|
|
io.stdout.write('>testee>err> $line\n');
|
|
});
|
|
process!.exitCode.then(_exitCodeCompleter.complete);
|
|
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 {
|
|
Future<void> run({
|
|
List<String>? mainArgs,
|
|
List<String>? extraArgs,
|
|
List<String>? experiments,
|
|
List<VMTest>? vmTests,
|
|
List<IsolateTest>? isolateTests,
|
|
required String scriptName,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
bool verboseVm = false,
|
|
bool pauseOnUnhandledExceptions = false,
|
|
bool testeeControlsServer = false,
|
|
bool useAuthToken = false,
|
|
bool shouldTesteeBeLaunchedWithDartRunResident = false,
|
|
bool allowForNonZeroExitCode = false,
|
|
VmServiceFactory serviceFactory = VmService.defaultFactory,
|
|
}) async {
|
|
final tempDir = io.Directory.systemTemp.createTempSync();
|
|
final String? residentCompilerInfoFilePath;
|
|
if (shouldTesteeBeLaunchedWithDartRunResident) {
|
|
residentCompilerInfoFilePath =
|
|
'${tempDir.path}${io.Platform.pathSeparator}'
|
|
'resident_compiler_info.txt';
|
|
} else {
|
|
residentCompilerInfoFilePath = null;
|
|
}
|
|
|
|
final process = _ServiceTesteeLauncher(scriptName);
|
|
late VmService vm;
|
|
late IsolateRef isolate;
|
|
setUp(() async {
|
|
await process
|
|
.launch(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
residentCompilerInfoFilePath,
|
|
experiments,
|
|
extraArgs,
|
|
)
|
|
.then((Uri serverAddress) async {
|
|
if (mainArgs!.contains('--gdb')) {
|
|
final pid = process.process!.pid;
|
|
final wait = Duration(seconds: 10);
|
|
print('Testee has pid $pid, waiting $wait before continuing');
|
|
io.sleep(wait);
|
|
}
|
|
setupAddresses(serverAddress);
|
|
vm = await vmServiceConnectUriWithFactory(
|
|
serviceWebsocketAddress,
|
|
vmServiceFactory: serviceFactory,
|
|
);
|
|
print('Done loading VM');
|
|
isolate = await getFirstIsolate(vm);
|
|
});
|
|
});
|
|
|
|
final name = _getTestUri(scriptName).pathSegments.last;
|
|
|
|
test(
|
|
name,
|
|
() async {
|
|
// Run vm tests.
|
|
if (vmTests != null) {
|
|
var testIndex = 1;
|
|
final totalTests = vmTests.length;
|
|
for (var t in vmTests) {
|
|
print('$name [$testIndex/$totalTests]');
|
|
await t(vm);
|
|
testIndex++;
|
|
}
|
|
}
|
|
|
|
// Run isolate tests.
|
|
if (isolateTests != null) {
|
|
var testIndex = 1;
|
|
final totalTests = isolateTests.length;
|
|
for (var t in isolateTests) {
|
|
print('$name [$testIndex/$totalTests]');
|
|
await t(vm, isolate);
|
|
testIndex++;
|
|
}
|
|
}
|
|
},
|
|
retry: 0,
|
|
timeout: Timeout.none,
|
|
);
|
|
|
|
tearDown(() async {
|
|
print('All service tests completed successfully.');
|
|
try {
|
|
await vm.dispose();
|
|
} catch (e, st) {
|
|
print('''
|
|
Ignoring exception during vm-service connection shutdown:
|
|
$e
|
|
$st
|
|
''');
|
|
}
|
|
process.requestExit();
|
|
});
|
|
|
|
final exitCode = await process.exitCode;
|
|
if (shouldTesteeBeLaunchedWithDartRunResident) {
|
|
print(
|
|
'** Shutting down resident frontend compiler associated with '
|
|
'$residentCompilerInfoFilePath that was used by VM '
|
|
'Service tests',
|
|
);
|
|
await io.Process.run(
|
|
io.Platform.executable,
|
|
[
|
|
'compilation-server',
|
|
'shutdown',
|
|
'--resident-compiler-info-file=$residentCompilerInfoFilePath',
|
|
],
|
|
);
|
|
tempDir.deleteSync(recursive: true);
|
|
}
|
|
if (exitCode != 0) {
|
|
if (!(process.killedByTester || allowForNonZeroExitCode)) {
|
|
throw 'Testee exited with unexpected exitCode: $exitCode';
|
|
}
|
|
}
|
|
print('** Process exited: $exitCode');
|
|
}
|
|
|
|
Future<IsolateRef> getFirstIsolate(VmService service) async {
|
|
var vm = await service.getVM();
|
|
final vmIsolates = vm.isolates!;
|
|
if (vmIsolates.isNotEmpty) {
|
|
return vmIsolates.first;
|
|
}
|
|
Completer<dynamic>? completer = Completer();
|
|
late StreamSubscription subscription;
|
|
subscription = service.onIsolateEvent.listen((Event event) async {
|
|
if (completer == null) {
|
|
await subscription.cancel();
|
|
return;
|
|
}
|
|
if (event.kind == EventKind.kIsolateRunnable) {
|
|
vm = await service.getVM();
|
|
await subscription.cancel();
|
|
await service.streamCancel(EventStreams.kIsolate);
|
|
completer!.complete(event.isolate!);
|
|
completer = null;
|
|
}
|
|
});
|
|
await service.streamListen(EventStreams.kIsolate);
|
|
|
|
// The isolate may have started before we subscribed.
|
|
vm = await service.getVM();
|
|
if (vmIsolates.isNotEmpty) {
|
|
await subscription.cancel();
|
|
completer!.complete(vmIsolates.first);
|
|
completer = null;
|
|
}
|
|
return (await completer!.future) as IsolateRef;
|
|
}
|
|
}
|
|
|
|
/// 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<void> runIsolateTests(
|
|
List<String> mainArgs,
|
|
List<IsolateTest> tests,
|
|
String scriptName, {
|
|
Function()? testeeBefore,
|
|
Function()? testeeConcurrent,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
bool verboseVm = false,
|
|
bool pauseOnUnhandledExceptions = false,
|
|
bool testeeControlsServer = false,
|
|
bool useAuthToken = false,
|
|
|
|
/// If [true], `dart run --resident` will be used to launch the testee.
|
|
bool shouldTesteeBeLaunchedWithDartRunResident = false,
|
|
bool allowForNonZeroExitCode = false,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
}) async {
|
|
assert(!pauseOnStart || testeeBefore == null);
|
|
if (_isTestee()) {
|
|
await _ServiceTesteeRunner().run(
|
|
testeeBefore: testeeBefore,
|
|
testeeConcurrent: testeeConcurrent,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
);
|
|
} else {
|
|
await _ServiceTesterRunner().run(
|
|
mainArgs: mainArgs,
|
|
scriptName: scriptName,
|
|
extraArgs: extraArgs,
|
|
isolateTests: tests,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
verboseVm: verboseVm,
|
|
experiments: experiments,
|
|
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
|
|
testeeControlsServer: testeeControlsServer,
|
|
useAuthToken: useAuthToken,
|
|
shouldTesteeBeLaunchedWithDartRunResident:
|
|
shouldTesteeBeLaunchedWithDartRunResident,
|
|
allowForNonZeroExitCode: allowForNonZeroExitCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
String scriptName, {
|
|
void Function()? testeeBefore,
|
|
void Function()? testeeConcurrent,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
bool verboseVm = false,
|
|
bool pauseOnUnhandledExceptions = false,
|
|
List<String>? extraArgs,
|
|
}) {
|
|
assert(!pauseOnStart || testeeBefore == null);
|
|
if (_isTestee()) {
|
|
_ServiceTesteeRunner().runSync(
|
|
testeeBeforeSync: testeeBefore,
|
|
testeeConcurrentSync: testeeConcurrent,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
);
|
|
} else {
|
|
_ServiceTesterRunner().run(
|
|
mainArgs: mainArgs,
|
|
scriptName: scriptName,
|
|
extraArgs: extraArgs,
|
|
isolateTests: tests,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
verboseVm: verboseVm,
|
|
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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<void> runVMTests(
|
|
List<String> mainArgs,
|
|
List<VMTest> tests,
|
|
String scriptName, {
|
|
Function()? testeeBefore,
|
|
Function()? testeeConcurrent,
|
|
bool pauseOnStart = false,
|
|
bool pauseOnExit = false,
|
|
bool verboseVm = false,
|
|
bool pauseOnUnhandledExceptions = false,
|
|
List<String>? extraArgs,
|
|
VmServiceFactory serviceFactory = VmService.defaultFactory,
|
|
}) async {
|
|
if (_isTestee()) {
|
|
await _ServiceTesteeRunner().run(
|
|
testeeBefore: testeeBefore,
|
|
testeeConcurrent: testeeConcurrent,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
);
|
|
} else {
|
|
await _ServiceTesterRunner().run(
|
|
mainArgs: mainArgs,
|
|
scriptName: scriptName,
|
|
extraArgs: extraArgs,
|
|
vmTests: tests,
|
|
pauseOnStart: pauseOnStart,
|
|
pauseOnExit: pauseOnExit,
|
|
verboseVm: verboseVm,
|
|
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
|
|
serviceFactory: serviceFactory,
|
|
);
|
|
}
|
|
}
|