drive.dart 11 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';
yjbanov's avatar
yjbanov committed
17 18 19 20 21 22 23 24 25 26 27
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
28 29
/// 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
30
/// `_test.dart` file would generally be a Dart program that uses
31
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
32 33 34 35 36 37 38
/// 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.
39 40
class DriveCommand extends RunCommandBase {
  DriveCommand() {
41 42
    requiresPubspecYaml();

43 44
    argParser.addFlag(
      'keep-app-running',
45
      defaultsTo: null,
46 47
      negatable: true,
      help:
48
        'Will keep the Flutter application running when done testing.\n'
49 50 51 52
        'By default, "flutter drive" stops the application after tests are finished,\n'
        'and --keep-app-running overrides this. On the other hand, if --use-existing-app\n'
        'is specified, then "flutter drive" instead defaults to leaving the application\n'
        'running, and --no-keep-app-running overrides it.'
53 54
    );

55
    argParser.addOption(
56 57
      'use-existing-app',
      help:
58
        'Connect to an already running instance via the given observatory URL.\n'
59 60 61 62
        'If this option is given, the application will not be automatically started,\n'
        'and it will only be stopped if --no-keep-app-running is explicitly set.',
      valueHelp:
        'url'
63
    );
64 65 66 67 68 69 70 71 72 73 74 75

    argParser.addOption(
      'driver',
      help:
        'The test file to run on the host (as opposed to the target file to run on\n'
        'the device). By default, this file has the same base name as the target\n'
        'file, but in the "test_driver/" directory instead, and with "_test" inserted\n'
        'just before the extension, so e.g. if the target is "lib/main.dart", the\n'
        'driver will be "test_driver/main_test.dart".',
      valueHelp:
        'path'
    );
yjbanov's avatar
yjbanov committed
76 77
  }

78
  @override
79
  final String name = 'drive';
80 81

  @override
82
  final String description = 'Runs Flutter Driver tests for the current project.';
83 84

  @override
85 86 87 88
  final List<String> aliases = <String>['driver'];

  Device _device;
  Device get device => _device;
yjbanov's avatar
yjbanov committed
89

90
  /// Subscription to log messages printed on the device or simulator.
91
  // ignore: cancel_subscriptions
92 93
  StreamSubscription<String> _deviceLogSubscription;

94
  @override
95
  Future<Null> runCommand() async {
96
    final String testFile = _getTestFile();
97 98
    if (testFile == null)
      throwToolExit(null);
yjbanov's avatar
yjbanov committed
99

100
    _device = await targetDeviceFinder();
101 102
    if (device == null)
      throwToolExit(null);
103

104 105
    if (await fs.type(testFile) != FileSystemEntityType.FILE)
      throwToolExit('Test file not found: $testFile');
yjbanov's avatar
yjbanov committed
106

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

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

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

130 131
    Cache.releaseLockEarly();

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

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

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

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

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

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

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

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

/// Finds a device to test on. May launch a simulator, if necessary.
typedef Future<Device> TargetDeviceFinder();
TargetDeviceFinder targetDeviceFinder = findTargetDevice;
void restoreTargetDeviceFinder() {
  targetDeviceFinder = findTargetDevice;
}

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

201
  if (deviceManager.hasSpecifiedDeviceId) {
202 203 204 205 206 207
    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}':");
208
      await Device.printDevices(devices);
209 210 211
      return null;
    }
    return devices.first;
212 213
  }

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

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

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

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

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

  printTrace('Installing application package.');
244
  final ApplicationPackage package = await command.applicationPackages
245 246
      .getPackageForPlatform(await command.device.targetPlatform);
  if (await command.device.isAppInstalled(package))
247 248
    await command.device.uninstallApp(package);
  await command.device.installApp(package);
249

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

254
  printTrace('Starting application.');
255 256

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

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

276 277
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
278
    return null;
279 280
  }

281
  return result;
282 283 284
}

/// Runs driver tests.
285 286
typedef Future<Null> TestRunner(List<String> testArgs, String observatoryUri);
TestRunner testRunner = _runTests;
287
void restoreTestRunner() {
288
  testRunner = _runTests;
289 290
}

291
Future<Null> _runTests(List<String> testArgs, String observatoryUri) async {
292
  printTrace('Running driver tests.');
293

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


/// Stops the application.
309 310
typedef Future<bool> AppStopper(DriveCommand command);
AppStopper appStopper = _stopApp;
311
void restoreAppStopper() {
312
  appStopper = _stopApp;
313 314
}

315
Future<bool> _stopApp(DriveCommand command) async {
316
  printTrace('Stopping application.');
317
  final ApplicationPackage package = await command.applicationPackages.getPackageForPlatform(await command.device.targetPlatform);
318
  final bool stopped = await command.device.stopApp(package);
319
  await command._deviceLogSubscription?.cancel();
320
  return stopped;
321
}