mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
226 lines
7.6 KiB
Dart
226 lines
7.6 KiB
Dart
// 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 '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:vm_service_client/vm_service_client.dart';
|
|
|
|
import '../src/common.dart';
|
|
|
|
// Set this to true for debugging to get JSON written to stdout.
|
|
const bool _printJsonAndStderr = false;
|
|
|
|
class FlutterTestDriver {
|
|
Directory _projectFolder;
|
|
Process _proc;
|
|
final StreamController<String> _stdout = new StreamController<String>.broadcast();
|
|
final StreamController<String> _stderr = new StreamController<String>.broadcast();
|
|
final StringBuffer _errorBuffer = new StringBuffer();
|
|
String _currentRunningAppId;
|
|
|
|
FlutterTestDriver(this._projectFolder);
|
|
|
|
VMServiceClient vmService;
|
|
String get lastErrorInfo => _errorBuffer.toString();
|
|
|
|
// TODO(dantup): Is there a better way than spawning a proc? This breaks debugging..
|
|
// However, there's a lot of logic inside RunCommand that wouldn't be good
|
|
// to duplicate here.
|
|
Future<void> run({bool withDebugger = false}) async {
|
|
_proc = await _runFlutter(_projectFolder);
|
|
_transformToLines(_proc.stdout).listen((String line) => _stdout.add(line));
|
|
_transformToLines(_proc.stderr).listen((String line) => _stderr.add(line));
|
|
|
|
// Capture stderr to a buffer so we can show it all if any requests fail.
|
|
_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);
|
|
}
|
|
|
|
// 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.
|
|
final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started');
|
|
|
|
if (withDebugger) {
|
|
final Future<Map<String, dynamic>> debugPort = _waitFor(event: 'app.debugPort');
|
|
final String wsUri = (await debugPort)['params']['wsUri'];
|
|
vmService = new VMServiceClient.connect(wsUri);
|
|
}
|
|
|
|
// Now await the started event; if it had already happened the future will
|
|
// have already completed.
|
|
_currentRunningAppId = (await started)['params']['appId'];
|
|
}
|
|
|
|
Future<void> hotReload() 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}
|
|
);
|
|
|
|
if (hotReloadResp == null || hotReloadResp['code'] != 0)
|
|
throw 'Hot reload request failed\n\n${_errorBuffer.toString()}';
|
|
}
|
|
|
|
Future<int> stop() async {
|
|
if (_currentRunningAppId != null) {
|
|
await _sendRequest(
|
|
'app.stop',
|
|
<String, dynamic>{'appId': _currentRunningAppId}
|
|
);
|
|
}
|
|
_currentRunningAppId = null;
|
|
return _proc.exitCode;
|
|
}
|
|
|
|
Future<Process> _runFlutter(Directory projectDir) async {
|
|
final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
|
|
final List<String> command = <String>[
|
|
flutterBin,
|
|
'run',
|
|
'--machine',
|
|
'-d',
|
|
'flutter-tester',
|
|
'--observatory-port=0',
|
|
];
|
|
if (_printJsonAndStderr) {
|
|
print('Spawning $command in ${projectDir.path}');
|
|
}
|
|
const ProcessManager _processManager = const LocalProcessManager();
|
|
return _processManager.start(
|
|
command,
|
|
workingDirectory: projectDir.path,
|
|
environment: <String, String>{'FLUTTER_TEST': 'true'}
|
|
);
|
|
}
|
|
|
|
Future<void> addBreakpoint(String path, int line) async {
|
|
final VM vm = await vmService.getVM();
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
await isolate.addBreakpoint(path, line);
|
|
}
|
|
|
|
Future<VMIsolate> waitForBreakpointHit() async {
|
|
final VM vm = await vmService.getVM();
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
await _withTimeout<void>(
|
|
isolate.waitUntilPaused(),
|
|
() => 'Isolate did not pause'
|
|
);
|
|
return isolate.load();
|
|
}
|
|
|
|
Future<VMIsolate> breakAt(String path, int line) async {
|
|
await addBreakpoint(path, line);
|
|
await hotReload();
|
|
return waitForBreakpointHit();
|
|
}
|
|
|
|
Future<VMInstanceRef> evaluateExpression(String expression) 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');
|
|
}
|
|
final VMFrame topFrame = stack.frames.first;
|
|
return _withTimeout(
|
|
topFrame.evaluate(expression),
|
|
() => 'Timed out evaluating expression'
|
|
);
|
|
}
|
|
|
|
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.
|
|
final StringBuffer messages = new StringBuffer();
|
|
_stdout.stream.listen(messages.writeln);
|
|
_stderr.stream.listen(messages.writeln);
|
|
|
|
final Completer<Map<String, dynamic>> response = new Completer<Map<String, dynamic>>();
|
|
final StreamSubscription<String> sub = _stdout.stream.listen((String line) {
|
|
final dynamic json = _parseFlutterResponse(line);
|
|
if (json == null) {
|
|
return;
|
|
} else if (
|
|
(event != null && json['event'] == event)
|
|
|| (id != null && json['id'] == id)) {
|
|
response.complete(json);
|
|
}
|
|
});
|
|
|
|
return _withTimeout(
|
|
response.future,
|
|
() {
|
|
if (event != null)
|
|
return '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());
|
|
}
|
|
|
|
Map<String, dynamic> _parseFlutterResponse(String line) {
|
|
if (line.startsWith('[') && line.endsWith(']')) {
|
|
try {
|
|
return json.decode(line)[0];
|
|
} catch (e) {
|
|
// Not valid JSON, so likely some other output that was surrounded by [brackets]
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int id = 1;
|
|
Future<dynamic> _sendRequest(String method, dynamic params) async {
|
|
final int requestId = id++;
|
|
final Map<String, dynamic> req = <String, dynamic>{
|
|
'id': requestId,
|
|
'method': method,
|
|
'params': params
|
|
};
|
|
final String jsonEncoded = json.encode(<Map<String, dynamic>>[req]);
|
|
if (_printJsonAndStderr) {
|
|
print(jsonEncoded);
|
|
}
|
|
// Set up the response future before we send the request to avoid any
|
|
// races.
|
|
final Future<Map<String, dynamic>> responseFuture = _waitFor(id: requestId);
|
|
_proc.stdin.writeln(jsonEncoded);
|
|
final Map<String, dynamic> resp = await responseFuture;
|
|
|
|
if (resp['error'] != null || resp['result'] == null)
|
|
throw 'Unexpected error response: ${resp['error']}\n\n${_errorBuffer.toString()}';
|
|
|
|
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]);
|
|
}
|
|
|
|
Stream<String> _transformToLines(Stream<List<int>> byteStream) {
|
|
return byteStream.transform(utf8.decoder).transform(const LineSplitter());
|
|
}
|