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); +}
diff --git a/packages/flutter_tools/test/tester/flutter_tester_test.dart b/packages/flutter_tools/test/tester/flutter_tester_test.dart new file mode 100644 index 0000000..af38a1b --- /dev/null +++ b/packages/flutter_tools/test/tester/flutter_tester_test.dart
@@ -0,0 +1,167 @@ +// 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 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/tester/flutter_tester.dart'; +import 'package:process/process.dart'; +import 'package:test/test.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; +import '../src/mocks.dart'; + +void main() { + MemoryFileSystem fs; + + setUp(() { + fs = new MemoryFileSystem(); + }); + + group('FlutterTesterApp', () { + testUsingContext('fromCurrentDirectory', () async { + const String projectPath = '/home/my/projects/my_project'; + await fs.directory(projectPath).create(recursive: true); + fs.currentDirectory = projectPath; + + final FlutterTesterApp app = new FlutterTesterApp.fromCurrentDirectory(); + expect(app.name, 'my_project'); + expect(app.packagePath, fs.path.join(projectPath, '.packages')); + }, overrides: <Type, Generator>{ + FileSystem: () => fs, + }); + }); + + group('FlutterTesterDevices', () { + tearDown(() { + FlutterTesterDevices.showFlutterTesterDevice = false; + }); + + testUsingContext('no device', () async { + final FlutterTesterDevices discoverer = new FlutterTesterDevices(); + + final List<Device> devices = await discoverer.devices; + expect(devices, isEmpty); + }); + + testUsingContext('has device', () async { + FlutterTesterDevices.showFlutterTesterDevice = true; + final FlutterTesterDevices discoverer = new FlutterTesterDevices(); + + final List<Device> devices = await discoverer.devices; + expect(devices, hasLength(1)); + + final Device device = devices.single; + expect(device, const isInstanceOf<FlutterTesterDevice>()); + expect(device.id, 'flutter-tester'); + }); + }); + + group('FlutterTesterDevice', () { + FlutterTesterDevice device; + List<String> logLines; + + setUp(() { + device = new FlutterTesterDevice('flutter-tester'); + + logLines = <String>[]; + device.getLogReader().logLines.listen(logLines.add); + }); + + testUsingContext('getters', () async { + expect(device.id, 'flutter-tester'); + expect(await device.isLocalEmulator, isFalse); + expect(device.name, 'Flutter test device'); + expect(device.portForwarder, isNull); + expect(await device.targetPlatform, TargetPlatform.tester); + + expect(await device.installApp(null), isTrue); + expect(await device.isAppInstalled(null), isFalse); + expect(await device.isLatestBuildInstalled(null), isFalse); + expect(await device.uninstallApp(null), isTrue); + + expect(device.isSupported(), isTrue); + }); + + group('startApp', () { + String flutterRoot; + String flutterTesterPath; + + String projectPath; + String mainPath; + + MockProcessManager mockProcessManager; + MockProcess mockProcess; + + final Map<Type, Generator> startOverrides = <Type, Generator>{ + Platform: () => new FakePlatform(operatingSystem: 'linux'), + FileSystem: () => fs, + Cache: () => new Cache(rootOverride: fs.directory(flutterRoot)), + ProcessManager: () => mockProcessManager, + }; + + setUp(() { + flutterRoot = fs.path.join('home', 'me', 'flutter'); + flutterTesterPath = fs.path.join(flutterRoot, 'bin', 'cache', + 'artifacts', 'engine', 'linux-x64', 'flutter_tester'); + + final File flutterTesterFile = fs.file(flutterTesterPath); + flutterTesterFile.parent.createSync(recursive: true); + flutterTesterFile.writeAsBytesSync(const <int>[]); + + projectPath = fs.path.join('home', 'me', 'hello'); + mainPath = fs.path.join(projectPath, 'lin', 'main.dart'); + + mockProcessManager = new MockProcessManager(); + mockProcessManager.processFactory = + (List<String> commands) => mockProcess; + }); + + testUsingContext('not debug', () async { + final LaunchResult result = await device.startApp(null, + mainPath: mainPath, + debuggingOptions: new DebuggingOptions.disabled( + const BuildInfo(BuildMode.release, null))); + expect(result.started, isFalse); + }, overrides: startOverrides); + + testUsingContext('no flutter_tester', () async { + fs.file(flutterTesterPath).deleteSync(); + expect(() async { + await device.startApp(null, + mainPath: mainPath, + debuggingOptions: new DebuggingOptions.disabled( + const BuildInfo(BuildMode.debug, null))); + }, throwsToolExit()); + }, overrides: startOverrides); + + testUsingContext('start', () async { + final Uri observatoryUri = Uri.parse('http://127.0.0.1:6666/'); + mockProcess = new MockProcess( + stdout: new Stream<List<int>>.fromIterable(<List<int>>[ + ''' +Observatory listening on $observatoryUri +Hello! +''' + .codeUnits + ])); + + final LaunchResult result = await device.startApp(null, + mainPath: mainPath, + debuggingOptions: new DebuggingOptions.enabled( + const BuildInfo(BuildMode.debug, null))); + expect(result.started, isTrue); + expect(result.observatoryUri, observatoryUri); + + expect(logLines.last, 'Hello!'); + }, overrides: startOverrides); + }); + }); +}