drive_service.dart 10.3 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:async';

7 8 9
import 'package:dds/dds.dart' as dds;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
10
import 'package:package_config/package_config_types.dart';
11 12 13 14 15 16 17 18
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';
19
import '../resident_runner.dart';
20
import '../sksl_writer.dart';
21 22 23 24 25
import '../vmservice.dart';
import 'web_driver_service.dart';

class FlutterDriverFactory {
  FlutterDriverFactory({
26 27 28 29 30
    required ApplicationPackageFactory applicationPackageFactory,
    required Logger logger,
    required ProcessUtils processUtils,
    required String dartSdkPath,
    required DevtoolsLauncher devtoolsLauncher,
31 32 33
  }) : _applicationPackageFactory = applicationPackageFactory,
       _logger = logger,
       _processUtils = processUtils,
34 35
       _dartSdkPath = dartSdkPath,
       _devtoolsLauncher = devtoolsLauncher;
36 37 38 39 40

  final ApplicationPackageFactory _applicationPackageFactory;
  final Logger _logger;
  final ProcessUtils _processUtils;
  final String _dartSdkPath;
41
  final DevtoolsLauncher _devtoolsLauncher;
42 43 44 45 46

  /// Create a driver service for running `flutter drive`.
  DriverService createDriverService(bool web) {
    if (web) {
      return WebDriverService(
47
        logger: _logger,
48 49 50 51 52 53 54 55 56
        processUtils: _processUtils,
        dartSdkPath: _dartSdkPath,
      );
    }
    return FlutterDriverService(
      logger: _logger,
      processUtils: _processUtils,
      dartSdkPath: _dartSdkPath,
      applicationPackageFactory: _applicationPackageFactory,
57
      devtoolsLauncher: _devtoolsLauncher,
58 59 60 61 62 63 64 65 66 67 68 69
    );
  }
}

/// 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, {
70 71 72 73
    File? applicationBinary,
    String? route,
    String? userIdentifier,
    String? mainPath,
74 75 76
    Map<String, Object> platformArgs = const <String, Object>{},
  });

77 78 79 80 81 82 83 84
  /// If --use-existing-app is provided, configured the correct VM Service URI.
  Future<void> reuseApplication(
    Uri vmServiceUri,
    Device device,
    DebuggingOptions debuggingOptions,
    bool ipv6,
  );

85 86
  /// Start the test file with the provided [arguments] and [environment], returning
  /// the test process exit code.
87 88 89
  ///
  /// if [profileMemory] is provided, it will be treated as a file path to write a
  /// devtools memory profile.
90 91 92
  Future<int> startTest(
    String testFile,
    List<String> arguments,
93 94
    Map<String, String> environment,
    PackageConfig packageConfig, {
95 96 97 98 99
    bool? headless,
    String? chromeBinary,
    String? browserName,
    bool? androidEmulator,
    int? driverPort,
100
    List<String> webBrowserFlags,
101 102
    List<String>? browserDimension,
    String? profileMemory,
103 104 105 106 107 108 109 110
  });

  /// 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({
111 112
    File? writeSkslOnExit,
    String? userIdentifier,
113 114 115 116 117 118 119
  });
}

/// An implementation of the driver service that connects to mobile and desktop
/// applications.
class FlutterDriverService extends DriverService {
  FlutterDriverService({
120 121 122 123 124
    required ApplicationPackageFactory applicationPackageFactory,
    required Logger logger,
    required ProcessUtils processUtils,
    required String dartSdkPath,
    required DevtoolsLauncher devtoolsLauncher,
125 126 127 128 129
    @visibleForTesting VMServiceConnector vmServiceConnector = connectToVmService,
  }) : _applicationPackageFactory = applicationPackageFactory,
       _logger = logger,
       _processUtils = processUtils,
       _dartSdkPath = dartSdkPath,
130 131
       _vmServiceConnector = vmServiceConnector,
       _devtoolsLauncher = devtoolsLauncher;
132 133 134 135 136 137 138 139

  static const int _kLaunchAttempts = 3;

  final ApplicationPackageFactory _applicationPackageFactory;
  final Logger _logger;
  final ProcessUtils _processUtils;
  final String _dartSdkPath;
  final VMServiceConnector _vmServiceConnector;
140
  final DevtoolsLauncher _devtoolsLauncher;
141

142 143 144 145
  Device? _device;
  ApplicationPackage? _applicationPackage;
  late String _vmServiceUri;
  late FlutterVmService _vmService;
146 147 148 149 150 151 152

