drive.dart 11.3 KB
Newer Older
yjbanov's avatar
yjbanov committed
1 2 3 4 5 6
// 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';

7
import '../application_package.dart';
Devon Carew's avatar
Devon Carew committed
8
import '../base/common.dart';
9
import '../base/file_system.dart';
10
import '../base/process.dart';
11
import '../cache.dart';
12
import '../dart/package_map.dart';
13
import '../dart/sdk.dart';
14
import '../device.dart';
yjbanov's avatar
yjbanov committed
15
import '../globals.dart';
16
import '../resident_runner.dart';
17
import '../runner/flutter_command.dart' show FlutterCommandResult;
yjbanov's avatar
yjbanov committed
18 19 20 21 22 23 24 25 26 27 28
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
29 30
/// 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
31
/// `_test.dart` file would generally be a Dart program that uses
32
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
33 34 35 36 37 38 39
/// 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.
40 41
class DriveCommand extends RunCommandBase {
  DriveCommand() {
42 43
    requiresPubspecYaml();

44 45 46 47
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
48 49 50
              '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 '
51 52 53
              'running, and --no-keep-app-running overrides it.',
      )
      ..addOption('use-existing-app',
54 55
        help: 'Connect to an already running instance via the given observatory URL. '
              'If this option is given, the application will not be automatically started, '
56 57 58 59
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..addOption('driver',
60 61 62 63 64 65
        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".',
66
        valueHelp: 'path',
67 68 69 70
      )
      ..addFlag('build',
        defaultsTo: true,
        help: 'Build the app before running.',
71
      );
yjbanov's avatar
yjbanov committed
72 73
  }

74
  @override
75
  final String name = 'drive';
76 77

  @override
78
  final String description = 'Runs Flutter Driver tests for the current project.';
79 80

  @override
81 82 83 84
  final List<String> aliases = <String>['driver'];

  Device _device;
  Device get device => _device;
85
  bool get shouldBuild => argResults['build'];
yjbanov's avatar
yjbanov committed
86

87
  /// Subscription to log messages printed on the device or simulator.
88
  // ignore: cancel_subscriptions
89 90
  StreamSubscription<String> _deviceLogSubscription;

91
  @override
92
  Future<FlutterCommandResult> runCommand() async {
93
    final String testFile = _getTestFile();
94 95
    if (testFile == null)
      throwToolExit(null);
yjbanov's avatar
yjbanov committed
96

97
    _device = await targetDeviceFinder();
98 99
    if (device == null)
      throwToolExit(null);
100

101
    if (await fs.type(testFile) != FileSystemEntityType.file)
102
      throwToolExit('Test file not found: $testFile');
yjbanov's avatar
yjbanov committed
103

104 105
    String observatoryUri;
    if (argResults['use-existing-app'] == null) {
106
      printStatus('Starting application: $targetFile');
107

108
      if (getBuildInfo().isRelease) {
109
        // This is because we need VM service to be able to drive the app.
110
        throwToolExit(
111 112 113 114 115 116 117
          'Flutter Driver 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).'
        );
      }

118
      final LaunchResult result = await appStarter(this);
119 120 121
      if (result == null)
        throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
      observatoryUri = result.observatoryUri.toString();
122
    } else {
123
      printStatus('Will connect to already running application instance.');
124
      observatoryUri = argResults['use-existing-app'];
yjbanov's avatar
yjbanov committed
125 126
    }

127 128
    Cache.releaseLockEarly();

yjbanov's avatar
yjbanov committed
129
    try {
130
      await testRunner(<String>[testFile], observatoryUri);
131 132 133 134
    } catch (error, stackTrace) {
      if (error is ToolExit)
        rethrow;
      throwToolExit('CAUGHT EXCEPTION: $error\n$stackTrace');
yjbanov's avatar
yjbanov committed
135
    } finally {
136 137 138
      if (argResults['keep-app-running'] ?? (argResults['use-existing-app'] != null)) {
        printStatus('Leaving the application running.');
      } else {
139
        printStatus('Stopping application instance.');
140
        await appStopper(this);
141
      }
yjbanov's avatar
yjbanov committed
142
    }
143 144

    return null;
yjbanov's avatar
yjbanov committed
145 146 147
  }

