// 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/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/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(<String>['run', '-t', 'abc123', '--no-pub']); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 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(); try { await createTestCommandRunner(command).run(<String>[ '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: <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(); try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]); fail('Expect exception'); } on Exception catch (e) { expect(e, isA<ToolExit>()); } 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(); try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', ]); fail('Expect exception'); } on Exception catch (e) { expect(e, isA<ToolExit>()); expect(e.toString(), contains('No pubspec.yaml file found')); } }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => BufferLogger.test(), }); group('run app', () { MemoryFileSystem fs; Artifacts artifacts; MockCache mockCache; MockProcessManager mockProcessManager; Usage usage; Directory tempDir; setUp(() { artifacts = Artifacts.test(); mockCache = MockCache(); usage = Usage.test(); 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<Device> noDevices = <Device>[]; when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future<List<Device>>.value(noDevices) ); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future<List<Device>>.value(noDevices) ); try { await createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]); fail('Expect exception'); } on ToolExit catch (e) { expect(e.message, null); } expect( testLogger.statusText, containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices), ); }, overrides: <Type, Generator>{ 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>[device]; }); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) async { return <Device>[device]; }); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer((Invocation invocation) async { return <Device>[device]; }); when(mockDeviceManager.hasSpecifiedAllDevices).thenReturn(false); when(mockDeviceManager.deviceDiscoverers).thenReturn(<DeviceDiscovery>[]); final RunCommand command = RunCommand(); 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: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => mockDeviceManager, Stdio: () => MockStdio(), }); 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<bool>.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<String>.value('api-14')); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future<List<Device>>.value(<Device>[ mockDevice, ]); }); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future<List<Device>>.value(<Device>[]), ); try { await createTestCommandRunner(command).run(<String>[ '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(<Device>[mockDevice]), ), ), ); }, overrides: <Type, Generator>{ 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<List<Device>>.value(<Device>[]) ); // 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<List<Device>>.value(<Device>[]) ); try { await createTestCommandRunner(command).run(<String>[ '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(<void>[ // cache update mockCache.updateAll(<DevelopmentArtifact>{DevelopmentArtifact.universal}), // as part of gathering `requiredArtifacts` mockDeviceManager.getDevices(), // in validateCommand() mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout')), ]); }, overrides: <Type, Generator>{ 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<bool>.value(false)); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(FakeDeviceLogReader()); when(mockDevice.supportsFastStart).thenReturn(true); when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future<String>.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<LaunchResult>.value(LaunchResult.failed())); when(mockDeviceManager.getDevices()).thenAnswer( (Invocation invocation) => Future<List<Device>>.value(<Device>[mockDevice]) ); when(mockDeviceManager.findTargetDevices(any, timeout: anyNamed('timeout'))).thenAnswer( (Invocation invocation) => Future<List<Device>>.value(<Device>[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; // Capture Usage.test() events. final StringBuffer buffer = await capturedConsolePrint(() => expectToolExitLater(createTestCommandRunner(command).run(<String>[ 'run', '--no-pub', '--no-hot', ]), isNull) ); // Allow any CustomDimensions.localTime (cd33) timestamp. final RegExp usageRegexp = RegExp( 'screenView {cd3: false, cd4: ios, cd22: iOS 13, cd23: debug, cd18: false, cd15: swift, cd31: false, cd33: .*, viewName: run' ); expect(buffer.toString(), matches(usageRegexp)); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => mockCache, DeviceManager: () => mockDeviceManager, FileSystem: () => fs, ProcessManager: () => mockProcessManager, Usage: () => usage, }); }); testUsingContext('should only request artifacts corresponding to connected devices', () async { when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future<List<Device>>.value(<Device>[ MockDevice(TargetPlatform.android_arm), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.androidGenSnapshot, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future<List<Device>>.value(<Device>[ MockDevice(TargetPlatform.ios), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future<List<Device>>.value(<Device>[ MockDevice(TargetPlatform.ios), MockDevice(TargetPlatform.android_arm), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.iOS, DevelopmentArtifact.androidGenSnapshot, })); when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) { return Future<List<Device>>.value(<Device>[ MockDevice(TargetPlatform.web_javascript), ]); }); expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{ DevelopmentArtifact.universal, DevelopmentArtifact.web, })); }, overrides: <Type, Generator>{ DeviceManager: () => mockDeviceManager, }); }); group('dart-defines and web-renderer options', () { List<String> dartDefines; setUp(() { dartDefines = <String>[]; }); test('auto web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, 'auto'); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=true']); }); test('canvaskit web-renderer with no dart-defines', () { dartDefines = FlutterCommand.updateDartDefines(dartDefines, '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, '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, 'auto'); 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, '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, 'html'); expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']); }); }); } class MockCache extends Mock implements Cache {} class MockUsage extends Mock implements Usage {} class MockDeviceManager extends Mock implements DeviceManager {} class MockDevice extends Mock implements Device { MockDevice(this._targetPlatform); final TargetPlatform _targetPlatform; @override Future<TargetPlatform> get targetPlatform async => Future<TargetPlatform>.value(_targetPlatform); } class TestRunCommand extends RunCommand { @override // ignore: must_call_super Future<void> 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<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator); @override bool supportsRuntimeMode(BuildMode mode) => true; @override bool get supportsHotReload => false; @override bool get supportsFastStart => false; @override Future<String> get sdkNameAndVersion => Future<String>.value(''); @override DeviceLogReader getLogReader({ ApplicationPackage app, bool includePastLogs = false, }) { return FakeDeviceLogReader(); } @override String get name => 'FakeDevice'; @override Future<TargetPlatform> get targetPlatform async => _targetPlatform; @override final PlatformType platformType = PlatformType.ios; @override Future<LaunchResult> startApp( ApplicationPackage package, { String mainPath, String route, DebuggingOptions debuggingOptions, Map<String, dynamic> 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; } }