Commit fca59175 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by Flutter GitHub Bot

Test hot reload targeting a Fuchsia device (#48472)

parent 8b139a8c
// Copyright 2014 The Flutter 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 'dart:io';
import 'dart:math';
import 'package:path/path.dart' as path;
import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
void generateMain(Directory appDir, String sentinel) {
final String mainCode = '''
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_driver/driver_extension.dart';
class ReassembleListener extends StatefulWidget {
const ReassembleListener({Key key, this.child})
: super(key: key);
final Widget child;
@override
_ReassembleListenerState createState() => _ReassembleListenerState();
}
class _ReassembleListenerState extends State<ReassembleListener> {
@override
initState() {
super.initState();
print('$sentinel');
}
@override
void reassemble() {
super.reassemble();
print('$sentinel');
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
void main() {
runApp(
ReassembleListener(
child: Text(
'Hello, word!',
textDirection: TextDirection.rtl,
)
)
);
}
''';
File(path.join(appDir.path, 'lib', 'fuchsia_main.dart'))
.writeAsStringSync(mainCode, flush: true);
}
void main() {
deviceOperatingSystem = DeviceOperatingSystem.fuchsia;
task(() async {
section('Checking environment variables');
if (Platform.environment['FUCHSIA_SSH_CONFIG'] == null &&
Platform.environment['FUCHSIA_BUILD_DIR'] == null) {
throw Exception('No FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR set');
}
final String flutterBinary = path.join(flutterDirectory.path, 'bin', 'flutter');
section('Downloading Fuchsia SDK and flutter runner');
// Download the Fuchsia SDK.
final int precacheResult = await exec(
flutterBinary,
<String>[
'precache',
'--fuchsia',
'--flutter_runner',
]
);
if (precacheResult != 0) {
throw Exception('flutter precache failed with exit code $precacheResult');
}
final Directory fuchsiaToolDirectory =
Directory(path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'fuchsia', 'tools'));
if (!fuchsiaToolDirectory.existsSync()) {
throw Exception('Expected Fuchsia tool directory at ${fuchsiaToolDirectory.path}');
}
final Device device = await devices.workingDevice;
final Directory appDir = dir(path.join(
flutterDirectory.path,
'dev',
'integration_tests',
'ui',
));
await inDirectory(appDir, () async {
final Random random = Random();
final Map<String, Completer<void>> sentinelMessage = <String, Completer<void>>{
'sentinel-${random.nextInt(1<<32)}': Completer<void>(),
'sentinel-${random.nextInt(1<<32)}': Completer<void>(),
};
Process runProcess;
Process logsProcess;
try {
section('Creating lib/fuchsia_main.dart');
generateMain(appDir, sentinelMessage.keys.toList()[0]);
section('Launching `flutter run` in ${appDir.path}');
runProcess = await startProcess(
flutterBinary,
<String>[
'run',
'--suppress-analytics',
'-d', device.deviceId,
'-t', 'lib/fuchsia_main.dart',
],
isBot: false, // We just want to test the output, not have any debugging info.
);
logsProcess = await startProcess(
flutterBinary,
<String>['logs', '--suppress-analytics', '-d', device.deviceId],
isBot: false, // We just want to test the output, not have any debugging info.
);
Future<dynamic> eventOrExit(Future<void> event) {
return Future.any<dynamic>(<Future<dynamic>>[
event,
runProcess.exitCode,
logsProcess.exitCode,
]);
}
logsProcess.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String log) {
print('logs:stdout: $log');
for (final String sentinel in sentinelMessage.keys) {
if (log.contains(sentinel)) {
if (sentinelMessage[sentinel].isCompleted) {
throw Exception(
'Expected a single `$sentinel` message in the device log, but found more than one'
);
}
sentinelMessage[sentinel].complete();
break;
}
}
});
final Completer<void> hotReloadCompleter = Completer<void>();
final Completer<void> reloadedCompleter = Completer<void>();
final RegExp observatoryRegexp = RegExp('An Observatory debugger and profiler on .+ is available at');
runProcess.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('run:stdout: $line');
if (observatoryRegexp.hasMatch(line)) {
hotReloadCompleter.complete();
} else if (line.contains('Reloaded')) {
reloadedCompleter.complete();
}
});
final List<String> runStderr = <String>[];
runProcess.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
runStderr.add(line);
print('run:stderr: $line');
});
section('Waiting for hot reload availability');
await eventOrExit(hotReloadCompleter.future);
section('Waiting for Dart VM');
// Wait for the first message in the log from the Dart VM.
await eventOrExit(sentinelMessage.values.toList()[0].future);
// Change the dart file.
generateMain(appDir, sentinelMessage.keys.toList()[1]);
section('Hot reload');
runProcess.stdin.write('r');
runProcess.stdin.flush();
await eventOrExit(reloadedCompleter.future);
section('Waiting for Dart VM');
// Wait for the second message in the log from the Dart VM.
await eventOrExit(sentinelMessage.values.toList()[1].future);
section('Quitting flutter run');
runProcess.stdin.write('q');
runProcess.stdin.flush();
final int runExitCode = await runProcess.exitCode;
if (runExitCode != 0 || runStderr.isNotEmpty) {
throw Exception(
'flutter run exited with code $runExitCode and errors: ${runStderr.join('\n')}.'
);
}
} finally {
runProcess.kill();
logsProcess.kill();
File(path.join(appDir.path, 'lib', 'fuchsia_main.dart')).deleteSync();
}
for (final String sentinel in sentinelMessage.keys) {
if (!sentinelMessage[sentinel].isCompleted) {
throw Exception('Expected $sentinel in the device logs.');
}
}
});
return TaskResult.success(null);
});
}
......@@ -12,11 +12,22 @@ import 'package:path/path.dart' as path;
import 'utils.dart';
/// Gets the artifact path relative to the current directory.
String getArtifactPath() {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
}
/// The root of the API for controlling devices.
DeviceDiscovery get devices => DeviceDiscovery();
/// Device operating system the test is configured to test.
enum DeviceOperatingSystem { android, ios }
enum DeviceOperatingSystem { android, ios, fuchsia }
/// Device OS to test on.
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
......@@ -29,6 +40,8 @@ abstract class DeviceDiscovery {
return AndroidDeviceDiscovery();
case DeviceOperatingSystem.ios:
return IosDeviceDiscovery();
case DeviceOperatingSystem.fuchsia:
return FuchsiaDeviceDiscovery();
default:
throw StateError('Unsupported device operating system: {config.deviceOperatingSystem}');
}
......@@ -198,6 +211,91 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
}
}
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
factory FuchsiaDeviceDiscovery() {
return _instance ??= FuchsiaDeviceDiscovery._();
}
FuchsiaDeviceDiscovery._();
static FuchsiaDeviceDiscovery _instance;
FuchsiaDevice _workingDevice;
String get _devFinder {
final String devFinder = path.join(getArtifactPath(), 'fuchsia', 'tools', 'dev_finder');
if (!File(devFinder).existsSync()) {
throw FileSystemException('Couldn\'t find dev_finder at location $devFinder');
}
return devFinder;
}
@override
Future<FuchsiaDevice> get workingDevice async {
if (_workingDevice == null) {
await chooseWorkingDevice();
}
return _workingDevice;
}
/// Picks the first connected Fuchsia device.
@override
Future<void> chooseWorkingDevice() async {
final List<FuchsiaDevice> allDevices = (await discoverDevices())
.map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw Exception('No Fuchsia devices detected');
}
_workingDevice = allDevices.first;
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(_devFinder, <String>['list', '-full']))
.trim()
.split('\n');
final List<String> devices = <String>[];
for (final String line in output) {
final List<String> parts = line.split(' ');
assert(parts.length == 2);
devices.add(parts.last); // The device id.
}
return devices;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
try {
final int resolveResult = await exec(
_devFinder,
<String>[
'resolve',
'-device-limit',
'1',
deviceId,
]
);
if (resolveResult == 0) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
} else {
results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
}
} catch (error, stacktrace) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
}
}
return results;
}
@override
Future<void> performPreflightTasks() async {}
}
class AndroidDevice implements Device {
AndroidDevice({@required this.deviceId});
......@@ -392,16 +490,6 @@ class IosDeviceDiscovery implements DeviceDiscovery {
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
}
// Returns the path to cached binaries relative to devicelab directory
String get _artifactDirPath {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
}
// Returns a colon-separated environment variable that contains the paths
// of linked libraries for idevice_id
Map<String, String> get _ideviceIdEnvironment {
......@@ -413,13 +501,13 @@ class IosDeviceDiscovery implements DeviceDiscovery {
'ideviceinstaller',
'ios-deploy',
'libzip',
].map((String packageName) => path.join(_artifactDirPath, packageName)).join(':');
].map((String packageName) => path.join(getArtifactPath(), packageName)).join(':');
return <String, String>{'DYLD_LIBRARY_PATH': libPath};
}
@override
Future<List<String>> discoverDevices() async {
final String ideviceIdPath = path.join(_artifactDirPath, 'libimobiledevice', 'idevice_id');
final String ideviceIdPath = path.join(getArtifactPath(), 'libimobiledevice', 'idevice_id');
final List<String> iosDeviceIDs = LineSplitter.split(await eval(ideviceIdPath, <String>['-l'], environment: _ideviceIdEnvironment))
.map<String>((String line) => line.trim())
.where((String line) => line.isNotEmpty)
......@@ -494,6 +582,49 @@ class IosDevice implements Device {
Future<void> stop(String packageName) async {}
}
/// Fuchsia device.
class FuchsiaDevice implements Device {
const FuchsiaDevice({ @required this.deviceId });
@override
final String deviceId;
// TODO(egarciad): Implement these for Fuchsia.
@override
Future<bool> isAwake() async => true;
@override
Future<bool> isAsleep() async => false;
@override
Future<void> wakeUp() async {}
@override
Future<void> sendToSleep() async {}
@override
Future<void> togglePower() async {}
@override
Future<void> unlock() async {}
@override
Future<void> tap(int x, int y) async {}
@override
Future<void> stop(String packageName) async {}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw 'Not implemented';
}
@override
Stream<String> get logcat {
throw 'Not implemented';
}
}
/// Path to the `adb` executable.
String get adbPath {
final String androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
......
......@@ -380,6 +380,8 @@ class CompileTest {
case DeviceOperatingSystem.android:
options.add('android-arm');
break;
case DeviceOperatingSystem.fuchsia:
throw Exception('Unsupported option for Fuchsia devices');
}
final String compileLog = await evalFlutter('build', options: options);
watch.stop();
......@@ -434,6 +436,8 @@ class CompileTest {
if (reportPackageContentSizes)
metrics.addAll(await getSizesFromApk(apkPath));
break;
case DeviceOperatingSystem.fuchsia:
throw Exception('Unsupported option for Fuchsia devices');
}
metrics.addAll(<String, dynamic>{
......@@ -456,6 +460,8 @@ class CompileTest {
options.insert(0, 'apk');
options.add('--target-platform=android-arm');
break;
case DeviceOperatingSystem.fuchsia:
throw Exception('Unsupported option for Fuchsia devices');
}
watch.start();
await flutter('build', options: options);
......
......@@ -340,7 +340,7 @@ tasks:
# required_agent_capabilities: ["linux/android"]
# flaky: true
flutter_attach_test:
flutter_attach_test_android:
description: >
Tests the `flutter attach` command.
stage: devicelab
......
{
"program": {
"data": "data/integration_ui"
},
"sandbox": {
"services": [
"fuchsia.cobalt.LoggerFactory",
"fuchsia.fonts.Provider",
"fuchsia.logger.LogSink",
"fuchsia.modular.Clipboard",
"fuchsia.modular.ContextWriter",
"fuchsia.modular.DeviceMap",
"fuchsia.modular.ModuleContext",
"fuchsia.sys.Environment",
"fuchsia.sys.Launcher",
"fuchsia.testing.runner.TestRunner",
"fuchsia.ui.input.ImeService",
"fuchsia.ui.policy.Presenter",
"fuchsia.ui.scenic.Scenic"
]
}
}
......@@ -218,7 +218,7 @@ class AttachCommand extends FlutterCommand {
if (module == null) {
throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
}
usesIpv6 = await device.ipv6;
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
......
......@@ -153,7 +153,7 @@ class FuchsiaDevices extends PollingDeviceDiscovery {
if (text == null || text.isEmpty) {
return <Device>[];
}
final List<FuchsiaDevice> devices = parseListDevices(text);
final List<FuchsiaDevice> devices = await parseListDevices(text);
return devices;
}
......@@ -162,7 +162,7 @@ class FuchsiaDevices extends PollingDeviceDiscovery {
}
@visibleForTesting
List<FuchsiaDevice> parseListDevices(String text) {
Future<List<FuchsiaDevice>> parseListDevices(String text) async {
final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
for (final String rawLine in text.trim().split('\n')) {
final String line = rawLine.trim();
......@@ -172,8 +172,15 @@ List<FuchsiaDevice> parseListDevices(String text) {
continue;
}
final String name = words[1];
final String id = words[0];
devices.add(FuchsiaDevice(id, name: name));
final String resolvedHost = await fuchsiaSdk.fuchsiaDevFinder.resolve(
name,
local: false,
);
if (resolvedHost == null) {
globals.printError('Failed to resolve host for Fuchsia device `$name`');
continue;
}
devices.add(FuchsiaDevice(resolvedHost, name: name));
}
return devices;
}
......@@ -240,7 +247,6 @@ class FuchsiaDevice extends Device {
}
// Stop the app if it's currently running.
await stopApp(package);
// Find out who the device thinks we are.
final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(
name,
local: true,
......@@ -249,6 +255,7 @@ class FuchsiaDevice extends Device {
globals.printError('Failed to resolve host for Fuchsia device');
return LaunchResult.failed();
}
// Find out who the device thinks we are.
final int port = await os.findFreePort();
if (port == 0) {
globals.printError('Failed to find a free port');
......@@ -475,11 +482,9 @@ class FuchsiaDevice extends Device {
@override
bool get supportsScreenshot => false;
Future<bool> get ipv6 async {
// Workaround for https://github.com/dart-lang/sdk/issues/29456
final String fragment = (await _resolvedIp).split('%').first;
bool get ipv6 {
try {
Uri.parseIPv6Address(fragment);
Uri.parseIPv6Address(id);
return true;
} on FormatException {
return false;
......@@ -525,15 +530,6 @@ class FuchsiaDevice extends Device {
return ports;
}
String _cachedResolvedIp;
Future<String> get _resolvedIp async {
return _cachedResolvedIp ??= await fuchsiaSdk.fuchsiaDevFinder.resolve(
name,
local: false,
);
}
/// Run `command` on the Fuchsia device shell.
Future<RunResult> shell(String command) async {
if (fuchsiaArtifacts.sshConfig == null) {
......@@ -544,7 +540,7 @@ class FuchsiaDevice extends Device {
'ssh',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
await _resolvedIp,
id, // Device's IP address.
command,
]);
}
......@@ -670,7 +666,7 @@ class FuchsiaIsolateDiscoveryProtocol {
}
final Uri address = flutterView.owner.vmService.httpAddress;
if (flutterView.uiIsolate.name.contains(_isolateName)) {
_foundUri.complete(await _device.ipv6
_foundUri.complete(_device.ipv6
? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
: Uri.parse('http://$_ipv4Loopback:${address.port}/'));
_status.stop();
......@@ -711,7 +707,7 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
'-f',
'-L',
'$hostPort:$_ipv4Loopback:$devicePort',
await device._resolvedIp,
device.id, // Device's IP address.
'true',
];
final Process process = await globals.processManager.start(command);
......@@ -743,7 +739,7 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
'-vvv',
'-L',
'${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
await device._resolvedIp,
device.id, // Device's IP address.
];
final ProcessResult result = await globals.processManager.run(command);
if (result.exitCode != 0) {
......@@ -753,7 +749,9 @@ class _FuchsiaPortForwarder extends DevicePortForwarder {
@override
Future<void> dispose() async {
for (final ForwardedPort port in forwardedPorts) {
final List<ForwardedPort> forwardedPortsCopy =
List<ForwardedPort>.from(forwardedPorts);
for (final ForwardedPort port in forwardedPortsCopy) {
await unforward(port);
}
}
......
......@@ -54,7 +54,8 @@ class FuchsiaSdk {
return devices.isNotEmpty ? devices[0] : null;
}
/// Returns the fuchsia system logs for an attached device.
/// Returns the fuchsia system logs for an attached device where
/// [id] is the IP address of the device.
Stream<String> syslogs(String id) {
Process process;
try {
......@@ -72,7 +73,7 @@ class FuchsiaSdk {
'ssh',
'-F',
fuchsiaArtifacts.sshConfig.absolute.path,
id,
id, // The device's IP.
remoteCommand,
];
globals.processManager.start(cmd).then((Process newProcess) {
......
......@@ -55,20 +55,24 @@ void main() {
expect(device.name, name);
});
test('parse dev_finder output', () {
const String example = '192.168.42.56 paper-pulp-bush-angel';
final List<FuchsiaDevice> names = parseListDevices(example);
testUsingContext('parse dev_finder output', () async {
const String example = '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel';
final List<FuchsiaDevice> names = await parseListDevices(example);
expect(names.length, 1);
expect(names.first.name, 'paper-pulp-bush-angel');
expect(names.first.id, '192.168.42.56');
expect(names.first.id, '192.168.42.10');
}, overrides: <Type, Generator>{
FuchsiaSdk: () => MockFuchsiaSdk(),
});
test('parse junk dev_finder output', () {
testUsingContext('parse junk dev_finder output', () async {
const String example = 'junk';
final List<FuchsiaDevice> names = parseListDevices(example);
final List<FuchsiaDevice> names = await parseListDevices(example);
expect(names.length, 0);
}, overrides: <Type, Generator>{
FuchsiaSdk: () => MockFuchsiaSdk(),
});
testUsingContext('disposing device disposes the portForwarder', () async {
......@@ -773,7 +777,7 @@ class MockFuchsiaDevice extends Mock implements FuchsiaDevice {
final bool _ipv6;
@override
Future<bool> get ipv6 async => _ipv6;
bool get ipv6 => _ipv6;
@override
final String id;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment