"flutter create" can generate a basic driver test

"flutter create" adds option `--with-driver-test` that adds
dependencies to `flutter_driver` in `pubspec.yaml` and creates
a basic driver test runnable via `flutter drive
--target=test_driver/e2e.dart`

"flutter drive" new options:

- `--keep-app-running` tells the driver to not stop the app after tests
  are done
- `--use-existing-app` tells the driver to not start a new app but use
  an already running instance
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 591124e..5d077ee 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io' as dart_io;
 import 'package:file/io.dart';
 import 'package:file/sync_io.dart';
 import 'package:path/path.dart' as path;
@@ -16,16 +17,42 @@
 FileSystem fs = new LocalFileSystem();
 SyncFileSystem syncFs = new SyncLocalFileSystem();
 
+typedef String CurrentDirectoryGetter();
+
+final CurrentDirectoryGetter _defaultCurrentDirectoryGetter = () {
+  return dart_io.Directory.current.path;
+};
+
+/// Points to the current working directory (like `pwd`).
+CurrentDirectoryGetter getCurrentDirectory = _defaultCurrentDirectoryGetter;
+
+/// Exits the process with the given [exitCode].
+typedef void ExitFunction([int exitCode]);
+
+final ExitFunction _defaultExitFunction = ([int exitCode]) {
+  dart_io.exit(exitCode);
+};
+
+/// Exits the process.
+ExitFunction exit = _defaultExitFunction;
+
 /// Restores [fs] and [syncFs] to the default local disk-based implementation.
 void restoreFileSystem() {
   fs = new LocalFileSystem();
   syncFs = new SyncLocalFileSystem();
+  getCurrentDirectory = _defaultCurrentDirectoryGetter;
+  exit = _defaultExitFunction;
 }
 
-void useInMemoryFileSystem() {
+/// Uses in-memory replacments for `dart:io` functionality. Useful in tests.
+void useInMemoryFileSystem({ cwd: '/', ExitFunction exitFunction }) {
   MemoryFileSystem memFs = new MemoryFileSystem();
   fs = memFs;
   syncFs = new SyncMemoryFileSystem(backedBy: memFs.storage);
+  getCurrentDirectory = () => cwd;
+  exit = exitFunction ?? ([int exitCode]) {
+    throw new Exception('Exited with code $exitCode');
+  };
 }
 
 /// Create the ancestor directories of a file path if they do not already exist.
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 6ec70ae..8277248 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -30,6 +30,12 @@
       defaultsTo: true,
       help: 'Whether to run "pub get" after the project has been created.'
     );
+    argParser.addFlag(
+      'with-driver-test',
+      negatable: true,
+      defaultsTo: false,
+      help: 'Also add Flutter Driver dependencies and generate a sample driver test.'
+    );
   }
 
   String get invocation => "${runner.executableName} $name <output directory>";
@@ -50,12 +56,19 @@
 
     String flutterRoot = path.absolute(ArtifactStore.flutterRoot);
 
-    String flutterPackagePath = path.join(flutterRoot, 'packages', 'flutter');
+    String flutterPackagesDirectory = path.join(flutterRoot, 'packages');
+    String flutterPackagePath = path.join(flutterPackagesDirectory, 'flutter');
     if (!FileSystemEntity.isFileSync(path.join(flutterPackagePath, 'pubspec.yaml'))) {
       printError('Unable to find package:flutter in $flutterPackagePath');
       return 2;
     }
 
+    String flutterDriverPackagePath = path.join(flutterRoot, 'packages', 'flutter_driver');
+    if (!FileSystemEntity.isFileSync(path.join(flutterDriverPackagePath, 'pubspec.yaml'))) {
+      printError('Unable to find package:flutter_driver in $flutterDriverPackagePath');
+      return 2;
+    }
+
     Directory out;
 
     if (argResults.wasParsed('out')) {
@@ -64,7 +77,15 @@
       out = new Directory(argResults.rest.first);
     }
 
