"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 'message.dart';
|
||||
|
||||
/// A function that connects to a Dart VM service given the [url].
|
||||
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;
|
||||
final Logger _log = new Logger('FlutterDriver');
|
||||
|
||||
/// Drives a Flutter Application running in another process.
|
||||
class FlutterDriver {
|
||||
|
||||
static const String _flutterExtensionMethod = 'ext.flutter_driver';
|
||||
static final Logger _log = new Logger('FlutterDriver');
|
||||
|
||||
/// Connects to a Flutter application.
|
||||
///
|
||||
@ -174,3 +161,42 @@ class FlutterDriver {
|
||||
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 {
|
||||
await logSub.cancel();
|
||||
vmServiceConnectFunction = vmServiceClientConnectFunction;
|
||||
restoreVmServiceConnectFunction();
|
||||
});
|
||||
|
||||
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/devices.dart';
|
||||
import 'src/commands/doctor.dart';
|
||||
import 'src/commands/drive.dart';
|
||||
import 'src/commands/install.dart';
|
||||
import 'src/commands/listen.dart';
|
||||
import 'src/commands/logs.dart';
|
||||
@ -51,6 +52,7 @@ Future main(List<String> args) async {
|
||||
..addCommand(new DaemonCommand(hideCommand: !verboseHelp))
|
||||
..addCommand(new DevicesCommand())
|
||||
..addCommand(new DoctorCommand())
|
||||
..addCommand(new DriveCommand())
|
||||
..addCommand(new InstallCommand())
|
||||
..addCommand(new ListenCommand())
|
||||
..addCommand(new LogsCommand())
|
||||
|
@ -2,14 +2,37 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// 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;
|
||||
|
||||
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.
|
||||
void ensureDirectoryExists(String filePath) {
|
||||
String dirPath = path.dirname(filePath);
|
||||
if (FileSystemEntity.isDirectorySync(dirPath))
|
||||
|
||||
if (syncFs.type(dirPath) == FileSystemEntityType.DIRECTORY)
|
||||
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 '../application_package.dart';
|
||||
import '../artifacts.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/file_system.dart' show ensureDirectoryExists;
|
||||
import '../base/os.dart';
|
||||
import '../base/process.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) {
|
||||
printTrace('Running stop command.');
|
||||
stopAll(devices, applicationPackages);
|
||||
await stopAll(devices, applicationPackages);
|
||||
}
|
||||
|
||||
if (install) {
|
||||
printTrace('Running install command.');
|
||||
installApp(devices, applicationPackages);
|
||||
await installApp(devices, applicationPackages);
|
||||
}
|
||||
|
||||
bool startedSomething = false;
|
||||
|
@ -13,7 +13,7 @@ import 'package:flx/signing.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import 'base/file_system.dart';
|
||||
import 'base/file_system.dart' show ensureDirectoryExists;
|
||||
import 'globals.dart';
|
||||
import 'toolchain.dart';
|
||||
|
||||
|
@ -13,6 +13,7 @@ dependencies:
|
||||
args: ^0.13.2+1
|
||||
crypto: ^0.9.1
|
||||
den_api: ^0.1.0
|
||||
file: ^0.1.0
|
||||
mustache4dart: ^1.0.0
|
||||
path: ^1.3.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