blob: 325af2209df66efe29b565bc05cce64a253a0047 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:vm_service/utils.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'test_utils.dart';
class AppFixture {
AppFixture._(
this.process,
this.lines,
this.serviceUri,
this.serviceConnection,
this.isolates,
this.onTeardown,
) {
// "starting app"
_onAppStarted = lines.first;
unawaited(serviceConnection.streamListen(EventStreams.kIsolate));
_isolateEventStreamSubscription =
serviceConnection.onIsolateEvent.listen((Event event) {
if (event.kind == EventKind.kIsolateExit) {
isolates.remove(event.isolate);
} else {
if (!isolates.contains(event.isolate)) {
isolates.add(event.isolate);
}
}
});
}
final Process process;
final Stream<String> lines;
final Uri serviceUri;
final VmService serviceConnection;
final List<IsolateRef?> isolates;
late final StreamSubscription<Event> _isolateEventStreamSubscription;
final Future<void> Function()? onTeardown;
late Future<void> _onAppStarted;
Future<void> get onAppStarted => _onAppStarted;
IsolateRef? get mainIsolate => isolates.isEmpty ? null : isolates.first;
Future<dynamic> invoke(String expression) async {
final IsolateRef isolateRef = mainIsolate!;
final String isolateId = isolateRef.id!;
final Isolate isolate = await serviceConnection.getIsolate(isolateId);
return await serviceConnection.evaluate(
isolateId,
isolate.rootLib!.id!,
expression,
);
}
Future<void> teardown() async {
if (onTeardown != null) {
await onTeardown!();
}
await _isolateEventStreamSubscription.cancel();
await serviceConnection.dispose();
process.kill();
}
}
// This is the fixture for Dart CLI applications.
class CliAppFixture extends AppFixture {
CliAppFixture._(
this.appScriptPath,
Process process,
Stream<String> lines,
Uri serviceUri,
VmService serviceConnection,
List<IsolateRef> isolates,
Future<void> Function()? onTeardown,
) : super._(
process,
lines,
serviceUri,
serviceConnection,
isolates,
onTeardown,
);
final String appScriptPath;
static Future<CliAppFixture> create(String appScriptPath) async {
final dartVmServicePrefix =
RegExp('(Observatory|The Dart VM service is) listening on ');
final Process process = await Process.start(
Platform.resolvedExecutable,
<String>['--observe=0', '--pause-isolates-on-start', appScriptPath],
);
final Stream<String> lines =
process.stdout.transform(utf8.decoder).transform(const LineSplitter());
final StreamController<String> lineController =
StreamController<String>.broadcast();
final Completer<String> completer = Completer<String>();
final linesSubscription = lines.listen((String line) {
if (completer.isCompleted) {
lineController.add(line);
} else if (line.contains(dartVmServicePrefix)) {
completer.complete(line);
} else {
// Often something like:
// "Waiting for another flutter command to release the startup lock...".
print(line);
}
});
// Observatory listening on http://127.0.0.1:9595/(token)
final String observatoryText = await completer.future;
final String observatoryUri =
observatoryText.replaceAll(dartVmServicePrefix, '');
var uri = Uri.parse(observatoryUri);
if (!uri.isAbsolute) {
throw 'Could not parse VM Service URI: "$observatoryText"';
}
// Map to WS URI.
uri = convertToWebSocketUrl(serviceProtocolUrl: uri);
final VmService serviceConnection =
await vmServiceConnectUri(uri.toString());
final VM vm = await serviceConnection.getVM();
final Isolate isolate =
await _waitForIsolate(serviceConnection, 'PauseStart');
await serviceConnection.resume(isolate.id!);
Future<void> _onTeardown() async {
await linesSubscription.cancel();
await lineController.close();
}
return CliAppFixture._(
appScriptPath,
process,
lineController.stream,
uri,
serviceConnection,
vm.isolates!,
_onTeardown,
);
}
static Future<Isolate> _waitForIsolate(
VmService serviceConnection,
String pauseEventKind,
) async {
Isolate? foundIsolate;
await waitFor(() async {
const skipId = 'skip';
final vm = await serviceConnection.getVM();
final List<Isolate?> isolates = await Future.wait(
vm.isolates!.map(
(ref) => serviceConnection
.getIsolate(ref.id!)
// Calling getIsolate() can sometimes return a collected sentinel
// for an isolate that hasn't started yet. We can just ignore these
// as on the next trip around the Isolate will be returned.
// https://github.com/dart-lang/sdk/issues/33747
.catchError((error) {
print('getIsolate(${ref.id}) failed, skipping\n$error');
return Future<Isolate>.value(Isolate(id: skipId));
}),
),
);
foundIsolate = isolates.firstWhere(
(isolate) =>
isolate!.id != skipId && isolate.pauseEvent?.kind == pauseEventKind,
orElse: () => null,
);
return foundIsolate != null;
});
return foundIsolate!;
}
String get scriptSource {
return File(appScriptPath).readAsStringSync();
}
static List<int> parseBreakpointLines(String source) {
return _parseLines(source, 'breakpoint');
}
static List<int> parseSteppingLines(String source) {
return _parseLines(source, 'step');
}
static List<int> parseExceptionLines(String source) {
return _parseLines(source, 'exception');
}
static List<int> _parseLines(String source, String keyword) {
final List<String> lines = source.replaceAll('\r', '').split('\n');
final List<int> matches = [];
for (int i = 0; i < lines.length; i++) {
if (lines[i].endsWith('// $keyword')) {
matches.add(i);
}
}
return matches;
}
}