drive.dart 11.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
yjbanov's avatar
yjbanov committed
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6 7

import 'package:meta/meta.dart';
8
import 'package:package_config/package_config_types.dart';
yjbanov's avatar
yjbanov committed
9

10
import '../android/android_device.dart';
11
import '../application_package.dart';
12
import '../artifacts.dart';
Devon Carew's avatar
Devon Carew committed
13
import '../base/common.dart';
14
import '../base/file_system.dart';
15
import '../base/logger.dart';
16
import '../build_info.dart';
17
import '../dart/package_map.dart';
18
import '../device.dart';
19
import '../drive/drive_service.dart';
20
import '../globals.dart' as globals;
21
import '../runner/flutter_command.dart' show FlutterCommandResult, FlutterOptions;
22
import '../web/web_device.dart';
yjbanov's avatar
yjbanov committed
23 24 25 26 27 28 29 30 31 32 33
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
34 35
/// 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
36
/// `_test.dart` file would generally be a Dart program that uses
37
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
38 39 40 41 42 43 44
/// 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.
45
class DriveCommand extends RunCommandBase {
46 47
  DriveCommand({
    bool verboseHelp = false,
48 49 50 51 52 53
    @visibleForTesting FlutterDriverFactory flutterDriverFactory,
    @required FileSystem fileSystem,
    @required Logger logger,
  }) : _flutterDriverFactory = flutterDriverFactory,
       _fileSystem = fileSystem,
       _logger = logger {
54
    requiresPubspecYaml();
55
    addEnableExperimentation(hide: !verboseHelp);
56 57 58 59 60

    // By default, the drive app should not publish the VM service port over mDNS
    // to prevent a local network permission dialog on iOS 14+,
    // which cannot be accepted or dismissed in a CI environment.
    addPublishPort(enabledByDefault: false, verboseHelp: verboseHelp);
61 62 63 64
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
65 66 67
              'By default, "flutter drive" stops the application after tests are finished, '
              'and --keep-app-running overrides this. On the other hand, if --use-existing-app '
              'is specified, then "flutter drive" instead defaults to leaving the application '
68 69 70
              'running, and --no-keep-app-running overrides it.',
      )
      ..addOption('use-existing-app',
71 72
        help: 'Connect to an already running instance via the given observatory URL. '
              'If this option is given, the application will not be automatically started, '
73 74 75 76
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..addOption('driver',
77 78 79 80 81 82
        help: 'The test file to run on the host (as opposed to the target file to run on '
              'the device).\n'
              'By default, this file has the same base name as the target file, but in the '
              '"test_driver/" directory instead, and with "_test" inserted just before the '
              'extension, so e.g. if the target is "lib/main.dart", the driver will be '
              '"test_driver/main_test.dart".',
83
        valueHelp: 'path',
84 85 86
      )
      ..addFlag('build',
        defaultsTo: true,
87 88
        help: '(Deprecated) Build the app before running. To use an existing app, pass the --use-application-binary '
          'flag with an existing APK',
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
      )
      ..addOption('driver-port',
        defaultsTo: '4444',
        help: 'The port where Webdriver server is launched at. Defaults to 4444.',
        valueHelp: '4444'
      )
      ..addFlag('headless',
        defaultsTo: true,
        help: 'Whether the driver browser is going to be launched in headless mode. Defaults to true.',
      )
      ..addOption('browser-name',
        defaultsTo: 'chrome',
        help: 'Name of browser where tests will be executed. \n'
              'Following browsers are supported: \n'
              'Chrome, Firefox, Safari (macOS and iOS) and Edge. Defaults to Chrome.',
        allowed: <String>[
105
          'android-chrome',
106 107 108 109 110
          'chrome',
          'edge',
          'firefox',
          'ios-safari',
          'safari',
111
        ],
112 113 114 115 116 117
      )
      ..addOption('browser-dimension',
        defaultsTo: '1600,1024',
        help: 'The dimension of browser when running Flutter Web test. \n'
              'This will affect screenshot and all offset-related actions. \n'
              'By default. it is set to 1600,1024 (1600 by 1024).',
118 119 120 121
      )
      ..addFlag('android-emulator',
        defaultsTo: true,
        help: 'Whether to perform Flutter Driver testing on Android Emulator.'
122 123 124
          'Works only if \'browser-name\' is set to \'android-chrome\'')
      ..addOption('chrome-binary',
        help: 'Location of Chrome binary. '
125 126 127 128
          'Works only if \'browser-name\' is set to \'chrome\'')
      ..addOption('write-sksl-on-exit',
        help:
          'Attempts to write an SkSL file when the drive process is finished '
129 130 131
          'to the provided file, overwriting it if necessary.')
      ..addMultiOption('test-arguments', help: 'Additional arguments to pass to the '
          'Dart VM running The test script.');
yjbanov's avatar
yjbanov committed
132 133
  }

134 135 136 137
  FlutterDriverFactory _flutterDriverFactory;
  final FileSystem _fileSystem;
  final Logger _logger;

138
  @override
139
  final String name = 'drive';
140 141

  @override
142
  final String description = 'Run integration tests for the project on an attached device or emulator.';
143 144

