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

45 46 47 48
    argParser
      ..addFlag('keep-app-running',
        defaultsTo: null,
        help: 'Will keep the Flutter application running when done testing.\n'
49 50 51
              '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 '
52 53 54
              'running, and --no-keep-app-running overrides it.',
      )
      ..addOption('use-existing-app',
55 56
        help: 'Connect to an already running instance via the given observatory URL. '
              'If this option is given, the application will not be automatically started, '
57 58 59 60
              'and it will only be stopped if --no-keep-app-running is explicitly set.',
        valueHelp: 'url',
      )
      ..addOption('driver',
61 62 63 64 65 66
        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".',
67
        valueHelp: 'path',
68 69 70 71
      )
      ..addFlag('build',
        defaultsTo: true,
        help: 'Build the app before running.',
72
      );
yjbanov's avatar
yjbanov committed
73 74
  }

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

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

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

  Device _device;
  Device get device => _device;
86
  bool get shouldBuild => argResults['build'];
yjbanov's avatar
yjbanov committed
87

88 89
  bool get verboseSystemLogs => argResults['verbose-system-logs'];

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

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

101
    _device = await findTargetDevice();
102
    if (device == null) {
103
      throwToolExit(null);
104
    }
105

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

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

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

124
      final LaunchResult result = await appStarter(this);
125
      if (result == null) {
126
        throwToolExit('Application failed to start. Will not run test. Quitting.', exitCode: 1);
127
      }
128
      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
    } catch (error, stackTrace) {
139
      if (error is ToolExit) {
140
        rethrow;
141
      }
142
      throwToolExit('CAUGHT EXCEPTION: $error\n$stackTrace');
yjbanov's avatar
yjbanov committed
143
    } finally {
144 145 146
      if (argResults['keep-app-running'] ?? (argResults['use-existing-app'] != null)) {
        printStatus('Leaving the application running.');
      } else {
147
        printStatus('Stopping application instance.');
148
        await appStopper(this);
149
      }
yjbanov's avatar
yjbanov committed
150
    }
151 152

    return null;
yjbanov's avatar
yjbanov committed
153 154 155
  }

  String _getTestFile() {
156
    if (argResults['driver'] != null) {
157
      return argResults['driver'];
158
    }
159 160 161

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

164
    // This command extends `flutter run` and therefore CWD == package dir
165
    final String packageDir = fs.currentDirectory.path;
166 167 168

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

177
      appFile = fs.path.relative(appFile, from: packageDir);
178 179
    }

180
    final List<String> parts = fs.path.split(appFile);
181 182 183 184 185 186 187 188 189 190 191 192

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

Future<Device> findTargetDevice() async {
200
  final List<Device> devices = await deviceManager.findTargetDevices(FlutterProject.current());
201

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

215 216
  if (devices.isEmpty) {
    printError('No devices found.');
217
    return null;
218 219
  } else if (devices.length > 1) {
    printStatus('Found multiple connected devices:');
220
    await Device.printDevices(devices);
221
  }
222 223
  printStatus('Using device ${devices.first.name}.');
  return devices.first;
224 225 226
}

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

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

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

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

244
  final ApplicationPackage package = await command.applicationPackages
245
      .getPackageForPlatform(await command.device.targetPlatform);
246 247 248

  if (command.shouldBuild) {
    printTrace('Installing application package.');
249
    if (await command.device.isAppInstalled(package)) {
250
      await command.device.uninstallApp(package);
251
    }
252 253
    await command.device.installApp(package);
  }
254

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

260
  printTrace('Starting application.');
261 262

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

269
  final LaunchResult result = await command.device.startApp(
270 271 272
    package,
    mainPath: mainPath,
    route: command.route,
273
    debuggingOptions: DebuggingOptions.enabled(
274
      command.getBuildInfo(),
Devon Carew's avatar
Devon Carew committed
275
      startPaused: true,
276
      observatoryPort: command.observatoryPort,
277
      verboseSystemLogs: command.verboseSystemLogs,
278
      cacheSkSL: command.cacheSkSL,
Devon Carew's avatar
Devon Carew committed
279
    ),
280
    platformArgs: platformArgs,
281
    prebuiltApplication: !command.shouldBuild,
282 283
  );

284 285
  if (!result.started) {
    await command._deviceLogSubscription.cancel();
286
    return null;
287 288
  }

289
  return result;
290 291 292
}

/// Runs driver tests.
293
typedef TestRunner = Future<void> Function(List<String> testArgs, String observatoryUri);
294
TestRunner testRunner = _runTests;
295
void restoreTestRunner() {
296
  testRunner = _runTests;
297 298
}

299
Future<void> _runTests(List<String> testArgs, String observatoryUri) async {
300
  printTrace('Running driver tests.');
301

302
  PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath));
303
  final String dartVmPath = fs.path.join(dartSdkPath, 'bin', 'dart');
304
  final int result = await processUtils.stream(
305 306 307 308 309 310 311
    <String>[
      dartVmPath,
      ...dartVmFlags,
      ...testArgs,
      '--packages=${PackageMap.globalPackagesPath}',
      '-rexpanded',
    ],
312
    environment: <String, String>{'VM_SERVICE_URL': observatoryUri},
313
  );
314
  if (result != 0) {
315
    throwToolExit('Driver tests failed: $result', exitCode: result);
316
  }
317 318 319 320
}


/// Stops the application.
321
typedef AppStopper = Future<bool> Function(DriveCommand command);
322
AppStopper appStopper = _stopApp;
323
void restoreAppStopper() {
324
  appStopper = _stopApp;
325 326
}

327
Future<bool> _stopApp(DriveCommand command) async {
328
  printTrace('Stopping application.');
329
  final ApplicationPackage package = await command.applicationPackages.getPackageForPlatform(await command.device.targetPlatform);
330
  final bool stopped = await command.device.stopApp(package);
331
  await command._deviceLogSubscription?.cancel();
332
  return stopped;
333
}