diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index b4fbfe3eae..89212e3bfe 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -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 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 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 _waitAndConnect(String url) async { + Stopwatch timer = new Stopwatch(); + Future 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(); +} diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index c2386b9187..bc724ffcee 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -40,7 +40,7 @@ main() { tearDown(() async { await logSub.cancel(); - vmServiceConnectFunction = vmServiceClientConnectFunction; + restoreVmServiceConnectFunction(); }); test('connects to isolate paused at start', () async { diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 9410c03a57..b58126b7b5 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -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 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()) diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 2010803d9b..591124e606 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -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); } diff --git a/packages/flutter_tools/lib/src/commands/apk.dart b/packages/flutter_tools/lib/src/commands/apk.dart index 57fb54e22f..e43686d9d9 100644 --- a/packages/flutter_tools/lib/src/commands/apk.dart +++ b/packages/flutter_tools/lib/src/commands/apk.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart new file mode 100644 index 0000000000..8d338e36e0 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -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 RunAppFunction(); +typedef Future RunTestsFunction(List testArgs); +typedef Future 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 aliases = ['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 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 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'; + } +} diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index c4fcb7b900..35abd8149b 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -164,12 +164,12 @@ Future 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; diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart index 0aab619f68..ad6d58b9df 100644 --- a/packages/flutter_tools/lib/src/flx.dart +++ b/packages/flutter_tools/lib/src/flx.dart @@ -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'; diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index be82b6507a..e4ef1cdae2 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -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 diff --git a/packages/flutter_tools/test/drive_test.dart b/packages/flutter_tools/test/drive_test.dart new file mode 100644 index 0000000000..0a87b72693 --- /dev/null +++ b/packages/flutter_tools/test/drive_test.dart @@ -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 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 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.value(0); + }), + runTestsFn: expectAsync((List testArgs) { + expect(testArgs, [testFile]); + return new Future.value(); + }), + stopAppFn: expectAsync(() { + return new Future.value(0); + }) + ); + applyMocksToCommand(command); + + MemoryFileSystem memFs = fs; + await memFs.file(testApp).writeAsString('main() {}'); + await memFs.file(testFile).writeAsString('main() {}'); + + List args = [ + 'drive', + '--target=$testApp', + ]; + return createTestCommandRunner(command).run(args).then((int code) { + expect(code, equals(0)); + BufferLogger buffer = logger; + expect(buffer.errorText, isEmpty); + }); + }); + }); +}