blob: b3340db461fe24142f44721d3f56879d1311d2a3 [file] [log] [blame]
// Copyright 2014 The Flutter 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:dds/dds.dart' as dds;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../application_package.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../device.dart';
import '../resident_runner.dart';
import '../sksl_writer.dart';
import '../vmservice.dart';
import 'web_driver_service.dart';
class FlutterDriverFactory {
FlutterDriverFactory({
required ApplicationPackageFactory applicationPackageFactory,
required Logger logger,
required ProcessUtils processUtils,
required String dartSdkPath,
required DevtoolsLauncher devtoolsLauncher,
}) : _applicationPackageFactory = applicationPackageFactory,
_logger = logger,
_processUtils = processUtils,
_dartSdkPath = dartSdkPath,
_devtoolsLauncher = devtoolsLauncher;
final ApplicationPackageFactory _applicationPackageFactory;
final Logger _logger;
final ProcessUtils _processUtils;
final String _dartSdkPath;
final DevtoolsLauncher _devtoolsLauncher;
/// Create a driver service for running `flutter drive`.
DriverService createDriverService(bool web) {
if (web) {
return WebDriverService(
logger: _logger,
processUtils: _processUtils,
dartSdkPath: _dartSdkPath,
);
}
return FlutterDriverService(
logger: _logger,
processUtils: _processUtils,
dartSdkPath: _dartSdkPath,
applicationPackageFactory: _applicationPackageFactory,
devtoolsLauncher: _devtoolsLauncher,
);
}
}
/// An interface for the `flutter driver` integration test operations.
abstract class DriverService {
/// Install and launch the application for the provided [device].
Future<void> start(
BuildInfo buildInfo,
Device device,
DebuggingOptions debuggingOptions,
bool ipv6, {
File? applicationBinary,
String? route,
String? userIdentifier,
String? mainPath,
Map<String, Object> platformArgs = const <String, Object>{},
});
/// If --use-existing-app is provided, configured the correct VM Service URI.
Future<void> reuseApplication(
Uri vmServiceUri,
Device device,
DebuggingOptions debuggingOptions,
bool ipv6,
);
/// Start the test file with the provided [arguments] and [environment], returning
/// the test process exit code.
///
/// if [profileMemory] is provided, it will be treated as a file path to write a
/// devtools memory profile.
Future<int> startTest(
String testFile,
List<String> arguments,
Map<String, String> environment,
PackageConfig packageConfig, {
bool? headless,
String? chromeBinary,
String? browserName,
bool? androidEmulator,
int? driverPort,
List<String> webBrowserFlags,
List<String>? browserDimension,
String? profileMemory,
});
/// Stop the running application and uninstall it from the device.
///
/// If [writeSkslOnExit] is non-null, will connect to the VM Service
/// and write SkSL to the file. This is only supported on mobile and
/// desktop devices.
Future<void> stop({
File? writeSkslOnExit,
String? userIdentifier,
});
}
/// An implementation of the driver service that connects to mobile and desktop
/// applications.
class FlutterDriverService extends DriverService {
FlutterDriverService({
required ApplicationPackageFactory applicationPackageFactory,
required Logger logger,
required ProcessUtils processUtils,
required String dartSdkPath,
required DevtoolsLauncher devtoolsLauncher,
@visibleForTesting VMServiceConnector vmServiceConnector = connectToVmService,
}) : _applicationPackageFactory = applicationPackageFactory,
_logger = logger,
_processUtils = processUtils,
_dartSdkPath = dartSdkPath,
_vmServiceConnector = vmServiceConnector,
_devtoolsLauncher = devtoolsLauncher;
static const int _kLaunchAttempts = 3;
final ApplicationPackageFactory _applicationPackageFactory;
final Logger _logger;
final ProcessUtils _processUtils;
final String _dartSdkPath;
final VMServiceConnector _vmServiceConnector;
final DevtoolsLauncher _devtoolsLauncher;
Device? _device;
ApplicationPackage? _applicationPackage;
late String _vmServiceUri;
late FlutterVmService _vmService;
@override
Future<void> start(
BuildInfo buildInfo,
Device device,
DebuggingOptions debuggingOptions,
bool ipv6, {
File? applicationBinary,
String? route,
String? userIdentifier,
Map<String, Object> platformArgs = const <String, Object>{},
String? mainPath,
}) async {
if (buildInfo.isRelease) {
throwToolExit(
'Flutter Driver (non-web) 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).'
);
}
_device = device;
final TargetPlatform targetPlatform = await device.targetPlatform;
_applicationPackage = await _applicationPackageFactory.getPackageForPlatform(
targetPlatform,
buildInfo: buildInfo,
applicationBinary: applicationBinary,
);
int attempt = 0;
LaunchResult? result;
bool prebuiltApplication = applicationBinary != null;
while (attempt < _kLaunchAttempts) {
result = await device.startApp(
_applicationPackage,
mainPath: mainPath,
route: route,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs,
userIdentifier: userIdentifier,
prebuiltApplication: prebuiltApplication,
);
if (result.started) {
break;
}
// On attempts past 1, assume the application is built correctly and re-use it.
attempt += 1;
prebuiltApplication = true;
_logger.printError('Application failed to start on attempt: $attempt');
}
if (result == null || !result.started) {
throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
}
return reuseApplication(
result.vmServiceUri!,
device,
debuggingOptions,
ipv6,
);
}
@override
Future<void> reuseApplication(
Uri vmServiceUri,
Device device,
DebuggingOptions debuggingOptions,
bool ipv6,
) async {
Uri uri;
if (vmServiceUri.scheme == 'ws') {
final List<String> segments = vmServiceUri.pathSegments.toList();
segments.remove('ws');
uri = vmServiceUri.replace(scheme: 'http', path: segments.join('/'));
} else {
uri = vmServiceUri;
}
_vmServiceUri = uri.toString();
_device = device;
if (debuggingOptions.enableDds) {
try {
await device.dds.startDartDevelopmentService(
uri,
hostPort: debuggingOptions.ddsPort,
ipv6: ipv6,
disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes,
logger: _logger,
);
_vmServiceUri = device.dds.uri.toString();
} on dds.DartDevelopmentServiceException {
// If there's another flutter_tools instance still connected to the target
// application, DDS will already be running remotely and this call will fail.
// This can be ignored to continue to use the existing remote DDS instance.
}
}
_vmService = await _vmServiceConnector(uri, device: _device, logger: _logger);
final DeviceLogReader logReader = await device.getLogReader(app: _applicationPackage);
logReader.logLines.listen(_logger.printStatus);
final vm_service.VM vm = await _vmService.service.getVM();
logReader.appPid = vm.pid;
}
@override
Future<int> startTest(
String testFile,
List<String> arguments,
Map<String, String> environment,
PackageConfig packageConfig, {
bool? headless,
String? chromeBinary,
String? browserName,
bool? androidEmulator,
int? driverPort,
List<String> webBrowserFlags = const <String>[],
List<String>? browserDimension,
String? profileMemory,
}) async {
if (profileMemory != null) {
unawaited(_devtoolsLauncher.launch(
Uri.parse(_vmServiceUri),
additionalArguments: <String>['--record-memory-profile=$profileMemory'],
));
// When profiling memory the original launch future will never complete.
await _devtoolsLauncher.processStart;
}
try {
final int result = await _processUtils.stream(<String>[
_dartSdkPath,
...<String>[...arguments, testFile, '-rexpanded'],
], environment: <String, String>{
'VM_SERVICE_URL': _vmServiceUri,
...environment,
});
return result;
} finally {
if (profileMemory != null) {
await _devtoolsLauncher.close();
}
}
}
@override
Future<void> stop({
File? writeSkslOnExit,
String? userIdentifier,
}) async {
if (writeSkslOnExit != null) {
final FlutterView flutterView = (await _vmService.getFlutterViews()).first;
final Map<String, Object?>? result = await _vmService.getSkSLs(
viewId: flutterView.id
);
await sharedSkSlWriter(_device!, result, outputFile: writeSkslOnExit, logger: _logger);
}
// If the application package is available, stop and uninstall.
final ApplicationPackage? package = _applicationPackage;
if (package != null) {
if (!await _device!.stopApp(package, userIdentifier: userIdentifier)) {
_logger.printError('Failed to stop app');
}
if (!await _device!.uninstallApp(package, userIdentifier: userIdentifier)) {
_logger.printError('Failed to uninstall app');
}
} else if (_device!.supportsFlutterExit) {
// Otherwise use the VM Service URI to stop the app as a best effort approach.
final vm_service.VM vm = await _vmService.service.getVM();
final vm_service.IsolateRef isolateRef = vm.isolates!
.firstWhere((vm_service.IsolateRef element) {
return !element.isSystemIsolate!;
});
unawaited(_vmService.flutterExit(isolateId: isolateRef.id!));
} else {
_logger.printTrace('No application package for $_device, leaving app running');
}
await _device!.dispose();
}
}