Add a new Device backed by flutter_tester. (#16405)


diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index dda9105..6bc63d2 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -17,6 +17,7 @@
 import 'globals.dart';
 import 'ios/plist_utils.dart' as plist;
 import 'ios/xcodeproj.dart';
+import 'tester/flutter_tester.dart';
 
 abstract class ApplicationPackage {
   /// Package ID from the Android Manifest or equivalent.
@@ -274,6 +275,8 @@
       return applicationBinary == null
           ? new IOSApp.fromCurrentDirectory()
           : new IOSApp.fromIpa(applicationBinary);
+    case TargetPlatform.tester:
+      return new FlutterTesterApp.fromCurrentDirectory();
     case TargetPlatform.darwin_x64:
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
@@ -305,6 +308,7 @@
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
+      case TargetPlatform.tester:
         return null;
     }
     return null;
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index f539a5a..3757ac5 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -114,6 +114,7 @@
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
+      case TargetPlatform.tester:
         return _getHostArtifactPath(artifact, platform);
     }
     assert(false, 'Invalid platform $platform.');
@@ -205,6 +206,7 @@
       case TargetPlatform.darwin_x64:
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
+      case TargetPlatform.tester:
         assert(mode == null, 'Platform $platform does not support different build modes.');
         return fs.path.join(engineDir, platformName);
       case TargetPlatform.ios:
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index f0ecb7e..82e7dea 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -157,6 +157,7 @@
   linux_x64,
   windows_x64,
   fuchsia,
+  tester,
 }
 
 String getNameForTargetPlatform(TargetPlatform platform) {
@@ -179,6 +180,8 @@
       return 'windows-x64';
     case TargetPlatform.fuchsia:
       return 'fuchsia';
+    case TargetPlatform.tester:
+      return 'flutter-tester';
   }
   assert(false);
   return null;
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index 52a3fb6..6148550 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -247,6 +247,7 @@
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
     case TargetPlatform.fuchsia:
+    case TargetPlatform.tester:
       assert(false);
   }
 
@@ -347,6 +348,7 @@
     case TargetPlatform.linux_x64:
     case TargetPlatform.windows_x64:
     case TargetPlatform.fuchsia:
+    case TargetPlatform.tester:
       assert(false);
   }
 
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 112bb1a..60a0617 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -24,6 +24,7 @@
 import '../run_cold.dart';
 import '../run_hot.dart';
 import '../runner/flutter_command.dart';
+import '../tester/flutter_tester.dart';
 import '../vmservice.dart';
 
 const String protocolVersion = '0.2.0';
@@ -570,6 +571,7 @@
     addDeviceDiscoverer(new AndroidDevices());
     addDeviceDiscoverer(new IOSDevices());
     addDeviceDiscoverer(new IOSSimulators());
+    addDeviceDiscoverer(new FlutterTesterDevices());
   }
 
   void addDeviceDiscoverer(PollingDeviceDiscovery discoverer) {
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 1c5b2a7..dd7475c 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -16,6 +16,7 @@
 import 'globals.dart';
 import 'ios/devices.dart';
 import 'ios/simulators.dart';
+import 'tester/flutter_tester.dart';
 
 DeviceManager get deviceManager => context[DeviceManager];
 
@@ -28,6 +29,7 @@
     _deviceDiscoverers.add(new AndroidDevices());
     _deviceDiscoverers.add(new IOSDevices());
     _deviceDiscoverers.add(new IOSSimulators());
+    _deviceDiscoverers.add(new FlutterTesterDevices());
   }
 
   final List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index ec8f72c..9a02aaf 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -26,6 +26,7 @@
 import '../dart/package_map.dart';
 import '../device.dart';
 import '../globals.dart';
+import '../tester/flutter_tester.dart';
 import '../usage.dart';
 import '../version.dart';
 import '../vmservice.dart';
@@ -79,6 +80,11 @@
         help:
             'Captures a bug report file to submit to the Flutter team '
             '(contains local paths, device\nidentifiers, and log snippets).');
+    argParser.addFlag('show-test-device',
+        negatable: false,
+        hide: !verboseHelp,
+        help: 'List the special \'flutter-tester\' device in device listings. '
+              'This headless device is used to\ntest Flutter tooling.');
 
     String packagesHelp;
     if (fs.isFileSync(kPackagesFileName))
@@ -180,6 +186,11 @@
       contextOverrides[Logger] = new VerboseLogger(logger);
     }
 
+    if (topLevelResults['show-test-device'] ||
+        topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
+      FlutterTesterDevices.showFlutterTesterDevice = true;
+    }
+
     String recordTo = topLevelResults['record-to'];
     String replayFrom = topLevelResults['replay-from'];
 
diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
new file mode 100644
index 0000000..be03f16
--- /dev/null
+++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
@@ -0,0 +1,208 @@
+// Copyright 2018 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 'dart:convert';
+
+import 'package:meta/meta.dart';
+
+import '../application_package.dart';
+import '../artifacts.dart';
+import '../base/common.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
+import '../build_info.dart';
+import '../dart/package_map.dart';
+import '../device.dart';
+import '../globals.dart';
+import '../protocol_discovery.dart';
+import '../version.dart';
+
+class FlutterTesterApp extends ApplicationPackage {
+  final String _directory;
+
+  factory FlutterTesterApp.fromCurrentDirectory() {
+    return new FlutterTesterApp._(fs.currentDirectory.path);
+  }
+
+  FlutterTesterApp._(String directory)
+      : _directory = directory,
+        super(id: directory);
+
+  @override
+  String get name => fs.path.basename(_directory);
+
+  @override
+  String get packagePath => fs.path.join(_directory, '.packages');
+}
+
+// TODO(scheglov): This device does not currently work with full restarts.
+class FlutterTesterDevice extends Device {
+  final _FlutterTesterDeviceLogReader _logReader =
+      new _FlutterTesterDeviceLogReader();
+
+  Process _process;
+
+  FlutterTesterDevice(String deviceId) : super(deviceId);
+
+  @override
+  Future<bool> get isLocalEmulator async => false;
+
+  @override
+  String get name => 'Flutter test device';
+
+  @override
+  DevicePortForwarder get portForwarder => null;
+
+  @override
+  Future<String> get sdkNameAndVersion async {
+    final FlutterVersion flutterVersion = FlutterVersion.instance;
+    return 'Flutter ${flutterVersion.frameworkRevisionShort}';
+  }
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester;
+
+  @override
+  void clearLogs() {}
+
+  @override
+  DeviceLogReader getLogReader({ApplicationPackage app}) => _logReader;
+
+  @override
+  Future<bool> installApp(ApplicationPackage app) async => true;
+
+  @override
+  Future<bool> isAppInstalled(ApplicationPackage app) async => false;
+
+  @override
+  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
+
+  @override
+  bool isSupported() => true;
+
+  @override
+  Future<LaunchResult> startApp(
+    ApplicationPackage package, {
+    @required String mainPath,
+    String route,
+    @required DebuggingOptions debuggingOptions,
+    Map<String, dynamic> platformArgs,
+    bool prebuiltApplication: false,
+    bool applicationNeedsRebuild: false,
+    bool usesTerminalUi: true,
+    bool ipv6: false,
+  }) async {
+    if (!debuggingOptions.buildInfo.isDebug) {
+      printError('This device only supports debug mode.');
+      return new LaunchResult.failed();
+    }
+
+    final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester);
+    if (!fs.isFileSync(shellPath))
+      throwToolExit('Cannot find Flutter shell at $shellPath');
+
+    final List<String> command = <String>[
+      shellPath,
+      '--non-interactive',
+      '--enable-dart-profiling',
+      '--packages=${PackageMap.globalPackagesPath}',
+    ];
+    if (debuggingOptions.debuggingEnabled) {
+      if (debuggingOptions.startPaused) {
+        command.add('--start-paused');
+      }
+      if (debuggingOptions.hasObservatoryPort)
+        command
+            .add('--observatory-port=${debuggingOptions.hasObservatoryPort}');
+    }
+
+    // TODO(scheglov): Provide assets location, once it is possible.
+
+    if (mainPath != null) {
+      command.add(mainPath);
+    }
+
+    try {
+      printTrace('$shellPath ${command.join(' ')}');
+
+      _process = await processManager.start(command);
+      _process.stdout
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen((String line) {
+        _logReader.addLine(line);
+      });
+      _process.stderr
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen((String line) {
+        _logReader.addLine(line);
+      });
+
+      if (!debuggingOptions.debuggingEnabled)
+        return new LaunchResult.succeeded();
+
+      final ProtocolDiscovery observatoryDiscovery =
+          new ProtocolDiscovery.observatory(getLogReader(),
+              hostPort: debuggingOptions.observatoryPort);
+
+      final Uri observatoryUri = await observatoryDiscovery.uri;
+      return new LaunchResult.succeeded(observatoryUri: observatoryUri);
+    } catch (error) {
+      printError('Failed to launch $package: $error');
+      return new LaunchResult.failed();
+    }
+  }
+
+  @override
+  Future<bool> stopApp(ApplicationPackage app) async {
+    _process?.kill();
+    _process = null;
+
+    return true;
+  }
+
+  @override
+  Future<bool> uninstallApp(ApplicationPackage app) async => true;
+}
+
+class FlutterTesterDevices extends PollingDeviceDiscovery {
+  static const String kTesterDeviceId = 'flutter-tester';
+
+  static bool showFlutterTesterDevice = false;
+
+  final FlutterTesterDevice _testerDevice =
+      new FlutterTesterDevice(kTesterDeviceId);
+
+  FlutterTesterDevices() : super('Flutter tester');
+
+  @override
+  bool get canListAnything => true;
+
+  @override
+  bool get supportsPlatform => true;
+
+  @override
+  Future<List<Device>> pollingGetDevices() async {
+    return showFlutterTesterDevice ? <Device>[_testerDevice] : <Device>[];
+  }
+}
+
+class _FlutterTesterDeviceLogReader extends DeviceLogReader {
+  final StreamController<String> _logLinesController =
+      new StreamController<String>.broadcast();
+
+  @override
+  int get appPid => 0;
+
+  @override
+  Stream<String> get logLines => _logLinesController.stream;
+
+  @override
+  String get name => 'flutter tester log reader';
+
+  void addLine(String line) => _logLinesController.add(line);
+}