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
    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
      );
yjbanov's avatar
yjbanov committed
68 69
  }

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

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

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

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

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

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

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

96
    if (await fs.type(testFile) != FileSystemEntityType.file)
97
      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: $targetFile');
102

103
      if (getBuildInfo().isRelease) {
104
        // 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
      final LaunchResult result = await appStarter(this);
114 115 116
      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 132 133
      if (argResults['keep-app-running'] ?? (argResults['use-existing-app'] != null)) {
        printStatus('Leaving the application running.');
      } else {
134
        printStatus('Stopping application instance.');
135
        await appStopper(this);
136
      }
yjbanov's avatar
yjbanov committed
137
    }
138 139

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

275
  return result;
276 277 278
}

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

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

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


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

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