test_driver.dart 13 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// 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';
12 13
import 'package:source_span/source_span.dart';
import 'package:stream_channel/stream_channel.dart';
14
import 'package:vm_service_client/vm_service_client.dart';
15
import 'package:web_socket_channel/io.dart';
16 17 18 19 20

import '../src/common.dart';

// Set this to true for debugging to get JSON written to stdout.
const bool _printJsonAndStderr = false;
21 22 23
const Duration defaultTimeout = Duration(seconds: 20);
const Duration appStartTimeout = Duration(seconds: 60);
const Duration quitTimeout = Duration(seconds: 5);
24 25

class FlutterTestDriver {
26
  final Directory _projectFolder;
27
  Process _proc;
28
  int _procPid;
29 30
  final StreamController<String> _stdout = new StreamController<String>.broadcast();
  final StreamController<String> _stderr = new StreamController<String>.broadcast();
31
  final StreamController<String> _allMessages = new StreamController<String>.broadcast();
32
  final StringBuffer _errorBuffer = new StringBuffer();
33
  String _lastResponse;
34
  String _currentRunningAppId;
35 36
  Uri _vmServiceWsUri;
  int _vmServicePort;
37
  bool _hasExited = false;
38 39 40 41 42

  FlutterTestDriver(this._projectFolder);

  VMServiceClient vmService;
  String get lastErrorInfo => _errorBuffer.toString();
43
  int get vmServicePort => _vmServicePort;
44
  bool get hasExited => _hasExited;
45

46 47 48 49 50 51 52 53 54
  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;
55
  }
56

57
  Future<void> run({bool withDebugger = false, bool pauseOnExceptions = false}) async {
58 59 60 61 62
    await _setupProcess(<String>[
        'run',
        '--machine',
        '-d',
        'flutter-tester',
63
    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
64 65
  }

66
  Future<void> attach(int port, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
67 68 69 70 71 72 73
    await _setupProcess(<String>[
        'attach',
        '--machine',
        '-d',
        'flutter-tester',
        '--debug-port',
        '$port',
74
    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions);
75 76
  }

77
  Future<void> _setupProcess(List<String> args, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
78 79
    final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
    _debugPrint('Spawning flutter $args in ${_projectFolder.path}');
80

81
    const ProcessManager _processManager = LocalProcessManager();
82 83 84 85 86 87 88 89
    _proc = await _processManager.start(
        <String>[flutterBin]
            .followedBy(args)
            .followedBy(withDebugger ? <String>['--start-paused'] : <String>[])
            .toList(),
        workingDirectory: _projectFolder.path,
        environment: <String, String>{'FLUTTER_TEST': 'true'});

90
    _proc.exitCode.then((_) => _hasExited = true);
91 92 93 94 95 96 97
    _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.
98 99
    _stdout.stream.listen(_debugPrint);
    _stderr.stream.listen(_debugPrint);
100

101 102 103 104 105
    // 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'];
106

107 108
    // 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.
109 110
    final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started',
        timeout: appStartTimeout);
111 112

    if (withDebugger) {
113
      final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort',
114
          timeout: appStartTimeout);
115 116 117
      final String wsUriString = debugPort['params']['wsUri'];
      _vmServiceWsUri = Uri.parse(wsUriString);
      _vmServicePort = debugPort['params']['port'];
118
      // Proxy the stream/sink for the VM Client so we can debugPrint it.
119
      final StreamChannel<String> channel = new IOWebSocketChannel.connect(_vmServiceWsUri)
120
          .cast<String>()
121
          .changeStream((Stream<String> stream) => stream.map(_debugPrint))
122 123
          .changeSink((StreamSink<String> sink) =>
              new StreamController<String>()
124
                ..stream.listen((String s) => sink.add(_debugPrint(s))));
125 126 127 128
      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
129
      // to hit breakpoints, etc.
130
      await waitForPause();
131 132 133
      if (pauseOnExceptions) {
        await (await getFlutterIsolate()).setExceptionPauseMode(VMExceptionPauseMode.unhandled);
      }
134
      await resume(wait: false);
135 136 137 138 139 140 141
    }

    // Now await the started event; if it had already happened the future will
    // have already completed.
    _currentRunningAppId = (await started)['params']['appId'];
  }

142 143 144 145
  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 {
146 147 148 149 150
    if (_currentRunningAppId == null)
      throw new Exception('App has not started yet');

    final dynamic hotReloadResp = await _sendRequest(
        'app.restart',
151
        <String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause}
152 153 154
    );

    if (hotReloadResp == null || hotReloadResp['code'] != 0)
155
      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
156 157 158
  }

  Future<int> stop() async {
159
    if (vmService != null) {
160 161 162 163
      _debugPrint('Closing VM service');
      await vmService.close()
          .timeout(quitTimeout,
              onTimeout: () { _debugPrint('VM Service did not quit within $quitTimeout'); });
164
    }
165
    if (_currentRunningAppId != null) {
166
      _debugPrint('Stopping app');
167
      await _sendRequest(
168 169 170 171 172
        'app.stop',
        <String, dynamic>{'appId': _currentRunningAppId}
      ).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); }
