Unverified Commit 4630fa8e authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

Additional integration test driver functionality (#19085)

* Add additional functionality to integration test driver

* Typo

* Remove delays we shouldn't need + comments

* Fix typos
parent 064e40d8
......@@ -58,18 +58,11 @@ void main() {
testUsingContext('can hit breakpoints with file:// prefixes after reload', () async {
await _flutter.run(withDebugger: true);
// Add the breakpoint using a file:// URI.
await _flutter.addBreakpoint(
// Test currently passes with a FS path, but not with file:// URI.
// fs.path.join(_tempDir.path, 'lib', 'main.dart'),
new Uri.file(fs.path.join(_tempDir.path, 'lib', 'main.dart')).toString(),
9
);
await _flutter.hotReload();
// Ensure we hit the breakpoint.
final VMIsolate isolate = await _flutter.waitForBreakpointHit();
final VMIsolate isolate = await _flutter.breakAt(
new Uri.file(fs.path.join(_tempDir.path, 'lib', 'main.dart')).toString(),
9
);
expect(isolate.pauseEvent, const isInstanceOf<VMPauseBreakpointEvent>());
}, skip: true); // https://github.com/flutter/flutter/issues/18441
......
......@@ -9,12 +9,24 @@ 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 '../src/common.dart';
// Set this to true for debugging to get JSON written to stdout.
const bool _printJsonAndStderr = false;
const Duration defaultTimeout = const Duration(seconds: 20);
String debugPrint(String msg) {
const int maxLength = 200;
if (_printJsonAndStderr) {
print(msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg);
}
return msg;
}
class FlutterTestDriver {
Directory _projectFolder;
......@@ -22,6 +34,7 @@ class FlutterTestDriver {
final StreamController<String> _stdout = new StreamController<String>.broadcast();
final StreamController<String> _stderr = new StreamController<String>.broadcast();
final StringBuffer _errorBuffer = new StringBuffer();
String _lastResponse;
String _currentRunningAppId;
FlutterTestDriver(this._projectFolder);
......@@ -41,10 +54,8 @@ class FlutterTestDriver {
_stderr.stream.listen(_errorBuffer.writeln);
// This is just debug printing to aid running/debugging tests locally.
if (_printJsonAndStderr) {
_stdout.stream.listen(print);
_stderr.stream.listen(print);
}
_stdout.stream.listen(debugPrint);
_stderr.stream.listen(debugPrint);
// Set this up now, but we don't wait it yet. We want to make sure we don't
// miss it while waiting for debugPort below.
......@@ -52,8 +63,22 @@ class FlutterTestDriver {
if (withDebugger) {
final Future<Map<String, dynamic>> debugPort = _waitFor(event: 'app.debugPort');
final String wsUri = (await debugPort)['params']['wsUri'];
vmService = new VMServiceClient.connect(wsUri);
final String wsUriString = (await debugPort)['params']['wsUri'];
final Uri uri = Uri.parse(wsUriString);
// Proxy the stream/sink for the VM Client so we can debugPrint it.
final StreamChannel<String> channel = new IOWebSocketChannel.connect(uri)
.cast<String>()
.changeStream((Stream<String> stream) => stream.map(debugPrint))
.changeSink((StreamSink<String> sink) =>
new StreamController<String>()
..stream.listen((String s) => sink.add(debugPrint(s))));
vmService = new VMServiceClient(channel);
// 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();
await resume(wait: false);
}
// Now await the started event; if it had already happened the future will
......@@ -61,20 +86,26 @@ class FlutterTestDriver {
_currentRunningAppId = (await started)['params']['appId'];
}
Future<void> hotReload() async {
Future<void> hotRestart({bool pause = false}) => _restart(fullRestart: true, pause: pause);
Future<void> hotReload() => _restart(fullRestart: false);
Future<void> _restart({bool fullRestart = false, bool pause = false}) async {
if (_currentRunningAppId == null)
throw new Exception('App has not started yet');
final dynamic hotReloadResp = await _sendRequest(
'app.restart',
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': false}
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause}
);
if (hotReloadResp == null || hotReloadResp['code'] != 0)
throw 'Hot reload request failed\n\n${_errorBuffer.toString()}';
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
}
Future<int> stop() async {
if (vmService != null) {
await vmService.close();
}
if (_currentRunningAppId != null) {
await _sendRequest(
'app.stop',
......@@ -93,11 +124,10 @@ class FlutterTestDriver {
'--machine',
'-d',
'flutter-tester',
'--observatory-port=0',
'--start-paused',
];
if (_printJsonAndStderr) {
print('Spawning $command in ${projectDir.path}');
}
debugPrint('Spawning $command in ${projectDir.path}');
const ProcessManager _processManager = const LocalProcessManager();
return _processManager.start(
command,
......@@ -109,42 +139,72 @@ class FlutterTestDriver {
Future<void> addBreakpoint(String path, int line) async {
final VM vm = await vmService.getVM();
final VMIsolate isolate = await vm.isolates.first.load();
debugPrint('Sending breakpoint for $path:$line');
await isolate.addBreakpoint(path, line);
}
Future<VMIsolate> waitForBreakpointHit() async {
Future<VMIsolate> waitForPause() async {
final VM vm = await vmService.getVM();
final VMIsolate isolate = await vm.isolates.first.load();
await _withTimeout<void>(
isolate.waitUntilPaused(),
() => 'Isolate did not pause'
);
debugPrint('Waiting for isolate to pause');
await isolate.waitUntilPaused()
.timeout(defaultTimeout, onTimeout: () => throw 'Isolate did not pause');
return isolate.load();
}
Future<VMIsolate> breakAt(String path, int line) async {
await addBreakpoint(path, line);
await hotReload();
return waitForBreakpointHit();
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<VMIsolate> _resume({VMStep step, bool wait = true}) async {
final VM vm = await vmService.getVM();
final VMIsolate isolate = await vm.isolates.first.load();
debugPrint('Sending resume ($step)');
await isolate.resume(step: step)
.timeout(defaultTimeout, onTimeout: () => throw 'Isolate did not respond to resume ($step)');
return wait ? waitForPause() : null;
}
Future<VMIsolate> breakAt(String path, 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.
await hotRestart(pause: true);
await addBreakpoint(path, line);
return resume();
} else {
await addBreakpoint(path, line);
await hotReload();
return waitForPause();
}
}
Future<VMInstanceRef> evaluateExpression(String expression) async {
final VMFrame topFrame = await getTopStackFrame();
return topFrame.evaluate(expression)
.timeout(defaultTimeout, onTimeout: () => throw '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();
if (stack.frames.isEmpty) {
throw new Exception('Stack is empty; unable to evaluate expression');
throw new Exception('Stack is empty');
}
final VMFrame topFrame = stack.frames.first;
return _withTimeout(
topFrame.evaluate(expression),
() => 'Timed out evaluating expression'
);
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<Map<String, dynamic>> _waitFor({String event, int id}) async {
// Capture output to a buffer so if we don't get the repsonse we want we can show
// the output that did arrive in the timeout errr.
// the output that did arrive in the timeout error.
final StringBuffer messages = new StringBuffer();
_stdout.stream.listen(messages.writeln);
_stderr.stream.listen(messages.writeln);
......@@ -161,21 +221,20 @@ class FlutterTestDriver {
}
});
return _withTimeout(
response.future,
() {
return response.future.timeout(defaultTimeout, onTimeout: () {
if (event != null)
return 'Did not receive expected $event event.\nDid get:\n${messages.toString()}';
throw 'Did not receive expected $event event.\nDid get:\n${messages.toString()}';
else if (id != null)
return 'Did not receive response to request "$id".\nDid get:\n${messages.toString()}';
}
).whenComplete(() => sub.cancel());
throw 'Did not receive response to request "$id".\nDid get:\n${messages.toString()}';
}).whenComplete(() => sub.cancel());
}
Map<String, dynamic> _parseFlutterResponse(String line) {
if (line.startsWith('[') && line.endsWith(']')) {
try {
return json.decode(line)[0];
final Map<String, dynamic> resp = json.decode(line)[0];
_lastResponse = line;
return resp;
} catch (e) {
// Not valid JSON, so likely some other output that was surrounded by [brackets]
return null;
......@@ -193,9 +252,8 @@ class FlutterTestDriver {
'params': params
};
final String jsonEncoded = json.encode(<Map<String, dynamic>>[req]);
if (_printJsonAndStderr) {
print(jsonEncoded);
}
debugPrint(jsonEncoded);
// Set up the response future before we send the request to avoid any
// races.
final Future<Map<String, dynamic>> responseFuture = _waitFor(id: requestId);
......@@ -203,21 +261,14 @@ class FlutterTestDriver {
final Map<String, dynamic> resp = await responseFuture;
if (resp['error'] != null || resp['result'] == null)
throw 'Unexpected error response: ${resp['error']}\n\n${_errorBuffer.toString()}';
_throwErrorResponse('Unexpected error response');
return resp['result'];
}
}
Future<T> _withTimeout<T>(Future<T> f, [
String Function() getDebugMessage,
int timeoutSeconds = 20,
]) {
final Future<T> timeout =
new Future<T>.delayed(new Duration(seconds: timeoutSeconds))
.then((Object _) => throw new Exception(getDebugMessage()));
return Future.any(<Future<T>>[f, timeout]);
void _throwErrorResponse(String msg) {
throw '$msg\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
}
}
Stream<String> _transformToLines(Stream<List<int>> byteStream) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment