
This change is a major step towards moving away from shipping DDS via Pub. The first component of this PR is the move away from importing package:dds to launch DDS. Instead, DDS is launched out of process using the `dart development-service` command shipped with the Dart SDK. This makes Flutter's handling of DDS consistent with the standalone Dart VM. The second component of this PR is the initial work to prepare for the removal of instances of DevTools being served manually by the flutter_tool, instead relying on DDS to serve DevTools. This will be consistent with how the standalone Dart VM serves DevTools, tying the DevTools lifecycle to a live DDS instance. This will allow for the removal of much of the logic needed to properly manage the lifecycle of the DevTools server in a future PR. Also, by serving DevTools from DDS, users will no longer need to forward a secondary port in remote workflows as DevTools will be available on the DDS port. This code is currently commented out and will be enabled in a future PR. There's two remaining circumstances that will prevent us from removing DevtoolsRunner completely: - The daemon's `devtools.serve` endpoint - `flutter drive`'s `--profile-memory` flag used for recording memory profiles This PR also includes some refactoring around `DebuggingOptions` to reduce the number of debugging related arguments being passed as parameters adjacent to a `DebuggingOptions` instance.
1085 lines
35 KiB
Dart
1085 lines
35 KiB
Dart
// 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 'package:file/memory.dart';
|
|
import 'package:flutter_tools/src/application_package.dart';
|
|
import 'package:flutter_tools/src/artifacts.dart';
|
|
import 'package:flutter_tools/src/base/dds.dart';
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/base/platform.dart';
|
|
import 'package:flutter_tools/src/base/time.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/device_port_forwarder.dart';
|
|
import 'package:flutter_tools/src/features.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_ffx.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
|
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:flutter_tools/src/resident_runner.dart';
|
|
import 'package:flutter_tools/src/vmservice.dart';
|
|
import 'package:test/fake.dart';
|
|
import 'package:vm_service/vm_service.dart' as vm_service;
|
|
|
|
import '../../src/common.dart';
|
|
import '../../src/context.dart';
|
|
import '../../src/fake_vm_services.dart';
|
|
import '../../src/fakes.dart';
|
|
|
|
final vm_service.Isolate fakeIsolate = vm_service.Isolate(
|
|
id: '1',
|
|
pauseEvent: vm_service.Event(
|
|
kind: vm_service.EventKind.kResume,
|
|
timestamp: 0,
|
|
),
|
|
breakpoints: <vm_service.Breakpoint>[],
|
|
libraries: <vm_service.LibraryRef>[],
|
|
livePorts: 0,
|
|
name: 'wrong name',
|
|
number: '1',
|
|
pauseOnExit: false,
|
|
runnable: true,
|
|
startTime: 0,
|
|
isSystemIsolate: false,
|
|
isolateFlags: <vm_service.IsolateFlag>[],
|
|
);
|
|
|
|
void main() {
|
|
group('fuchsia device', () {
|
|
late MemoryFileSystem memoryFileSystem;
|
|
late File sshConfig;
|
|
late FakeProcessManager processManager;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
memoryFileSystem = MemoryFileSystem.test();
|
|
sshConfig = memoryFileSystem.file('ssh_config')..writeAsStringSync('\n');
|
|
processManager = FakeProcessManager.empty();
|
|
});
|
|
|
|
testWithoutContext('stores the requested id and name', () {
|
|
const String deviceId = 'e80::0000:a00a:f00f:2002/3';
|
|
const String name = 'halfbaked';
|
|
final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name, logger: logger);
|
|
|
|
expect(device.id, deviceId);
|
|
expect(device.name, name);
|
|
});
|
|
|
|
testWithoutContext('supports all runtime modes besides jitRelease', () {
|
|
const String deviceId = 'e80::0000:a00a:f00f:2002/3';
|
|
const String name = 'halfbaked';
|
|
final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name, logger: logger);
|
|
|
|
expect(device.supportsRuntimeMode(BuildMode.debug), true);
|
|
expect(device.supportsRuntimeMode(BuildMode.profile), true);
|
|
expect(device.supportsRuntimeMode(BuildMode.release), true);
|
|
expect(device.supportsRuntimeMode(BuildMode.jitRelease), false);
|
|
});
|
|
|
|
testWithoutContext('lists nothing when workflow cannot list devices',
|
|
() async {
|
|
final FakeFuchsiaWorkflow fuchsiaWorkflow =
|
|
FakeFuchsiaWorkflow(canListDevices: false);
|
|
final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
|
|
platform: FakePlatform(),
|
|
fuchsiaSdk: FakeFuchsiaSdk(devices: 'ignored'),
|
|
fuchsiaWorkflow: fuchsiaWorkflow,
|
|
logger: BufferLogger.test(),
|
|
);
|
|
|
|
expect(fuchsiaDevices.canListAnything, false);
|
|
expect(await fuchsiaDevices.pollingGetDevices(), isEmpty);
|
|
});
|
|
|
|
testWithoutContext('can parse ffx output for single device', () async {
|
|
final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
|
|
final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(
|
|
devices:
|
|
'2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel');
|
|
final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
|
|
platform: FakePlatform(environment: <String, String>{}),
|
|
fuchsiaSdk: fuchsiaSdk,
|
|
fuchsiaWorkflow: fuchsiaWorkflow,
|
|
logger: BufferLogger.test(),
|
|
);
|
|
|
|
final Device device = (await fuchsiaDevices.pollingGetDevices()).single;
|
|
|
|
expect(device.name, 'paper-pulp-bush-angel');
|
|
expect(device.id, '192.168.42.10');
|
|
});
|
|
|
|
testWithoutContext('can parse ffx output for multiple devices', () async {
|
|
final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
|
|
final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(
|
|
devices:
|
|
'2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel\n'
|
|
'2001:0db8:85a3:0000:0000:8a2e:0370:7335 foo-bar-fiz-buzz');
|
|
final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
|
|
platform: FakePlatform(),
|
|
fuchsiaSdk: fuchsiaSdk,
|
|
fuchsiaWorkflow: fuchsiaWorkflow,
|
|
logger: BufferLogger.test(),
|
|
);
|
|
|
|
final List<Device> devices = await fuchsiaDevices.pollingGetDevices();
|
|
|
|
expect(devices.first.name, 'paper-pulp-bush-angel');
|
|
expect(devices.first.id, '192.168.42.10');
|
|
expect(devices.last.name, 'foo-bar-fiz-buzz');
|
|
expect(devices.last.id, '192.168.42.10');
|
|
});
|
|
|
|
testWithoutContext('can parse junk output from ffx', () async {
|
|
final FakeFuchsiaWorkflow fuchsiaWorkflow =
|
|
FakeFuchsiaWorkflow(canListDevices: false);
|
|
final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(devices: 'junk');
|
|
final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
|
|
platform: FakePlatform(),
|
|
fuchsiaSdk: fuchsiaSdk,
|
|
fuchsiaWorkflow: fuchsiaWorkflow,
|
|
logger: BufferLogger.test(),
|
|
);
|
|
|
|
final List<Device> devices = await fuchsiaDevices.pollingGetDevices();
|
|
|
|
expect(devices, isEmpty);
|
|
});
|
|
|
|
testUsingContext('disposing device disposes the portForwarder', () async {
|
|
final FakePortForwarder portForwarder = FakePortForwarder();
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
device.portForwarder = portForwarder;
|
|
await device.dispose();
|
|
|
|
expect(portForwarder.disposed, true);
|
|
});
|
|
|
|
testWithoutContext('default capabilities', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
final FlutterProject project =
|
|
FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
|
|
memoryFileSystem.directory('fuchsia').createSync(recursive: true);
|
|
memoryFileSystem.file('pubspec.yaml').createSync();
|
|
|
|
expect(device.supportsHotReload, true);
|
|
expect(device.supportsHotRestart, false);
|
|
expect(device.supportsFlutterExit, false);
|
|
expect(device.isSupportedForProject(project), true);
|
|
});
|
|
|
|
test('is ephemeral', () {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(device.ephemeral, true);
|
|
});
|
|
|
|
testWithoutContext('supported for project', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
final FlutterProject project =
|
|
FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
|
|
memoryFileSystem.directory('fuchsia').createSync(recursive: true);
|
|
memoryFileSystem.file('pubspec.yaml').createSync();
|
|
|
|
expect(device.isSupportedForProject(project), true);
|
|
});
|
|
|
|
testWithoutContext('not supported for project', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
final FlutterProject project =
|
|
FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
|
|
memoryFileSystem.file('pubspec.yaml').createSync();
|
|
|
|
expect(device.isSupportedForProject(project), false);
|
|
});
|
|
|
|
testUsingContext('targetPlatform does not throw when sshConfig is missing',
|
|
() async {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('targetPlatform arm64 works', () async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
|
|
stdout: 'aarch64',
|
|
));
|
|
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('targetPlatform x64 works', () async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
|
|
stdout: 'x86_64',
|
|
));
|
|
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
expect(await device.targetPlatform, TargetPlatform.fuchsia_x64);
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('hostAddress parsing works', () async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
r'echo $SSH_CONNECTION'
|
|
],
|
|
stdout:
|
|
'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22',
|
|
));
|
|
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'device', logger: logger);
|
|
expect(await device.hostAddress, 'fe80::8c6c:2fff:fe3d:c5e1%25ethp0003');
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('hostAddress parsing throws tool error on failure',
|
|
() async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
r'echo $SSH_CONNECTION'
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'device', logger: logger);
|
|
await expectLater(() => device.hostAddress, throwsToolExit());
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
|
|
testUsingContext('hostAddress parsing throws tool error on empty response',
|
|
() async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
r'echo $SSH_CONNECTION'
|
|
],
|
|
));
|
|
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'device', logger: logger);
|
|
expect(() async => device.hostAddress, throwsToolExit());
|
|
}, overrides: <Type, Generator>{
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
ProcessManager: () => processManager,
|
|
});
|
|
});
|
|
|
|
group('displays friendly error when', () {
|
|
late File artifactFile;
|
|
late FakeProcessManager processManager;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
processManager = FakeProcessManager.empty();
|
|
artifactFile = MemoryFileSystem.test().file('artifact');
|
|
});
|
|
|
|
testUsingContext('No vmservices found', () async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/artifact',
|
|
'id',
|
|
'find /hub -name vmservice-port'
|
|
],
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'device', logger: logger);
|
|
|
|
await expectLater(
|
|
device.servicePorts,
|
|
throwsToolExit(
|
|
message:
|
|
'No Dart Observatories found. Are you running a debug build?'));
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(
|
|
sshConfig: artifactFile,
|
|
ffx: artifactFile,
|
|
),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
});
|
|
|
|
group('device logs', () {
|
|
const String exampleUtcLogs = '''
|
|
[2018-11-09 01:27:45][3][297950920][log] INFO: example_app.cm(flutter): Error doing thing
|
|
[2018-11-09 01:27:58][46257][46269][foo] INFO: Using a thing
|
|
[2018-11-09 01:29:58][46257][46269][foo] INFO: Blah blah blah
|
|
[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app.cm(flutter): Do thing
|
|
[2018-11-09 01:30:02][41175][41187][bar] INFO: Invoking a bar
|
|
[2018-11-09 01:30:12][52580][52983][log] INFO: example_app.cm(flutter): Did thing this time
|
|
|
|
''';
|
|
late FakeProcessManager processManager;
|
|
late File ffx;
|
|
late File sshConfig;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
processManager = FakeProcessManager.empty();
|
|
final FileSystem memoryFileSystem = MemoryFileSystem.test();
|
|
ffx = memoryFileSystem.file('ffx')..writeAsStringSync('\n');
|
|
sshConfig = memoryFileSystem.file('ssh_config')
|
|
..writeAsStringSync('\n');
|
|
});
|
|
|
|
testUsingContext('can be parsed for an app', () async {
|
|
final Completer<void> lock = Completer<void>();
|
|
processManager.addCommand(FakeCommand(
|
|
command: const <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
'log_listener --clock Local'
|
|
],
|
|
stdout: exampleUtcLogs,
|
|
completer: lock,
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
final DeviceLogReader reader =
|
|
device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
|
|
final List<String> logLines = <String>[];
|
|
reader.logLines.listen((String line) {
|
|
logLines.add(line);
|
|
if (logLines.length == 2) {
|
|
lock.complete();
|
|
}
|
|
});
|
|
expect(logLines, isEmpty);
|
|
|
|
await lock.future;
|
|
expect(logLines, <String>[
|
|
'[2018-11-09 01:27:45.000] Flutter: Error doing thing',
|
|
'[2018-11-09 01:30:12.000] Flutter: Did thing this time',
|
|
]);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
|
|
FuchsiaArtifacts: () =>
|
|
FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
|
|
});
|
|
|
|
testUsingContext('cuts off prior logs', () async {
|
|
final Completer<void> lock = Completer<void>();
|
|
processManager.addCommand(FakeCommand(
|
|
command: const <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
'log_listener --clock Local'
|
|
],
|
|
stdout: exampleUtcLogs,
|
|
completer: lock,
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
final DeviceLogReader reader =
|
|
device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
|
|
final List<String> logLines = <String>[];
|
|
reader.logLines.listen((String line) {
|
|
logLines.add(line);
|
|
lock.complete();
|
|
});
|
|
expect(logLines, isEmpty);
|
|
|
|
await lock.future.timeout(const Duration(seconds: 1));
|
|
|
|
expect(logLines, <String>[
|
|
'[2018-11-09 01:30:12.000] Flutter: Did thing this time',
|
|
]);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 29, 45)),
|
|
FuchsiaArtifacts: () =>
|
|
FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
|
|
});
|
|
|
|
testUsingContext('can be parsed for all apps', () async {
|
|
final Completer<void> lock = Completer<void>();
|
|
processManager.addCommand(FakeCommand(
|
|
command: const <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'id',
|
|
'log_listener --clock Local'
|
|
],
|
|
stdout: exampleUtcLogs,
|
|
completer: lock,
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
final DeviceLogReader reader = device.getLogReader();
|
|
final List<String> logLines = <String>[];
|
|
reader.logLines.listen((String line) {
|
|
logLines.add(line);
|
|
if (logLines.length == 3) {
|
|
lock.complete();
|
|
}
|
|
});
|
|
expect(logLines, isEmpty);
|
|
|
|
await lock.future.timeout(const Duration(seconds: 1));
|
|
|
|
expect(logLines, <String>[
|
|
'[2018-11-09 01:27:45.000] Flutter: Error doing thing',
|
|
'[2018-11-09 01:29:58.000] Flutter: Do thing',
|
|
'[2018-11-09 01:30:12.000] Flutter: Did thing this time',
|
|
]);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
|
|
FuchsiaArtifacts: () =>
|
|
FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
|
|
});
|
|
});
|
|
});
|
|
|
|
group('screenshot', () {
|
|
late FakeProcessManager processManager;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
processManager = FakeProcessManager.empty();
|
|
});
|
|
|
|
testUsingContext('is supported on posix platforms', () {
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
expect(device.supportsScreenshot, true);
|
|
}, overrides: <Type, Generator>{
|
|
Platform: () => FakePlatform(),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
});
|
|
|
|
testUsingContext('is not supported on Windows', () {
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
|
|
expect(device.supportsScreenshot, false);
|
|
}, overrides: <Type, Generator>{
|
|
Platform: () => FakePlatform(
|
|
operatingSystem: 'windows',
|
|
),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
});
|
|
|
|
test("takeScreenshot throws if file isn't .ppm", () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
await expectLater(
|
|
() => device.takeScreenshot(globals.fs.file('file.invalid')),
|
|
throwsA(isA<Exception>().having(
|
|
(Exception exception) => exception.toString(),
|
|
'message',
|
|
contains('file.invalid must be a .ppm file'))),
|
|
);
|
|
});
|
|
|
|
testUsingContext('takeScreenshot throws if screencap failed', () async {
|
|
processManager.addCommand(const FakeCommand(command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'screencap > /tmp/screenshot.ppm',
|
|
], exitCode: 1, stderr: '<error-message>'));
|
|
final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester', logger: logger);
|
|
|
|
await expectLater(
|
|
() => device.takeScreenshot(globals.fs.file('file.ppm')),
|
|
throwsA(isA<Exception>().having(
|
|
(Exception exception) => exception.toString(),
|
|
'message',
|
|
contains(
|
|
'Could not take a screenshot on device tester:\n<error-message>'))),
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
Platform: () => FakePlatform(
|
|
environment: <String, String>{
|
|
'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
|
|
},
|
|
),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
});
|
|
|
|
testUsingContext('takeScreenshot throws if scp failed', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester', logger: logger);
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'screencap > /tmp/screenshot.ppm',
|
|
],
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'scp',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0:/tmp/screenshot.ppm',
|
|
'file.ppm',
|
|
],
|
|
exitCode: 1,
|
|
stderr: '<error-message>',
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'rm /tmp/screenshot.ppm',
|
|
],
|
|
));
|
|
|
|
await expectLater(
|
|
() => device.takeScreenshot(globals.fs.file('file.ppm')),
|
|
throwsA(isA<Exception>().having(
|
|
(Exception exception) => exception.toString(),
|
|
'message',
|
|
contains(
|
|
'Failed to copy screenshot from device:\n<error-message>'))),
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
Platform: () => FakePlatform(
|
|
environment: <String, String>{
|
|
'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
|
|
},
|
|
),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
});
|
|
|
|
testUsingContext(
|
|
"takeScreenshot prints error if can't delete file from device",
|
|
() async {
|
|
final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester', logger: logger);
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'screencap > /tmp/screenshot.ppm',
|
|
],
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'scp',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0:/tmp/screenshot.ppm',
|
|
'file.ppm',
|
|
],
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'rm /tmp/screenshot.ppm',
|
|
],
|
|
exitCode: 1,
|
|
stderr: '<error-message>',
|
|
));
|
|
|
|
await device.takeScreenshot(globals.fs.file('file.ppm'));
|
|
expect(
|
|
testLogger.errorText,
|
|
contains(
|
|
'Failed to delete screenshot.ppm from the device:\n<error-message>'),
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
Platform: () => FakePlatform(
|
|
environment: <String, String>{
|
|
'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
|
|
},
|
|
),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
}, testOn: 'posix');
|
|
|
|
testUsingContext('takeScreenshot returns', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester', logger: logger);
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'screencap > /tmp/screenshot.ppm',
|
|
],
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'scp',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0:/tmp/screenshot.ppm',
|
|
'file.ppm',
|
|
],
|
|
));
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/fuchsia/out/default/.ssh',
|
|
'0.0.0.0',
|
|
'rm /tmp/screenshot.ppm',
|
|
],
|
|
));
|
|
|
|
expect(() => device.takeScreenshot(globals.fs.file('file.ppm')),
|
|
returnsNormally);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
Platform: () => FakePlatform(
|
|
environment: <String, String>{
|
|
'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
|
|
},
|
|
),
|
|
FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
|
|
});
|
|
});
|
|
|
|
group('portForwarder', () {
|
|
late FakeProcessManager processManager;
|
|
late File sshConfig;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
processManager = FakeProcessManager.empty();
|
|
sshConfig = MemoryFileSystem.test().file('irrelevant')
|
|
..writeAsStringSync('\n');
|
|
});
|
|
|
|
testUsingContext(
|
|
'`unforward` prints stdout and stderr if ssh command failed', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester', logger: logger);
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/irrelevant',
|
|
'-O',
|
|
'cancel',
|
|
'-vvv',
|
|
'-L',
|
|
'0:127.0.0.1:1',
|
|
'id'
|
|
],
|
|
exitCode: 1,
|
|
stdout: '<stdout>',
|
|
stderr: '<stderr>',
|
|
));
|
|
|
|
await expectLater(
|
|
() => device.portForwarder
|
|
.unforward(ForwardedPort(/*hostPort=*/ 0, /*devicePort=*/ 1)),
|
|
throwsToolExit(
|
|
message:
|
|
'Unforward command failed:\nstdout: <stdout>\nstderr: <stderr>'),
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
});
|
|
});
|
|
|
|
group('FuchsiaIsolateDiscoveryProtocol', () {
|
|
Future<Uri> findUri(
|
|
List<FlutterView> views, String expectedIsolateName) async {
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
|
|
requests: <VmServiceExpectation>[
|
|
FakeVmServiceRequest(
|
|
method: kListViewsMethod,
|
|
jsonResponse: <String, Object>{
|
|
'views': <Object>[
|
|
for (final FlutterView view in views) view.toJson(),
|
|
],
|
|
},
|
|
),
|
|
],
|
|
httpAddress: Uri.parse('example'),
|
|
);
|
|
final MockFuchsiaDevice fuchsiaDevice =
|
|
MockFuchsiaDevice('123', const NoOpDevicePortForwarder(), false);
|
|
final FuchsiaIsolateDiscoveryProtocol discoveryProtocol =
|
|
FuchsiaIsolateDiscoveryProtocol(
|
|
fuchsiaDevice,
|
|
expectedIsolateName,
|
|
(Uri uri) async => fakeVmServiceHost.vmService,
|
|
(Device device, Uri uri, bool enableServiceAuthCodes) async {},
|
|
true, // only poll once.
|
|
);
|
|
return discoveryProtocol.uri;
|
|
}
|
|
|
|
testUsingContext('can find flutter view with matching isolate name',
|
|
() async {
|
|
const String expectedIsolateName = 'foobar';
|
|
final Uri uri = await findUri(<FlutterView>[
|
|
// no ui isolate.
|
|
FlutterView(id: '1', uiIsolate: fakeIsolate),
|
|
// wrong name.
|
|
FlutterView(
|
|
id: '2',
|
|
uiIsolate: vm_service.Isolate.parse(<String, dynamic>{
|
|
...fakeIsolate.toJson(),
|
|
'name': 'Wrong name',
|
|
}),
|
|
),
|
|
// matching name.
|
|
FlutterView(
|
|
id: '3',
|
|
uiIsolate: vm_service.Isolate.parse(<String, dynamic>{
|
|
...fakeIsolate.toJson(),
|
|
'name': expectedIsolateName,
|
|
}),
|
|
),
|
|
], expectedIsolateName);
|
|
|
|
expect(
|
|
uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
|
|
});
|
|
|
|
testUsingContext('can handle flutter view without matching isolate name',
|
|
() async {
|
|
const String expectedIsolateName = 'foobar';
|
|
final Future<Uri> uri = findUri(<FlutterView>[
|
|
// no ui isolate.
|
|
FlutterView(id: '1', uiIsolate: fakeIsolate),
|
|
// wrong name.
|
|
FlutterView(
|
|
id: '2',
|
|
uiIsolate: vm_service.Isolate.parse(<String, Object?>{
|
|
...fakeIsolate.toJson(),
|
|
'name': 'wrong name',
|
|
})),
|
|
], expectedIsolateName);
|
|
|
|
expect(uri, throwsException);
|
|
});
|
|
|
|
testUsingContext('can handle non flutter view', () async {
|
|
const String expectedIsolateName = 'foobar';
|
|
final Future<Uri> uri = findUri(<FlutterView>[
|
|
FlutterView(id: '1', uiIsolate: fakeIsolate), // no ui isolate.
|
|
], expectedIsolateName);
|
|
|
|
expect(uri, throwsException);
|
|
});
|
|
});
|
|
|
|
testUsingContext('Correct flutter runner', () async {
|
|
final Cache cache = Cache.test(
|
|
processManager: FakeProcessManager.any(),
|
|
);
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final CachedArtifacts artifacts = CachedArtifacts(
|
|
cache: cache,
|
|
fileSystem: fileSystem,
|
|
platform: FakePlatform(),
|
|
operatingSystemUtils: globals.os,
|
|
);
|
|
expect(
|
|
artifacts.getArtifactPath(
|
|
Artifact.fuchsiaFlutterRunner,
|
|
platform: TargetPlatform.fuchsia_x64,
|
|
mode: BuildMode.debug,
|
|
),
|
|
contains('flutter_jit_runner'),
|
|
);
|
|
expect(
|
|
artifacts.getArtifactPath(
|
|
Artifact.fuchsiaFlutterRunner,
|
|
platform: TargetPlatform.fuchsia_x64,
|
|
mode: BuildMode.profile,
|
|
),
|
|
contains('flutter_aot_runner'),
|
|
);
|
|
expect(
|
|
artifacts.getArtifactPath(
|
|
Artifact.fuchsiaFlutterRunner,
|
|
platform: TargetPlatform.fuchsia_x64,
|
|
mode: BuildMode.release,
|
|
),
|
|
contains('flutter_aot_product_runner'),
|
|
);
|
|
expect(
|
|
artifacts.getArtifactPath(
|
|
Artifact.fuchsiaFlutterRunner,
|
|
platform: TargetPlatform.fuchsia_x64,
|
|
mode: BuildMode.jitRelease,
|
|
),
|
|
contains('flutter_jit_product_runner'),
|
|
);
|
|
});
|
|
|
|
group('sdkNameAndVersion: ', () {
|
|
late File sshConfig;
|
|
late FakeProcessManager processManager;
|
|
final Logger logger = FakeLogger();
|
|
|
|
setUp(() {
|
|
sshConfig = MemoryFileSystem.test().file('ssh_config')
|
|
..writeAsStringSync('\n');
|
|
processManager = FakeProcessManager.empty();
|
|
});
|
|
|
|
testUsingContext('does not throw on non-existent ssh config', () async {
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(await device.sdkNameAndVersion, equals('Fuchsia'));
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
});
|
|
|
|
testUsingContext('returns what we get from the device on success',
|
|
() async {
|
|
processManager.addCommand(const FakeCommand(command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'123',
|
|
'cat /pkgfs/packages/build-info/0/data/version'
|
|
], stdout: 'version'));
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(await device.sdkNameAndVersion, equals('Fuchsia version'));
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
});
|
|
|
|
testUsingContext('returns "Fuchsia" when device command fails', () async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'123',
|
|
'cat /pkgfs/packages/build-info/0/data/version'
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(await device.sdkNameAndVersion, equals('Fuchsia'));
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
});
|
|
|
|
testUsingContext('returns "Fuchsia" when device gives an empty result',
|
|
() async {
|
|
processManager.addCommand(const FakeCommand(
|
|
command: <String>[
|
|
'ssh',
|
|
'-F',
|
|
'/ssh_config',
|
|
'123',
|
|
'cat /pkgfs/packages/build-info/0/data/version'
|
|
],
|
|
));
|
|
final FuchsiaDevice device = FuchsiaDevice('123', name: 'device', logger: logger);
|
|
|
|
expect(await device.sdkNameAndVersion, equals('Fuchsia'));
|
|
}, overrides: <Type, Generator>{
|
|
ProcessManager: () => processManager,
|
|
FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
|
|
FuchsiaSdk: () => FakeFuchsiaSdk(),
|
|
});
|
|
});
|
|
}
|
|
|
|
class FuchsiaModulePackage extends ApplicationPackage {
|
|
FuchsiaModulePackage({required this.name}) : super(id: name);
|
|
|
|
@override
|
|
final String name;
|
|
}
|
|
|
|
class MockFuchsiaDevice extends Fake implements FuchsiaDevice {
|
|
MockFuchsiaDevice(this.id, this.portForwarder, this._ipv6);
|
|
|
|
final bool _ipv6;
|
|
|
|
@override
|
|
bool get ipv6 => _ipv6;
|
|
|
|
@override
|
|
final String id;
|
|
|
|
@override
|
|
final DevicePortForwarder portForwarder;
|
|
|
|
@override
|
|
Future<TargetPlatform> get targetPlatform async =>
|
|
TargetPlatform.fuchsia_arm64;
|
|
|
|
@override
|
|
String get name => 'fuchsia';
|
|
|
|
@override
|
|
Future<List<int>> servicePorts() async => <int>[1];
|
|
|
|
@override
|
|
DartDevelopmentService get dds => FakeDartDevelopmentService();
|
|
}
|
|
|
|
class FakePortForwarder extends Fake implements DevicePortForwarder {
|
|
bool disposed = false;
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
class FakeFuchsiaFfx implements FuchsiaFfx {
|
|
@override
|
|
Future<List<String>> list({Duration? timeout}) async {
|
|
return <String>['192.168.42.172 scare-cable-skip-ffx'];
|
|
}
|
|
|
|
@override
|
|
Future<String> resolve(String deviceName) async {
|
|
return '192.168.42.10';
|
|
}
|
|
|
|
@override
|
|
Future<String?> sessionShow() async {
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Future<bool> sessionAdd(String url) async {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class FakeFuchsiaPM extends Fake implements FuchsiaPM {}
|
|
|
|
class FakeFuchsiaKernelCompiler extends Fake implements FuchsiaKernelCompiler {}
|
|
|
|
class FakeFuchsiaSdk extends Fake implements FuchsiaSdk {
|
|
FakeFuchsiaSdk({
|
|
FuchsiaPM? pm,
|
|
FuchsiaKernelCompiler? compiler,
|
|
FuchsiaFfx? ffx,
|
|
String? devices,
|
|
}) : fuchsiaPM = pm ?? FakeFuchsiaPM(),
|
|
fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(),
|
|
fuchsiaFfx = ffx ?? FakeFuchsiaFfx(),
|
|
_devices = devices;
|
|
|
|
@override
|
|
final FuchsiaPM fuchsiaPM;
|
|
|
|
@override
|
|
final FuchsiaKernelCompiler fuchsiaKernelCompiler;
|
|
|
|
@override
|
|
final FuchsiaFfx fuchsiaFfx;
|
|
|
|
final String? _devices;
|
|
|
|
@override
|
|
Future<String?> listDevices({Duration? timeout}) async {
|
|
return _devices;
|
|
}
|
|
}
|
|
|
|
class FakeDartDevelopmentService extends Fake
|
|
implements DartDevelopmentService {
|
|
@override
|
|
Future<void> startDartDevelopmentService(
|
|
Uri vmServiceUri, {
|
|
FlutterDevice? device,
|
|
int? ddsPort,
|
|
bool? ipv6,
|
|
bool? disableServiceAuthCodes,
|
|
bool cacheStartupProfile = false,
|
|
bool enableDevTools = false,
|
|
String? google3WorkspaceRoot,
|
|
Uri? devToolsServerAddress,
|
|
}) async {}
|
|
|
|
@override
|
|
Uri get uri => Uri.parse('example');
|
|
}
|
|
|
|
class FakeFuchsiaWorkflow implements FuchsiaWorkflow {
|
|
FakeFuchsiaWorkflow({
|
|
this.appliesToHostPlatform = true,
|
|
this.canLaunchDevices = true,
|
|
this.canListDevices = true,
|
|
this.canListEmulators = true,
|
|
});
|
|
|
|
@override
|
|
bool appliesToHostPlatform;
|
|
|
|
@override
|
|
bool canLaunchDevices;
|
|
|
|
@override
|
|
bool canListDevices;
|
|
|
|
@override
|
|
bool canListEmulators;
|
|
}
|