// 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. // @dart = 2.8 import 'dart:async'; import 'package:file/file.dart'; 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/common.dart'; import 'package:flutter_tools/src/base/context.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/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:mockito/mockito.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; import '../../src/mocks.dart'; import '../../src/testbed.dart'; void main() { group('run', () { MockDeviceManager mockDeviceManager; FileSystem fileSystem; setUpAll(() { Cache.disableLocking(); }); setUp(() { mockDeviceManager = MockDeviceManager(); fileSystem = MemoryFileSystem.test(); }); testUsingContext('fails when target not found', () async { final RunCommand command = RunCommand(); try { await createTestCommandRunner(command).run(['run', '-t', 'abc123', '--no-pub']); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 1); } }, overrides: { 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(); try { await createTestCommandRunner(command).run([ 'run', '--use-application-binary=app/bar/faz', '--fast-start', '--no-pub', '--show-test-device', ]); fail('Expect exception'); } on Exception catch (e) { expect(e.toString(), isNot(contains('--fast-start is not supported with --use-application-binary'))); } }, overrides: { 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(); try { await createTestCommandRunner(command).run([ 'run', '--no-pub', ]); fail('Expect exception'); } on Exception catch (e) { expect(e, isA()); } final BufferLogger bufferLogger = globals.logger as BufferLogger; expect( bufferLogger.statusText, containsIgnoringWhitespace('Changing current working directory to:'), ); }, overrides: { 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(); try { await createTestCommandRunner(command).run([ 'run', '--no-pub', ]); fail('Expect exception'); } on Exception catch (e) { expect(e, isA()); expect(e.toString(), contains('No pubspec.yaml file found')); } }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); group('run app', () { MemoryFileSystem fs; Artifacts artifacts; MockCache mockCache; MockProcessManager mockProcessManager; TestUsage usage; Directory tempDir; setUp(() { artifacts = Artifacts.test(); mockCache = MockCache(); usage = TestUsage(); fs = MemoryFileSystem.test(); mockProcessManager = MockProcessManager(); tempDir = fs.systemTempDirectory.createTempSync('flutter_run_test.'); fs.currentDirectory = tempDir; tempDir.childFile('pubspec.yaml') .writeAsStringSync('name: flutter_app'); tempDir.childFile('.packages') .writeAsStringSync('# Generated by pub on 2019-11-25 12:38:01.801784.'); final Directory libDir = tempDir.childDirectory('lib'); libDir.createSync(); final File mainFile = libDir.childFile('main.dart'); mainFile.writeAsStringSync('void main() {}'); when(mockDeviceManager.hasSpecifiedDeviceId).thenReturn(false); when(mockDeviceManager.hasSpecifiedAllDevices).thenReturn(false); }); testUsingContext('exits with a user message when no supported devices attached', () async { final RunCommand command = RunCommand(); const List noDevices = []; when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future>.value(noDevices) ); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value(noDevices) ); try { await createTestCommandRunner(command).run([ 'run', '--no-pub', '--no-hot', ]); fail('Expect exception'); } on ToolExit catch (e) { expect(e.message, null); } expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices), ); }, overrides: { DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, }); testUsingContext('fails when targeted device is not Android with --device-user', () async { globals.fs.file('pubspec.yaml').createSync(); globals.fs.file('.packages').writeAsStringSync('\n'); globals.fs.file('lib/main.dart').createSync(recursive: true); final FakeDevice device = FakeDevice(isLocalEmulator: true); when(mockDeviceManager.getAllConnectedDevices()).thenAnswer((Invocation invocation) async { return [device]; }); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) async { return [device]; }); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer((Invocation invocation) async { return [device]; }); when(mockDeviceManager.hasSpecifiedAllDevices).thenReturn(false); when(mockDeviceManager.deviceDiscoverers).thenReturn([]); final RunCommand command = RunCommand(); await expectLater(createTestCommandRunner(command).run([ 'run', '--no-pub', '--device-user', '10', ]), throwsToolExit(message: '--device-user is only supported for Android. At least one Android device is required.')); }, overrides: { FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => mockDeviceManager, Stdio: () => FakeStdio(), }); testUsingContext('shows unsupported devices when no supported devices are found', () async { final RunCommand command = RunCommand(); final MockDevice mockDevice = MockDevice(TargetPlatform.android_arm); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future.value(true)); when(mockDevice.isSupported()).thenAnswer((Invocation invocation) => true); when(mockDevice.supportsFastStart).thenReturn(true); when(mockDevice.id).thenReturn('mock-id'); when(mockDevice.name).thenReturn('mock-name'); when(mockDevice.platformType).thenReturn(PlatformType.android); when(mockDevice.targetPlatformDisplayName) .thenAnswer((Invocation invocation) async => 'mock-platform'); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future.value('api-14')); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([ mockDevice, ]); }); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([]), ); try { await createTestCommandRunner(command).run([ 'run', '--no-pub', '--no-hot', ]); fail('Expect exception'); } on ToolExit catch (e) { expect(e.message, null); } expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices), ); expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterFoundButUnsupportedDevices), ); expect( testLogger.statusText, containsIgnoringWhitespace( userMessages.flutterMissPlatformProjects( Device.devicesPlatformTypes([mockDevice]), ), ), ); }, overrides: { DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, }); testUsingContext('updates cache before checking for devices', () async { final RunCommand command = RunCommand(); // Called as part of requiredArtifacts() when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future>.value([]) ); // No devices are attached, we just want to verify update the cache // BEFORE checking for devices const Duration timeout = Duration(seconds: 10); when(mockDeviceManager.findTargetDevices(any, timeout: timeout)).thenAnswer( (Invocation invocation) => Future>.value([]) ); try { await createTestCommandRunner(command).run([ 'run', '--no-pub', '--device-timeout', '10', ]); fail('Exception expected'); } on ToolExit catch (e) { // We expect a ToolExit because no devices are attached expect(e.message, null); } on Exception catch (e) { fail('ToolExit expected, got $e'); } verifyInOrder([ // cache update mockCache.updateAll({DevelopmentArtifact.universal}), // as part of gathering `requiredArtifacts` mockDeviceManager.getDevices(), // in validateCommand() mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout')), ]); }, overrides: { Cache: () => mockCache, DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, }); testUsingContext('passes device target platform to usage', () async { final RunCommand command = RunCommand(); final MockDevice mockDevice = MockDevice(TargetPlatform.ios); when(mockDevice.supportsRuntimeMode(any)).thenAnswer((Invocation invocation) => true); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future.value(false)); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(FakeDeviceLogReader()); when(mockDevice.supportsFastStart).thenReturn(true); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future.value('iOS 13')); // App fails to start because we're only interested in usage when(mockDevice.startApp( any, mainPath: anyNamed('mainPath'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), route: anyNamed('route'), prebuiltApplication: anyNamed('prebuiltApplication'), ipv6: anyNamed('ipv6'), userIdentifier: anyNamed('userIdentifier'), )).thenAnswer((Invocation invocation) => Future.value(LaunchResult.failed())); when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future>.value([mockDevice]) ); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([mockDevice]) ); final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_run_test.'); tempDir.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true); tempDir.childFile('.packages').createSync(); tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true); tempDir.childFile('pubspec.yaml') ..createSync() ..writeAsStringSync('# Hello, World'); globals.fs.currentDirectory = tempDir; await expectToolExitLater(createTestCommandRunner(command).run([ 'run', '--no-pub', '--no-hot', ]), isNull); expect(usage.commands, contains( const TestUsageCommand('run', parameters: { 'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13', 'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'false', } ))); }, overrides: { Artifacts: () => artifacts, Cache: () => mockCache, DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, Usage: () => usage, }); testUsingContext('No web renderer options are added to non web device', () async { final FakeApplicationPackageFactory applicationPackageFactory = ApplicationPackageFactory.instance as FakeApplicationPackageFactory; final RunCommand command = RunCommand(); final MockDevice mockDevice = MockDevice(TargetPlatform.ios); when(mockDevice.supportsRuntimeMode(any)).thenAnswer((Invocation invocation) => true); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future.value(false)); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(FakeDeviceLogReader()); when(mockDevice.supportsFastStart).thenReturn(true); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future.value('iOS 13')); applicationPackageFactory.package = PrebuiltIOSApp(projectBundleId: 'test'); DebuggingOptions debuggingOptions; when(mockDevice.startApp( any, mainPath: anyNamed('mainPath'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), route: anyNamed('route'), prebuiltApplication: anyNamed('prebuiltApplication'), ipv6: anyNamed('ipv6'), userIdentifier: anyNamed('userIdentifier'), )).thenAnswer((Invocation invocation) { debuggingOptions = invocation.namedArguments[#debuggingOptions] as DebuggingOptions; return Future.value(LaunchResult.failed()); }); when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future>.value([mockDevice]) ); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future>.value([mockDevice]) ); final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_run_test.'); tempDir.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true); tempDir.childFile('.dart_tool/package_config') ..createSync(recursive: true) ..writeAsStringSync(json.encode({'configVersion': 2, 'packages': []})); tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true); tempDir.childFile('pubspec.yaml').writeAsStringSync('name: test'); globals.fs.currentDirectory = tempDir; await expectToolExitLater(createTestCommandRunner(command).run([ 'run', '--no-pub', '--no-hot', ]), isNull); // No web renderer options are added. expect(debuggingOptions.buildInfo.dartDefines, isEmpty); }, overrides: { Artifacts: () => artifacts, Cache: () => mockCache, DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, ApplicationPackageFactory: () => FakeApplicationPackageFactory(), }); }); testUsingContext('should only request artifacts corresponding to connected devices', () async { when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([ MockDevice(TargetPlatform.android_arm), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals({ DevelopmentArtifact.universal, DevelopmentArtifact.androidGenSnapshot, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([ MockDevice(TargetPlatform.ios), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals({ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([ MockDevice(TargetPlatform.ios), MockDevice(TargetPlatform.android_arm), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals({ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, DevelopmentArtifact.androidGenSnapshot, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future>.value([ MockDevice(TargetPlatform.web_javascript), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals({ DevelopmentArtifact.universal, DevelopmentArtifact.web, })); }, overrides: { DeviceManager: () => mockDeviceManager, }); }); group('dart-defines and web-renderer options', () { List dartDefines; setUp(() { dartDefines = []; }); test('auto web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'auto'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); }); test('canvaskit web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'canvaskit'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); }); test('html web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'html'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); }); test('auto web-renderer with existing dart-defines', () { dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'auto'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=true']); }); test('canvaskit web-renderer with no dart-defines', () { dartDefines = ['FLUTTER_WEB_USE_SKIA=false']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'canvaskit'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']); }); test('html web-renderer with no dart-defines', () { dartDefines = ['FLUTTER_WEB_USE_SKIA=true']; dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'html'); expect(dartDefines, ['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); }); }); } class MockCache extends Mock implements Cache {} class MockDeviceManager extends Mock implements DeviceManager {} class MockDevice extends Mock implements Device { MockDevice(this._targetPlatform); final TargetPlatform _targetPlatform; @override Future get targetPlatform async => Future.value(_targetPlatform); } class TestRunCommand extends RunCommand { @override // ignore: must_call_super Future validateCommand() async { devices = await globals.deviceManager.getDevices(); } } class FakeDevice extends Fake implements Device { FakeDevice({bool isLocalEmulator = false}) : _isLocalEmulator = isLocalEmulator; static const int kSuccess = 1; static const int kFailure = -1; final TargetPlatform _targetPlatform = TargetPlatform.ios; final bool _isLocalEmulator; @override String get id => 'fake_device'; void _throwToolExit(int code) => throwToolExit(null, exitCode: code); @override Future get isLocalEmulator => Future.value(_isLocalEmulator); @override bool supportsRuntimeMode(BuildMode mode) => true; @override bool get supportsHotReload => false; @override bool get supportsFastStart => false; @override Future get sdkNameAndVersion => Future.value(''); @override DeviceLogReader getLogReader({ ApplicationPackage app, bool includePastLogs = false, }) { return FakeDeviceLogReader(); } @override String get name => 'FakeDevice'; @override Future get targetPlatform async => _targetPlatform; @override final PlatformType platformType = PlatformType.ios; @override Future startApp( ApplicationPackage package, { String mainPath, String route, DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication = false, bool usesTerminalUi = true, bool ipv6 = false, String userIdentifier, }) async { 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); } return null; } } class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory { ApplicationPackage package; @override Future getPackageForPlatform( TargetPlatform platform, { BuildInfo buildInfo, File applicationBinary, }) async { return package; } }