// 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 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart'; import 'package:test/fake.dart'; import 'package:test/test.dart'; import 'package:vm_service/vm_service.dart' as vms; void main() { group('FuchsiaRemoteConnection.connect', () { late List forwardedPorts; List fakeVmServices; late List uriConnections; setUp(() { final flutterViewCannedResponses = >[ { 'views': >[ {'type': 'FlutterView', 'id': 'flutterView0'}, ], }, { 'views': >[ { 'type': 'FlutterView', 'id': 'flutterView1', 'isolate': { 'type': '@Isolate', 'fixedId': 'true', 'id': 'isolates/1', 'name': 'file://flutterBinary1', 'number': '1', }, }, ], }, { 'views': >[ { 'type': 'FlutterView', 'id': 'flutterView2', 'isolate': { 'type': '@Isolate', 'fixedId': 'true', 'id': 'isolates/2', 'name': 'file://flutterBinary2', 'number': '2', }, }, ], }, ]; forwardedPorts = []; fakeVmServices = []; uriConnections = []; Future fakeVmConnectionFunction(Uri uri, {Duration? timeout}) { return Future(() async { final service = FakeVmService(); fakeVmServices.add(service); uriConnections.add(uri); service.flutterListViews = vms.Response.parse(flutterViewCannedResponses[uri.port]); return service; }); } fuchsiaVmServiceConnectionFunction = fakeVmConnectionFunction; }); tearDown(() { /// Most tests will fake out the port forwarding and connection /// functions. restoreFuchsiaPortForwardingFunction(); restoreVmServiceConnectionFunction(); }); test('end-to-end with one vm connection and flutter view query', () async { var port = 0; Future fakePortForwardingFunction( String address, int remotePort, [ String? interface = '', String? configFile, ]) { return Future(() { final pf = FakePortForwarder(); forwardedPorts.add(pf); pf.port = port++; pf.remotePort = remotePort; return pf; }); } fuchsiaPortForwardingFunction = fakePortForwardingFunction; final fakeRunner = FakeSshCommandRunner(); // Adds some extra junk to make sure the strings will be cleaned up. fakeRunner.iqueryResponse = [ '[', ' {', ' "data_source": "Inspect",', ' "metadata": {', ' "filename": "fuchsia.inspect.Tree",', ' "component_url": "fuchsia-pkg://fuchsia.com/flutter_runner#meta/flutter_runner.cm",', ' "timestamp": 12345678901234', ' },', ' "moniker": "core/session-manager/session/flutter_runner",', ' "payload": {', ' "root": {', ' "vm_service_port": "12345",', ' "16859221": {', ' "empty_tree": "this semantic tree is empty"', ' },', ' "build_info": {', ' "dart_sdk_git_revision": "77e83fcc14fa94049f363d554579f48fbd6bb7a1",', ' "dart_sdk_semantic_version": "2.19.0-317.0.dev",', ' "flutter_engine_git_revision": "563b8e830c697a543bf0a8a9f4ae3edfad86ea86",', ' "fuchsia_sdk_version": "10.20221018.0.1"', ' },', ' "vm": {', ' "dst_status": 1,', ' "get_profile_status": 0,', ' "num_get_profile_calls": 1,', ' "num_intl_provider_errors": 0,', ' "num_on_change_calls": 0,', ' "timezone_content_status": 0,', ' "tz_data_close_status": -1,', ' "tz_data_status": -1', ' }', ' }', ' },', ' "version": 1', ' }', ' ]', ]; fakeRunner.address = 'fe80::8eae:4cff:fef4:9247'; fakeRunner.interface = 'eno1'; final FuchsiaRemoteConnection connection = await FuchsiaRemoteConnection.connectWithSshCommandRunner(fakeRunner); expect(forwardedPorts.length, 1); expect(forwardedPorts[0].remotePort, 12345); // VMs should be accessed via localhost ports given by // [fakePortForwardingFunction]. expect(uriConnections[0], Uri(scheme: 'ws', host: '[::1]', port: 0, path: '/ws')); final List views = await connection.getFlutterViews(); expect(views, isNot(null)); expect(views.length, 1); // Since name can be null, check for the ID on all of them. expect(views[0].id, 'flutterView0'); expect(views[0].name, equals(null)); // Ensure the ports are all closed after stop was called. await connection.stop(); expect(forwardedPorts[0].stopped, true); }); test('end-to-end with one vm and remote open port', () async { var port = 0; Future fakePortForwardingFunction( String address, int remotePort, [ String? interface = '', String? configFile, ]) { return Future(() { final pf = FakePortForwarder(); forwardedPorts.add(pf); pf.port = port++; pf.remotePort = remotePort; pf.openPortAddress = 'fe80::1:2%eno2'; return pf; }); } fuchsiaPortForwardingFunction = fakePortForwardingFunction; final fakeRunner = FakeSshCommandRunner(); // Adds some extra junk to make sure the strings will be cleaned up. fakeRunner.iqueryResponse = [ '[', ' {', ' "data_source": "Inspect",', ' "metadata": {', ' "filename": "fuchsia.inspect.Tree",', ' "component_url": "fuchsia-pkg://fuchsia.com/flutter_runner#meta/flutter_runner.cm",', ' "timestamp": 12345678901234', ' },', ' "moniker": "core/session-manager/session/flutter_runner",', ' "payload": {', ' "root": {', ' "vm_service_port": "12345",', ' "16859221": {', ' "empty_tree": "this semantic tree is empty"', ' },', ' "build_info": {', ' "dart_sdk_git_revision": "77e83fcc14fa94049f363d554579f48fbd6bb7a1",', ' "dart_sdk_semantic_version": "2.19.0-317.0.dev",', ' "flutter_engine_git_revision": "563b8e830c697a543bf0a8a9f4ae3edfad86ea86",', ' "fuchsia_sdk_version": "10.20221018.0.1"', ' },', ' "vm": {', ' "dst_status": 1,', ' "get_profile_status": 0,', ' "num_get_profile_calls": 1,', ' "num_intl_provider_errors": 0,', ' "num_on_change_calls": 0,', ' "timezone_content_status": 0,', ' "tz_data_close_status": -1,', ' "tz_data_status": -1', ' }', ' }', ' },', ' "version": 1', ' }', ' ]', ]; fakeRunner.address = 'fe80::8eae:4cff:fef4:9247'; fakeRunner.interface = 'eno1'; final FuchsiaRemoteConnection connection = await FuchsiaRemoteConnection.connectWithSshCommandRunner(fakeRunner); expect(forwardedPorts.length, 1); expect(forwardedPorts[0].remotePort, 12345); // VMs should be accessed via the alternate address given by // [fakePortForwardingFunction]. expect( uriConnections[0], Uri(scheme: 'ws', host: '[fe80::1:2%25eno2]', port: 0, path: '/ws'), ); final List views = await connection.getFlutterViews(); expect(views, isNot(null)); expect(views.length, 1); // Since name can be null, check for the ID on all of them. expect(views[0].id, 'flutterView0'); expect(views[0].name, equals(null)); // Ensure the ports are all closed after stop was called. await connection.stop(); expect(forwardedPorts[0].stopped, true); }); test('end-to-end with one vm and ipv4', () async { var port = 0; Future fakePortForwardingFunction( String address, int remotePort, [ String? interface = '', String? configFile, ]) { return Future(() { final pf = FakePortForwarder(); forwardedPorts.add(pf); pf.port = port++; pf.remotePort = remotePort; return pf; }); } fuchsiaPortForwardingFunction = fakePortForwardingFunction; final fakeRunner = FakeSshCommandRunner(); // Adds some extra junk to make sure the strings will be cleaned up. fakeRunner.iqueryResponse = [ '[', ' {', ' "data_source": "Inspect",', ' "metadata": {', ' "filename": "fuchsia.inspect.Tree",', ' "component_url": "fuchsia-pkg://fuchsia.com/flutter_runner#meta/flutter_runner.cm",', ' "timestamp": 12345678901234', ' },', ' "moniker": "core/session-manager/session/flutter_runner",', ' "payload": {', ' "root": {', ' "vm_service_port": "12345",', ' "16859221": {', ' "empty_tree": "this semantic tree is empty"', ' },', ' "build_info": {', ' "dart_sdk_git_revision": "77e83fcc14fa94049f363d554579f48fbd6bb7a1",', ' "dart_sdk_semantic_version": "2.19.0-317.0.dev",', ' "flutter_engine_git_revision": "563b8e830c697a543bf0a8a9f4ae3edfad86ea86",', ' "fuchsia_sdk_version": "10.20221018.0.1"', ' },', ' "vm": {', ' "dst_status": 1,', ' "get_profile_status": 0,', ' "num_get_profile_calls": 1,', ' "num_intl_provider_errors": 0,', ' "num_on_change_calls": 0,', ' "timezone_content_status": 0,', ' "tz_data_close_status": -1,', ' "tz_data_status": -1', ' }', ' }', ' },', ' "version": 1', ' }', ' ]', ]; fakeRunner.address = '196.168.1.4'; final FuchsiaRemoteConnection connection = await FuchsiaRemoteConnection.connectWithSshCommandRunner(fakeRunner); expect(forwardedPorts.length, 1); expect(forwardedPorts[0].remotePort, 12345); // VMs should be accessed via the ipv4 loopback. expect(uriConnections[0], Uri(scheme: 'ws', host: '127.0.0.1', port: 0, path: '/ws')); final List views = await connection.getFlutterViews(); expect(views, isNot(null)); expect(views.length, 1); // Since name can be null, check for the ID on all of them. expect(views[0].id, 'flutterView0'); expect(views[0].name, equals(null)); // Ensure the ports are all closed after stop was called. await connection.stop(); expect(forwardedPorts[0].stopped, true); }); test('env variable test without remote addr', () async { Future failingFunction() async { await FuchsiaRemoteConnection.connect(); } // Should fail as no env variable has been passed. expect(failingFunction, throwsA(isA())); }); }); } class FakeSshCommandRunner extends Fake implements SshCommandRunner { List? iqueryResponse; @override Future> run(String command) async { if (command.startsWith('iquery --format json show')) { return iqueryResponse!; } throw UnimplementedError(command); } @override String interface = ''; @override String address = ''; @override String get sshConfigPath => '~/.ssh'; } class FakePortForwarder extends Fake implements PortForwarder { @override int port = 0; @override int remotePort = 0; @override String? openPortAddress; bool stopped = false; @override Future stop() async { stopped = true; } } class FakeVmService extends Fake implements vms.VmService { bool disposed = false; vms.Response? flutterListViews; @override Future dispose() async { disposed = true; } @override Future callMethod( String method, { String? isolateId, Map? args, }) async { if (method == '_flutter.listViews') { return flutterListViews!; } throw UnimplementedError(method); } @override Future onDone = Future.value(); @override Future getVersion() async { return vms.Version(major: -1, minor: -1); } }