// 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/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/desktop_device.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/fake_process_manager.dart'; void main() { group('Basic info', () { testWithoutContext('Category is desktop', () async { final FakeDesktopDevice device = setUpDesktopDevice(); expect(device.category, Category.desktop); }); testWithoutContext('Not an emulator', () async { final FakeDesktopDevice device = setUpDesktopDevice(); expect(await device.isLocalEmulator, false); expect(await device.emulatorId, null); }); testWithoutContext('Uses OS name as SDK name', () async { final FakeDesktopDevice device = setUpDesktopDevice(); expect(await device.sdkNameAndVersion, 'Example'); }); }); group('Install', () { testWithoutContext('Install checks always return true', () async { final FakeDesktopDevice device = setUpDesktopDevice(); expect(await device.isAppInstalled(FakeApplicationPackage()), true); expect(await device.isLatestBuildInstalled(FakeApplicationPackage()), true); expect(device.category, Category.desktop); }); testWithoutContext('Install and uninstall are no-ops that report success', () async { final FakeDesktopDevice device = setUpDesktopDevice(); final FakeApplicationPackage package = FakeApplicationPackage(); expect(await device.uninstallApp(package), true); expect(await device.isAppInstalled(package), true); expect(await device.isLatestBuildInstalled(package), true); expect(await device.installApp(package), true); expect(await device.isAppInstalled(package), true); expect(await device.isLatestBuildInstalled(package), true); expect(device.category, Category.desktop); }); }); group('Starting and stopping application', () { testWithoutContext('Stop without start is a successful no-op', () async { final FakeDesktopDevice device = setUpDesktopDevice(); final FakeApplicationPackage package = FakeApplicationPackage(); expect(await device.stopApp(package), true); }); testWithoutContext('Can run from prebuilt application', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final Completer<void> completer = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug'], stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n', completer: completer, ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager, fileSystem: fileSystem); final String? executableName = device.executablePathForDevice(FakeApplicationPackage(), BuildInfo.debug); fileSystem.file(executableName).writeAsStringSync('\n'); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ); expect(result.started, true); expect(result.vmServiceUri, Uri.parse('http://127.0.0.1/0')); }); testWithoutContext('Null executable path fails gracefully', () async { final BufferLogger logger = BufferLogger.test(); final DesktopDevice device = setUpDesktopDevice(nullExecutablePathForDevice: true, logger: logger); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ); expect(result.started, false); expect(logger.errorText, contains('Unable to find executable to run')); }); testWithoutContext('stopApp kills process started by startApp', () async { final Completer<void> completer = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug'], stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n', completer: completer, ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ); expect(result.started, true); expect(await device.stopApp(package), true); }); }); testWithoutContext('startApp supports DebuggingOptions through FLUTTER_ENGINE_SWITCH environment variables', () async { final Completer<void> completer = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug'], stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n', completer: completer, environment: const <String, String>{ 'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true', 'FLUTTER_ENGINE_SWITCH_2': 'trace-startup=true', 'FLUTTER_ENGINE_SWITCH_3': 'enable-software-rendering=true', 'FLUTTER_ENGINE_SWITCH_4': 'skia-deterministic-rendering=true', 'FLUTTER_ENGINE_SWITCH_5': 'trace-skia=true', 'FLUTTER_ENGINE_SWITCH_6': 'trace-allowlist=foo,bar', 'FLUTTER_ENGINE_SWITCH_7': 'trace-skia-allowlist=skia.a,skia.b', 'FLUTTER_ENGINE_SWITCH_8': 'trace-systrace=true', 'FLUTTER_ENGINE_SWITCH_9': 'endless-trace-buffer=true', 'FLUTTER_ENGINE_SWITCH_10': 'dump-skp-on-shader-compilation=true', 'FLUTTER_ENGINE_SWITCH_11': 'cache-sksl=true', 'FLUTTER_ENGINE_SWITCH_12': 'purge-persistent-cache=true', 'FLUTTER_ENGINE_SWITCH_13': 'enable-checked-mode=true', 'FLUTTER_ENGINE_SWITCH_14': 'verify-entry-points=true', 'FLUTTER_ENGINE_SWITCH_15': 'start-paused=true', 'FLUTTER_ENGINE_SWITCH_16': 'disable-service-auth-codes=true', 'FLUTTER_ENGINE_SWITCH_17': 'dart-flags=--null_assertions', 'FLUTTER_ENGINE_SWITCH_18': 'use-test-fonts=true', 'FLUTTER_ENGINE_SWITCH_19': 'verbose-logging=true', 'FLUTTER_ENGINE_SWITCHES': '19', } ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, platformArgs: <String, Object>{ 'trace-startup': true, }, debuggingOptions: DebuggingOptions.enabled( BuildInfo.debug, startPaused: true, disableServiceAuthCodes: true, enableSoftwareRendering: true, skiaDeterministicRendering: true, traceSkia: true, traceAllowlist: 'foo,bar', traceSkiaAllowlist: 'skia.a,skia.b', traceSystrace: true, endlessTraceBuffer: true, dumpSkpOnShaderCompilation: true, cacheSkSL: true, purgePersistentCache: true, useTestFonts: true, verboseSystemLogs: true, nullAssertions: true, ), ); expect(result.started, true); }); testWithoutContext('startApp supports DebuggingOptions through FLUTTER_ENGINE_SWITCH environment variables when debugging is disabled', () async { final Completer<void> completer = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug'], stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n', completer: completer, environment: const <String, String>{ 'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true', 'FLUTTER_ENGINE_SWITCH_2': 'trace-startup=true', 'FLUTTER_ENGINE_SWITCH_3': 'trace-allowlist=foo,bar', 'FLUTTER_ENGINE_SWITCH_4': 'cache-sksl=true', 'FLUTTER_ENGINE_SWITCHES': '4', } ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, platformArgs: <String, Object>{ 'trace-startup': true, }, debuggingOptions: DebuggingOptions.disabled( BuildInfo.debug, traceAllowlist: 'foo,bar', cacheSkSL: true, ), ); expect(result.started, true); }); testWithoutContext('Port forwarder is a no-op', () async { final FakeDesktopDevice device = setUpDesktopDevice(); final DevicePortForwarder portForwarder = device.portForwarder; final int result = await portForwarder.forward(2); expect(result, 2); expect(portForwarder.forwardedPorts.isEmpty, true); }); testWithoutContext('createDevFSWriter returns a LocalDevFSWriter', () { final FakeDesktopDevice device = setUpDesktopDevice(); expect(device.createDevFSWriter(FakeApplicationPackage(), ''), isA<LocalDevFSWriter>()); }); testWithoutContext('startApp supports dartEntrypointArgs', () async { final Completer<void> completer = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug', 'arg1', 'arg2'], stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n', completer: completer, ), ]); final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager); final FakeApplicationPackage package = FakeApplicationPackage(); final LaunchResult result = await device.startApp( package, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled( BuildInfo.debug, dartEntrypointArgs: <String>['arg1', 'arg2'], ), ); expect(result.started, true); }); testWithoutContext('Device logger captures all output', () async { final Completer<void> exitCompleter = Completer<void>(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>['debug', 'arg1', 'arg2'], exitCode: -1, stderr: 'Oops\n', completer: exitCompleter, outputFollowsExit: true, ), ]); final FakeDesktopDevice device = setUpDesktopDevice( processManager: processManager, ); unawaited(Future<void>(() { exitCompleter.complete(); })); // Start looking for 'Oops' in the stream before starting the app. expect(device.getLogReader().logLines, emits('Oops')); final FakeApplicationPackage package = FakeApplicationPackage(); await device.startApp( package, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled( BuildInfo.debug, dartEntrypointArgs: <String>['arg1', 'arg2'], ), ); }); } FakeDesktopDevice setUpDesktopDevice({ FileSystem? fileSystem, Logger? logger, ProcessManager? processManager, OperatingSystemUtils? operatingSystemUtils, bool nullExecutablePathForDevice = false, }) { return FakeDesktopDevice( fileSystem: fileSystem ?? MemoryFileSystem.test(), logger: logger ?? BufferLogger.test(), processManager: processManager ?? FakeProcessManager.any(), operatingSystemUtils: operatingSystemUtils ?? FakeOperatingSystemUtils(), nullExecutablePathForDevice: nullExecutablePathForDevice, ); } /// A trivial subclass of DesktopDevice for testing the shared functionality. class FakeDesktopDevice extends DesktopDevice { FakeDesktopDevice({ required ProcessManager processManager, required Logger logger, required FileSystem fileSystem, required OperatingSystemUtils operatingSystemUtils, this.nullExecutablePathForDevice = false, }) : super( 'dummy', platformType: PlatformType.linux, ephemeral: false, processManager: processManager, logger: logger, fileSystem: fileSystem, operatingSystemUtils: operatingSystemUtils, ); /// The [mainPath] last passed to [buildForDevice]. String? lastBuiltMainPath; /// The [buildInfo] last passed to [buildForDevice]. BuildInfo? lastBuildInfo; final bool nullExecutablePathForDevice; @override String get name => 'dummy'; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester; @override bool isSupported() => true; @override bool isSupportedForProject(FlutterProject flutterProject) => true; @override Future<void> buildForDevice({ String? mainPath, BuildInfo? buildInfo, }) async { lastBuiltMainPath = mainPath; lastBuildInfo = buildInfo; } // Dummy implementation that just returns the build mode name. @override String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo) { if (nullExecutablePathForDevice) { return null; } return getNameForBuildMode(buildInfo.mode); } } class FakeApplicationPackage extends Fake implements ApplicationPackage { } class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils { @override String get name => 'Example'; }