173
      );
174
      _currentRunningAppId = null;
175
    }
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
    _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);
191 192 193
    return _proc.exitCode;
  }

194 195 196
  Future<VMIsolate> getFlutterIsolate() async {
    // Currently these tests only have a single isolate. If this
    // ceases to be the case, this code will need changing.
197
    final VM vm = await vmService.getVM();
198
    return await vm.isolates.single.load();
199 200 201 202
  }

  Future<void> addBreakpoint(String path, int line) async {
    final VMIsolate isolate = await getFlutterIsolate();
203
    _debugPrint('Sending breakpoint for $path:$line');
204 205 206
    await isolate.addBreakpoint(path, line);
  }

207
  Future<VMIsolate> waitForPause() async {
208 209
    final VM vm = await vmService.getVM();
    final VMIsolate isolate = await vm.isolates.first.load();
210 211 212
    _debugPrint('Waiting for isolate to pause');
    await _timeoutWithMessages<dynamic>(isolate.waitUntilPaused,
        message: 'Isolate did not pause');
213 214 215
    return isolate.load();
  }

216 217 218 219 220 221 222 223
  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();
224 225 226
    _debugPrint('Sending resume ($step)');
    await _timeoutWithMessages<dynamic>(() => isolate.resume(step: step),
        message: 'Isolate did not respond to resume ($step)');
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
    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();
    }
242 243 244
  }

  Future<VMInstanceRef> evaluateExpression(String expression) async {
245
    final VMFrame topFrame = await getTopStackFrame();
246 247
    return _timeoutWithMessages(() => topFrame.evaluate(expression),
        message: 'Timed out evaluating expression ($expression)');
248 249 250
  }

  Future<VMFrame> getTopStackFrame() async {
251 252 253 254
    final VM vm = await vmService.getVM();
    final VMIsolate isolate = await vm.isolates.first.load();
    final VMStack stack = await isolate.getStack();
    if (stack.frames.isEmpty) {
255
      throw new Exception('Stack is empty');
256
    }
257 258 259 260 261 262 263
    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);
264 265
  }

266
  Future<Map<String, dynamic>> _waitFor({String event, int id, Duration timeout}) async {
267 268 269 270 271 272 273 274 275 276 277
    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);
      }
    });
278

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
    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);
297

298 299 300 301
    return f().timeout(timeout ?? defaultTimeout, onTimeout: () {
      logMessage('<timed out>');
      throw '$message\nReceived:\n${messages.toString()}';
    }).whenComplete(() => sub.cancel());
302 303 304 305 306
  }

  Map<String, dynamic> _parseFlutterResponse(String line) {
    if (line.startsWith('[') && line.endsWith(']')) {
      try {
307 308 309
        final Map<String, dynamic> resp = json.decode(line)[0];
        _lastResponse = line;
        return resp;
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
      } 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]);
327
    _debugPrint(jsonEncoded);
328

329 330 331 332 333 334 335
    // 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)
336
      _throwErrorResponse('Unexpected error response');
337 338 339 340

    return resp['result'];
  }

341 342 343
  void _throwErrorResponse(String msg) {
    throw '$msg\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
  }
344 345 346 347 348
}

Stream<String> _transformToLines(Stream<List<int>> byteStream) {
  return byteStream.transform(utf8.decoder).transform(const LineSplitter());
}