drive.dart 11.5 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 8
import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart';
Devon Carew's avatar
Devon Carew committed
9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../base/platform.dart';
12
import '../base/process.dart';
13
import '../build_info.dart';
14
import '../cache.dart';
15
import '../dart/package_map.dart';
16
import '../dart/sdk.dart';
17
import '../device.dart';
yjbanov's avatar
yjbanov committed
18
import '../globals.dart';
19
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
20
import '../resident_runner.dart';
yjbanov's avatar
yjbanov committed
21 22 23 24 25 26 27 28 29 30 31
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
32 33 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
/// `_test.dart` file would generall be a Dart program that uses
/// `package:flutter_driver` and exercises your application. Most commonly it
yjbanov's avatar
yjbanov committed
36 37 38 39 40 41 42
/// 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.
43 44
class DriveCommand extends RunCommandBase {
  DriveCommand() {
45 46 47 48 49
    argParser.addFlag(
      'keep-app-running',
      negatable: true,
      defaultsTo: false,
      help:
50 51 52
        'Will keep the Flutter application running when done testing.\n'
        'By default, Flutter drive stops the application after tests are finished.\n'
        'Ignored if --use-existing-app is specified.'
53 54
    );

55
    argParser.addOption(
56 57
      'use-existing-app',
      help:
58 59 60
        'Connect to an already running instance via the given observatory URL.\n'
        'If this option is given, the application will not be automatically started\n'
        'or stopped.'
61
    );
yjbanov's avatar
yjbanov committed
62 63
  }

64
  @override
65
  final String name = 'drive';
66 67

  @override
68
  final String description = 'Runs Flutter Driver tests for the current project.';
69 70

  @override
71 72 73 74
  final List<String> aliases = <String>['driver'];

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

76
  /// Subscription to log messages printed on the device or simulator.
77
  // ignore: cancel_subscriptions
78 79
  StreamSubscription<String> _deviceLogSubscription;

yjbanov's avatar
yjbanov committed
80
  @override
81
  Future<Null> verifyThenRunCommand() async {
82
    commandValidator();
83 84 85 86
    return super.verifyThenRunCommand();
  }