  @override
  Future<void> start(
    BuildInfo buildInfo,
    Device device,
    DebuggingOptions debuggingOptions,
    bool ipv6, {
153 154 155
    File? applicationBinary,
    String? route,
    String? userIdentifier,
156
    Map<String, Object> platformArgs = const <String, Object>{},
157
    String? mainPath,
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
  }) 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;
175
    LaunchResult? result;
176 177 178 179 180 181 182 183 184 185 186
    bool prebuiltApplication = applicationBinary != null;
    while (attempt < _kLaunchAttempts) {
      result = await device.startApp(
        _applicationPackage,
        mainPath: mainPath,
        route: route,
        debuggingOptions: debuggingOptions,
        platformArgs: platformArgs,
        userIdentifier: userIdentifier,
        prebuiltApplication: prebuiltApplication,
      );
187
      if (result.started) {
188 189 190 191 192 193 194 195 196 197
        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);
    }
198
    return reuseApplication(
199
      result.vmServiceUri!,
200 201 202 203 204 205 206 207 208 209 210 211 212
      device,
      debuggingOptions,
      ipv6,
    );
  }

  @override
  Future<void> reuseApplication(
    Uri vmServiceUri,
    Device device,
    DebuggingOptions debuggingOptions,
    bool ipv6,
  ) async {
213 214
    Uri uri;
    if (vmServiceUri.scheme == 'ws') {
215 216 217
      final List<String> segments = vmServiceUri.pathSegments.toList();
      segments.remove('ws');
      uri = vmServiceUri.replace(scheme: 'http', path: segments.join('/'));
218 219 220 221
    } else {
      uri = vmServiceUri;
    }
    _vmServiceUri = uri.toString();
222
    _device = device;
223 224 225 226
    if (debuggingOptions.enableDds) {
      try {
        await device.dds.startDartDevelopmentService(
          uri,
227 228 229
          hostPort: debuggingOptions.ddsPort,
          ipv6: ipv6,
          disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes,
230 231 232 233 234 235 236 237
          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.
      }
238
    }
239
    _vmService = await _vmServiceConnector(uri, device: _device, logger: _logger);
240 241 242
    final DeviceLogReader logReader = await device.getLogReader(app: _applicationPackage);
    logReader.logLines.listen(_logger.printStatus);

243
    final vm_service.VM vm = await _vmService.service.getVM();
244 245 246 247 248 249 250
    logReader.appPid = vm.pid;
  }

  @override
  Future<int> startTest(
    String testFile,
    List<String> arguments,
251 252
    Map<String, String> environment,
    PackageConfig packageConfig, {
253 254 255 256 257
    bool? headless,
    String? chromeBinary,
    String? browserName,
    bool? androidEmulator,
    int? driverPort,
258
    List<String> webBrowserFlags = const <String>[],
259 260
    List<String>? browserDimension,
    String? profileMemory,
261
  }) async {
262 263 264
    if (profileMemory != null) {
      unawaited(_devtoolsLauncher.launch(
        Uri.parse(_vmServiceUri),
265
        additionalArguments: <String>['--record-memory-profile=$profileMemory'],
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
      ));
      // 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();
      }
    }
284 285 286 287
  }

  @override
  Future<void> stop({
288 289
    File? writeSkslOnExit,
    String? userIdentifier,
290 291 292
  }) async {
    if (writeSkslOnExit != null) {
      final FlutterView flutterView = (await _vmService.getFlutterViews()).first;
293
      final Map<String, Object?>? result = await _vmService.getSkSLs(
294
        viewId: flutterView.id
295
      );
296
      await sharedSkSlWriter(_device!, result, outputFile: writeSkslOnExit, logger: _logger);
297
    }
298
    // If the application package is available, stop and uninstall.
299 300 301
    final ApplicationPackage? package = _applicationPackage;
    if (package != null) {
      if (!await _device!.stopApp(package, userIdentifier: userIdentifier)) {
302 303
        _logger.printError('Failed to stop app');
      }
304
      if (!await _device!.uninstallApp(package, userIdentifier: userIdentifier)) {
305
        _logger.printError('Failed to uninstall app');
306
      }
307
    } else if (_device!.supportsFlutterExit) {
308
      // Otherwise use the VM Service URI to stop the app as a best effort approach.
309
      final vm_service.VM vm = await _vmService.service.getVM();
310
      final vm_service.IsolateRef isolateRef = vm.isolates!
311
        .firstWhere((vm_service.IsolateRef element) {
312 313 314
          return !element.isSystemIsolate!;
        });
      unawaited(_vmService.flutterExit(isolateId: isolateRef.id!));
315 316
    } else {
      _logger.printTrace('No application package for $_device, leaving app running');
317
    }
318
    await _device!.dispose();
319 320
  }
}