// 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 != null && 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.observatoryUri!, 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. if (_applicationPackage != null) { if (!await _device!.stopApp(_applicationPackage, userIdentifier: userIdentifier)) { _logger.printError('Failed to stop app'); } if (!await _device!.uninstallApp(_applicationPackage!, 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(); } }