// 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 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/fuchsia/application_package.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/pkgctl.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; void main() { group('Fuchsia app start and stop: ', () { late MemoryFileSystem memoryFileSystem; late FakeOperatingSystemUtils osUtils; late FakeFuchsiaDeviceTools fuchsiaDeviceTools; late FakeFuchsiaSdk fuchsiaSdk; late Artifacts artifacts; late FakeProcessManager fakeSuccessfulProcessManager; late FakeProcessManager fakeFailedProcessManagerForHostAddress; late File sshConfig; setUp(() { memoryFileSystem = MemoryFileSystem.test(); osUtils = FakeOperatingSystemUtils(); fuchsiaDeviceTools = FakeFuchsiaDeviceTools(); fuchsiaSdk = FakeFuchsiaSdk(); sshConfig = MemoryFileSystem.test().file('ssh_config') ..writeAsStringSync('\n'); artifacts = Artifacts.test(); for (final BuildMode mode in <BuildMode>[ BuildMode.debug, BuildMode.release ]) { memoryFileSystem .file( artifacts.getArtifactPath(Artifact.fuchsiaKernelCompiler, platform: TargetPlatform.fuchsia_arm64, mode: mode), ) .createSync(); memoryFileSystem .file( artifacts.getArtifactPath(Artifact.platformKernelDill, platform: TargetPlatform.fuchsia_arm64, mode: mode), ) .createSync(); memoryFileSystem .file( artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, platform: TargetPlatform.fuchsia_arm64, mode: mode), ) .createSync(); memoryFileSystem .file( artifacts.getArtifactPath(Artifact.fuchsiaFlutterRunner, platform: TargetPlatform.fuchsia_arm64, mode: mode), ) .createSync(); } fakeSuccessfulProcessManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ 'ssh', '-F', sshConfig.absolute.path, '123', r'echo $SSH_CONNECTION' ], stdout: 'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22', ), ]); fakeFailedProcessManagerForHostAddress = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: <String>[ 'ssh', '-F', sshConfig.absolute.path, '123', r'echo $SSH_CONNECTION' ], stdout: 'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22', exitCode: 1, ), ]); }); Future<LaunchResult> setupAndStartApp({ required bool prebuilt, required BuildMode mode, }) async { const String appName = 'app_name'; final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123'); globals.fs.directory('fuchsia').createSync(recursive: true); final File pubspecFile = globals.fs.file('pubspec.yaml')..createSync(); pubspecFile.writeAsStringSync('name: $appName'); FuchsiaApp? app; if (prebuilt) { final File far = globals.fs.file('app_name-0.far')..createSync(); app = FuchsiaApp.fromPrebuiltApp(far); } else { globals.fs.file(globals.fs.path.join('fuchsia', 'meta', '$appName.cm')) ..createSync(recursive: true) ..writeAsStringSync('{}'); globals.fs.file('.packages').createSync(); globals.fs .file(globals.fs.path.join('lib', 'main.dart')) .createSync(recursive: true); app = BuildableFuchsiaApp( project: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory) .fuchsia); } final DebuggingOptions debuggingOptions = DebuggingOptions.disabled( BuildInfo(mode, null, treeShakeIcons: false)); return device.startApp( app!, prebuiltApplication: prebuilt, debuggingOptions: debuggingOptions, ); } testUsingContext( 'start prebuilt in release mode fails without session', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.release); expect(launchResult.started, isFalse); expect(launchResult.hasVmService, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); testUsingContext('start prebuilt in release mode with session', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.release); expect(launchResult.started, isTrue); expect(launchResult.hasVmService, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start and stop prebuilt in release mode fails without session', () async { const String appName = 'app_name'; final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123'); globals.fs.directory('fuchsia').createSync(recursive: true); final File pubspecFile = globals.fs.file('pubspec.yaml')..createSync(); pubspecFile.writeAsStringSync('name: $appName'); final File far = globals.fs.file('app_name-0.far')..createSync(); final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far)!; final DebuggingOptions debuggingOptions = DebuggingOptions.disabled( const BuildInfo(BuildMode.release, null, treeShakeIcons: false)); final LaunchResult launchResult = await device.startApp(app, prebuiltApplication: true, debuggingOptions: debuggingOptions); expect(launchResult.started, isFalse); expect(launchResult.hasVmService, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); testUsingContext('start and stop prebuilt in release mode with session', () async { const String appName = 'app_name'; final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123'); globals.fs.directory('fuchsia').createSync(recursive: true); final File pubspecFile = globals.fs.file('pubspec.yaml')..createSync(); pubspecFile.writeAsStringSync('name: $appName'); final File far = globals.fs.file('app_name-0.far')..createSync(); final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far)!; final DebuggingOptions debuggingOptions = DebuggingOptions.disabled( const BuildInfo(BuildMode.release, null, treeShakeIcons: false)); final LaunchResult launchResult = await device.startApp(app, prebuiltApplication: true, debuggingOptions: debuggingOptions); expect(launchResult.started, isTrue); expect(launchResult.hasVmService, isFalse); expect(await device.stopApp(app), isTrue); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start prebuilt in debug mode fails without session', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.debug); expect(launchResult.started, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); testUsingContext('start prebuilt in debug mode with session', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.debug); expect(launchResult.started, isTrue); expect(launchResult.hasVmService, isTrue); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start buildable in release mode fails without session', () async { expect( () async => setupAndStartApp(prebuilt: false, mode: BuildMode.release), throwsToolExit( message: 'This tool does not currently build apps for fuchsia.\n' 'Build the app using a supported Fuchsia workflow.\n' 'Then use the --use-application-binary flag.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ const FakeCommand( command: <String>[ 'Artifact.genSnapshot.TargetPlatform.fuchsia_arm64.release', '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/fuchsia/elf.aotsnapshot', 'build/fuchsia/app_name.dil', ], ), FakeCommand( command: <String>[ 'ssh', '-F', sshConfig.absolute.path, '123', r'echo $SSH_CONNECTION' ], stdout: 'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22', ), ]), FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start buildable in release mode with session fails, does not build apps yet', () async { expect( () async => setupAndStartApp(prebuilt: false, mode: BuildMode.release), throwsToolExit( message: 'This tool does not currently build apps for fuchsia.\n' 'Build the app using a supported Fuchsia workflow.\n' 'Then use the --use-application-binary flag.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ const FakeCommand( command: <String>[ 'Artifact.genSnapshot.TargetPlatform.fuchsia_arm64.release', '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/fuchsia/elf.aotsnapshot', 'build/fuchsia/app_name.dil', ], ), FakeCommand( command: <String>[ 'ssh', '-F', sshConfig.absolute.path, '123', r'echo $SSH_CONNECTION' ], stdout: 'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22', ), ]), FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start buildable in debug mode fails without session', () async { expect( () async => setupAndStartApp(prebuilt: false, mode: BuildMode.debug), throwsToolExit( message: 'This tool does not currently build apps for fuchsia.\n' 'Build the app using a supported Fuchsia workflow.\n' 'Then use the --use-application-binary flag.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); testUsingContext( 'start buildable in debug mode with session fails, does not build apps yet', () async { expect( () async => setupAndStartApp(prebuilt: false, mode: BuildMode.debug), throwsToolExit( message: 'This tool does not currently build apps for fuchsia.\n' 'Build the app using a supported Fuchsia workflow.\n' 'Then use the --use-application-binary flag.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext('fail when cant get ssh config', () async { expect( () async => setupAndStartApp(prebuilt: true, mode: BuildMode.release), throwsToolExit( message: 'Cannot interact with device. No ssh config.\n' 'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => FakeProcessManager.any(), FuchsiaArtifacts: () => FuchsiaArtifacts(), FuchsiaSdk: () => FakeFuchsiaSdk(ffx: FakeFuchsiaFfxWithSession()), OperatingSystemUtils: () => osUtils, }); testUsingContext('fail when cant get host address', () async { expect(() async => FuchsiaDeviceWithFakeDiscovery('123').hostAddress, throwsToolExit(message: 'Failed to get local address, aborting.')); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeFailedProcessManagerForHostAddress, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), OperatingSystemUtils: () => osUtils, Platform: () => FakePlatform(), }); testUsingContext('fail with correct LaunchResult when pm fails', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.release); expect(launchResult.started, isFalse); expect(launchResult.hasVmService, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => fuchsiaDeviceTools, FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => FakeFuchsiaSdk(pm: FailingPM()), OperatingSystemUtils: () => osUtils, }); testUsingContext('fail with correct LaunchResult when pkgctl fails', () async { final LaunchResult launchResult = await setupAndStartApp(prebuilt: true, mode: BuildMode.release); expect(launchResult.started, isFalse); expect(launchResult.hasVmService, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, FileSystem: () => memoryFileSystem, ProcessManager: () => fakeSuccessfulProcessManager, FuchsiaDeviceTools: () => FakeFuchsiaDeviceTools(pkgctl: FailingPkgctl()), FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig), FuchsiaSdk: () => fuchsiaSdk, OperatingSystemUtils: () => osUtils, }); }); } Process _createFakeProcess({ int exitCode = 0, String stdout = '', String stderr = '', bool persistent = false, }) { final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[ utf8.encode(stdout), ]); final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[ utf8.encode(stderr), ]); final Completer<int> exitCodeCompleter = Completer<int>(); final Process process = FakeProcess( stdout: stdoutStream, stderr: stderrStream, exitCode: persistent ? exitCodeCompleter.future : Future<int>.value(exitCode), ); return process; } class FuchsiaDeviceWithFakeDiscovery extends FuchsiaDevice { FuchsiaDeviceWithFakeDiscovery(super.id, {super.name = ''}); @override FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol( String isolateName) { return FakeFuchsiaIsolateDiscoveryProtocol(); } @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.fuchsia_arm64; } class FakeFuchsiaIsolateDiscoveryProtocol implements FuchsiaIsolateDiscoveryProtocol { @override FutureOr<Uri> get uri => Uri.parse('http://[::1]:37'); @override void dispose() {} } class FakeFuchsiaPkgctl implements FuchsiaPkgctl { @override Future<bool> addRepo( FuchsiaDevice device, FuchsiaPackageServer server) async { return true; } @override Future<bool> resolve( FuchsiaDevice device, String serverName, String packageName) async { return true; } @override Future<bool> rmRepo(FuchsiaDevice device, FuchsiaPackageServer server) async { return true; } } class FailingPkgctl implements FuchsiaPkgctl { @override Future<bool> addRepo( FuchsiaDevice device, FuchsiaPackageServer server) async { return false; } @override Future<bool> resolve( FuchsiaDevice device, String serverName, String packageName) async { return false; } @override Future<bool> rmRepo(FuchsiaDevice device, FuchsiaPackageServer server) async { return false; } } class FakeFuchsiaDeviceTools implements FuchsiaDeviceTools { FakeFuchsiaDeviceTools({ FuchsiaPkgctl? pkgctl, FuchsiaFfx? ffx, }) : pkgctl = pkgctl ?? FakeFuchsiaPkgctl(), ffx = ffx ?? FakeFuchsiaFfx(); @override final FuchsiaPkgctl pkgctl; @override final FuchsiaFfx ffx; } class FakeFuchsiaPM implements FuchsiaPM { String? _appName; @override Future<bool> init(String buildPath, String appName) async { if (!globals.fs.directory(buildPath).existsSync()) { return false; } globals.fs .file(globals.fs.path.join(buildPath, 'meta', 'package')) .createSync(recursive: true); _appName = appName; return true; } @override Future<bool> build(String buildPath, String manifestPath) async { if (!globals.fs .file(globals.fs.path.join(buildPath, 'meta', 'package')) .existsSync() || !globals.fs.file(manifestPath).existsSync()) { return false; } globals.fs .file(globals.fs.path.join(buildPath, 'meta.far')) .createSync(recursive: true); return true; } @override Future<bool> archive(String buildPath, String manifestPath) async { if (!globals.fs .file(globals.fs.path.join(buildPath, 'meta', 'package')) .existsSync() || !globals.fs.file(manifestPath).existsSync()) { return false; } if (_appName == null) { return false; } globals.fs .file(globals.fs.path.join(buildPath, '$_appName-0.far')) .createSync(recursive: true); return true; } @override Future<bool> newrepo(String repoPath) async { if (!globals.fs.directory(repoPath).existsSync()) { return false; } return true; } @override Future<Process> serve(String repoPath, String host, int port) async { return _createFakeProcess(persistent: true); } @override Future<bool> publish(String repoPath, String packagePath) async { if (!globals.fs.directory(repoPath).existsSync()) { return false; } if (!globals.fs.file(packagePath).existsSync()) { return false; } return true; } } class FailingPM implements FuchsiaPM { @override Future<bool> init(String buildPath, String appName) async { return false; } @override Future<bool> build(String buildPath, String manifestPath) async { return false; } @override Future<bool> archive(String buildPath, String manifestPath) async { return false; } @override Future<bool> newrepo(String repoPath) async { return false; } @override Future<Process> serve(String repoPath, String host, int port) async { return _createFakeProcess(exitCode: 6); } @override Future<bool> publish(String repoPath, String packagePath) async { return false; } } class FakeFuchsiaKernelCompiler implements FuchsiaKernelCompiler { @override Future<void> build({ required FuchsiaProject fuchsiaProject, required String target, // E.g., lib/main.dart BuildInfo buildInfo = BuildInfo.debug, }) async { final String outDir = getFuchsiaBuildDirectory(); final String appName = fuchsiaProject.project.manifest.appName; final String manifestPath = globals.fs.path.join(outDir, '$appName.dilpmanifest'); globals.fs.file(manifestPath).createSync(recursive: 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 FakeFuchsiaFfxWithSession 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 'session info'; } @override Future<bool> sessionAdd(String url) async { return true; } } class FakeFuchsiaSdk extends Fake implements FuchsiaSdk { FakeFuchsiaSdk({ FuchsiaPM? pm, FuchsiaKernelCompiler? compiler, FuchsiaFfx? ffx, }) : fuchsiaPM = pm ?? FakeFuchsiaPM(), fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(), fuchsiaFfx = ffx ?? FakeFuchsiaFfx(); @override final FuchsiaPM fuchsiaPM; @override final FuchsiaKernelCompiler fuchsiaKernelCompiler; @override final FuchsiaFfx fuchsiaFfx; }