blob: fd882f1007ecd8c140d03a1701c21e78e26a4a75 [file] [log] [blame]
// 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 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable;
import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/os.dart';
import '../device.dart';
import '../globals.dart';
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
import 'apk.dart' as 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: observatoryDefaultPort.toString(),
help: 'Listen to the given port for a debug connection.');
}
final String name = 'drive';
final String description = 'Runs Flutter Driver tests for the current project.';
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 {
await toolchainDownloader(this);
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"]}');
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.');
}
try {
return await testRunner([testFile])
.then((_) => 0)
.catchError((error, 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(
[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(
(d) => d.isLocalEmulator,
orElse: () {
return devices.firstWhere((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((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;
}
if (command.device is AndroidDevice) {
printTrace('Building an APK.');
int result = await apk.build(command.toolchain, command.buildConfigurations,
enginePath: command.runner.enginePath, target: command.target);
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);
await command.device.installApp(package);
printTrace('Starting application.');
bool started = await command.device.startApp(
package,
command.toolchain,
mainPath: mainPath,
route: command.route,
checked: command.checked,
clearLogs: true,
startPaused: true,
debugPort: command.debugPort,
platformArgs: <String, dynamic>{
'trace-startup': command.traceStartup,
}
);
if (command.device.supportsStartPaused) {
await delayUntilObservatoryAvailable('localhost', command.debugPort);
}
return started ? 0 : 2;
}
/// Runs driver tests.
typedef Future<Null> TestRunner(List<String> testArgs);
TestRunner testRunner = runTests;
void restoreTestRunner() {
testRunner = runTests;
}
Future<Null> runTests(List<String> testArgs) {
printTrace('Running driver tests.');
return executable.main(testArgs);
}
/// 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;
}
/// Downloads Flutter toolchain.
typedef Future<Null> ToolchainDownloader(DriveCommand command);
ToolchainDownloader toolchainDownloader = downloadToolchain;
void restoreToolchainDownloader() {
toolchainDownloader = downloadToolchain;
}
Future<Null> downloadToolchain(DriveCommand command) async {
printTrace('Downloading toolchain.');
await Future.wait([
command.downloadToolchain(),
command.downloadApplicationPackagesAndConnectToDevices(),
], eagerError: true);
}