1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// Copyright 2016 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:io' as io;
import 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable; // ignore: implementation_imports
import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../dart/sdk.dart';
import '../device.dart';
import '../globals.dart';
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
import 'build_apk.dart' as build_apk;
import 'run.dart';
/// Runs integration (a.k.a. end-to-end) tests.
///
/// An integration test is a program that runs in a separate process from your
/// Flutter application. It connects to the application and acts like a user,
/// performing taps, scrolls, reading out widget properties and verifying their
/// correctness.
///
/// This command takes a target Flutter application that you would like to test
/// as the `--target` option (defaults to `lib/main.dart`). It then looks for a
/// corresponding test file within the `test_driver` directory. The test file is
/// expected to have the same name but contain the `_test.dart` suffix. The
/// `_test.dart` file would generall be a Dart program that uses
/// `package:flutter_driver` and exercises your application. Most commonly it
/// is a test written using `package:test`, but you are free to use something
/// else.
///
/// The app and the test are launched simultaneously. Once the test completes
/// the application is stopped and the command exits. If all these steps are
/// successful the exit code will be `0`. Otherwise, you will see a non-zero
/// exit code.
class DriveCommand extends RunCommandBase {
DriveCommand() {
argParser.addFlag(
'keep-app-running',
negatable: true,
defaultsTo: false,
help:
'Will keep the Flutter application running when done testing. By '
'default Flutter Driver stops the application after tests are finished.'
);
argParser.addFlag(
'use-existing-app',
negatable: true,
defaultsTo: false,
help:
'Will not start a new Flutter application but connect to an '
'already running instance. This will also cause the driver to keep '
'the application running after tests are done.'
);
argParser.addOption('debug-port',
defaultsTo: kDefaultDrivePort.toString(),
help: 'Listen to the given port for a debug connection.'
);
}
@override
final String name = 'drive';
@override
final String description = 'Runs Flutter Driver tests for the current project.';
@override
final List<String> aliases = <String>['driver'];
Device _device;
Device get device => _device;
int get debugPort => int.parse(argResults['debug-port']);
@override
Future<int> runInProject() async {
String testFile = _getTestFile();
if (testFile == null) {
return 1;
}
this._device = await targetDeviceFinder();
if (device == null) {
return 1;
}
if (await fs.type(testFile) != FileSystemEntityType.FILE) {
printError('Test file not found: $testFile');
return 1;
}
if (!argResults['use-existing-app']) {
printStatus('Starting application: ${argResults["target"]}');
if (getBuildMode() == BuildMode.release) {
// This is because we need VM service to be able to drive the app.
printError(
'Flutter Driver does not support running in release mode.\n'
'\n'
'Use --profile mode for testing application performance.\n'
'Use --debug (default) mode for testing correctness (with assertions).'
);
return 1;
}
int result = await appStarter(this);
if (result != 0) {
printError('Application failed to start. Will not run test. Quitting.');
return result;
}
} else {
printStatus('Will connect to already running application instance.');
}
// Check for the existance of a `packages/` directory; pub test does not yet
// support running without symlinks.
if (!new io.Directory('packages').existsSync()) {
Status status = logger.startProgress(
'Missing packages directory; running `pub get` (to work around https://github.com/dart-lang/test/issues/327):'
);
await runAsync(<String>[sdkBinaryName('pub'), 'get', '--no-precompile']);
status.stop(showElapsedTime: true);
}
try {
return await testRunner(<String>[testFile])
.catchError((dynamic error, dynamic stackTrace) {
printError('CAUGHT EXCEPTION: $error\n$stackTrace');
return 1;
});
} finally {
if (!argResults['keep-app-running'] && !argResults['use-existing-app']) {
printStatus('Stopping application instance.');
try {
await appStopper(this);
} catch(error, stackTrace) {
// TODO(yjbanov): remove this guard when this bug is fixed: https://github.com/dart-lang/sdk/issues/25862
printTrace('Could not stop application: $error\n$stackTrace');
}
} else {
printStatus('Leaving the application running.');
}
}
}
String _getTestFile() {
String appFile = path.normalize(target);
// This command extends `flutter start` and therefore CWD == package dir
String packageDir = getCurrentDirectory();
// Make appFile path relative to package directory because we are looking
// for the corresponding test file relative to it.
if (!path.isRelative(appFile)) {
if (!path.isWithin(packageDir, appFile)) {
printError(
'Application file $appFile is outside the package directory $packageDir'
);
return null;
}
appFile = path.relative(appFile, from: packageDir);
}
List<String> parts = path.split(appFile);
if (parts.length < 2) {
printError(
'Application file $appFile must reside in one of the sub-directories '
'of the package structure, not in the root directory.'
);
return null;
}
// Look for the test file inside `test_driver/` matching the sub-path, e.g.
// if the application is `lib/foo/bar.dart`, the test file is expected to
// be `test_driver/foo/bar_test.dart`.
String pathWithNoExtension = path.withoutExtension(path.joinAll(
<String>[packageDir, 'test_driver']..addAll(parts.skip(1))));
return '${pathWithNoExtension}_test${path.extension(appFile)}';
}
}
/// Finds a device to test on. May launch a simulator, if necessary.
typedef Future<Device> TargetDeviceFinder();
TargetDeviceFinder targetDeviceFinder = findTargetDevice;
void restoreTargetDeviceFinder() {
targetDeviceFinder = findTargetDevice;
}
Future<Device> findTargetDevice() async {
if (deviceManager.hasSpecifiedDeviceId) {
return deviceManager.getDeviceById(deviceManager.specifiedDeviceId);
}
List<Device> devices = await deviceManager.getAllConnectedDevices();
if (os.isMacOS) {
// On Mac we look for the iOS Simulator. If available, we use that. Then
// we look for an Android device. If there's one, we use that. Otherwise,
// we launch a new iOS Simulator.
Device reusableDevice = devices.firstWhere(
(Device d) => d.isLocalEmulator,
orElse: () {
return devices.firstWhere((Device d) => d is AndroidDevice,
orElse: () => null);
}
);
if (reusableDevice != null) {
printStatus('Found connected ${reusableDevice.isLocalEmulator ? "emulator" : "device"} "${reusableDevice.name}"; will reuse it.');
return reusableDevice;
}
// No running emulator found. Attempt to start one.
printStatus('Starting iOS Simulator, because did not find existing connected devices.');
bool started = await SimControl.instance.boot();
if (started) {
return IOSSimulatorUtils.instance.getAttachedDevices().first;
} else {
printError('Failed to start iOS Simulator.');
return null;
}
} else if (os.isLinux) {
// On Linux, for now, we just grab the first connected device we can find.
if (devices.isEmpty) {
printError('No devices found.');
return null;
} else if (devices.length > 1) {
printStatus('Found multiple connected devices:');
printStatus(devices.map((Device d) => ' - ${d.name}\n').join(''));
}
printStatus('Using device ${devices.first.name}.');
return devices.first;
} else if (os.isWindows) {
printError('Windows is not yet supported.');
return null;
} else {
printError('The operating system on this computer is not supported.');
return null;
}
}
/// Starts the application on the device given command configuration.
typedef Future<int> AppStarter(DriveCommand command);
AppStarter appStarter = startApp;
void restoreAppStarter() {
appStarter = startApp;
}
Future<int> startApp(DriveCommand command) async {
String mainPath = findMainDartFile(command.target);
if (await fs.type(mainPath) != FileSystemEntityType.FILE) {
printError('Tried to run $mainPath, but that file does not exist.');
return 1;
}
// TODO(devoncarew): We should remove the need to special case here.
if (command.device is AndroidDevice) {
printTrace('Building an APK.');
int result = await build_apk.buildApk(
command.device.platform,
target: command.target,
buildMode: command.getBuildMode()
);
if (result != 0)
return result;
}
printTrace('Stopping previously running application, if any.');
await appStopper(command);
printTrace('Installing application package.');
ApplicationPackage package = command.applicationPackages
.getPackageForPlatform(command.device.platform);
if (command.device.isAppInstalled(package))
command.device.uninstallApp(package);
command.device.installApp(package);
Map<String, dynamic> platformArgs = <String, dynamic>{};
if (command.traceStartup)
platformArgs['trace-startup'] = command.traceStartup;
printTrace('Starting application.');
LaunchResult result = await command.device.startApp(
package,
command.getBuildMode(),
mainPath: mainPath,
route: command.route,
debuggingOptions: new DebuggingOptions.enabled(
command.getBuildMode(),
startPaused: true,
observatoryPort: command.debugPort
),
platformArgs: platformArgs
);
return result.started ? 0 : 2;
}
/// Runs driver tests.
typedef Future<int> TestRunner(List<String> testArgs);
TestRunner testRunner = runTests;
void restoreTestRunner() {
testRunner = runTests;
}
Future<int> runTests(List<String> testArgs) async {
printTrace('Running driver tests.');
List<String> args = testArgs.toList()..add('-rexpanded');
await executable.main(args);
return io.exitCode;
}
/// Stops the application.
typedef Future<int> AppStopper(DriveCommand command);
AppStopper appStopper = stopApp;
void restoreAppStopper() {
appStopper = stopApp;
}
Future<int> stopApp(DriveCommand command) async {
printTrace('Stopping application.');
ApplicationPackage package = command.applicationPackages.getPackageForPlatform(command.device.platform);
bool stopped = await command.device.stopApp(package);
return stopped ? 0 : 1;
}