Commit a2b1bd46 authored by yjbanov's avatar yjbanov

"flutter drive" command

Runs a test app and a driver test simultaneously, then stops the app.

Usage:

```
flutter drive --target=/path/to/test/app.dart
```

This command will look for `/path/to/test/app_test.dart` by
convention. We will expand into other ways of discovering tests in the
future.
parent 73ea415a
...@@ -12,25 +12,12 @@ import 'gesture.dart'; ...@@ -12,25 +12,12 @@ import 'gesture.dart';
import 'health.dart'; import 'health.dart';
import 'message.dart'; import 'message.dart';
/// A function that connects to a Dart VM service given the [url]. final Logger _log = new Logger('FlutterDriver');
typedef Future<VMServiceClient> VMServiceConnectFunction(String url);
/// Connects to a real Dart VM service using the [VMServiceClient].
final VMServiceConnectFunction vmServiceClientConnectFunction =
VMServiceClient.connect;
/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a different method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction =
vmServiceClientConnectFunction;
/// Drives a Flutter Application running in another process. /// Drives a Flutter Application running in another process.
class FlutterDriver { class FlutterDriver {
static const String _flutterExtensionMethod = 'ext.flutter_driver'; static const String _flutterExtensionMethod = 'ext.flutter_driver';
static final Logger _log = new Logger('FlutterDriver');
/// Connects to a Flutter application. /// Connects to a Flutter application.
/// ///
...@@ -174,3 +161,42 @@ class FlutterDriver { ...@@ -174,3 +161,42 @@ class FlutterDriver {
return null; return null;
}); });
} }
/// A function that connects to a Dart VM service given the [url].
typedef Future<VMServiceClient> VMServiceConnectFunction(String url);
/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;
/// Restores [vmServiceConnectFunction] to its default value.
void restoreVmServiceConnectFunction() {
vmServiceConnectFunction = _waitAndConnect;
}
/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
///
/// Times out after 30 seconds.
Future<VMServiceClient> _waitAndConnect(String url) async {
Stopwatch timer = new Stopwatch();
Future<VMServiceClient> attemptConnection() {
return VMServiceClient.connect(url)
.catchError((e) async {
if (timer.elapsed < const Duration(seconds: 30)) {
_log.info('Waiting for application to start');
await new Future.delayed(const Duration(seconds: 1));
return attemptConnection();
} else {
_log.critical(
'Application has not started in 30 seconds. '
'Giving up.'
);
throw e;
}
});
}
return attemptConnection();
}
...@@ -40,7 +40,7 @@ main() { ...@@ -40,7 +40,7 @@ main() {
tearDown(() async { tearDown(() async {
await logSub.cancel(); await logSub.cancel();
vmServiceConnectFunction = vmServiceClientConnectFunction; restoreVmServiceConnectFunction();
}); });
test('connects to isolate paused at start', () async { test('connects to isolate paused at start', () async {
......
...@@ -19,6 +19,7 @@ import 'src/commands/create.dart'; ...@@ -19,6 +19,7 @@ import 'src/commands/create.dart';
import 'src/commands/daemon.dart'; import 'src/commands/daemon.dart';
import 'src/commands/devices.dart'; import 'src/commands/devices.dart';
import 'src/commands/doctor.dart'; import 'src/commands/doctor.dart';
import 'src/commands/drive.dart';
import 'src/commands/install.dart'; import 'src/commands/install.dart';
import 'src/commands/listen.dart'; import 'src/commands/listen.dart';
import 'src/commands/logs.dart'; import 'src/commands/logs.dart';
...@@ -51,6 +52,7 @@ Future main(List<String> args) async { ...@@ -51,6 +52,7 @@ Future main(List<String> args) async {
..addCommand(new DaemonCommand(hideCommand: !verboseHelp)) ..addCommand(new DaemonCommand(hideCommand: !verboseHelp))
..addCommand(new DevicesCommand()) ..addCommand(new DevicesCommand())
..addCommand(new DoctorCommand()) ..addCommand(new DoctorCommand())
..addCommand(new DriveCommand())
..addCommand(new InstallCommand()) ..addCommand(new InstallCommand())
..addCommand(new ListenCommand()) ..addCommand(new ListenCommand())
..addCommand(new LogsCommand()) ..addCommand(new LogsCommand())
......
...@@ -2,14 +2,37 @@ ...@@ -2,14 +2,37 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io'; import 'package:file/io.dart';
import 'package:file/sync_io.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
export 'package:file/io.dart';
export 'package:file/sync_io.dart';
/// Currently active implmenetation of the file system.
///
/// By default it uses local disk-based implementation. Override this in tests
/// with [MemoryFileSystem].
FileSystem fs = new LocalFileSystem();
SyncFileSystem syncFs = new SyncLocalFileSystem();
/// Restores [fs] and [syncFs] to the default local disk-based implementation.
void restoreFileSystem() {
fs = new LocalFileSystem();
syncFs = new SyncLocalFileSystem();
}
void useInMemoryFileSystem() {
MemoryFileSystem memFs = new MemoryFileSystem();
fs = memFs;
syncFs = new SyncMemoryFileSystem(backedBy: memFs.storage);
}
/// Create the ancestor directories of a file path if they do not already exist. /// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) { void ensureDirectoryExists(String filePath) {
String dirPath = path.dirname(filePath); String dirPath = path.dirname(filePath);
if (FileSystemEntity.isDirectorySync(dirPath))
if (syncFs.type(dirPath) == FileSystemEntityType.DIRECTORY)
return; return;
new Directory(dirPath).createSync(recursive: true); syncFs.directory(dirPath).create(recursive: true);
} }
...@@ -10,7 +10,7 @@ import 'package:path/path.dart' as path; ...@@ -10,7 +10,7 @@ import 'package:path/path.dart' as path;
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/file_system.dart'; import '../base/file_system.dart' show ensureDirectoryExists;
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
......
// 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';
import 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable;
import '../base/file_system.dart';
import '../globals.dart';
import 'run.dart';
import 'stop.dart';
typedef Future<int> RunAppFunction();
typedef Future<Null> RunTestsFunction(List<String> testArgs);
typedef Future<int> StopAppFunction();
/// 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
/// file with the same name but containing the `_test.dart` suffix. The
/// `_test.dart` file is expected to be a program that uses
/// `package:flutter_driver` that exercises your application. Most commonly it
/// 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.
class DriveCommand extends RunCommand {
final String name = 'drive';
final String description = 'Runs Flutter Driver tests for the current project.';
final List<String> aliases = <String>['driver'];
RunAppFunction _runApp;
RunTestsFunction _runTests;
StopAppFunction _stopApp;
/// Creates a drive command with custom process management functions.
///
/// [runAppFn] starts a Flutter application.
///
/// [runTestsFn] runs tests.
///
/// [stopAppFn] stops the test app after tests are finished.
DriveCommand.custom({
RunAppFunction runAppFn,
RunTestsFunction runTestsFn,
StopAppFunction stopAppFn
}) {
_runApp = runAppFn ?? super.runInProject;
_runTests = runTestsFn ?? executable.main;
_stopApp = stopAppFn ?? this.stop;
}
DriveCommand() : this.custom();
@override
Future<int> runInProject() async {
String testFile = _getTestFile();
if (await fs.type(testFile) != FileSystemEntityType.FILE) {
printError('Test file not found: $testFile');
return 1;
}
int result = await _runApp();
if (result != 0) {
printError('Application failed to start. Will not run test. Quitting.');
return result;
}
try {
return await _runTests([testFile])
.then((_) => 0)
.catchError((error, stackTrace) {
printError('ERROR: $error\n$stackTrace');
return 1;
});
} finally {
await _stopApp();
}
}
Future<int> stop() async {
return await stopAll(devices, applicationPackages) ? 0 : 2;
}
String _getTestFile() {
String appFile = argResults['target'];
String extension = path.extension(appFile);
String name = path.withoutExtension(appFile);
return '${name}_test$extension';
}
}
...@@ -164,12 +164,12 @@ Future<int> startApp( ...@@ -164,12 +164,12 @@ Future<int> startApp(
if (stop) { if (stop) {
printTrace('Running stop command.'); printTrace('Running stop command.');
stopAll(devices, applicationPackages); await stopAll(devices, applicationPackages);
} }
if (install) { if (install) {
printTrace('Running install command.'); printTrace('Running install command.');
installApp(devices, applicationPackages); await installApp(devices, applicationPackages);
} }
bool startedSomething = false; bool startedSomething = false;
......
...@@ -13,7 +13,7 @@ import 'package:flx/signing.dart'; ...@@ -13,7 +13,7 @@ import 'package:flx/signing.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'base/file_system.dart'; import 'base/file_system.dart' show ensureDirectoryExists;
import 'globals.dart'; import 'globals.dart';
import 'toolchain.dart'; import 'toolchain.dart';
......
...@@ -13,6 +13,7 @@ dependencies: ...@@ -13,6 +13,7 @@ dependencies:
args: ^0.13.2+1 args: ^0.13.2+1
crypto: ^0.9.1 crypto: ^0.9.1
den_api: ^0.1.0 den_api: ^0.1.0
file: ^0.1.0
mustache4dart: ^1.0.0 mustache4dart: ^1.0.0
path: ^1.3.0 path: ^1.3.0
pub_semver: ^1.0.0 pub_semver: ^1.0.0
......
// 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';
import 'package:file/file.dart';
import 'package:flutter_tools/src/commands/drive.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:test/test.dart';
import 'src/common.dart';
import 'src/context.dart';
import 'src/mocks.dart';
main() => defineTests();
defineTests() {
group('drive', () {
setUp(() {
useInMemoryFileSystem();
});
tearDown(() {
restoreFileSystem();
});
testUsingContext('returns 1 when test file is not found', () {
DriveCommand command = new DriveCommand();
applyMocksToCommand(command);
List<String> args = [
'drive',
'--target=/some/app/test/e2e.dart',
];
return createTestCommandRunner(command).run(args).then((int code) {
expect(code, equals(1));
BufferLogger buffer = logger;
expect(buffer.errorText,
contains('Test file not found: /some/app/test/e2e_test.dart'));
});
});
testUsingContext('returns 1 when app fails to run', () async {
DriveCommand command = new DriveCommand.custom(runAppFn: expectAsync(() {
return new Future.value(1);
}));
applyMocksToCommand(command);
String testApp = '/some/app/test/e2e.dart';
String testFile = '/some/app/test/e2e_test.dart';
MemoryFileSystem memFs = fs;
await memFs.file(testApp).writeAsString('main() {}');
await memFs.file(testFile).writeAsString('main() {}');
List<String> args = [
'drive',
'--target=$testApp',
];
return createTestCommandRunner(command).run(args).then((int code) {
expect(code, equals(1));
BufferLogger buffer = logger;
expect(buffer.errorText, contains(
'Application failed to start. Will not run test. Quitting.'
));
});
});
testUsingContext('returns 0 when test ends successfully', () async {
String testApp = '/some/app/test/e2e.dart';
String testFile = '/some/app/test/e2e_test.dart';
DriveCommand command = new DriveCommand.custom(
runAppFn: expectAsync(() {
return new Future<int>.value(0);
}),
runTestsFn: expectAsync((List<String> testArgs) {
expect(testArgs, [testFile]);
return new Future<Null>.value();
}),
stopAppFn: expectAsync(() {
return new Future<int>.value(0);
})
);
applyMocksToCommand(command);
MemoryFileSystem memFs = fs;
await memFs.file(testApp).writeAsString('main() {}');
await memFs.file(testFile).writeAsString('main() {}');
List<String> args = [
'drive',
'--target=$testApp',
];
return createTestCommandRunner(command).run(args).then((int code) {
expect(code, equals(0));
BufferLogger buffer = logger;
expect(buffer.errorText, isEmpty);
});
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment