mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

All temporary directory start with `flutter_` and have their random component separated from the name by a period, as in `flutter_test_bundle.YFYQMY`. I've tried to find some of the places where we didn't cleanly delete temporary directories, too. This greatly reduces, though it does not entirely eliminate, the directories we leave behind when running tests, especially `flutter_tools` tests. While I was at it I standardized on `tempDir` as the variable name for temporary directories, since it was the most common, removing occurrences of `temp` and `tmp`, among others. Also I factored out some common code that used to catch exceptions that happen on Windows, and made more places use that pattern.
349 lines
13 KiB
Dart
349 lines
13 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: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 = Duration(seconds: 20);
|
|
const Duration appStartTimeout = Duration(seconds: 60);
|
|
const Duration quitTimeout = Duration(seconds: 5);
|
|
|
|
class FlutterTestDriver {
|
|
FlutterTestDriver(this._projectFolder);
|
|
|
|
final Directory _projectFolder;
|
|
Process _proc;
|
|
int _procPid;
|
|
final StreamController<String> _stdout = new StreamController<String>.broadcast();
|
|
final StreamController<String> _stderr = new StreamController<String>.broadcast();
|
|
final StreamController<String> _allMessages = new StreamController<String>.broadcast();
|
|
final StringBuffer _errorBuffer = new StringBuffer();
|
|
String _lastResponse;
|
|
String _currentRunningAppId;
|
|
Uri _vmServiceWsUri;
|
|
int _vmServicePort;
|
|
bool _hasExited = false;
|
|
|
|
VMServiceClient vmService;
|
|
String get lastErrorInfo => _errorBuffer.toString();
|
|
int get vmServicePort => _vmServicePort;
|
|
bool get hasExited => _hasExited;
|
|
|
|
String _debugPrint(String msg) {
|
|
const int maxLength = 500;
|
|
final String truncatedMsg =
|
|
msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg;
|
|
_allMessages.add(truncatedMsg);
|
|
if (_printJsonAndStderr) {
|
|
print(truncatedMsg);
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
Future<void> run({bool withDebugger = false, bool pauseOnExceptions = false}) async {
|
|
await _setupProcess(<String>[
|
|
'run',
|
|
'--machine',
|
|
'-d',
|
|
'flutter-tester',
|
|
], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
|
|
}
|
|
|
|
Future<void> attach(int port, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
|
|
await _setupProcess(<String>[
|
|
'attach',
|
|
'--machine',
|
|
'-d',
|
|
'flutter-tester',
|
|
'--debug-port',
|
|
'$port',
|
|
], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
|
|
}
|
|
|
|
Future<void> _setupProcess(List<String> args, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
|
|
final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
|
|
_debugPrint('Spawning flutter $args in ${_projectFolder.path}');
|
|
|
|
const ProcessManager _processManager = LocalProcessManager();
|
|
_proc = await _processManager.start(
|
|
<String>[flutterBin]
|
|
.followedBy(args)
|
|
.followedBy(withDebugger ? <String>['--start-paused'] : <String>[])
|
|
.toList(),
|
|
workingDirectory: _projectFolder.path,
|
|
environment: <String, String>{'FLUTTER_TEST': 'true'});
|
|
|
|
_proc.exitCode.then((_) => _hasExited = true);
|
|
_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.
|
|
_stdout.stream.listen(_debugPrint);
|
|
_stderr.stream.listen(_debugPrint);
|
|
|
|
// Stash the PID so that we can terminate the VM more reliably than using
|
|
// _proc.kill() (because _proc is a shell, because `flutter` is a shell
|
|
// script).
|
|
final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
|
|
_procPid = connected['params']['pid'];
|
|
|
|
// 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',
|
|
timeout: appStartTimeout);
|
|
|
|
if (withDebugger) {
|
|
final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort',
|
|
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 = new IOWebSocketChannel.connect(_vmServiceWsUri)
|
|
.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();
|
|
if (pauseOnExceptions) {
|
|
await (await getFlutterIsolate()).setExceptionPauseMode(VMExceptionPauseMode.unhandled);
|
|
}
|
|
await resume(wait: false);
|
|
}
|
|
|
|
// Now await the started event; if it had already happened the future will
|
|
// have already completed.
|
|
_currentRunningAppId = (await started)['params']['appId'];
|
|
}
|
|
|
|
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': fullRestart, 'pause': pause}
|
|
);
|
|
|
|
if (hotReloadResp == null || hotReloadResp['code'] != 0)
|
|
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
|
|
}
|
|
|
|
Future<int> stop() async {
|
|
if (vmService != null) {
|
|
_debugPrint('Closing VM service');
|
|
await vmService.close()
|
|
.timeout(quitTimeout,
|
|
onTimeout: () { _debugPrint('VM Service did not quit within $quitTimeout'); });
|
|
}
|
|
if (_currentRunningAppId != null) {
|
|
_debugPrint('Stopping app');
|
|
await _sendRequest(
|
|
'app.stop',
|
|
<String, dynamic>{'appId': _currentRunningAppId}
|
|
).timeout(
|
|
quitTimeout,
|
|
onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); }
|
|
);
|
|
_currentRunningAppId = null;
|
|
}
|
|
_debugPrint('Waiting for process to end');
|
|
return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
|
}
|
|
|
|
Future<int> _killGracefully() async {
|
|
if (_procPid == null)
|
|
return -1;
|
|
_debugPrint('Sending SIGTERM to $_procPid..');
|
|
Process.killPid(_procPid);
|
|
return _proc.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
|
|
}
|
|
|
|
Future<int> _killForcefully() {
|
|
_debugPrint('Sending SIGKILL to $_procPid..');
|
|
Process.killPid(_procPid, ProcessSignal.SIGKILL);
|
|
return _proc.exitCode;
|
|
}
|
|
|
|
Future<VMIsolate> getFlutterIsolate() 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();
|
|
}
|
|
|
|
Future<void> addBreakpoint(String path, int line) async {
|
|
final VMIsolate isolate = await getFlutterIsolate();
|
|
_debugPrint('Sending breakpoint for $path:$line');
|
|
await isolate.addBreakpoint(path, line);
|
|
}
|
|
|
|
Future<VMIsolate> waitForPause() async {
|
|
final VM vm = await vmService.getVM();
|
|
final VMIsolate isolate = await vm.isolates.first.load();
|
|
_debugPrint('Waiting for isolate to pause');
|
|
await _timeoutWithMessages<dynamic>(isolate.waitUntilPaused,
|
|
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<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 _timeoutWithMessages<dynamic>(() => isolate.resume(step: step),
|
|
message: '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 _timeoutWithMessages(() => topFrame.evaluate(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();
|
|
if (stack.frames.isEmpty) {
|
|
throw new 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<Map<String, dynamic>> _waitFor({String event, int id, Duration timeout}) async {
|
|
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 _timeoutWithMessages(() => response.future,
|
|
timeout: timeout,
|
|
message: event != null
|
|
? 'Did not receive expected $event event.'
|
|
: 'Did not receive response to request "$id".')
|
|
.whenComplete(() => sub.cancel());
|
|
}
|
|
|
|
Future<T> _timeoutWithMessages<T>(Future<T> Function() f, {Duration timeout, String message}) {
|
|
// Capture output to a buffer so if we don't get the response we want we can show
|
|
// the output that did arrive in the timeout error.
|
|
final StringBuffer messages = new StringBuffer();
|
|
final DateTime start = new DateTime.now();
|
|
void logMessage(String m) {
|
|
final int ms = new DateTime.now().difference(start).inMilliseconds;
|
|
messages.writeln('[+ ${ms.toString().padLeft(5)}] $m');
|
|
}
|
|
final StreamSubscription<String> sub = _allMessages.stream.listen(logMessage);
|
|
|
|
return f().timeout(timeout ?? defaultTimeout, onTimeout: () {
|
|
logMessage('<timed out>');
|
|
throw '$message\nReceived:\n${messages.toString()}';
|
|
}).whenComplete(() => sub.cancel());
|
|
}
|
|
|
|
Map<String, dynamic> _parseFlutterResponse(String line) {
|
|
if (line.startsWith('[') && line.endsWith(']')) {
|
|
try {
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int id = 1;
|
|
Future<dynamic> _sendRequest(String method, dynamic params) async {
|
|
final int requestId = id++;
|
|
final Map<String, dynamic> request = <String, dynamic>{
|
|
'id': requestId,
|
|
'method': method,
|
|
'params': params
|
|
};
|
|
final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
|
|
_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);
|
|
_proc.stdin.writeln(jsonEncoded);
|
|
final Map<String, dynamic> response = await responseFuture;
|
|
|
|
if (response['error'] != null || response['result'] == null)
|
|
_throwErrorResponse('Unexpected error response');
|
|
|
|
return response['result'];
|
|
}
|
|
|
|
void _throwErrorResponse(String msg) {
|
|
throw '$msg\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
|
|
}
|
|
}
|
|
|
|
Stream<String> _transformToLines(Stream<List<int>> byteStream) {
|
|
return byteStream.transform(utf8.decoder).transform(const LineSplitter());
|
|
}
|