Add '-t' option to 'attach' command. (#20539)

* Add '-t' option to 'attach' command.

* Add test

* Make analyzer happy

* Fix tests so they use memory file system and can find lib/main.dart
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index cd550f6..2812b4c 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -35,7 +35,7 @@
 /// As soon as a new observatory is detected the command attaches to it and
 /// enables hot reloading.
 class AttachCommand extends FlutterCommand {
-  AttachCommand({bool verboseHelp = false}) {
+  AttachCommand({bool verboseHelp = false, this.hotRunnerFactory}) {
     addBuildModeFlags(defaultToRelease: false);
     argParser
       ..addOption(
@@ -53,8 +53,12 @@
           help: 'Handle machine structured JSON command input and provide output\n'
                 'and progress in machine friendly format.',
       );
+    usesTargetOption();
+    hotRunnerFactory ??= new HotRunnerFactory();
   }
 
+  HotRunnerFactory hotRunnerFactory;
+
   @override
   final String name = 'attach';
 
@@ -116,8 +120,9 @@
       final FlutterDevice flutterDevice = new FlutterDevice(device,
           trackWidgetCreation: false, previewDart2: argResults['preview-dart-2']);
       flutterDevice.observatoryUris = <Uri>[ observatoryUri ];
-      final HotRunner hotRunner = new HotRunner(
+      final HotRunner hotRunner = hotRunnerFactory.build(
         <FlutterDevice>[flutterDevice],
+        target: targetFile,
         debuggingOptions: new DebuggingOptions.enabled(getBuildInfo()),
         packagesFilePath: globalResults['packages'],
         usesTerminalUI: daemon == null,
@@ -145,3 +150,32 @@
 
   Future<void> _validateArguments() async {}
 }
+
+class HotRunnerFactory {
+  HotRunner build(List<FlutterDevice> devices, {
+      String target,
+      DebuggingOptions debuggingOptions,
+      bool usesTerminalUI = true,
+      bool benchmarkMode = false,
+      File applicationBinary,
+      bool hostIsIde = false,
+      String projectRootPath,
+      String packagesFilePath,
+      String dillOutputPath,
+      bool stayResident = true,
+      bool ipv6 = false,
+  }) => new HotRunner(
+    devices,
+    target: target,
+    debuggingOptions: debuggingOptions,
+    usesTerminalUI: usesTerminalUI,
+    benchmarkMode: benchmarkMode,
+    applicationBinary: applicationBinary,
+    hostIsIde: hostIsIde,
+    projectRootPath: projectRootPath,
+    packagesFilePath: packagesFilePath,
+    dillOutputPath: dillOutputPath,
+    stayResident: stayResident,
+    ipv6: ipv6,
+  );
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/test/commands/attach_test.dart b/packages/flutter_tools/test/commands/attach_test.dart
index ed1da2c..475b2d0 100644
--- a/packages/flutter_tools/test/commands/attach_test.dart
+++ b/packages/flutter_tools/test/commands/attach_test.dart
@@ -4,10 +4,14 @@
 
 import 'dart:async';
 
+import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/attach.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/run_hot.dart';
 import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
@@ -16,8 +20,15 @@
 
 void main() {
   group('attach', () {
+    final FileSystem testFileSystem = new MemoryFileSystem(
+      style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle
+          .posix,
+    );
+
     setUpAll(() {
       Cache.disableLocking();
+      testFileSystem.directory('lib').createSync();
+      testFileSystem.file('lib/main.dart').createSync();
     });
 
     testUsingContext('finds observatory port and forwards', () async {
@@ -30,14 +41,17 @@
         // Now that the reader is used, start writing messages to it.
         Timer.run(() {
           mockLogReader.addLine('Foo');
-          mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
+          mockLogReader.addLine(
+              'Observatory listening on http://127.0.0.1:$devicePort');
         });
 
         return mockLogReader;
       });
       when(device.portForwarder).thenReturn(portForwarder);
-      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort'))).thenAnswer((_) async => hostPort);
-      when(portForwarder.forwardedPorts).thenReturn(<ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
+      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+          .thenAnswer((_) async => hostPort);
+      when(portForwarder.forwardedPorts).thenReturn(
+          <ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
       when(portForwarder.unforward(any)).thenAnswer((_) async => null);
       testDeviceManager.addDevice(device);
 
@@ -45,10 +59,62 @@
 
       await createTestCommandRunner(command).run(<String>['attach']);
 
-      verify(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort'))).called(1);
+      verify(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+          .called(1);
 
       mockLogReader.dispose();
-    });
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    },
+    );
+
+    testUsingContext('selects specified target', () async {
+      const int devicePort = 499;
+      const int hostPort = 42;
+      final MockDeviceLogReader mockLogReader = new MockDeviceLogReader();
+      final MockPortForwarder portForwarder = new MockPortForwarder();
+      final MockAndroidDevice device = new MockAndroidDevice();
+      final MockHotRunnerFactory mockHotRunnerFactory = new MockHotRunnerFactory();
+      when(device.portForwarder).thenReturn(portForwarder);
+      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+          .thenAnswer((_) async => hostPort);
+      when(portForwarder.forwardedPorts).thenReturn(
+          <ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
+      when(portForwarder.unforward(any)).thenAnswer((_) async => null);
+      when(mockHotRunnerFactory.build(any,
+          target: anyNamed('target'),
+          debuggingOptions: anyNamed('debuggingOptions'),
+          packagesFilePath: anyNamed('packagesFilePath'),
+          usesTerminalUI: anyNamed('usesTerminalUI'))).thenReturn(
+          new MockHotRunner());
+
+      testDeviceManager.addDevice(device);
+      when(device.getLogReader()).thenAnswer((_) {
+        // Now that the reader is used, start writing messages to it.
+        Timer.run(() {
+          mockLogReader.addLine('Foo');
+          mockLogReader.addLine(
+              'Observatory listening on http://127.0.0.1:$devicePort');
+        });
+
+        return mockLogReader;
+      });
+      final File foo = fs.file('lib/foo.dart')
+        ..createSync();
+
+      final AttachCommand command = new AttachCommand(
+          hotRunnerFactory: mockHotRunnerFactory);
+      await createTestCommandRunner(command).run(
+          <String>['attach', '-t', foo.path, '-v']);
+
+      verify(mockHotRunnerFactory.build(any,
+          target: foo.path,
+          debuggingOptions: anyNamed('debuggingOptions'),
+          packagesFilePath: anyNamed('packagesFilePath'),
+          usesTerminalUI: anyNamed('usesTerminalUI'))).called(1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    },);
 
     testUsingContext('forwards to given port', () async {
       const int devicePort = 499;
@@ -58,16 +124,20 @@
 
       when(device.portForwarder).thenReturn(portForwarder);
       when(portForwarder.forward(devicePort)).thenAnswer((_) async => hostPort);
-      when(portForwarder.forwardedPorts).thenReturn(<ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
+      when(portForwarder.forwardedPorts).thenReturn(
+          <ForwardedPort>[new ForwardedPort(hostPort, devicePort)]);
       when(portForwarder.unforward(any)).thenAnswer((_) async => null);
       testDeviceManager.addDevice(device);
 
       final AttachCommand command = new AttachCommand();
 
-      await createTestCommandRunner(command).run(<String>['attach', '--debug-port', '$devicePort']);
+      await createTestCommandRunner(command).run(
+          <String>['attach', '--debug-port', '$devicePort']);
 
       verify(portForwarder.forward(devicePort)).called(1);
-    });
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    },);
 
     testUsingContext('exits when no device connected', () async {
       final AttachCommand command = new AttachCommand();
@@ -76,7 +146,9 @@
         throwsA(isInstanceOf<ToolExit>()),
       );
       expect(testLogger.statusText, contains('No connected devices'));
-    });
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    },);
 
     testUsingContext('exits when multiple devices connected', () async {
       Device aDeviceWithId(String id) {
@@ -98,8 +170,14 @@
       expect(testLogger.statusText, contains('More than one device'));
       expect(testLogger.statusText, contains('xx1'));
       expect(testLogger.statusText, contains('yy2'));
-    });
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    },);
   });
 }
 
 class MockPortForwarder extends Mock implements DevicePortForwarder {}
+
+class MockHotRunner extends Mock implements HotRunner {}
+
+class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}