0b68c62fc5
TEST=Existing, ran locally. Change-Id: Idc1f35eb3d4cf0c7251a64b02b801e8110cc323b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499000 Reviewed-by: Nicholas Shahan <nshahan@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
612 lines
18 KiB
Dart
612 lines
18 KiB
Dart
// Copyright (c) 2024, 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.
|
|
|
|
// NOTE: this file was originally copied from package:vm_service.
|
|
|
|
// ignore_for_file: constant_identifier_names
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io' as io;
|
|
import 'dart:isolate' as isolate;
|
|
|
|
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'};
|
|
|
|
late Uri remoteVmServiceUri;
|
|
|
|
/// Resolves a path as if it was relative to the `test` dir in this package.
|
|
Uri resolveTestRelativePath(String relativePath) =>
|
|
isolate.Isolate.resolvePackageUriSync(Uri.parse('package:dds/'))!
|
|
.resolve('../test/$relativePath');
|
|
|
|
Future<io.Process> spawnDartProcess(
|
|
String script, {
|
|
bool serveObservatory = true,
|
|
bool pauseOnStart = true,
|
|
bool disableServiceAuthCodes = false,
|
|
bool subscribeToStdio = true,
|
|
}) async {
|
|
final executable = io.Platform.executable;
|
|
final tmpDir = await io.Directory.systemTemp.createTemp('dart_service');
|
|
final serviceInfoUri = tmpDir.uri.resolve('service_info.json');
|
|
final serviceInfoFile = await io.File.fromUri(serviceInfoUri).create();
|
|
|
|
final arguments = [
|
|
'--no-dds',
|
|
'--observe=0',
|
|
if (!serveObservatory) '--no-serve-observatory',
|
|
if (pauseOnStart) '--pause-isolates-on-start',
|
|
if (disableServiceAuthCodes) '--disable-service-auth-codes',
|
|
'--write-service-info=$serviceInfoUri',
|
|
...io.Platform.executableArguments,
|
|
resolveTestRelativePath(script).toFilePath(),
|
|
];
|
|
final process = await io.Process.start(executable, arguments);
|
|
if (subscribeToStdio) {
|
|
process.stdout
|
|
.transform(utf8.decoder)
|
|
.listen((line) => print('TESTEE OUT: $line'));
|
|
process.stderr
|
|
.transform(utf8.decoder)
|
|
.listen((line) => print('TESTEE ERR: $line'));
|
|
}
|
|
while ((await serviceInfoFile.length()) <= 5) {
|
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
}
|
|
final content = await serviceInfoFile.readAsString();
|
|
final infoJson = json.decode(content);
|
|
remoteVmServiceUri = Uri.parse(infoJson['uri']);
|
|
return process;
|
|
}
|
|
|
|
Future<void> executeUntilNextPause(VmService service) async {
|
|
final vm = await service.getVM();
|
|
final isolate = await service.getIsolate(vm.isolates!.first.id!);
|
|
|
|
final completer = Completer<void>();
|
|
late StreamSubscription sub;
|
|
sub = service.onDebugEvent.listen((event) async {
|
|
if (event.kind == EventKind.kPauseBreakpoint) {
|
|
completer.complete();
|
|
await sub.cancel();
|
|
}
|
|
});
|
|
await service.streamListen(EventStreams.kDebug);
|
|
await service.resume(isolate.id!);
|
|
await completer.future;
|
|
}
|
|
|
|
/// Returns the resolved URI to the pre-built devtools app.
|
|
Uri devtoolsAppUri() {
|
|
return resolveTestRelativePath('../../../third_party/devtools/web');
|
|
}
|
|
|
|
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('out.aotsnapshot')) {
|
|
// 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 resolveTestRelativePath(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,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
) {
|
|
return _spawnDartProcess(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
experiments,
|
|
extraArgs,
|
|
);
|
|
}
|
|
|
|
Future<io.Process> _spawnDartProcess(
|
|
bool pauseOnStart,
|
|
bool pauseOnExit,
|
|
bool pauseOnUnhandledExceptions,
|
|
bool testeeControlsServer,
|
|
bool useAuthToken,
|
|
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');
|
|
}
|
|
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,
|
|
List<String>? experiments,
|
|
List<String>? extraArgs,
|
|
) {
|
|
return _spawnProcess(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
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 = serverAddress.replace(
|
|
scheme: 'ws',
|
|
pathSegments: [...serverAddress.pathSegments, 'ws'],
|
|
).toString();
|
|
serviceHttpAddress = serverAddress.replace(scheme: 'http').toString();
|
|
}
|
|
|
|
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 allowForNonZeroExitCode = false,
|
|
VmServiceFactory serviceFactory = VmService.defaultFactory,
|
|
}) async {
|
|
final process = _ServiceTesteeLauncher(scriptName);
|
|
late VmService vm;
|
|
late IsolateRef isolate;
|
|
setUp(() async {
|
|
await process
|
|
.launch(
|
|
pauseOnStart,
|
|
pauseOnExit,
|
|
pauseOnUnhandledExceptions,
|
|
testeeControlsServer,
|
|
useAuthToken,
|
|
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(() {
|
|
print('All service tests completed successfully.');
|
|
process.requestExit();
|
|
});
|
|
|
|
final exitCode = await process.exitCode;
|
|
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,
|
|
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,
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Runs the given [testBody] against the [VmService] at [serviceUri].
|
|
///
|
|
/// The test will fail if connection goes away prematurely.
|
|
Future<void> withServiceConnection(
|
|
Uri serviceUri, Future<void> Function(VmService) testBody) async {
|
|
var disposed = false;
|
|
final service = await vmServiceConnectUri(serviceUri.toString());
|
|
await Future.wait([
|
|
() async {
|
|
try {
|
|
await testBody(service);
|
|
} finally {
|
|
disposed = true;
|
|
await service.dispose();
|
|
}
|
|
}(),
|
|
service.onDone.then((_) {
|
|
expect(disposed, isTrue);
|
|
}),
|
|
], eagerError: true);
|
|
}
|