// 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 _stdout = new StreamController.broadcast(); final StreamController _stderr = new StreamController.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 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> started = _waitFor(event: 'app.started'); if (withDebugger) { final Future> 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 hotReload() async { if (_currentRunningAppId == null) throw new Exception('App has not started yet'); final dynamic hotReloadResp = await _sendRequest( 'app.restart', {'appId': _currentRunningAppId, 'fullRestart': false} ); if (hotReloadResp == null || hotReloadResp['code'] != 0) throw 'Hot reload request failed\n\n${_errorBuffer.toString()}'; } Future stop() async { if (_currentRunningAppId != null) { await _sendRequest( 'app.stop', {'appId': _currentRunningAppId} ); } _currentRunningAppId = null; return _proc.exitCode; } Future _runFlutter(Directory projectDir) async { final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter'); final List command = [ 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: {'FLUTTER_TEST': 'true'} ); } Future 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 waitForBreakpointHit() async { final VM vm = await vmService.getVM(); final VMIsolate isolate = await vm.isolates.first.load(); await _withTimeout( isolate.waitUntilPaused(), () => 'Isolate did not pause' ); return isolate.load(); } Future breakAt(String path, int line) async { await addBreakpoint(path, line); await hotReload(); return waitForBreakpointHit(); } Future 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> _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> response = new Completer>(); final StreamSubscription 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 _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 _sendRequest(String method, dynamic params) async { final int requestId = id++; final Map req = { 'id': requestId, 'method': method, 'params': params }; final String jsonEncoded = json.encode(>[req]); if (_printJsonAndStderr) { print(jsonEncoded); } // Set up the response future before we send the request to avoid any // races. final Future> responseFuture = _waitFor(id: requestId); _proc.stdin.writeln(jsonEncoded); final Map resp = await responseFuture; if (resp['error'] != null || resp['result'] == null) throw 'Unexpected error response: ${resp['error']}\n\n${_errorBuffer.toString()}'; return resp['result']; } } Future _withTimeout(Future f, [ String Function() getDebugMessage, int timeoutSeconds = 20, ]) { final Future timeout = new Future.delayed(new Duration(seconds: timeoutSeconds)) .then((Object _) => throw new Exception(getDebugMessage())); return Future.any(>[f, timeout]); } Stream _transformToLines(Stream> byteStream) { return byteStream.transform(utf8.decoder).transform(const LineSplitter()); }