"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.
This commit is contained in:
parent
73ea415a5a
commit
a2b1bd4673
@ -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 {
|
|||||||
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() {
|
|||||||
|
|
||||||
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';
|
|||||||
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 {
|
|||||||
..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 @@
|
|||||||
// 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;
|
|||||||
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';
|
||||||
|
103
packages/flutter_tools/lib/src/commands/drive.dart
Normal file
103
packages/flutter_tools/lib/src/commands/drive.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// 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(
|
|||||||
|
|
||||||
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';
|
|||||||
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:
|
|||||||
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
|
||||||
|
105
packages/flutter_tools/test/drive_test.dart
Normal file
105
packages/flutter_tools/test/drive_test.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user