drive.dart 11.1 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
    argParser.addFlag(
      'keep-app-running',
43
      defaultsTo: null,
44 45
      negatable: true,
      help:
46
        'Will keep the Flutter application running when done testing.\n'
47 48 49 50
        '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.'
51 52
    );

53
    argParser.addOption(
54 55
      'use-existing-app',
      help:
56
        'Connect to an already running instance via the given observatory URL.\n'
57 58 59 60
        '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'
61
    );
62 63 64 65 66 67 68 69 70 71 72 73

    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
74 75
  }

76
  @override
77
  final String name = 'drive';
78 79

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

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

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

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

yjbanov's avatar
yjbanov committed
92
  @override
93
  Future<Null> verifyThenRunCommand() async {
94
    commandValidator();
95 96 97 98
    return super.verifyThenRunCommand();
  }

  @override
99
  Future<Null> runCommand() async {
100
    final String testFile = _getTestFile();
101 102
    if (testFile == null)
      throwToolExit(null);
yjbanov's avatar
yjbanov committed
103

104
    _device = await targetDeviceFinder();
105 106
    if (device == null)
      throwToolExit(null);
107

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

111 112
    String observatoryUri;
    if (argResults['use-existing-app'] == null) {
113
      printStatus('Starting application: $targetFile');
114

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

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

134 135
    Cache.releaseLockEarly();

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

  String _getTestFile() {
153 154 155 156 157
    if (argResults['driver'] != null)
      return argResults['driver'];

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

160
    // This command extends `flutter run` and therefore CWD == package dir
161
    final String packageDir = fs.currentDirectory.path;
162 163 164

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

173
      appFile = fs.path.relative(appFile, from: packageDir);
174 175
    }

176
    final List<String> parts = fs.path.split(appFile);
177 178 179 180 181 182 183 184 185 186 187 188

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

/// 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 {
203
  final List<Device> devices = await deviceManager.getDevices().toList();
204

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

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

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

232
AppStarter appStarter = _startApp; // (mutable for testing)
233
void restoreAppStarter() {
234
  appStarter = _startApp;
235 236
}

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

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

  printTrace('Installing application package.');
248
  final ApplicationPackage package = await command.applicationPackages
249 250
      .getPackageForPlatform(await command.device.targetPlatform);
  if (await command.device.isAppInstalled(package))
251 252
    await command.device.uninstallApp(package);
  await command.device.installApp(package);
253

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

258
  printTrace('Starting application.');
259 260

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

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

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

286
  return result;
287 288 289
}

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

296
Future<Null> _runTests(List<String> testArgs, String observatoryUri) async {
297
  printTrace('Running driver tests.');
298

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


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

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