2018-06-27 07:04:55 +00:00
|
|
|
// 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';
|
2019-02-05 23:00:51 +00:00
|
|
|
import 'package:flutter_tools/src/base/common.dart';
|
2018-06-27 07:04:55 +00:00
|
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
|
|
import 'package:flutter_tools/src/base/io.dart';
|
2019-01-19 08:31:05 +00:00
|
|
|
import 'package:meta/meta.dart';
|
2018-06-27 07:04:55 +00:00
|
|
|
import 'package:process/process.dart';
|
2018-11-08 07:55:54 +00:00
|
|
|
import 'package:vm_service_lib/vm_service_lib.dart';
|
|
|
|
import 'package:vm_service_lib/vm_service_lib_io.dart';
|
2018-06-27 07:04:55 +00:00
|
|
|
|
|
|
|
import '../src/common.dart';
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
// Set this to true for debugging to get verbose logs written to stdout.
|
|
|
|
// The logs include the following:
|
|
|
|
// <=stdout= data that the flutter tool running in --verbose mode wrote to stdout.
|
|
|
|
// <=stderr= data that the flutter tool running in --verbose mode wrote to stderr.
|
|
|
|
// =stdin=> data that the test sent to the flutter tool over stdin.
|
|
|
|
// =vm=> data that was sent over the VM service channel to the app running on the test device.
|
|
|
|
// <=vm= data that was sent from the app on the test device over the VM service channel.
|
|
|
|
// Messages regarding what the test is doing.
|
|
|
|
// If this is false, then only critical errors and logs when things appear to be
|
|
|
|
// taking a long time are printed to the console.
|
2018-11-08 07:55:54 +00:00
|
|
|
const bool _printDebugOutputToStdOut = false;
|
2019-01-19 08:31:05 +00:00
|
|
|
|
|
|
|
final DateTime startTime = DateTime.now();
|
|
|
|
|
|
|
|
const Duration defaultTimeout = Duration(seconds: 5);
|
2018-08-22 16:13:23 +00:00
|
|
|
const Duration appStartTimeout = Duration(seconds: 120);
|
|
|
|
const Duration quitTimeout = Duration(seconds: 10);
|
2018-06-27 07:04:55 +00:00
|
|
|
|
2018-11-19 08:25:42 +00:00
|
|
|
abstract class FlutterTestDriver {
|
2019-01-19 08:31:05 +00:00
|
|
|
FlutterTestDriver(
|
|
|
|
this._projectFolder, {
|
|
|
|
String logPrefix,
|
|
|
|
}) : _logPrefix = logPrefix != null ? '$logPrefix: ' : '';
|
2018-08-17 20:17:23 +00:00
|
|
|
|
2018-07-19 12:38:29 +00:00
|
|
|
final Directory _projectFolder;
|
2018-09-06 14:26:55 +00:00
|
|
|
final String _logPrefix;
|
2019-01-19 08:31:05 +00:00
|
|
|
Process _process;
|
|
|
|
int _processPid;
|
2018-09-12 06:29:29 +00:00
|
|
|
final StreamController<String> _stdout = StreamController<String>.broadcast();
|
|
|
|
final StreamController<String> _stderr = StreamController<String>.broadcast();
|
|
|
|
final StreamController<String> _allMessages = StreamController<String>.broadcast();
|
|
|
|
final StringBuffer _errorBuffer = StringBuffer();
|
2018-07-09 09:41:16 +00:00
|
|
|
String _lastResponse;
|
2018-07-19 12:38:29 +00:00
|
|
|
Uri _vmServiceWsUri;
|
2018-07-25 16:29:35 +00:00
|
|
|
bool _hasExited = false;
|
2018-06-27 07:04:55 +00:00
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
VmService _vmService;
|
2018-06-27 07:04:55 +00:00
|
|
|
String get lastErrorInfo => _errorBuffer.toString();
|
2018-10-30 16:59:49 +00:00
|
|
|
Stream<String> get stdout => _stdout.stream;
|
2018-11-08 07:55:54 +00:00
|
|
|
int get vmServicePort => _vmServiceWsUri.port;
|
2018-07-25 16:29:35 +00:00
|
|
|
bool get hasExited => _hasExited;
|
2018-06-27 07:04:55 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
String lastTime = '';
|
|
|
|
void _debugPrint(String message, { String topic = '' }) {
|
|
|
|
const int maxLength = 2500;
|
|
|
|
final String truncatedMessage = message.length > maxLength ? message.substring(0, maxLength) + '...' : message;
|
|
|
|
final String line = '${topic.padRight(10)} $truncatedMessage';
|
|
|
|
_allMessages.add(line);
|
|
|
|
final int timeInSeconds = DateTime.now().difference(startTime).inSeconds;
|
|
|
|
String time = timeInSeconds.toString().padLeft(5) + 's ';
|
|
|
|
if (time == lastTime) {
|
|
|
|
time = ' ' * time.length;
|
|
|
|
} else {
|
|
|
|
lastTime = time;
|
2018-07-12 17:17:33 +00:00
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
if (_printDebugOutputToStdOut)
|
|
|
|
print('$time$_logPrefix$line');
|
2018-07-19 12:38:29 +00:00
|
|
|
}
|
2018-07-12 17:17:33 +00:00
|
|
|
|
2018-10-24 06:21:36 +00:00
|
|
|
Future<void> _setupProcess(
|
2019-01-19 08:31:05 +00:00
|
|
|
List<String> arguments, {
|
2019-01-10 15:33:59 +00:00
|
|
|
String script,
|
2018-10-24 06:21:36 +00:00
|
|
|
bool withDebugger = false,
|
|
|
|
File pidFile,
|
|
|
|
}) async {
|
2018-07-19 12:38:29 +00:00
|
|
|
final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
|
2019-01-19 08:31:05 +00:00
|
|
|
if (withDebugger)
|
2019-01-23 14:43:27 +00:00
|
|
|
arguments.add('--start-paused');
|
2019-01-19 08:31:05 +00:00
|
|
|
if (_printDebugOutputToStdOut)
|
2019-01-23 14:43:27 +00:00
|
|
|
arguments.add('--verbose');
|
2018-10-24 06:21:36 +00:00
|
|
|
if (pidFile != null) {
|
2019-01-23 14:43:27 +00:00
|
|
|
arguments.addAll(<String>['--pid-file', pidFile.path]);
|
2018-10-24 06:21:36 +00:00
|
|
|
}
|
2019-01-10 15:33:59 +00:00
|
|
|
if (script != null) {
|
2019-01-19 08:31:05 +00:00
|
|
|
arguments.add(script);
|
2019-01-10 15:33:59 +00:00
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Spawning flutter $arguments in ${_projectFolder.path}');
|
2018-07-20 06:03:58 +00:00
|
|
|
|
2018-08-02 10:02:32 +00:00
|
|
|
const ProcessManager _processManager = LocalProcessManager();
|
2019-01-19 08:31:05 +00:00
|
|
|
_process = await _processManager.start(
|
|
|
|
<String>[flutterBin]
|
|
|
|
.followedBy(arguments)
|
|
|
|
.toList(),
|
|
|
|
workingDirectory: _projectFolder.path,
|
|
|
|
environment: <String, String>{'FLUTTER_TEST': 'true'},
|
|
|
|
);
|
2018-07-19 12:38:29 +00:00
|
|
|
|
2018-08-31 03:57:44 +00:00
|
|
|
// This class doesn't use the result of the future. It's made available
|
|
|
|
// via a getter for external uses.
|
2019-02-05 23:00:51 +00:00
|
|
|
unawaited(_process.exitCode.then((int code) {
|
2018-08-28 17:26:46 +00:00
|
|
|
_debugPrint('Process exited ($code)');
|
|
|
|
_hasExited = true;
|
2019-02-05 23:00:51 +00:00
|
|
|
}));
|
2019-01-19 08:31:05 +00:00
|
|
|
transformToLines(_process.stdout).listen((String line) => _stdout.add(line));
|
|
|
|
transformToLines(_process.stderr).listen((String line) => _stderr.add(line));
|
2018-06-27 07:04:55 +00:00
|
|
|
|
|
|
|
// 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.
|
2019-01-19 08:31:05 +00:00
|
|
|
_stdout.stream.listen((String message) => _debugPrint(message, topic: '<=stdout='));
|
|
|
|
_stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
|
2018-07-19 10:37:40 +00:00
|
|
|
}
|
|
|
|
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<void> connectToVmService({ bool pauseOnExceptions = false }) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
_vmService = await vmServiceConnectUri('$_vmServiceWsUri');
|
|
|
|
_vmService.onSend.listen((String s) => _debugPrint(s, topic: '=vm=>'));
|
|
|
|
_vmService.onReceive.listen((String s) => _debugPrint(s, topic: '<=vm='));
|
|
|
|
_vmService.onIsolateEvent.listen((Event event) {
|
|
|
|
if (event.kind == EventKind.kIsolateExit && event.isolate.id == _flutterIsolateId) {
|
|
|
|
// Hot restarts cause all the isolates to exit, so we need to refresh
|
|
|
|
// our idea of what the Flutter isolate ID is.
|
|
|
|
_flutterIsolateId = null;
|
2019-01-10 15:33:59 +00:00
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
await Future.wait(<Future<Success>>[
|
|
|
|
_vmService.streamListen('Isolate'),
|
|
|
|
_vmService.streamListen('Debug'),
|
|
|
|
]);
|
|
|
|
|
|
|
|
await waitForPause();
|
|
|
|
if (pauseOnExceptions) {
|
|
|
|
await _vmService.setExceptionPauseMode(
|
|
|
|
await _getFlutterIsolateId(),
|
|
|
|
ExceptionPauseMode.kUnhandled,
|
|
|
|
);
|
|
|
|
}
|
2019-01-10 15:33:59 +00:00
|
|
|
}
|
|
|
|
|
2018-08-22 16:13:34 +00:00
|
|
|
Future<int> quit() => _killGracefully();
|
|
|
|
|
2018-07-19 10:37:40 +00:00
|
|
|
Future<int> _killGracefully() async {
|
2019-01-19 08:31:05 +00:00
|
|
|
if (_processPid == null)
|
2018-07-19 10:37:40 +00:00
|
|
|
return -1;
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Sending SIGTERM to $_processPid..');
|
|
|
|
Process.killPid(_processPid);
|
|
|
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
|
2018-07-19 10:37:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<int> _killForcefully() {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Sending SIGKILL to $_processPid..');
|
|
|
|
Process.killPid(_processPid, ProcessSignal.SIGKILL);
|
|
|
|
return _process.exitCode;
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
String _flutterIsolateId;
|
|
|
|
Future<String> _getFlutterIsolateId() async {
|
2018-07-25 16:29:35 +00:00
|
|
|
// Currently these tests only have a single isolate. If this
|
|
|
|
// ceases to be the case, this code will need changing.
|
2018-11-08 07:55:54 +00:00
|
|
|
if (_flutterIsolateId == null) {
|
|
|
|
final VM vm = await _vmService.getVM();
|
|
|
|
_flutterIsolateId = vm.isolates.first.id;
|
|
|
|
}
|
|
|
|
return _flutterIsolateId;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Isolate> _getFlutterIsolate() async {
|
|
|
|
final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId());
|
|
|
|
return isolate;
|
2018-07-25 16:29:35 +00:00
|
|
|
}
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
/// Add a breakpoint and wait for it to trip the program execution.
|
|
|
|
///
|
|
|
|
/// Only call this when you are absolutely sure that the program under test
|
|
|
|
/// will hit the breakpoint _in the future_.
|
|
|
|
///
|
|
|
|
/// In particular, do not call this if the program is currently racing to pass
|
|
|
|
/// the line of code you are breaking on. Pretend that calling this will take
|
|
|
|
/// an hour before setting the breakpoint. Would the code still eventually hit
|
|
|
|
/// the breakpoint and stop?
|
|
|
|
Future<void> breakAt(Uri uri, int line) async {
|
|
|
|
await addBreakpoint(uri, line);
|
|
|
|
await waitForPause();
|
|
|
|
}
|
|
|
|
|
2018-10-19 11:51:31 +00:00
|
|
|
Future<void> addBreakpoint(Uri uri, int line) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Sending breakpoint for: $uri:$line');
|
2018-11-08 07:55:54 +00:00
|
|
|
await _vmService.addBreakpointWithScriptUri(
|
2019-01-19 08:31:05 +00:00
|
|
|
await _getFlutterIsolateId(),
|
|
|
|
uri.toString(),
|
|
|
|
line,
|
|
|
|
);
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
// This method isn't racy. If the isolate is already paused,
|
|
|
|
// it will immediately return.
|
2018-11-08 07:55:54 +00:00
|
|
|
Future<Isolate> waitForPause() async {
|
2019-01-19 08:31:05 +00:00
|
|
|
return _timeoutWithMessages<Isolate>(
|
|
|
|
() async {
|
|
|
|
final String flutterIsolate = await _getFlutterIsolateId();
|
|
|
|
final Completer<Event> pauseEvent = Completer<Event>();
|
|
|
|
|
|
|
|
// Start listening for pause events.
|
|
|
|
final StreamSubscription<Event> pauseSubscription = _vmService.onDebugEvent
|
|
|
|
.where((Event event) {
|
|
|
|
return event.isolate.id == flutterIsolate
|
|
|
|
&& event.kind.startsWith('Pause');
|
|
|
|
})
|
|
|
|
.listen((Event event) {
|
|
|
|
if (!pauseEvent.isCompleted)
|
|
|
|
pauseEvent.complete(event);
|
|
|
|
});
|
|
|
|
|
|
|
|
// But also check if the isolate was already paused (only after we've set
|
|
|
|
// up the subscription) to avoid races. If it was paused, we don't need to wait
|
|
|
|
// for the event.
|
|
|
|
final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
|
|
|
|
if (isolate.pauseEvent.kind.startsWith('Pause')) {
|
|
|
|
_debugPrint('Isolate was already paused (${isolate.pauseEvent.kind}).');
|
|
|
|
} else {
|
|
|
|
_debugPrint('Isolate is not already paused, waiting for event to arrive...');
|
|
|
|
await pauseEvent.future;
|
|
|
|
}
|
2018-11-08 07:55:54 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
// Cancel the subscription on either of the above.
|
|
|
|
await pauseSubscription.cancel();
|
2018-11-08 07:55:54 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
return _getFlutterIsolate();
|
|
|
|
},
|
|
|
|
task: 'Waiting for isolate to pause',
|
|
|
|
);
|
2018-12-21 02:46:36 +00:00
|
|
|
}
|
2018-12-21 01:10:40 +00:00
|
|
|
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<Isolate> resume({ bool waitForNextPause = false }) => _resume(null, waitForNextPause);
|
|
|
|
Future<Isolate> stepOver({ bool waitForNextPause = true }) => _resume(StepOption.kOver, waitForNextPause);
|
2019-01-19 08:31:05 +00:00
|
|
|
Future<Isolate> stepOverAsync({ bool waitForNextPause = true }) => _resume(StepOption.kOverAsyncSuspension, waitForNextPause);
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<Isolate> stepInto({ bool waitForNextPause = true }) => _resume(StepOption.kInto, waitForNextPause);
|
|
|
|
Future<Isolate> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause);
|
2019-01-19 08:31:05 +00:00
|
|
|
|
2018-12-12 10:56:54 +00:00
|
|
|
Future<bool> isAtAsyncSuspension() async {
|
|
|
|
final Isolate isolate = await _getFlutterIsolate();
|
|
|
|
return isolate.pauseEvent.atAsyncSuspension == true;
|
|
|
|
}
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
Future<Isolate> stepOverOrOverAsyncSuspension({ bool waitForNextPause = true }) async {
|
|
|
|
if (await isAtAsyncSuspension())
|
|
|
|
return await stepOverAsync(waitForNextPause: waitForNextPause);
|
|
|
|
return await stepOver(waitForNextPause: waitForNextPause);
|
2018-12-12 10:56:54 +00:00
|
|
|
}
|
2018-12-21 02:46:36 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
Future<Isolate> _resume(String step, bool waitForNextPause) async {
|
|
|
|
assert(waitForNextPause != null);
|
|
|
|
await _timeoutWithMessages<dynamic>(
|
|
|
|
() async => _vmService.resume(await _getFlutterIsolateId(), step: step),
|
|
|
|
task: 'Resuming isolate (step=$step)',
|
|
|
|
);
|
|
|
|
return waitForNextPause ? waitForPause() : null;
|
2018-07-09 09:41:16 +00:00
|
|
|
}
|
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
Future<InstanceRef> evaluateInFrame(String expression) async {
|
|
|
|
return _timeoutWithMessages<InstanceRef>(
|
2019-01-19 08:31:05 +00:00
|
|
|
() async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
|
|
|
|
task: 'Evaluating expression ($expression)',
|
|
|
|
);
|
2018-07-09 09:41:16 +00:00
|
|
|
}
|
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
Future<InstanceRef> evaluate(String targetId, String expression) async {
|
|
|
|
return _timeoutWithMessages<InstanceRef>(
|
2019-01-19 08:31:05 +00:00
|
|
|
() async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression),
|
|
|
|
task: 'Evaluating expression ($expression for $targetId)',
|
|
|
|
);
|
2018-11-08 07:55:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<Frame> getTopStackFrame() async {
|
|
|
|
final String flutterIsolateId = await _getFlutterIsolateId();
|
|
|
|
final Stack stack = await _vmService.getStack(flutterIsolateId);
|
2018-06-27 07:04:55 +00:00
|
|
|
if (stack.frames.isEmpty) {
|
2018-09-12 06:29:29 +00:00
|
|
|
throw Exception('Stack is empty');
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
2018-07-09 09:41:16 +00:00
|
|
|
return stack.frames.first;
|
|
|
|
}
|
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
Future<SourcePosition> getSourceLocation() async {
|
|
|
|
final String flutterIsolateId = await _getFlutterIsolateId();
|
|
|
|
final Frame frame = await getTopStackFrame();
|
|
|
|
final Script script = await _vmService.getObject(flutterIsolateId, frame.location.script.id);
|
|
|
|
return _lookupTokenPos(script.tokenPosTable, frame.location.tokenPos);
|
|
|
|
}
|
|
|
|
|
|
|
|
SourcePosition _lookupTokenPos(List<List<int>> table, int tokenPos) {
|
|
|
|
for (List<int> row in table) {
|
|
|
|
final int lineNumber = row[0];
|
|
|
|
int index = 1;
|
|
|
|
|
|
|
|
for (index = 1; index < row.length - 1; index += 2) {
|
|
|
|
if (row[index] == tokenPos) {
|
|
|
|
return SourcePosition(lineNumber, row[index + 1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
|
2018-08-21 19:39:35 +00:00
|
|
|
Future<Map<String, dynamic>> _waitFor({
|
|
|
|
String event,
|
|
|
|
int id,
|
2019-01-19 08:31:05 +00:00
|
|
|
Duration timeout = defaultTimeout,
|
2018-08-21 19:39:35 +00:00
|
|
|
bool ignoreAppStopEvent = false,
|
|
|
|
}) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
assert(timeout != null);
|
|
|
|
assert(event != null || id != null);
|
|
|
|
assert(event == null || id == null);
|
|
|
|
final String interestingOccurrence = event != null ? '$event event' : 'response to request $id';
|
2018-09-12 06:29:29 +00:00
|
|
|
final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
|
2019-01-19 08:31:05 +00:00
|
|
|
StreamSubscription<String> subscription;
|
|
|
|
subscription = _stdout.stream.listen((String line) async {
|
2018-12-17 20:38:14 +00:00
|
|
|
final dynamic json = parseFlutterResponse(line);
|
|
|
|
_lastResponse = line;
|
2019-01-19 08:31:05 +00:00
|
|
|
if (json == null)
|
2018-06-27 07:04:55 +00:00
|
|
|
return;
|
2019-01-19 08:31:05 +00:00
|
|
|
if ((event != null && json['event'] == event) ||
|
|
|
|
(id != null && json['id'] == id)) {
|
|
|
|
await subscription.cancel();
|
|
|
|
_debugPrint('OK ($interestingOccurrence)');
|
2018-06-27 07:04:55 +00:00
|
|
|
response.complete(json);
|
2018-08-21 19:39:35 +00:00
|
|
|
} else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
|
2019-01-19 08:31:05 +00:00
|
|
|
await subscription.cancel();
|
2018-09-12 06:29:29 +00:00
|
|
|
final StringBuffer error = StringBuffer();
|
2019-01-19 08:31:05 +00:00
|
|
|
error.write('Received app.stop event while waiting for $interestingOccurrence\n\n');
|
2018-08-21 19:39:35 +00:00
|
|
|
if (json['params'] != null && json['params']['error'] != null) {
|
|
|
|
error.write('${json['params']['error']}\n\n');
|
|
|
|
}
|
|
|
|
if (json['params'] != null && json['params']['trace'] != null) {
|
|
|
|
error.write('${json['params']['trace']}\n\n');
|
|
|
|
}
|
|
|
|
response.completeError(error.toString());
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
});
|
2018-07-20 06:03:58 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
return _timeoutWithMessages(
|
|
|
|
() => response.future,
|
|
|
|
timeout: timeout,
|
|
|
|
task: 'Expecting $interestingOccurrence',
|
|
|
|
).whenComplete(subscription.cancel);
|
2018-07-12 17:17:33 +00:00
|
|
|
}
|
|
|
|
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<T> _timeoutWithMessages<T>(
|
|
|
|
Future<T> Function() callback, {
|
2019-01-19 08:31:05 +00:00
|
|
|
@required String task,
|
|
|
|
Duration timeout = defaultTimeout,
|
|
|
|
}) {
|
|
|
|
assert(task != null);
|
|
|
|
assert(timeout != null);
|
|
|
|
|
|
|
|
if (_printDebugOutputToStdOut) {
|
|
|
|
_debugPrint('$task...');
|
|
|
|
return callback()..timeout(timeout, onTimeout: () {
|
|
|
|
_debugPrint('$task is taking longer than usual...');
|
2019-05-09 19:43:51 +00:00
|
|
|
return null;
|
2019-01-19 08:31:05 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// We're not showing all output to the screen, so let's capture the output
|
|
|
|
// that we would have printed if we were, and output it if we take longer
|
|
|
|
// than the timeout or if we get an error.
|
|
|
|
final StringBuffer messages = StringBuffer('$task\n');
|
2018-09-12 06:29:29 +00:00
|
|
|
final DateTime start = DateTime.now();
|
2019-01-19 08:31:05 +00:00
|
|
|
bool timeoutExpired = false;
|
|
|
|
void logMessage(String logLine) {
|
2018-09-12 06:29:29 +00:00
|
|
|
final int ms = DateTime.now().difference(start).inMilliseconds;
|
2019-01-19 08:31:05 +00:00
|
|
|
final String formattedLine = '[+ ${ms.toString().padLeft(5)}] $logLine';
|
|
|
|
messages.writeln(formattedLine);
|
2018-07-12 17:17:33 +00:00
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
final StreamSubscription<String> subscription = _allMessages.stream.listen(logMessage);
|
2018-12-21 02:46:36 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
final Future<T> future = callback();
|
|
|
|
|
|
|
|
future.timeout(timeout ?? defaultTimeout, onTimeout: () {
|
2019-05-30 02:04:35 +00:00
|
|
|
_debugPrint(messages.toString());
|
2019-01-19 08:31:05 +00:00
|
|
|
timeoutExpired = true;
|
2019-05-30 02:04:35 +00:00
|
|
|
_debugPrint('$task is taking longer than usual...');
|
2019-05-09 19:43:51 +00:00
|
|
|
return null;
|
2019-01-19 08:31:05 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return future.catchError((dynamic error) {
|
|
|
|
if (!timeoutExpired) {
|
|
|
|
timeoutExpired = true;
|
2019-05-30 02:04:35 +00:00
|
|
|
_debugPrint(messages.toString());
|
2019-01-19 08:31:05 +00:00
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}).whenComplete(() => subscription.cancel());
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class FlutterRunTestDriver extends FlutterTestDriver {
|
2019-01-19 08:31:05 +00:00
|
|
|
FlutterRunTestDriver(
|
|
|
|
Directory projectFolder, {
|
|
|
|
String logPrefix,
|
|
|
|
}) : super(projectFolder, logPrefix: logPrefix);
|
2018-11-19 08:25:42 +00:00
|
|
|
|
|
|
|
String _currentRunningAppId;
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
Future<void> run({
|
2018-11-19 08:25:42 +00:00
|
|
|
bool withDebugger = false,
|
2019-01-19 08:31:05 +00:00
|
|
|
bool startPaused = false,
|
2018-11-19 08:25:42 +00:00
|
|
|
bool pauseOnExceptions = false,
|
|
|
|
File pidFile,
|
|
|
|
}) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
await _setupProcess(
|
|
|
|
<String>[
|
2018-11-19 08:25:42 +00:00
|
|
|
'run',
|
2019-05-06 12:09:54 +00:00
|
|
|
'--disable-service-auth-codes',
|
2018-11-19 08:25:42 +00:00
|
|
|
'--machine',
|
|
|
|
'-d',
|
|
|
|
'flutter-tester',
|
2019-01-19 08:31:05 +00:00
|
|
|
],
|
|
|
|
withDebugger: withDebugger,
|
|
|
|
startPaused: startPaused,
|
|
|
|
pauseOnExceptions: pauseOnExceptions,
|
|
|
|
pidFile: pidFile,
|
|
|
|
);
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> attach(
|
|
|
|
int port, {
|
|
|
|
bool withDebugger = false,
|
2019-01-19 08:31:05 +00:00
|
|
|
bool startPaused = false,
|
2018-11-19 08:25:42 +00:00
|
|
|
bool pauseOnExceptions = false,
|
|
|
|
File pidFile,
|
|
|
|
}) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
await _setupProcess(
|
|
|
|
<String>[
|
2018-11-19 08:25:42 +00:00
|
|
|
'attach',
|
|
|
|
'--machine',
|
|
|
|
'-d',
|
|
|
|
'flutter-tester',
|
|
|
|
'--debug-port',
|
|
|
|
'$port',
|
2019-01-19 08:31:05 +00:00
|
|
|
],
|
|
|
|
withDebugger: withDebugger,
|
|
|
|
startPaused: startPaused,
|
|
|
|
pauseOnExceptions: pauseOnExceptions,
|
|
|
|
pidFile: pidFile,
|
|
|
|
);
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> _setupProcess(
|
|
|
|
List<String> args, {
|
2019-01-10 15:33:59 +00:00
|
|
|
String script,
|
2018-11-19 08:25:42 +00:00
|
|
|
bool withDebugger = false,
|
2019-01-19 08:31:05 +00:00
|
|
|
bool startPaused = false,
|
2018-11-19 08:25:42 +00:00
|
|
|
bool pauseOnExceptions = false,
|
|
|
|
File pidFile,
|
|
|
|
}) async {
|
2019-01-19 08:31:05 +00:00
|
|
|
assert(!startPaused || withDebugger);
|
2018-11-19 08:25:42 +00:00
|
|
|
await super._setupProcess(
|
|
|
|
args,
|
2019-01-10 15:33:59 +00:00
|
|
|
script: script,
|
2018-11-19 08:25:42 +00:00
|
|
|
withDebugger: withDebugger,
|
|
|
|
pidFile: pidFile,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Stash the PID so that we can terminate the VM more reliably than using
|
2019-01-19 08:31:05 +00:00
|
|
|
// _process.kill() (`flutter` is a shell script so _process itself is a
|
|
|
|
// shell, not the flutter tool's Dart process).
|
2018-11-19 08:25:42 +00:00
|
|
|
final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
|
2019-01-19 08:31:05 +00:00
|
|
|
_processPid = connected['params']['pid'];
|
2018-11-19 08:25:42 +00:00
|
|
|
|
|
|
|
// 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.
|
2019-01-19 08:31:05 +00:00
|
|
|
final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started', timeout: appStartTimeout);
|
2018-11-19 08:25:42 +00:00
|
|
|
|
|
|
|
if (withDebugger) {
|
2019-01-19 08:31:05 +00:00
|
|
|
final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
|
2018-11-19 08:25:42 +00:00
|
|
|
final String wsUriString = debugPort['params']['wsUri'];
|
|
|
|
_vmServiceWsUri = Uri.parse(wsUriString);
|
2019-01-10 15:33:59 +00:00
|
|
|
await connectToVmService(pauseOnExceptions: pauseOnExceptions);
|
2019-01-19 08:31:05 +00:00
|
|
|
if (!startPaused)
|
|
|
|
await resume(waitForNextPause: false);
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Now await the started event; if it had already happened the future will
|
|
|
|
// have already completed.
|
|
|
|
_currentRunningAppId = (await started)['params']['appId'];
|
|
|
|
}
|
|
|
|
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<void> hotRestart({ bool pause = false }) => _restart(fullRestart: true, pause: pause);
|
2018-11-19 08:25:42 +00:00
|
|
|
Future<void> hotReload() => _restart(fullRestart: false);
|
|
|
|
|
2019-02-21 08:27:07 +00:00
|
|
|
Future<void> _restart({ bool fullRestart = false, bool pause = false }) async {
|
2018-11-19 08:25:42 +00:00
|
|
|
if (_currentRunningAppId == null)
|
|
|
|
throw Exception('App has not started yet');
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Performing ${ pause ? "paused " : "" }${ fullRestart ? "hot restart" : "hot reload" }...');
|
|
|
|
final dynamic hotReloadResponse = await _sendRequest(
|
|
|
|
'app.restart',
|
2019-03-01 07:17:55 +00:00
|
|
|
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
|
2018-11-19 08:25:42 +00:00
|
|
|
);
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('${ fullRestart ? "Hot restart" : "Hot reload" } complete.');
|
2018-11-19 08:25:42 +00:00
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
if (hotReloadResponse == null || hotReloadResponse['code'] != 0)
|
2018-11-19 08:25:42 +00:00
|
|
|
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<int> detach() async {
|
2019-04-19 00:59:14 +00:00
|
|
|
if (_process == null) {
|
|
|
|
return 0;
|
|
|
|
}
|
2018-11-19 08:25:42 +00:00
|
|
|
if (_vmService != null) {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Closing VM service...');
|
2018-11-19 08:25:42 +00:00
|
|
|
_vmService.dispose();
|
|
|
|
}
|
|
|
|
if (_currentRunningAppId != null) {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Detaching from app...');
|
2018-11-19 08:25:42 +00:00
|
|
|
await Future.any<void>(<Future<void>>[
|
2019-01-19 08:31:05 +00:00
|
|
|
_process.exitCode,
|
2018-11-19 08:25:42 +00:00
|
|
|
_sendRequest(
|
|
|
|
'app.detach',
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId},
|
|
|
|
),
|
|
|
|
]).timeout(
|
|
|
|
quitTimeout,
|
|
|
|
onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
|
|
|
|
);
|
|
|
|
_currentRunningAppId = null;
|
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Waiting for process to end...');
|
|
|
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<int> stop() async {
|
|
|
|
if (_vmService != null) {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Closing VM service...');
|
2018-11-19 08:25:42 +00:00
|
|
|
_vmService.dispose();
|
|
|
|
}
|
|
|
|
if (_currentRunningAppId != null) {
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint('Stopping application...');
|
2018-11-19 08:25:42 +00:00
|
|
|
await Future.any<void>(<Future<void>>[
|
2019-01-19 08:31:05 +00:00
|
|
|
_process.exitCode,
|
2018-11-19 08:25:42 +00:00
|
|
|
_sendRequest(
|
|
|
|
'app.stop',
|
|
|
|
<String, dynamic>{'appId': _currentRunningAppId},
|
|
|
|
),
|
|
|
|
]).timeout(
|
|
|
|
quitTimeout,
|
|
|
|
onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
|
|
|
|
);
|
|
|
|
_currentRunningAppId = null;
|
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
if (_process != null) {
|
|
|
|
_debugPrint('Waiting for process to end...');
|
|
|
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
2018-11-19 08:25:42 +00:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-06-27 07:04:55 +00:00
|
|
|
int id = 1;
|
|
|
|
Future<dynamic> _sendRequest(String method, dynamic params) async {
|
|
|
|
final int requestId = id++;
|
2018-08-17 20:17:23 +00:00
|
|
|
final Map<String, dynamic> request = <String, dynamic>{
|
2018-06-27 07:04:55 +00:00
|
|
|
'id': requestId,
|
|
|
|
'method': method,
|
2019-03-01 07:17:55 +00:00
|
|
|
'params': params,
|
2018-06-27 07:04:55 +00:00
|
|
|
};
|
2018-08-17 20:17:23 +00:00
|
|
|
final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
|
2019-01-19 08:31:05 +00:00
|
|
|
_debugPrint(jsonEncoded, topic: '=stdin=>');
|
2018-07-09 09:41:16 +00:00
|
|
|
|
2018-06-27 07:04:55 +00:00
|
|
|
// Set up the response future before we send the request to avoid any
|
2019-01-19 08:31:05 +00:00
|
|
|
// races. If the method we're calling is app.stop then we tell _waitFor not
|
2018-08-21 19:39:35 +00:00
|
|
|
// to throw if it sees an app.stop event before the response to this request.
|
|
|
|
final Future<Map<String, dynamic>> responseFuture = _waitFor(
|
|
|
|
id: requestId,
|
|
|
|
ignoreAppStopEvent: method == 'app.stop',
|
|
|
|
);
|
2019-01-19 08:31:05 +00:00
|
|
|
_process.stdin.writeln(jsonEncoded);
|
2018-08-17 20:17:23 +00:00
|
|
|
final Map<String, dynamic> response = await responseFuture;
|
2018-06-27 07:04:55 +00:00
|
|
|
|
2018-08-17 20:17:23 +00:00
|
|
|
if (response['error'] != null || response['result'] == null)
|
2018-07-09 09:41:16 +00:00
|
|
|
_throwErrorResponse('Unexpected error response');
|
2018-06-27 07:04:55 +00:00
|
|
|
|
2018-08-17 20:17:23 +00:00
|
|
|
return response['result'];
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
|
2019-01-19 08:31:05 +00:00
|
|
|
void _throwErrorResponse(String message) {
|
|
|
|
throw '$message\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
|
2018-07-09 09:41:16 +00:00
|
|
|
}
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
|
|
|
|
2019-01-10 15:33:59 +00:00
|
|
|
class FlutterTestTestDriver extends FlutterTestDriver {
|
2019-01-29 20:47:16 +00:00
|
|
|
FlutterTestTestDriver(Directory _projectFolder, {String logPrefix})
|
|
|
|
: super(_projectFolder, logPrefix: logPrefix);
|
2019-01-10 15:33:59 +00:00
|
|
|
|
|
|
|
Future<void> test({
|
|
|
|
String testFile = 'test/test.dart',
|
|
|
|
bool withDebugger = false,
|
|
|
|
bool pauseOnExceptions = false,
|
|
|
|
File pidFile,
|
|
|
|
Future<void> Function() beforeStart,
|
|
|
|
}) async {
|
|
|
|
await _setupProcess(<String>[
|
|
|
|
'test',
|
2019-05-06 12:09:54 +00:00
|
|
|
'--disable-service-auth-codes',
|
2019-01-10 15:33:59 +00:00
|
|
|
'--machine',
|
|
|
|
'-d',
|
2019-03-01 07:17:55 +00:00
|
|
|
'flutter-tester',
|
2019-01-15 15:39:35 +00:00
|
|
|
], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile, beforeStart: beforeStart);
|
2019-01-10 15:33:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> _setupProcess(
|
|
|
|
List<String> args, {
|
|
|
|
String script,
|
|
|
|
bool withDebugger = false,
|
|
|
|
bool pauseOnExceptions = false,
|
|
|
|
File pidFile,
|
|
|
|
Future<void> Function() beforeStart,
|
|
|
|
}) async {
|
|
|
|
await super._setupProcess(
|
|
|
|
args,
|
|
|
|
script: script,
|
|
|
|
withDebugger: withDebugger,
|
|
|
|
pidFile: pidFile,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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> version = await _waitForJson();
|
2019-01-19 08:31:05 +00:00
|
|
|
_processPid = version['pid'];
|
2019-01-10 15:33:59 +00:00
|
|
|
|
|
|
|
if (withDebugger) {
|
|
|
|
final Map<String, dynamic> startedProcess = await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout);
|
|
|
|
final String vmServiceHttpString = startedProcess['params']['observatoryUri'];
|
|
|
|
_vmServiceWsUri = Uri.parse(vmServiceHttpString).replace(scheme: 'ws', path: '/ws');
|
|
|
|
await connectToVmService(pauseOnExceptions: pauseOnExceptions);
|
|
|
|
// Allow us to run code before we start, eg. to set up breakpoints.
|
|
|
|
if (beforeStart != null) {
|
|
|
|
await beforeStart();
|
|
|
|
}
|
2019-01-19 08:31:05 +00:00
|
|
|
await resume(waitForNextPause: false);
|
2019-01-10 15:33:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Map<String, dynamic>> _waitForJson({
|
|
|
|
Duration timeout,
|
|
|
|
}) async {
|
|
|
|
return _timeoutWithMessages<Map<String, dynamic>>(
|
|
|
|
() => _stdout.stream.map<Map<String, dynamic>>(_parseJsonResponse).first,
|
|
|
|
timeout: timeout,
|
2019-01-19 08:31:05 +00:00
|
|
|
task: 'Waiting for JSON',
|
2019-01-10 15:33:59 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, dynamic> _parseJsonResponse(String line) {
|
|
|
|
try {
|
|
|
|
return json.decode(line);
|
|
|
|
} catch (e) {
|
|
|
|
// Not valid JSON, so likely some other output.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-17 20:38:14 +00:00
|
|
|
Stream<String> transformToLines(Stream<List<int>> byteStream) {
|
2018-10-01 19:29:08 +00:00
|
|
|
return byteStream.transform<String>(utf8.decoder).transform<String>(const LineSplitter());
|
2018-06-27 07:04:55 +00:00
|
|
|
}
|
2018-11-08 07:55:54 +00:00
|
|
|
|
2018-12-17 20:38:14 +00:00
|
|
|
Map<String, dynamic> parseFlutterResponse(String line) {
|
|
|
|
if (line.startsWith('[') && line.endsWith(']')) {
|
|
|
|
try {
|
2019-01-19 08:31:05 +00:00
|
|
|
final Map<String, dynamic> response = json.decode(line)[0];
|
|
|
|
return response;
|
2018-12-17 20:38:14 +00:00
|
|
|
} catch (e) {
|
|
|
|
// Not valid JSON, so likely some other output that was surrounded by [brackets]
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-11-08 07:55:54 +00:00
|
|
|
class SourcePosition {
|
|
|
|
SourcePosition(this.line, this.column);
|
|
|
|
|
|
|
|
final int line;
|
|
|
|
final int column;
|
|
|
|
}
|