-    new FlutterSimpleTemplate().generateInto(out, flutterPackagePath);
+    FlutterSimpleTemplate template = new FlutterSimpleTemplate();
+
+    if (argResults['with-driver-test'])
+      template.withDriverTest();
+
+    template.generateInto(
+      dir: out,
+      flutterPackagesDirectory: flutterPackagesDirectory
+    );
 
     printStatus('');
 
@@ -90,19 +111,23 @@
 abstract class Template {
   final String name;
   final String description;
-
-  Map<String, String> files = <String, String>{};
+  final Map<String, String> files = <String, String>{};
+  final Map<String, dynamic> additionalTemplateVariables = <String, dynamic>{};
 
   Template(this.name, this.description);
 
-  void generateInto(Directory dir, String flutterPackagePath) {
+  void generateInto({
+    Directory dir,
+    String flutterPackagesDirectory
+  }) {
     String dirPath = path.normalize(dir.absolute.path);
     String projectName = _normalizeProjectName(path.basename(dirPath));
     String projectIdentifier = _createProjectIdentifier(path.basename(dirPath));
     printStatus('Creating ${path.basename(projectName)}...');
     dir.createSync(recursive: true);
 
-    String relativeFlutterPackagePath = path.relative(flutterPackagePath, from: dirPath);
+    String relativeFlutterPackagesDirectory =
+        path.relative(flutterPackagesDirectory, from: dirPath);
     Iterable<String> paths = files.keys.toList()..sort();
 
     for (String filePath in paths) {
@@ -111,8 +136,9 @@
         'projectName': projectName,
         'projectIdentifier': projectIdentifier,
         'description': description,
-        'flutterPackagePath': relativeFlutterPackagePath
+        'flutterPackagesDirectory': relativeFlutterPackagesDirectory,
       };
+      m.addAll(additionalTemplateVariables);
       contents = mustache.render(contents, m);
       filePath = filePath.replaceAll('/', Platform.pathSeparator);
       File file = new File(path.join(dir.path, filePath));
@@ -142,6 +168,12 @@
     // iOS files.
     files.addAll(iosTemplateFiles);
   }
+
+  void withDriverTest() {
+    additionalTemplateVariables['withDriverTest?'] = {};
+    files['test_driver/e2e.dart'] = _e2eApp;
+    files['test_driver/e2e_test.dart'] = _e2eTest;
+  }
 }
 
 String _normalizeProjectName(String name) {
@@ -194,7 +226,12 @@
 description: {{description}}
 dependencies:
   flutter:
-    path: {{flutterPackagePath}}
+    path: {{flutterPackagesDirectory}}/flutter
+{{#withDriverTest?}}
+dev_dependencies:
+  flutter_driver:
+    path: {{flutterPackagesDirectory}}/flutter_driver
+{{/withDriverTest?}}
 ''';
 
 const String _flutterYaml = r'''
@@ -240,10 +277,14 @@
       ),
       body: new Material(
         child: new Center(
-          child: new Text('Button tapped $_counter times.')
+          child: new Text(
+            'Button tapped $_counter times.',
+            key: const ValueKey('counter')
+          )
         )
       ),
       floatingActionButton: new FloatingActionButton(
+        key: const ValueKey('fab'),
         child: new Icon(
           icon: 'content/add'
         ),
@@ -254,6 +295,65 @@
 }
 ''';
 
+const String _e2eApp = '''
+// Starts the app with Flutter Driver extension enabled to allow Flutter Driver
+// to test the app.
+import 'package:{{projectName}}/main.dart' as app;
+import 'package:flutter_driver/driver_extension.dart';
+
+main() {
+  enableFlutterDriverExtension();
+  app.main();
+}
+''';
+
+const String _e2eTest = '''
+// This is a basic Flutter Driver test for the application. A Flutter Driver
+// test is an end-to-end test that "drives" your application from another
+// process or even from another computer. If you are familiar with
+// Selenium/WebDriver for web, Espresso for Android or UI Automation for iOS,
+// this is simply Flutter's version of that.
+//
+// To start the test run the following command from the root of your application
+// package:
+//
+//     flutter drive --target=test_driver/e2e.dart
+//
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart';
+
+main() {
+  group('end-to-end test', () {
+    FlutterDriver driver;
+
+    setUpAll(() async {
+      // Connect to a running Flutter application instance.
+      driver = await FlutterDriver.connect();
+    });
+
+    tearDownAll(() async {
+      if (driver != null) driver.close();
+    });
+
+    test('find the floating action button by value key', () async {
+      ObjectRef elem = await driver.findByValueKey('fab');
+      expect(elem, isNotNull);
+      expect(elem.objectReferenceKey, isNotNull);
+    });
+
+    test('tap on the floating action button; verify counter', () async {
+      ObjectRef fab = await driver.findByValueKey('fab');
+      expect(fab, isNotNull);
+      await driver.tap(fab);
+      ObjectRef counter = await driver.findByValueKey('counter');
+      expect(counter, isNotNull);
+      String text = await driver.getText(counter);
+      expect(text, contains("Button tapped 1 times."));
+    });
+  });
+}
+''';
+
 final String _apkManifest = '''
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="{{projectIdentifier}}"
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index 8d338e3..4e5c849 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -25,9 +25,10 @@
 ///
 /// 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
+/// 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
+/// `_test.dart` file would generall be a Dart program that uses
+/// `package:flutter_driver` and exercises your application. Most commonly it
 /// is a test written using `package:test`, but you are free to use something
 /// else.
 ///
@@ -59,6 +60,25 @@
     _runApp = runAppFn ?? super.runInProject;
     _runTests = runTestsFn ?? executable.main;
     _stopApp = stopAppFn ?? this.stop;
+
+    argParser.addFlag(
+      'keep-app-running',
+      negatable: true,
+      defaultsTo: false,
+      help:
+        'Will keep the Flutter application running when done testing. By '
+        'default Flutter Driver stops the application after tests are finished.'
+    );
+
+    argParser.addFlag(
+      'use-existing-app',
+      negatable: true,
+      defaultsTo: false,
+      help:
+        'Will not start a new Flutter application but connect to an '
+        'already running instance. This will also cause the driver to keep '
+        'the application running after tests are done.'
+    );
   }
 
   DriveCommand() : this.custom();
@@ -66,27 +86,40 @@
   @override
   Future<int> runInProject() async {
     String testFile = _getTestFile();
+    if (testFile == null) {
+      return 1;
+    }
 
     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;
+    if (!argResults['use-existing-app']) {
+      printStatus('Starting application: ${argResults["target"]}');
+      int result = await _runApp();
+      if (result != 0) {
+        printError('Application failed to start. Will not run test. Quitting.');
+        return result;
+      }
+    } else {
+      printStatus('Will connect to already running application instance');
     }
 
     try {
       return await _runTests([testFile])
         .then((_) => 0)
         .catchError((error, stackTrace) {
-          printError('ERROR: $error\n$stackTrace');
+          printError('CAUGHT EXCEPTION: $error\n$stackTrace');
           return 1;
         });
     } finally {
-      await _stopApp();
+      if (!argResults['keep-app-running'] && !argResults['use-existing-app']) {
+        printStatus('Stopping application instance');
+        await _stopApp();
+      } else {
+        printStatus('Leaving the application running');
+      }
     }
   }
 
@@ -95,9 +128,39 @@
   }
 
   String _getTestFile() {
-    String appFile = argResults['target'];
-    String extension = path.extension(appFile);
-    String name = path.withoutExtension(appFile);
-    return '${name}_test$extension';
+    String appFile = path.normalize(argResults['target']);
+
+    // This command extends `flutter start` and therefore CWD == package dir
+    String packageDir = getCurrentDirectory();
+
+    // Make appFile path relative to package directory because we are looking
+    // for the corresponding test file relative to it.
+    if (!path.isRelative(appFile)) {
+      if (!path.isWithin(packageDir, appFile)) {
+        printError(
+          'Application file $appFile is outside the package directory $packageDir'
+        );
+        return null;
+      }
+
+      appFile = path.relative(appFile, from: packageDir);
+    }
+
+    List<String> parts = path.split(appFile);
+
+    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`.
+    String pathWithNoExtension = path.withoutExtension(path.joinAll(
+      [packageDir, 'test_driver']..addAll(parts.skip(1))));
+    return '${pathWithNoExtension}_test${path.extension(appFile)}';
   }
 }
diff --git a/packages/flutter_tools/test/drive_test.dart b/packages/flutter_tools/test/drive_test.dart
index 0a87b72..ca29bd6 100644
--- a/packages/flutter_tools/test/drive_test.dart
+++ b/packages/flutter_tools/test/drive_test.dart
@@ -20,7 +20,7 @@
 defineTests() {
   group('drive', () {
     setUp(() {
-      useInMemoryFileSystem();
+      useInMemoryFileSystem(cwd: '/some/app');
     });
 
     tearDown(() {
@@ -39,7 +39,7 @@
         expect(code, equals(1));
         BufferLogger buffer = logger;
         expect(buffer.errorText,
-            contains('Test file not found: /some/app/test/e2e_test.dart'));
+            contains('Test file not found: /some/app/test_driver/e2e_test.dart'));
       });
     });
 
@@ -49,8 +49,8 @@
       }));
       applyMocksToCommand(command);
 
-      String testApp = '/some/app/test/e2e.dart';
-      String testFile = '/some/app/test/e2e_test.dart';
+      String testApp = '/some/app/test_driver/e2e.dart';
+      String testFile = '/some/app/test_driver/e2e_test.dart';
 
       MemoryFileSystem memFs = fs;
       await memFs.file(testApp).writeAsString('main() {}');
@@ -69,9 +69,50 @@
       });
     });
 
+    testUsingContext('returns 1 when app file is outside package', () async {
+      String packageDir = '/my/app';
+      useInMemoryFileSystem(cwd: packageDir);
+      DriveCommand command = new DriveCommand();
+      applyMocksToCommand(command);
+
+      String appFile = '/not/in/my/app.dart';
+      List<String> args = [
+        'drive',
+        '--target=$appFile',
+      ];
+      return createTestCommandRunner(command).run(args).then((int code) {
+        expect(code, equals(1));
+        BufferLogger buffer = logger;
+        expect(buffer.errorText, contains(
+          'Application file $appFile is outside the package directory $packageDir'
+        ));
+      });
+    });
+
+    testUsingContext('returns 1 when app file is in the root dir', () async {
+      String packageDir = '/my/app';
+      useInMemoryFileSystem(cwd: packageDir);
+      DriveCommand command = new DriveCommand();
+      applyMocksToCommand(command);
+
+      String appFile = '/my/app/main.dart';
+      List<String> args = [
+        'drive',
+        '--target=$appFile',
+      ];
+      return createTestCommandRunner(command).run(args).then((int code) {
+        expect(code, equals(1));
+        BufferLogger buffer = logger;
+        expect(buffer.errorText, contains(
+          'Application file main.dart must reside in one of the '
+          'sub-directories of the package structure, not in the root directory.'
+        ));
+      });
+    });
+
     testUsingContext('returns 0 when test ends successfully', () async {
       String testApp = '/some/app/test/e2e.dart';
-      String testFile = '/some/app/test/e2e_test.dart';
+      String testFile = '/some/app/test_driver/e2e_test.dart';
 
       DriveCommand command = new DriveCommand.custom(
         runAppFn: expectAsync(() {