  @override
87
  Future<Null> runCommand() async {
yjbanov's avatar
yjbanov committed
88
    String testFile = _getTestFile();
89 90
    if (testFile == null)
      throwToolExit(null);
yjbanov's avatar
yjbanov committed
91

92
    this._device = await targetDeviceFinder();
93 94
    if (device == null)
      throwToolExit(null);
95

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

99 100
    String observatoryUri;
    if (argResults['use-existing-app'] == null) {
101
      printStatus('Starting application: ${argResults["target"]}');
102 103 104

      if (getBuildMode() == BuildMode.release) {
        // This is because we need VM service to be able to drive the app.
105
        throwToolExit(
106 107 108 109 110 111 112
          '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).'
        );
      }

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

122 123
    Cache.releaseLockEarly();

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

  String _getTestFile() {
141
    String appFile = fs.path.normalize(targetFile);
142 143

    // This command extends `flutter start` and therefore CWD == package dir
144
    String packageDir = fs.currentDirectory.path;
145 146 147

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
148 149
    if (!fs.path.isRelative(appFile)) {
      if (!fs.path.isWithin(packageDir, appFile)) {
150 151 152 153 154 155
        printError(
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

156
      appFile = fs.path.relative(appFile, from: packageDir);
157 158
    }

159
    List<String> parts = fs.path.split(appFile);
160 161 162 163 164 165 166 167 168 169 170 171

    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`.
172
    String pathWithNoExtension = fs.path.withoutExtension(fs.path.joinAll(
173
      <String>[packageDir, 'test_driver']..addAll(parts.skip(1))));
174
    return '${pathWithNoExtension}_test${fs.path.extension(appFile)}';
yjbanov's avatar
yjbanov committed
175 176
  }
}
177 178 179 180 181 182 183 184 185

/// 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 {
186 187
  List<Device> devices = await deviceManager.getDevices();

188
  if (deviceManager.hasSpecifiedDeviceId) {
189 190 191 192 193 194 195 196 197 198
    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}':");
      Device.printDevices(devices);
      return null;
    }
    return devices.first;
199 200 201
  }


202
  if (platform.isMacOS) {
203 204 205 206
    // On Mac we look for the iOS Simulator. If available, we use that. Then
    // we look for an Android device. If there's one, we use that. Otherwise,
    // we launch a new iOS Simulator.
    Device reusableDevice = devices.firstWhere(
Ian Hickson's avatar
Ian Hickson committed
207
      (Device d) => d.isLocalEmulator,
208
      orElse: () {
Ian Hickson's avatar
Ian Hickson committed
209
        return devices.firstWhere((Device d) => d is AndroidDevice,
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
            orElse: () => null);
      }
    );

    if (reusableDevice != null) {
      printStatus('Found connected ${reusableDevice.isLocalEmulator ? "emulator" : "device"} "${reusableDevice.name}"; will reuse it.');
      return reusableDevice;
    }

    // No running emulator found. Attempt to start one.
    printStatus('Starting iOS Simulator, because did not find existing connected devices.');
    bool started = await SimControl.instance.boot();
    if (started) {
      return IOSSimulatorUtils.instance.getAttachedDevices().first;
    } else {
      printError('Failed to start iOS Simulator.');
      return null;
    }
228
  } else if (platform.isLinux || platform.isWindows) {
229
    // On Linux and Windows, for now, we just grab the first connected device we can find.
230 231 232 233 234
    if (devices.isEmpty) {
      printError('No devices found.');
      return null;
    } else if (devices.length > 1) {
      printStatus('Found multiple connected devices:');
Ian Hickson's avatar
Ian Hickson committed
235
      printStatus(devices.map((Device d) => '  - ${d.name}\n').join(''));
236 237 238 239 240 241 242 243 244 245
    }
    printStatus('Using device ${devices.first.name}.');
    return devices.first;
  } else {
    printError('The operating system on this computer is not supported.');
    return null;
  }
}

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

248
AppStarter appStarter = _startApp;
249
void restoreAppStarter() {
250
  appStarter = _startApp;
251 252
}

253
Future<LaunchResult> _startApp(DriveCommand command) async {
254
  String mainPath = findMainDartFile(command.targetFile);
255 256
  if (await fs.type(mainPath) != FileSystemEntityType.FILE) {
    printError('Tried to run $mainPath, but that file does not exist.');
257
    return null;
258 259 260 261 262 263 264 265
  }

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

  printTrace('Installing application package.');
  ApplicationPackage package = command.applicationPackages
      .getPackageForPlatform(command.device.platform);
266 267
  if (command.device.isAppInstalled(package))
    command.device.uninstallApp(package);
268
  command.device.installApp(package);
269

270 271 272 273
  Map<String, dynamic> platformArgs = <String, dynamic>{};
  if (command.traceStartup)
    platformArgs['trace-startup'] = command.traceStartup;

274
  printTrace('Starting application.');
275 276

  // Forward device log messages to the terminal window running the "drive" command.
277 278 279 280 281
  command._deviceLogSubscription = command
      .device
      .getLogReader(app: package)
      .logLines
      .listen(printStatus);
282

Devon Carew's avatar
Devon Carew committed
283
  LaunchResult result = await command.device.startApp(
284
    package,
285
    command.getBuildMode(),
286 287
    mainPath: mainPath,
    route: command.route,
Devon Carew's avatar
Devon Carew committed
288
    debuggingOptions: new DebuggingOptions.enabled(
289
      command.getBuildMode(),
Devon Carew's avatar
Devon Carew committed
290
      startPaused: true,
291 292
      observatoryPort: command.observatoryPort,
      diagnosticPort: command.diagnosticPort,
Devon Carew's avatar
Devon Carew committed
293
    ),
294
    platformArgs: platformArgs
295 296
  );

297 298
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
299
    return null;
300 301
  }

302
  return result;
303 304 305
}

/// Runs driver tests.
306 307
typedef Future<Null> TestRunner(List<String> testArgs, String observatoryUri);
TestRunner testRunner = _runTests;
308
void restoreTestRunner() {
309
  testRunner = _runTests;
310 311
}

312
Future<Null> _runTests(List<String> testArgs, String observatoryUri) async {
313
  printTrace('Running driver tests.');
314

315
  PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath));
316 317 318
  List<String> args = testArgs.toList()
    ..add('--packages=${PackageMap.globalPackagesPath}')
    ..add('-rexpanded');
319
  String dartVmPath = fs.path.join(dartSdkPath, 'bin', 'dart');
320 321 322 323
  int result = await runCommandAndStreamOutput(
    <String>[dartVmPath]..addAll(args),
    environment: <String, String>{ 'VM_SERVICE_URL': observatoryUri }
  );
324 325
  if (result != 0)
    throwToolExit('Driver tests failed: $result', exitCode: result);
326 327 328 329
}


/// Stops the application.
330 331
typedef Future<bool> AppStopper(DriveCommand command);
AppStopper appStopper = _stopApp;
332
void restoreAppStopper() {
333
  appStopper = _stopApp;
334 335
}

336
Future<bool> _stopApp(DriveCommand command) async {
337
  printTrace('Stopping application.');
338
  ApplicationPackage package = command.applicationPackages.getPackageForPlatform(command.device.platform);
339
  bool stopped = await command.device.stopApp(package);
340
  await command._deviceLogSubscription?.cancel();
341
  return stopped;
342
}