// 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/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.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/terminal.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/daemon.dart'; import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/web/compile.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_devices.dart'; import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; void main() { setUpAll(() { Cache.disableLocking(); }); group('run', () { late TestDeviceManager testDeviceManager; late FileSystem fileSystem; setUp(() { testDeviceManager = TestDeviceManager(logger: BufferLogger.test()); fileSystem = MemoryFileSystem.test(); }); testUsingContext('fails when target not found', () async { final RunCommand command = RunCommand(); expect( () => createTestCommandRunner(command).run(<String>['run', '-t', 'abc123', '--no-pub']), throwsA(isA<ToolExit>().having((ToolExit error) => error.exitCode, 'exitCode', anyOf(isNull, 1))), ); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); testUsingContext('does not support "--use-application-binary" and "--fast-start"', () async { fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.packages').createSync(); final RunCommand command = RunCommand(); await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--use-application-binary=app/bar/faz', '--fast-start', '--no-pub', '--show-test-device', ]), throwsA(isException.having( (Exception exception) => exception.toString(), 'toString', isNot(contains('--fast-start is not supported with --use-application-binary')), )), ); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); testUsingContext('Walks upward looking for a pubspec.yaml and succeeds if found', () async { fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.packages') .writeAsStringSync('\n'); fileSystem.file('lib/main.dart') .createSync(recursive: true); fileSystem.currentDirectory = fileSystem.directory('a/b/c') ..createSync(recursive: true); final RunCommand command = RunCommand(); await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]), throwsToolExit(), ); final BufferLogger bufferLogger = globals.logger as BufferLogger; expect( bufferLogger.statusText, containsIgnoringWhitespace('Changing current working directory to:'), ); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); testUsingContext('Walks upward looking for a pubspec.yaml and exits if missing', () async { fileSystem.currentDirectory = fileSystem.directory('a/b/c') ..createSync(recursive: true); fileSystem.file('lib/main.dart') .createSync(recursive: true); final RunCommand command = RunCommand(); await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]), throwsToolExit(message: 'No pubspec.yaml file found'), ); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); group('run app', () { late MemoryFileSystem fs; late Artifacts artifacts; late TestUsage usage; late FakeAnsiTerminal fakeTerminal; setUpAll(() { Cache.disableLocking(); }); setUp(() { fakeTerminal = FakeAnsiTerminal(); artifacts = Artifacts.test(); usage = TestUsage(); fs = MemoryFileSystem.test(); fs.currentDirectory.childFile('pubspec.yaml') .writeAsStringSync('name: flutter_app'); fs.currentDirectory.childFile('.packages') .writeAsStringSync('# Generated by pub on 2019-11-25 12:38:01.801784.'); final Directory libDir = fs.currentDirectory.childDirectory('lib'); libDir.createSync(); final File mainFile = libDir.childFile('main.dart'); mainFile.writeAsStringSync('void main() {}'); }); testUsingContext('exits with a user message when no supported devices attached', () async { final RunCommand command = RunCommand(); testDeviceManager.devices = <Device>[]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]), throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)), ); expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices), ); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('exits and lists available devices when specified device not found', () async { final RunCommand command = RunCommand(); final FakeDevice device = FakeDevice(isLocalEmulator: true); testDeviceManager ..devices = <Device>[device] ..specifiedDeviceId = 'invalid-device-id'; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '-d', 'invalid-device-id', '--no-pub', '--no-hot', ]), throwsToolExit(), ); expect(testLogger.statusText, contains("No supported devices found with name or id matching 'invalid-device-id'")); expect(testLogger.statusText, contains('The following devices were found:')); expect(testLogger.statusText, contains('FakeDevice (mobile) • fake_device • ios • (simulator)')); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('fails when targeted device is not Android with --device-user', () async { final FakeDevice device = FakeDevice(isLocalEmulator: true); testDeviceManager.devices = <Device>[device]; final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates(); await expectLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--device-user', '10', ]), throwsToolExit(message: '--device-user is only supported for Android. At least one Android device is required.')); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => testDeviceManager, Stdio: () => FakeStdio(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('succeeds when targeted device is an Android device with --device-user', () async { final FakeDevice device = FakeDevice(isLocalEmulator: true, platformType: PlatformType.android); testDeviceManager.devices = <Device>[device]; final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates(); await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--device-user', '10', ]); // Finishes normally without error. }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => testDeviceManager, Stdio: () => FakeStdio(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('fails when v1 FlutterApplication is detected', () async { fs.file('pubspec.yaml').createSync(); fs.file('android/AndroidManifest.xml') ..createSync(recursive: true) ..writeAsStringSync(''' <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.v1"> <application android:name="io.flutter.app.FlutterApplication"> </application> </manifest> ''', flush: true); fs.file('.packages').writeAsStringSync('\n'); fs.file('lib/main.dart').createSync(recursive: true); final AndroidDevice device = AndroidDevice('1234', modelID: 'TestModel', logger: testLogger, platform: FakePlatform(), androidSdk: FakeAndroidSdk(), fileSystem: fs, processManager: FakeProcessManager.any(), ); testDeviceManager.devices = <Device>[device]; final RunCommand command = RunCommand(); await expectLater(createTestCommandRunner(command).run(<String>[ 'run', '--pub', ]), throwsToolExit(message: 'Build failed due to use of deprecated Android v1 embedding.')); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => testDeviceManager, Stdio: () => FakeStdio(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('fails when v1 metadata is detected', () async { fs.file('pubspec.yaml').createSync(); fs.file('android/AndroidManifest.xml') ..createSync(recursive: true) ..writeAsStringSync(''' <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.v1"> <application > <meta-data android:name="flutterEmbedding" android:value="1" /> </application> </manifest> ''', flush: true); fs.file('.packages').writeAsStringSync('\n'); fs.file('lib/main.dart').createSync(recursive: true); final AndroidDevice device = AndroidDevice('1234', modelID: 'TestModel', logger: testLogger, platform: FakePlatform(), androidSdk: FakeAndroidSdk(), fileSystem: fs, processManager: FakeProcessManager.any(), ); testDeviceManager.devices = <Device>[device]; final RunCommand command = RunCommand(); await expectLater(createTestCommandRunner(command).run(<String>[ 'run', '--pub', ]), throwsToolExit(message: 'Build failed due to use of deprecated Android v1 embedding.')); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => testDeviceManager, Stdio: () => FakeStdio(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('shows unsupported devices when no supported devices are found', () async { final RunCommand command = RunCommand(); final FakeDevice mockDevice = FakeDevice( targetPlatform: TargetPlatform.android_arm, isLocalEmulator: true, sdkNameAndVersion: 'api-14', isSupported: false, ); testDeviceManager.devices = <Device>[mockDevice]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]), throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)), ); expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices), ); expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterFoundButUnsupportedDevices), ); expect( testLogger.statusText, containsIgnoringWhitespace( userMessages.flutterMissPlatformProjects( Device.devicesPlatformTypes(<Device>[mockDevice]), ), ), ); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), }); testUsingContext('forwards --uninstall-only to DebuggingOptions', () async { final RunCommand command = RunCommand(); final FakeDevice mockDevice = FakeDevice( sdkNameAndVersion: 'iOS 13', )..startAppSuccess = false; testDeviceManager.devices = <Device>[mockDevice]; // Causes swift to be detected in the analytics. fs.currentDirectory.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true); await expectToolExitLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', '--uninstall-first', ]), isNull); final DebuggingOptions options = await command.createDebuggingOptions(false); expect(options.uninstallFirst, isTrue); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Usage: () => usage, }); testUsingContext('passes device target platform to usage', () async { final RunCommand command = RunCommand(); final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13') ..startAppSuccess = false; testDeviceManager.devices = <Device>[mockDevice]; // Causes swift to be detected in the analytics. fs.currentDirectory.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true); await expectToolExitLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]), isNull); expect(usage.commands, contains( TestUsageCommand('run', parameters: CustomDimensions.fromMap(<String, String>{ 'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13', 'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'true', 'cd56': 'false', 'cd57': 'usb', }) ))); }, overrides: <Type, Generator>{ AnsiTerminal: () => fakeTerminal, Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Stdio: () => FakeStdio(), Usage: () => usage, }); group('--machine', () { testUsingContext('enables multidex by default', () async { final DaemonCapturingRunCommand command = DaemonCapturingRunCommand(); final FakeDevice device = FakeDevice(); testDeviceManager.devices = <Device>[device]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--machine', '-d', device.id, ]), throwsToolExit(), ); expect(command.appDomain.multidexEnabled, isTrue); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Usage: () => usage, Stdio: () => FakeStdio(), Logger: () => AppRunLogger(parent: BufferLogger.test()), }); testUsingContext('can disable multidex with --no-multidex', () async { final DaemonCapturingRunCommand command = DaemonCapturingRunCommand(); final FakeDevice device = FakeDevice(); testDeviceManager.devices = <Device>[device]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-multidex', '--machine', '-d', device.id, ]), throwsToolExit(), ); expect(command.appDomain.multidexEnabled, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Usage: () => usage, Stdio: () => FakeStdio(), Logger: () => AppRunLogger(parent: BufferLogger.test()), }); testUsingContext('can pass --device-user', () async { final DaemonCapturingRunCommand command = DaemonCapturingRunCommand(); final FakeDevice device = FakeDevice(platformType: PlatformType.android); testDeviceManager.devices = <Device>[device]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--machine', '--device-user', '10', '-d', device.id, ]), throwsToolExit(), ); expect(command.appDomain.userIdentifier, '10'); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Usage: () => usage, Stdio: () => FakeStdio(), Logger: () => AppRunLogger(parent: BufferLogger.test()), }); testUsingContext('can disable devtools with --no-devtools', () async { final DaemonCapturingRunCommand command = DaemonCapturingRunCommand(); final FakeDevice device = FakeDevice(); testDeviceManager.devices = <Device>[device]; await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-devtools', '--machine', '-d', device.id, ]), throwsToolExit(), ); expect(command.appDomain.enableDevTools, isFalse); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => Cache.test(processManager: FakeProcessManager.any()), DeviceManager: () => testDeviceManager, FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Usage: () => usage, Stdio: () => FakeStdio(), Logger: () => AppRunLogger(parent: BufferLogger.test()), }); }); }); group('Fatal Logs', () { late TestRunCommandWithFakeResidentRunner command; late MemoryFileSystem fs; setUp(() { command = TestRunCommandWithFakeResidentRunner() ..fakeResidentRunner = FakeResidentRunner(); fs = MemoryFileSystem.test(); }); testUsingContext("doesn't fail if --fatal-warnings specified and no warnings occur", () async { try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}', ]); } on Exception { fail('Unexpected exception thrown'); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext("doesn't fail if --fatal-warnings not specified", () async { testLogger.printWarning('Warning: Mild annoyance Will Robinson!'); try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]); } on Exception { fail('Unexpected exception thrown'); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('fails if --fatal-warnings specified and warnings emitted', () async { testLogger.printWarning('Warning: Mild annoyance Will Robinson!'); await expectLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}', ]), throwsToolExit(message: 'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.')); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('fails if --fatal-warnings specified and errors emitted', () async { testLogger.printError('Error: Danger Will Robinson!'); await expectLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', '--${FlutterOptions.kFatalWarnings}', ]), throwsToolExit(message: 'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.')); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); }); testUsingContext('should only request artifacts corresponding to connected devices', () async { testDeviceManager.devices = <Device>[FakeDevice(targetPlatform: TargetPlatform.android_arm)]; expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.androidGenSnapshot, })); testDeviceManager.devices = <Device>[FakeDevice()]; expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, })); testDeviceManager.devices = <Device>[ FakeDevice(), FakeDevice(targetPlatform: TargetPlatform.android_arm), ]; expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, DevelopmentArtifact.androidGenSnapshot, })); testDeviceManager.devices = <Device>[ FakeDevice(targetPlatform: TargetPlatform.web_javascript), ]; expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.web, })); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); group('usageValues', () { testUsingContext('with only non-iOS usb device', () async { final List<Device> devices = <Device>[ FakeDevice(targetPlatform: TargetPlatform.android_arm, platformType: PlatformType.android), ]; final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices); final CustomDimensions dimensions = await command.usageValues; expect(dimensions, const CustomDimensions( commandRunIsEmulator: false, commandRunTargetName: 'android-arm', commandRunTargetOsVersion: '', commandRunModeName: 'debug', commandRunProjectModule: false, commandRunProjectHostLanguage: '', commandRunEnableImpeller: false, )); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('with only iOS usb device', () async { final List<Device> devices = <Device>[ FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.usb, sdkNameAndVersion: 'iOS 16.2'), ]; final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices); final CustomDimensions dimensions = await command.usageValues; expect(dimensions, const CustomDimensions( commandRunIsEmulator: false, commandRunTargetName: 'ios', commandRunTargetOsVersion: 'iOS 16.2', commandRunModeName: 'debug', commandRunProjectModule: false, commandRunProjectHostLanguage: '', commandRunEnableImpeller: false, commandRunIOSInterfaceType: 'usb', )); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('with only iOS network device', () async { final List<Device> devices = <Device>[ FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.network, sdkNameAndVersion: 'iOS 16.2'), ]; final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices); final CustomDimensions dimensions = await command.usageValues; expect(dimensions, const CustomDimensions( commandRunIsEmulator: false, commandRunTargetName: 'ios', commandRunTargetOsVersion: 'iOS 16.2', commandRunModeName: 'debug', commandRunProjectModule: false, commandRunProjectHostLanguage: '', commandRunEnableImpeller: false, commandRunIOSInterfaceType: 'wireless', )); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('with both iOS usb and network devices', () async { final List<Device> devices = <Device>[ FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.network, sdkNameAndVersion: 'iOS 16.2'), FakeIOSDevice(interfaceType: IOSDeviceConnectionInterface.usb, sdkNameAndVersion: 'iOS 16.2'), ]; final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices); final CustomDimensions dimensions = await command.usageValues; expect(dimensions, const CustomDimensions( commandRunIsEmulator: false, commandRunTargetName: 'multiple', commandRunTargetOsVersion: 'multiple', commandRunModeName: 'debug', commandRunProjectModule: false, commandRunProjectHostLanguage: '', commandRunEnableImpeller: false, commandRunIOSInterfaceType: 'wireless', )); }, overrides: <Type, Generator>{ DeviceManager: () => testDeviceManager, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); }); }); group('dart-defines and web-renderer options', () { late List<String> dartDefines; setUp(() { dartDefines = <String>[]; }); test('auto web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.autoDetect); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=true']); }); test('canvaskit web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); }); test('html web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); }); test('auto web-renderer with existing dart-defines', () { dartDefines = <String>['FLUTTER_WEB_USE_SKIA=false']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.autoDetect); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=true']); }); test('canvaskit web-renderer with no dart-defines', () { dartDefines = <String>['FLUTTER_WEB_USE_SKIA=false']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); }); test('html web-renderer with no dart-defines', () { dartDefines = <String>['FLUTTER_WEB_USE_SKIA=true']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); }); }); group('terminal', () { late FakeAnsiTerminal fakeTerminal; setUp(() { fakeTerminal = FakeAnsiTerminal(); }); testUsingContext('Flutter run sets terminal singleCharMode to false on exit', () async { final FakeResidentRunner residentRunner = FakeResidentRunner(); final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner(); command.fakeResidentRunner = residentRunner; await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]); // The sync completer where we initially set `terminal.singleCharMode` to // `true` does not execute in unit tests, so explicitly check the // `setSingleCharModeHistory` that the finally block ran, setting this // back to `false`. expect(fakeTerminal.setSingleCharModeHistory, contains(false)); }, overrides: <Type, Generator>{ AnsiTerminal: () => fakeTerminal, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Flutter run catches StdinException while setting terminal singleCharMode to false', () async { fakeTerminal.hasStdin = false; final FakeResidentRunner residentRunner = FakeResidentRunner(); final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner(); command.fakeResidentRunner = residentRunner; try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]); } catch (err) { // ignore: avoid_catches_without_on_clauses fail('Expected no error, got $err'); } expect(fakeTerminal.setSingleCharModeHistory, isEmpty); }, overrides: <Type, Generator>{ AnsiTerminal: () => fakeTerminal, Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); }); testUsingContext('Flutter run catches service has disappear errors and throws a tool exit', () async { final FakeResidentRunner residentRunner = FakeResidentRunner(); residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, ''); final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner(); command.fakeResidentRunner = residentRunner; await expectToolExitLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]), contains('Lost connection to device.')); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Flutter run does not catch other RPC errors', () async { final FakeResidentRunner residentRunner = FakeResidentRunner(); residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, ''); final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner(); command.fakeResidentRunner = residentRunner; await expectLater(() => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]), throwsA(isA<RPCError>())); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Passes sksl bundle info the build options', () async { final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner(); await expectLater(() => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--bundle-sksl-path=foo.json', ]), throwsToolExit(message: 'No SkSL shader bundle found at foo.json')); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Configures web connection options to use web sockets by default', () async { final RunCommand command = RunCommand(); await expectLater(() => createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(true); expect(options.webUseSseForDebugBackend, false); expect(options.webUseSseForDebugProxy, false); expect(options.webUseSseForInjectedClient, false); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('flags propagate to debugging options', () async { final RunCommand command = RunCommand(); await expectLater(() => createTestCommandRunner(command).run(<String>[ 'run', '--start-paused', '--disable-service-auth-codes', '--use-test-fonts', '--trace-skia', '--trace-systrace', '--verbose-system-logs', '--null-assertions', '--native-null-assertions', '--enable-impeller', '--trace-systrace', '--enable-software-rendering', '--skia-deterministic-rendering', '--enable-embedder-api', ]), throwsToolExit()); final DebuggingOptions options = await command.createDebuggingOptions(false); expect(options.startPaused, true); expect(options.disableServiceAuthCodes, true); expect(options.useTestFonts, true); expect(options.traceSkia, true); expect(options.traceSystrace, true); expect(options.verboseSystemLogs, true); expect(options.nullAssertions, true); expect(options.nativeNullAssertions, true); expect(options.traceSystrace, true); expect(options.enableImpeller, true); expect(options.enableSoftwareRendering, true); expect(options.skiaDeterministicRendering, true); }, overrides: <Type, Generator>{ Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('fails when "--web-launch-url" is not supported', () async { final RunCommand command = RunCommand(); await expectLater( () => createTestCommandRunner(command).run(<String>[ 'run', '--web-launch-url=http://flutter.dev', ]), throwsA(isException.having( (Exception exception) => exception.toString(), 'toString', isNot(contains('web-launch-url')), )), ); final DebuggingOptions options = await command.createDebuggingOptions(true); expect(options.webLaunchUrl, 'http://flutter.dev'); final RegExp pattern = RegExp(r'^((http)?:\/\/)[^\s]+'); expect(pattern.hasMatch(options.webLaunchUrl!), true); }, overrides: <Type, Generator>{ ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); } class TestDeviceManager extends DeviceManager { TestDeviceManager({required this.logger}) : super(logger: logger); List<Device> devices = <Device>[]; final Logger logger; @override List<DeviceDiscovery> get deviceDiscoverers { final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); devices.forEach(discoverer.addDevice); return <DeviceDiscovery>[discoverer]; } } class FakeAndroidSdk extends Fake implements AndroidSdk { @override String get adbPath => 'adb'; } // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeDevice extends Fake implements Device { FakeDevice({ bool isLocalEmulator = false, TargetPlatform targetPlatform = TargetPlatform.ios, String sdkNameAndVersion = '', PlatformType platformType = PlatformType.ios, bool isSupported = true, }): _isLocalEmulator = isLocalEmulator, _targetPlatform = targetPlatform, _sdkNameAndVersion = sdkNameAndVersion, _platformType = platformType, _isSupported = isSupported; static const int kSuccess = 1; static const int kFailure = -1; final TargetPlatform _targetPlatform; final bool _isLocalEmulator; final String _sdkNameAndVersion; final PlatformType _platformType; final bool _isSupported; @override Category get category => Category.mobile; @override String get id => 'fake_device'; Never _throwToolExit(int code) => throwToolExit('FakeDevice tool exit', exitCode: code); @override Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator); @override bool supportsRuntimeMode(BuildMode mode) => true; @override Future<bool> get supportsHardwareRendering async => true; @override bool supportsHotReload = false; @override bool get supportsHotRestart => true; @override bool get supportsFastStart => false; @override bool get isConnected => true; bool supported = true; @override bool isSupportedForProject(FlutterProject flutterProject) => _isSupported; @override bool isSupported() => supported; @override Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion); @override Future<String> get targetPlatformDisplayName async => getNameForTargetPlatform(await targetPlatform); @override DeviceLogReader getLogReader({ ApplicationPackage? app, bool includePastLogs = false, }) { return FakeDeviceLogReader(); } @override String get name => 'FakeDevice'; @override Future<TargetPlatform> get targetPlatform async => _targetPlatform; @override PlatformType get platformType => _platformType; late bool startAppSuccess; @override DevFSWriter? createDevFSWriter( ApplicationPackage? app, String? userIdentifier, ) { return null; } @override Future<LaunchResult> startApp( ApplicationPackage? package, { String? mainPath, String? route, required DebuggingOptions debuggingOptions, Map<String, Object?> platformArgs = const <String, Object?>{}, bool prebuiltApplication = false, bool usesTerminalUi = true, bool ipv6 = false, String? userIdentifier, }) async { if (startAppSuccess == false) { return LaunchResult.failed(); } if (startAppSuccess == true) { return LaunchResult.succeeded(); } final String dartFlags = debuggingOptions.dartFlags; // In release mode, --dart-flags should be set to the empty string and // provided flags should be dropped. In debug and profile modes, // --dart-flags should not be empty. if (debuggingOptions.buildInfo.isRelease) { if (dartFlags.isNotEmpty) { _throwToolExit(kFailure); } _throwToolExit(kSuccess); } else { if (dartFlags.isEmpty) { _throwToolExit(kFailure); } _throwToolExit(kSuccess); } } } // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeIOSDevice extends Fake implements IOSDevice { FakeIOSDevice({ this.interfaceType = IOSDeviceConnectionInterface.none, bool isLocalEmulator = false, String sdkNameAndVersion = '', }): _isLocalEmulator = isLocalEmulator, _sdkNameAndVersion = sdkNameAndVersion; final bool _isLocalEmulator; final String _sdkNameAndVersion; @override Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator); @override Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion); @override final IOSDeviceConnectionInterface interfaceType; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios; } class TestRunCommandForUsageValues extends RunCommand { TestRunCommandForUsageValues({ this.devices, }); @override // devices is not set within usageValues, so we override the field // ignore: overridden_fields List<Device>? devices; @override Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async { return const BuildInfo(BuildMode.debug, null, treeShakeIcons: false); } } class TestRunCommandWithFakeResidentRunner extends RunCommand { late FakeResidentRunner fakeResidentRunner; @override Future<ResidentRunner> createRunner({ required bool hotMode, required List<FlutterDevice> flutterDevices, required String? applicationBinaryPath, required FlutterProject flutterProject, }) async { return fakeResidentRunner; } @override // ignore: must_call_super Future<void> validateCommand() async { devices = <Device>[FakeDevice()..supportsHotReload = true]; } } class TestRunCommandThatOnlyValidates extends RunCommand { @override Future<FlutterCommandResult> runCommand() async { return FlutterCommandResult.success(); } } class FakeResidentRunner extends Fake implements ResidentRunner { RPCError? rpcError; @override Future<int> run({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool enableDevTools = false, String? route, }) async { await null; if (rpcError != null) { throw rpcError!; } return 0; } } class DaemonCapturingRunCommand extends RunCommand { late Daemon daemon; late CapturingAppDomain appDomain; @override Daemon createMachineDaemon() { daemon = super.createMachineDaemon(); appDomain = daemon.appDomain = CapturingAppDomain(daemon); daemon.registerDomain(appDomain); return daemon; } } class CapturingAppDomain extends AppDomain { CapturingAppDomain(super.daemon); bool? multidexEnabled; String? userIdentifier; bool? enableDevTools; @override Future<AppInstance> startApp( Device device, String projectDirectory, String target, String? route, DebuggingOptions options, bool enableHotReload, { File? applicationBinary, required bool trackWidgetCreation, String? projectRootPath, String? packagesFilePath, String? dillOutputPath, bool ipv6 = false, bool multidexEnabled = false, String? isolateFilter, bool machine = true, String? userIdentifier, bool enableDevTools = true, }) async { this.multidexEnabled = multidexEnabled; this.userIdentifier = userIdentifier; this.enableDevTools = enableDevTools; throwToolExit(''); } } class FakeAnsiTerminal extends Fake implements AnsiTerminal { /// Setting to false will cause operations to Stdin to throw a [StdinException]. bool hasStdin = true; @override bool usesTerminalUi = false; /// A list of all the calls to the [singleCharMode] setter. List<bool> setSingleCharModeHistory = <bool>[]; @override set singleCharMode(bool value) { if (!hasStdin) { throw const StdinException('Error setting terminal line mode', OSError('The handle is invalid', 6)); } setSingleCharModeHistory.add(value); } @override bool get singleCharMode => setSingleCharModeHistory.last; }