  @override
145 146
  final List<String> aliases = <String>['driver'];

147
  String get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
148

149 150
  @override
  bool get startPausedDefault => true;
151

152 153 154
  @override
  Future<void> validateCommand() async {
    if (userIdentifier != null) {
155
      final Device device = await findTargetDevice();
156 157 158 159 160 161 162
      if (device is! AndroidDevice) {
        throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
      }
    }
    return super.validateCommand();
  }

163
  @override
164
  Future<FlutterCommandResult> runCommand() async {
165
    final String testFile = _getTestFile();
166
    if (testFile == null) {
167
      throwToolExit(null);
168
    }
169 170 171 172
    if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
      throwToolExit('Test file not found: $testFile');
    }
    final Device device = await findTargetDevice(includeUnsupportedDevices: stringArg('use-application-binary') == null);
173
    if (device == null) {
174
      throwToolExit(null);
175
    }
176

177 178 179 180 181 182
    final bool web = device is WebServerDevice || device is ChromiumDevice;
    _flutterDriverFactory ??= FlutterDriverFactory(
      applicationPackageFactory: ApplicationPackageFactory.instance,
      logger: _logger,
      processUtils: globals.processUtils,
      dartSdkPath: globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
183 184 185 186 187 188
    );
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
      globals.fs.file('.packages'),
      logger: _logger,
      throwOnError: false,
    ) ?? PackageConfig.empty;
189
    final DriverService driverService = _flutterDriverFactory.createDriverService(web);
190
    final BuildInfo buildInfo = getBuildInfo();
191
    final DebuggingOptions debuggingOptions = createDebuggingOptions();
192 193
    final File applicationBinary = stringArg('use-application-binary') == null
      ? null
194
      : _fileSystem.file(stringArg('use-application-binary'));
195

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
    if (stringArg('use-existing-app') == null) {
      await driverService.start(
        buildInfo,
        device,
        debuggingOptions,
        ipv6,
        applicationBinary: applicationBinary,
        route: route,
        userIdentifier: userIdentifier,
        mainPath: targetFile,
        platformArgs: <String, Object>{
          if (traceStartup)
            'trace-startup': traceStartup,
          if (web)
            '--no-launch-chrome': true,
        }
      );
    } else {
      final Uri uri = Uri.tryParse(stringArg('use-existing-app'));
      if (uri == null) {
        throwToolExit('Invalid VM Service URI: ${stringArg('use-existing-app')}');
217
      }
218 219 220 221 222 223 224
      await driverService.reuseApplication(
        uri,
        device,
        debuggingOptions,
        ipv6,
      );
    }
225

226 227 228 229
    final int testResult = await driverService.startTest(
      testFile,
      stringsArg('test-arguments'),
      <String, String>{},
230
      packageConfig,
231 232 233 234 235 236 237 238 239
      chromeBinary: stringArg('chrome-binary'),
      headless: boolArg('headless'),
      browserDimension: stringArg('browser-dimension').split(','),
      browserName: stringArg('browser-name'),
      driverPort: stringArg('driver-port') != null
        ? int.tryParse(stringArg('driver-port'))
        : null,
      androidEmulator: boolArg('android-emulator'),
    );
240

241 242
    if (boolArg('keep-app-running') ?? (argResults['use-existing-app'] != null)) {
      _logger.printStatus('Leaving the application running.');
243
    } else {
244 245 246 247
      final File skslFile = stringArg('write-sksl-on-exit') != null
        ? _fileSystem.file(stringArg('write-sksl-on-exit'))
        : null;
      await driverService.stop(userIdentifier: userIdentifier, writeSkslOnExit: skslFile);
yjbanov's avatar
yjbanov committed
248
    }
249
    if (testResult != 0) {
250
      throwToolExit(null);
251
    }
252
    return FlutterCommandResult.success();
yjbanov's avatar
yjbanov committed
253 254 255
  }

  String _getTestFile() {
256
    if (argResults['driver'] != null) {
257
      return stringArg('driver');
258
    }
259 260 261

    // If the --driver argument wasn't provided, then derive the value from
    // the target file.
262
    String appFile = _fileSystem.path.normalize(targetFile);
263

264
    // This command extends `flutter run` and therefore CWD == package dir
265
    final String packageDir = _fileSystem.currentDirectory.path;
266 267 268

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
269 270 271
    if (!_fileSystem.path.isRelative(appFile)) {
      if (!_fileSystem.path.isWithin(packageDir, appFile)) {
        _logger.printError(
272 273 274 275 276
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

277
      appFile = _fileSystem.path.relative(appFile, from: packageDir);
278 279
    }

280
    final List<String> parts = _fileSystem.path.split(appFile);
281 282

    if (parts.length < 2) {
283
      _logger.printError(
284 285 286 287 288 289 290 291 292
        '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`.
293
    final String pathWithNoExtension = _fileSystem.path.withoutExtension(_fileSystem.path.joinAll(
294
      <String>[packageDir, 'test_driver', ...parts.skip(1)]));
295
    return '${pathWithNoExtension}_test${_fileSystem.path.extension(appFile)}';
296 297
  }
}