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';
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
              '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.',
      )
      ..addOption('use-existing-app',
        help: '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'
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..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',
66
      );
yjbanov's avatar
yjbanov committed
67 68
  }

69
  @override
70
  final String name = 'drive';
71 72

  @override
73
  final String description = 'Runs Flutter Driver tests for the current project.';
74 75

  @override
76 77 78 79
  final List<String> aliases = <String>['driver'];

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

81
  /// Subscription to log messages printed on the device or simulator.
82
  // ignore: cancel_subscriptions
83 84
  StreamSubscription<String> _deviceLogSubscription;

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

91
    _device = await targetDeviceFinder();
92 93
    if (device == null)
      throwToolExit(null);
94

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

98 99
    String observatoryUri;
    if (argResults['use-existing-app'] == null) {
100
      printStatus('Starting application: $targetFile');
101

102
      if (getBuildInfo().isRelease) {
103
        // This is because we need VM service to be able to drive the app.
104
        throwToolExit(
105 106 107 108 109 110 111
          '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).'
        );
      }

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

121 122
    Cache.releaseLockEarly();

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

    return null;
yjbanov's avatar
yjbanov committed
139 140 141
  }

  String _getTestFile() {
142 143 144 145 146
    if (argResults['driver'] != null)
      return argResults['driver'];

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

149
    // This command extends `flutter run` and therefore CWD == package dir
150
    final String packageDir = fs.currentDirectory.path;
151 152 153

    // Make appFile path relative to package directory because we are looking
    // for the corresponding test file relative to it.
154 155
    if (!fs.path.isRelative(appFile)) {
      if (!fs.path.isWithin(packageDir, appFile)) {
156 157 158 159 160 161
        printError(
          'Application file $appFile is outside the package directory $packageDir'
        );
        return null;
      }

162
      appFile = fs.path.relative(appFile, from: packageDir);
163 164
    }

165
    final List<String> parts = fs.path.split(appFile);
166 167 168 169 170 171 172 173 174 175 176 177

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

/// Finds a device to test on. May launch a simulator, if necessary.
185
typedef TargetDeviceFinder = Future<Device> Function();
186 187 188 189 190 191
TargetDeviceFinder targetDeviceFinder = findTargetDevice;
void restoreTargetDeviceFinder() {
  targetDeviceFinder = findTargetDevice;
}

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

194
  if (deviceManager.hasSpecifiedDeviceId) {
195 196 197 198 199 200
    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}':");
201
      await Device.printDevices(devices);
202 203 204
      return null;
    }
    return devices.first;
205 206
  }

207 208
  if (devices.isEmpty) {
    printError('No devices found.');
209
    return null;
210 211
  } else if (devices.length > 1) {
    printStatus('Found multiple connected devices:');
212
    printStatus(devices.map<String>((Device d) => '  - ${d.name}\n').join(''));
213
  }
214 215
  printStatus('Using device ${devices.first.name}.');
  return devices.first;
216 217 218
}

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

221
AppStarter appStarter = _startApp; // (mutable for testing)
222
void restoreAppStarter() {
223
  appStarter = _startApp;
224 225
}

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

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

  printTrace('Installing application package.');
237
  final ApplicationPackage package = await command.applicationPackages
238 239
      .getPackageForPlatform(await command.device.targetPlatform);
  if (await command.device.isAppInstalled(package))
240 241
    await command.device.uninstallApp(package);
  await command.device.installApp(package);
242

243
  final Map<String, dynamic> platformArgs = <String, dynamic>{};
244 245 246
  if (command.traceStartup)
    platformArgs['trace-startup'] = command.traceStartup;

247
  printTrace('Starting application.');
248 249

  // Forward device log messages to the terminal window running the "drive" command.
250 251 252 253 254
  command._deviceLogSubscription = command
      .device
      .getLogReader(app: package)
      .logLines
      .listen(printStatus);
255

256
  final LaunchResult result = await command.device.startApp(
257 258 259
    package,
    mainPath: mainPath,
    route: command.route,
260
    debuggingOptions: DebuggingOptions.enabled(
261
      command.getBuildInfo(),
Devon Carew's avatar
Devon Carew committed
262
      startPaused: true,
263
      observatoryPort: command.observatoryPort,
Devon Carew's avatar
Devon Carew committed
264
    ),
265
    platformArgs: platformArgs,
266
    usesTerminalUi: false,
267 268
  );

269 270
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
271
    return null;
272 273
  }

274
  return result;
275 276 277
}

/// Runs driver tests.
278
typedef TestRunner = Future<void> Function(List<String> testArgs, String observatoryUri);
279
TestRunner testRunner = _runTests;
280
void restoreTestRunner() {
281
  testRunner = _runTests;
282 283
}

284
Future<void> _runTests(List<String> testArgs, String observatoryUri) async {
285
  printTrace('Running driver tests.');
286

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


/// Stops the application.
305
typedef AppStopper = Future<bool> Function(DriveCommand command);
306
AppStopper appStopper = _stopApp;
307
void restoreAppStopper() {
308
  appStopper = _stopApp;
309 310
}

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