  String _getTestFile() {
148 149 150 151 152
    if (argResults['driver'] != null)
      return argResults['driver'];

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

155
    // This command extends `flutter run` and therefore CWD == package dir
156
    final String packageDir = fs.currentDirectory.path;
157 158 159

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
160 161
    if (!fs.path.isRelative(appFile)) {
      if (!fs.path.isWithin(packageDir, appFile)) {
162 163 164 165 166 167
        printError(
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

168
      appFile = fs.path.relative(appFile, from: packageDir);
169 170
    }

171
    final List<String> parts = fs.path.split(appFile);
172 173 174 175 176 177 178 179 180 181 182 183

    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`.
184
    final String pathWithNoExtension = fs.path.withoutExtension(fs.path.joinAll(
185
      <String>[packageDir, 'test_driver']..addAll(parts.skip(1))));
186
    return '${pathWithNoExtension}_test${fs.path.extension(appFile)}';
yjbanov's avatar
yjbanov committed
187 188
  }
}
189 190

/// Finds a device to test on. May launch a simulator, if necessary.
191
typedef TargetDeviceFinder = Future<Device> Function();
192 193 194 195 196 197
TargetDeviceFinder targetDeviceFinder = findTargetDevice;
void restoreTargetDeviceFinder() {
  targetDeviceFinder = findTargetDevice;
}

Future<Device> findTargetDevice() async {
198
  final List<Device> devices = await deviceManager.getDevices().toList();
199

200
  if (deviceManager.hasSpecifiedDeviceId) {
201 202 203 204 205 206
    if (devices.isEmpty) {
      printStatus("No devices found with name or id matching '${deviceManager.specifiedDeviceId}'");
      return null;
    }
    if (devices.length > 1) {
      printStatus("Found ${devices.length} devices with name or id matching '${deviceManager.specifiedDeviceId}':");
207
      await Device.printDevices(devices);
208 209 210
      return null;
    }
    return devices.first;
211 212
  }

213 214
  if (devices.isEmpty) {
    printError('No devices found.');
215
    return null;
216 217
  } else if (devices.length > 1) {
    printStatus('Found multiple connected devices:');
218
    printStatus(devices.map<String>((Device d) => '  - ${d.name}\n').join(''));
219
  }
220 221
  printStatus('Using device ${devices.first.name}.');
  return devices.first;
222 223 224
}

/// Starts the application on the device given command configuration.
225
typedef AppStarter = Future<LaunchResult> Function(DriveCommand command);
226

227
AppStarter appStarter = _startApp; // (mutable for testing)
228
void restoreAppStarter() {
229
  appStarter = _startApp;
230 231
}

232
Future<LaunchResult> _startApp(DriveCommand command) async {
233
  final String mainPath = findMainDartFile(command.targetFile);
234
  if (await fs.type(mainPath) != FileSystemEntityType.file) {
235
    printError('Tried to run $mainPath, but that file does not exist.');
236
    return null;
237 238 239 240 241
  }

  printTrace('Stopping previously running application, if any.');
  await appStopper(command);

242
  final ApplicationPackage package = await command.applicationPackages
243
      .getPackageForPlatform(await command.device.targetPlatform);
244 245 246 247 248 249 250

  if (command.shouldBuild) {
    printTrace('Installing application package.');
    if (await command.device.isAppInstalled(package))
      await command.device.uninstallApp(package);
    await command.device.installApp(package);
  }
251

252
  final Map<String, dynamic> platformArgs = <String, dynamic>{};
253 254 255
  if (command.traceStartup)
    platformArgs['trace-startup'] = command.traceStartup;

256
  printTrace('Starting application.');
257 258

  // Forward device log messages to the terminal window running the "drive" command.
259 260 261 262 263
  command._deviceLogSubscription = command
      .device
      .getLogReader(app: package)
      .logLines
      .listen(printStatus);
264

265
  final LaunchResult result = await command.device.startApp(
266 267 268
    package,
    mainPath: mainPath,
    route: command.route,
269
    debuggingOptions: DebuggingOptions.enabled(
270
      command.getBuildInfo(),
Devon Carew's avatar
Devon Carew committed
271
      startPaused: true,
272
      observatoryPort: command.observatoryPort,
Devon Carew's avatar
Devon Carew committed
273
    ),
274
    platformArgs: platformArgs,
275
    prebuiltApplication: !command.shouldBuild,
276
    usesTerminalUi: false,
277 278
  );

279 280
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
281
    return null;
282 283
  }

284
  return result;
285 286 287
}

/// Runs driver tests.
288
typedef TestRunner = Future<void> Function(List<String> testArgs, String observatoryUri);
289
TestRunner testRunner = _runTests;
290
void restoreTestRunner() {
291
  testRunner = _runTests;
292 293
}

294
Future<void> _runTests(List<String> testArgs, String observatoryUri) async {
295
  printTrace('Running driver tests.');
296

297
  PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath));
298
  final String dartVmPath = fs.path.join(dartSdkPath, 'bin', 'dart');
299 300 301
  final int result = await runCommandAndStreamOutput(
    <String>[dartVmPath]
      ..addAll(dartVmFlags)
302 303 304 305
      ..addAll(testArgs)
      ..addAll(<String>[
        '--packages=${PackageMap.globalPackagesPath}',
        '-rexpanded',
306
      ]),
307
    environment: <String, String>{'VM_SERVICE_URL': observatoryUri},
308
  );
309
  if (result != 0)
310
    throwToolExit('Driver tests failed: $result', exitCode: result);
311 312 313 314
}


/// Stops the application.
315
typedef AppStopper = Future<bool> Function(DriveCommand command);
316
AppStopper appStopper = _stopApp;
317
void restoreAppStopper() {
318
  appStopper = _stopApp;
319 320
}

321
Future<bool> _stopApp(DriveCommand command) async {
322
  printTrace('Stopping application.');
323
  final ApplicationPackage package = await command.applicationPackages.getPackageForPlatform(await command.device.targetPlatform);
324
  final bool stopped = await command.device.stopApp(package);
325
  await command._deviceLogSubscription?.cancel();
326
  return stopped;
327
}