|
|
|
|
@@ -9,15 +9,13 @@ import 'package:file/file.dart';
|
|
|
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
|
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
|
|
|
import 'package:process/process.dart';
|
|
|
|
|
import 'package:source_span/source_span.dart';
|
|
|
|
|
import 'package:stream_channel/stream_channel.dart';
|
|
|
|
|
import 'package:vm_service_client/vm_service_client.dart';
|
|
|
|
|
import 'package:web_socket_channel/io.dart';
|
|
|
|
|
import 'package:vm_service_lib/vm_service_lib.dart';
|
|
|
|
|
import 'package:vm_service_lib/vm_service_lib_io.dart';
|
|
|
|
|
|
|
|
|
|
import '../src/common.dart';
|
|
|
|
|
|
|
|
|
|
// Set this to true for debugging to get JSON written to stdout.
|
|
|
|
|
const bool _printJsonAndStderr = false;
|
|
|
|
|
const bool _printDebugOutputToStdOut = false;
|
|
|
|
|
const Duration defaultTimeout = Duration(seconds: 40);
|
|
|
|
|
const Duration appStartTimeout = Duration(seconds: 120);
|
|
|
|
|
const Duration quitTimeout = Duration(seconds: 10);
|
|
|
|
|
@@ -37,13 +35,12 @@ class FlutterTestDriver {
|
|
|
|
|
String _lastResponse;
|
|
|
|
|
String _currentRunningAppId;
|
|
|
|
|
Uri _vmServiceWsUri;
|
|
|
|
|
int _vmServicePort;
|
|
|
|
|
bool _hasExited = false;
|
|
|
|
|
|
|
|
|
|
VMServiceClient vmService;
|
|
|
|
|
VmService _vmService;
|
|
|
|
|
String get lastErrorInfo => _errorBuffer.toString();
|
|
|
|
|
Stream<String> get stdout => _stdout.stream;
|
|
|
|
|
int get vmServicePort => _vmServicePort;
|
|
|
|
|
int get vmServicePort => _vmServiceWsUri.port;
|
|
|
|
|
bool get hasExited => _hasExited;
|
|
|
|
|
|
|
|
|
|
String _debugPrint(String msg) {
|
|
|
|
|
@@ -51,7 +48,7 @@ class FlutterTestDriver {
|
|
|
|
|
final String truncatedMsg =
|
|
|
|
|
msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg;
|
|
|
|
|
_allMessages.add(truncatedMsg);
|
|
|
|
|
if (_printJsonAndStderr) {
|
|
|
|
|
if (_printDebugOutputToStdOut) {
|
|
|
|
|
print('$_logPrefix$truncatedMsg');
|
|
|
|
|
}
|
|
|
|
|
return msg;
|
|
|
|
|
@@ -141,22 +138,21 @@ class FlutterTestDriver {
|
|
|
|
|
timeout: appStartTimeout);
|
|
|
|
|
final String wsUriString = debugPort['params']['wsUri'];
|
|
|
|
|
_vmServiceWsUri = Uri.parse(wsUriString);
|
|
|
|
|
_vmServicePort = debugPort['params']['port'];
|
|
|
|
|
// Proxy the stream/sink for the VM Client so we can debugPrint it.
|
|
|
|
|
final StreamChannel<String> channel = IOWebSocketChannel.connect(_vmServiceWsUri)
|
|
|
|
|
.cast<String>()
|
|
|
|
|
.changeStream((Stream<String> stream) => stream.map<String>(_debugPrint))
|
|
|
|
|
.changeSink((StreamSink<String> sink) =>
|
|
|
|
|
StreamController<String>()
|
|
|
|
|
..stream.listen((String s) => sink.add(_debugPrint(s))));
|
|
|
|
|
vmService = VMServiceClient(channel);
|
|
|
|
|
_vmService =
|
|
|
|
|
await vmServiceConnectUri(_vmServiceWsUri.toString());
|
|
|
|
|
_vmService.onSend.listen((String s) => _debugPrint('==> $s'));
|
|
|
|
|
_vmService.onReceive.listen((String s) => _debugPrint('<== $s'));
|
|
|
|
|
await Future.wait(<Future<Success>>[
|
|
|
|
|
_vmService.streamListen('Isolate'),
|
|
|
|
|
_vmService.streamListen('Debug'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Because we start paused, resume so the app is in a "running" state as
|
|
|
|
|
// expected by tests. Tests will reload/restart as required if they need
|
|
|
|
|
// to hit breakpoints, etc.
|
|
|
|
|
await waitForPause();
|
|
|
|
|
if (pauseOnExceptions) {
|
|
|
|
|
await (await getFlutterIsolate()).setExceptionPauseMode(VMExceptionPauseMode.unhandled);
|
|
|
|
|
await _vmService.setExceptionPauseMode(await _getFlutterIsolateId(), ExceptionPauseMode.kUnhandled);
|
|
|
|
|
}
|
|
|
|
|
await resume(wait: false);
|
|
|
|
|
}
|
|
|
|
|
@@ -175,7 +171,7 @@ class FlutterTestDriver {
|
|
|
|
|
|
|
|
|
|
final dynamic hotReloadResp = await _sendRequest(
|
|
|
|
|
'app.restart',
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause}
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (hotReloadResp == null || hotReloadResp['code'] != 0)
|
|
|
|
|
@@ -183,11 +179,9 @@ class FlutterTestDriver {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<int> detach() async {
|
|
|
|
|
if (vmService != null) {
|
|
|
|
|
if (_vmService != null) {
|
|
|
|
|
_debugPrint('Closing VM service');
|
|
|
|
|
await vmService.close()
|
|
|
|
|
.timeout(quitTimeout,
|
|
|
|
|
onTimeout: () { _debugPrint('VM Service did not quit within $quitTimeout'); });
|
|
|
|
|
_vmService.dispose();
|
|
|
|
|
}
|
|
|
|
|
if (_currentRunningAppId != null) {
|
|
|
|
|
_debugPrint('Detaching from app');
|
|
|
|
|
@@ -195,11 +189,11 @@ class FlutterTestDriver {
|
|
|
|
|
_proc.exitCode,
|
|
|
|
|
_sendRequest(
|
|
|
|
|
'app.detach',
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId}
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId},
|
|
|
|
|
),
|
|
|
|
|
]).timeout(
|
|
|
|
|
quitTimeout,
|
|
|
|
|
onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); }
|
|
|
|
|
onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
|
|
|
|
|
);
|
|
|
|
|
_currentRunningAppId = null;
|
|
|
|
|
}
|
|
|
|
|
@@ -208,11 +202,9 @@ class FlutterTestDriver {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<int> stop() async {
|
|
|
|
|
if (vmService != null) {
|
|
|
|
|
if (_vmService != null) {
|
|
|
|
|
_debugPrint('Closing VM service');
|
|
|
|
|
await vmService.close()
|
|
|
|
|
.timeout(quitTimeout,
|
|
|
|
|
onTimeout: () { _debugPrint('VM Service did not quit within $quitTimeout'); });
|
|
|
|
|
_vmService.dispose();
|
|
|
|
|
}
|
|
|
|
|
if (_currentRunningAppId != null) {
|
|
|
|
|
_debugPrint('Stopping app');
|
|
|
|
|
@@ -220,11 +212,11 @@ class FlutterTestDriver {
|
|
|
|
|
_proc.exitCode,
|
|
|
|
|
_sendRequest(
|
|
|
|
|
'app.stop',
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId}
|
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId},
|
|
|
|
|
),
|
|
|
|
|
]).timeout(
|
|
|
|
|
quitTimeout,
|
|
|
|
|
onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); }
|
|
|
|
|
onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
|
|
|
|
|
);
|
|
|
|
|
_currentRunningAppId = null;
|
|
|
|
|
}
|
|
|
|
|
@@ -248,43 +240,73 @@ class FlutterTestDriver {
|
|
|
|
|
return _proc.exitCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMIsolate> getFlutterIsolate() async {
|
|
|
|
|
String _flutterIsolateId;
|
|
|
|
|
Future<String> _getFlutterIsolateId() async {
|
|
|
|
|
// Currently these tests only have a single isolate. If this
|
|
|
|
|
// ceases to be the case, this code will need changing.
|
|
|
|
|
final VM vm = await vmService.getVM();
|
|
|
|
|
return await vm.isolates.single.load();
|
|
|
|
|
if (_flutterIsolateId == null) {
|
|
|
|
|
final VM vm = await _vmService.getVM();
|
|
|
|
|
_flutterIsolateId = vm.isolates.first.id;
|
|
|
|
|
}
|
|
|
|
|
return _flutterIsolateId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Isolate> _getFlutterIsolate() async {
|
|
|
|
|
final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId());
|
|
|
|
|
return isolate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> addBreakpoint(Uri uri, int line) async {
|
|
|
|
|
final VMIsolate isolate = await getFlutterIsolate();
|
|
|
|
|
_debugPrint('Sending breakpoint for $uri:$line');
|
|
|
|
|
await isolate.addBreakpoint(uri, line);
|
|
|
|
|
await _vmService.addBreakpointWithScriptUri(
|
|
|
|
|
await _getFlutterIsolateId(), uri.toString(), line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMIsolate> waitForPause() async {
|
|
|
|
|
final VM vm = await vmService.getVM();
|
|
|
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
|
|
|
Future<Isolate> waitForPause() async {
|
|
|
|
|
_debugPrint('Waiting for isolate to pause');
|
|
|
|
|
await _timeoutWithMessages<dynamic>(isolate.waitUntilPaused,
|
|
|
|
|
final String flutterIsolate = await _getFlutterIsolateId();
|
|
|
|
|
|
|
|
|
|
Future<Isolate> waitForPause() async {
|
|
|
|
|
final Completer<Event> pauseEvent = Completer<Event>();
|
|
|
|
|
|
|
|
|
|
// Start listening for pause events.
|
|
|
|
|
final StreamSubscription<Event> pauseSub = _vmService.onDebugEvent
|
|
|
|
|
.where((Event event) =>
|
|
|
|
|
event.isolate.id == flutterIsolate &&
|
|
|
|
|
event.kind.startsWith('Pause'))
|
|
|
|
|
.listen(pauseEvent.complete);
|
|
|
|
|
|
|
|
|
|
// But also check if the isolate was already paused (only after we've set
|
|
|
|
|
// up the sub) to avoid races. If it was paused, we don't need to wait
|
|
|
|
|
// for the event.
|
|
|
|
|
final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
|
|
|
|
|
if (!isolate.pauseEvent.kind.startsWith('Pause')) {
|
|
|
|
|
await pauseEvent.future;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel the sub on either of the above.
|
|
|
|
|
await pauseSub.cancel();
|
|
|
|
|
|
|
|
|
|
return _getFlutterIsolate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _timeoutWithMessages<Isolate>(waitForPause,
|
|
|
|
|
message: 'Isolate did not pause');
|
|
|
|
|
return isolate.load();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMIsolate> resume({ bool wait = true }) => _resume(wait: wait);
|
|
|
|
|
Future<VMIsolate> stepOver({ bool wait = true }) => _resume(step: VMStep.over, wait: wait);
|
|
|
|
|
Future<VMIsolate> stepInto({ bool wait = true }) => _resume(step: VMStep.into, wait: wait);
|
|
|
|
|
Future<VMIsolate> stepOut({ bool wait = true }) => _resume(step: VMStep.out, wait: wait);
|
|
|
|
|
Future<Isolate> resume({bool wait = true}) => _resume(wait: wait);
|
|
|
|
|
Future<Isolate> stepOver({bool wait = true}) => _resume(step: StepOption.kOver, wait: wait);
|
|
|
|
|
Future<Isolate> stepInto({bool wait = true}) => _resume(step: StepOption.kInto, wait: wait);
|
|
|
|
|
Future<Isolate> stepOut({bool wait = true}) => _resume(step: StepOption.kOut, wait: wait);
|
|
|
|
|
|
|
|
|
|
Future<VMIsolate> _resume({VMStep step, bool wait = true}) async {
|
|
|
|
|
final VM vm = await vmService.getVM();
|
|
|
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
|
|
|
Future<Isolate> _resume({String step, bool wait = true}) async {
|
|
|
|
|
_debugPrint('Sending resume ($step)');
|
|
|
|
|
await _timeoutWithMessages<dynamic>(() => isolate.resume(step: step),
|
|
|
|
|
await _timeoutWithMessages<dynamic>(() async => _vmService.resume(await _getFlutterIsolateId(), step: step),
|
|
|
|
|
message: 'Isolate did not respond to resume ($step)');
|
|
|
|
|
return wait ? waitForPause() : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMIsolate> breakAt(Uri uri, int line, { bool restart = false }) async {
|
|
|
|
|
Future<Isolate> breakAt(Uri uri, int line, {bool restart = false}) async {
|
|
|
|
|
if (restart) {
|
|
|
|
|
// For a hot restart, we need to send the breakpoints after the restart
|
|
|
|
|
// so we need to pause during the restart to avoid races.
|
|
|
|
|
@@ -298,26 +320,47 @@ class FlutterTestDriver {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMInstanceRef> evaluateExpression(String expression) async {
|
|
|
|
|
final VMFrame topFrame = await getTopStackFrame();
|
|
|
|
|
return _timeoutWithMessages<VMInstanceRef>(() => topFrame.evaluate(expression),
|
|
|
|
|
Future<InstanceRef> evaluateInFrame(String expression) async {
|
|
|
|
|
return _timeoutWithMessages<InstanceRef>(
|
|
|
|
|
() async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
|
|
|
|
|
message: 'Timed out evaluating expression ($expression)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<VMFrame> getTopStackFrame() async {
|
|
|
|
|
final VM vm = await vmService.getVM();
|
|
|
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
|
|
|
final VMStack stack = await isolate.getStack();
|
|
|
|
|
Future<InstanceRef> evaluate(String targetId, String expression) async {
|
|
|
|
|
return _timeoutWithMessages<InstanceRef>(
|
|
|
|
|
() async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression),
|
|
|
|
|
message: 'Timed out evaluating expression ($expression for $targetId)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Frame> getTopStackFrame() async {
|
|
|
|
|
final String flutterIsolateId = await _getFlutterIsolateId();
|
|
|
|
|
final Stack stack = await _vmService.getStack(flutterIsolateId);
|
|
|
|
|
if (stack.frames.isEmpty) {
|
|
|
|
|
throw Exception('Stack is empty');
|
|
|
|
|
}
|
|
|
|
|
return stack.frames.first;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<FileLocation> getSourceLocation() async {
|
|
|
|
|
final VMFrame frame = await getTopStackFrame();
|
|
|
|
|
final VMScript script = await frame.location.script.load();
|
|
|
|
|
return script.sourceLocation(frame.location.token);
|
|
|
|
|
Future<SourcePosition> getSourceLocation() async {
|
|
|
|
|
final String flutterIsolateId = await _getFlutterIsolateId();
|
|
|
|
|
final Frame frame = await getTopStackFrame();
|
|
|
|
|
final Script script = await _vmService.getObject(flutterIsolateId, frame.location.script.id);
|
|
|
|
|
return _lookupTokenPos(script.tokenPosTable, frame.location.tokenPos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SourcePosition _lookupTokenPos(List<List<int>> table, int tokenPos) {
|
|
|
|
|
for (List<int> row in table) {
|
|
|
|
|
final int lineNumber = row[0];
|
|
|
|
|
int index = 1;
|
|
|
|
|
|
|
|
|
|
for (index = 1; index < row.length - 1; index += 2) {
|
|
|
|
|
if (row[index] == tokenPos) {
|
|
|
|
|
return SourcePosition(lineNumber, row[index + 1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Map<String, dynamic>> _waitFor({
|
|
|
|
|
@@ -428,3 +471,10 @@ class FlutterTestDriver {
|
|
|
|
|
Stream<String> _transformToLines(Stream<List<int>> byteStream) {
|
|
|
|
|
return byteStream.transform<String>(utf8.decoder).transform<String>(const LineSplitter());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class SourcePosition {
|
|
|
|
|
SourcePosition(this.line, this.column);
|
|
|
|
|
|
|
|
|
|
final int line;
|
|
|
|
|
final int column;
|
|
|
|
|
}
|
|
|
|
|
|