From 2717eb6413b40d9202dd2967ff175bc61ae88086 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 26 Mar 2020 17:36:02 -0700 Subject: [PATCH] [flutter tools] rewrite launch non-prebuilt app tests (#53351) --- .../test/general.shard/ios/devices_test.dart | 318 +----------------- .../ios_device_start_nonprebuilt_test.dart | 300 +++++++++++++++++ 2 files changed, 304 insertions(+), 314 deletions(-) create mode 100644 packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 84d9f6e5b81..a5fa58db650 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; -import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; + import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -15,26 +16,15 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; -import 'package:flutter_tools/src/commands/create.dart'; import 'package:flutter_tools/src/device.dart'; -import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/ios_workflow.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; -import 'package:flutter_tools/src/project.dart'; -import 'package:flutter_tools/src/globals.dart' as globals; - -import 'package:meta/meta.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:process/process.dart'; -import 'package:quiver/testing/async.dart'; import '../../src/common.dart'; import '../../src/context.dart'; -import '../../src/fakes.dart'; import '../../src/mocks.dart'; void main() { @@ -59,7 +49,6 @@ void main() { mockCache = MockCache(); const MapEntry dyLdLibEntry = MapEntry('DYLD_LIBRARY_PATH', '/path/to/libs'); when(mockCache.dyLdLibEntry).thenReturn(dyLdLibEntry); - mockFileSystem = MockFileSystem(); logger = BufferLogger.test(); iosDeploy = IOSDeploy( artifacts: mockArtifacts, @@ -231,7 +220,6 @@ void main() { forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3); mockArtifacts = MockArtifacts(); mockCache = MockCache(); - mockFileSystem = MockFileSystem(); iosDeploy = IOSDeploy( artifacts: mockArtifacts, cache: mockCache, @@ -268,271 +256,12 @@ void main() { verify(mockProcess3.kill()); }); }); - - group('startApp', () { - MockIOSApp mockApp; - MockArtifacts mockArtifacts; - MockCache mockCache; - MockFileSystem mockFileSystem; - MockPlatform mockPlatform; - MockProcessManager mockProcessManager; - FakeDeviceLogReader mockLogReader; - MockPortForwarder mockPortForwarder; - MockIMobileDevice mockIMobileDevice; - MockIOSDeploy mockIosDeploy; - - Directory tempDir; - Directory projectDir; - - const int devicePort = 499; - const int hostPort = 42; - const String installerPath = '/path/to/ideviceinstaller'; - const String iosDeployPath = '/path/to/iosdeploy'; - const String iproxyPath = '/path/to/iproxy'; - const MapEntry libraryEntry = MapEntry( - 'DYLD_LIBRARY_PATH', - '/path/to/libraries', - ); - final Map env = Map.fromEntries( - >[libraryEntry] - ); - - setUp(() { - Cache.disableLocking(); - - mockApp = MockIOSApp(); - mockArtifacts = MockArtifacts(); - mockCache = MockCache(); - when(mockCache.dyLdLibEntry).thenReturn(libraryEntry); - mockFileSystem = MockFileSystem(); - mockPlatform = MockPlatform(); - when(mockPlatform.isMacOS).thenReturn(true); - mockProcessManager = MockProcessManager(); - mockLogReader = FakeDeviceLogReader(); - mockPortForwarder = MockPortForwarder(); - mockIMobileDevice = MockIMobileDevice(); - mockIosDeploy = MockIOSDeploy(); - - tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_create_test.'); - projectDir = tempDir.childDirectory('flutter_project'); - - when( - mockArtifacts.getArtifactPath( - Artifact.ideviceinstaller, - platform: anyNamed('platform'), - ), - ).thenReturn(installerPath); - - when( - mockArtifacts.getArtifactPath( - Artifact.iosDeploy, - platform: anyNamed('platform'), - ), - ).thenReturn(iosDeployPath); - - when( - mockArtifacts.getArtifactPath( - Artifact.iproxy, - platform: anyNamed('platform'), - ), - ).thenReturn(iproxyPath); - - when(mockPortForwarder.forward(devicePort, hostPort: anyNamed('hostPort'))) - .thenAnswer((_) async => hostPort); - when(mockPortForwarder.forwardedPorts) - .thenReturn([ForwardedPort(hostPort, devicePort)]); - when(mockPortForwarder.unforward(any)) - .thenAnswer((_) async => null); - - final MemoryFileSystem memoryFileSystem = MemoryFileSystem(); - when(mockFileSystem.currentDirectory) - .thenReturn(memoryFileSystem.currentDirectory); - - const String bundlePath = '/path/to/bundle'; - final List installArgs = [installerPath, '-i', bundlePath]; - when(mockApp.deviceBundlePath).thenReturn(bundlePath); - final MockDirectory directory = MockDirectory(); - when(mockFileSystem.directory(bundlePath)).thenReturn(directory); - when(directory.existsSync()).thenReturn(true); - when(mockProcessManager.run( - installArgs, - workingDirectory: anyNamed('workingDirectory'), - environment: env, - )).thenAnswer( - (_) => Future.value(ProcessResult(1, 0, '', '')) - ); - }); - - tearDown(() { - mockLogReader.dispose(); - tryToDelete(tempDir); - - Cache.enableLocking(); - }); - - void testNonPrebuilt( - String name, { - @required bool showBuildSettingsFlakes, - void Function() additionalSetup, - void Function() additionalExpectations, - }) { - testUsingContext('non-prebuilt succeeds in debug mode $name', () async { - final Directory targetBuildDir = - projectDir.childDirectory('build/ios/iphoneos/Debug-arm64'); - - // The -showBuildSettings calls have a timeout and so go through - // globals.processManager.start(). - mockProcessManager.processFactory = flakyProcessFactory( - flakes: showBuildSettingsFlakes ? 1 : 0, - delay: const Duration(seconds: 62), - filter: (List args) => args.contains('-showBuildSettings'), - stdout: - () => Stream - .fromIterable( - ['TARGET_BUILD_DIR = ${targetBuildDir.path}\n']) - .transform(utf8.encoder), - ); - - // Make all other subcommands succeed. - when(mockProcessManager.run( - any, - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'), - )).thenAnswer((Invocation inv) { - return Future.value(ProcessResult(0, 0, '', '')); - }); - - when(mockProcessManager.run( - argThat(contains('find-identity')), - environment: anyNamed('environment'), - workingDirectory: anyNamed('workingDirectory'), - )).thenAnswer((_) => Future.value(ProcessResult( - 1, // pid - 0, // exitCode - ''' - 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" - 2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)" - 3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)" - 3 valid identities found''', - '', - ))); - - // Deploy works. - when(mockIosDeploy.runApp( - deviceId: anyNamed('deviceId'), - bundlePath: anyNamed('bundlePath'), - launchArguments: anyNamed('launchArguments'), - )).thenAnswer((_) => Future.value(0)); - - // Create a dummy project to avoid mocking out the whole directory - // structure expected by device.startApp(). - Cache.flutterRoot = '../..'; - final CreateCommand command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); - await runner.run([ - 'create', - '--no-pub', - projectDir.path, - ]); - - if (additionalSetup != null) { - additionalSetup(); - } - - final IOSApp app = await AbsoluteBuildableIOSApp.fromProject( - FlutterProject.fromDirectory(projectDir).ios); - final IOSDevice device = IOSDevice( - '123', - name: 'iPhone 1', - sdkVersion: '13.3', - artifacts: mockArtifacts, - fileSystem: globals.fs, - logger: testLogger, - platform: macPlatform, - iosDeploy: mockIosDeploy, - iMobileDevice: iMobileDevice, - cpuArchitecture: DarwinArch.arm64, - ); - - // Pre-create the expected build products. - targetBuildDir.createSync(recursive: true); - projectDir.childDirectory('build/ios/iphoneos/Runner.app').createSync(recursive: true); - - final Completer completer = Completer(); - FakeAsync().run((FakeAsync time) { - device.startApp( - app, - prebuiltApplication: false, - debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)), - platformArgs: {}, - ).then((LaunchResult result) { - completer.complete(result); - }); - time.flushMicrotasks(); - time.elapse(const Duration(seconds: 65)); - }); - final LaunchResult launchResult = await completer.future; - expect(launchResult.started, isTrue); - expect(launchResult.hasObservatory, isFalse); - expect(await device.stopApp(mockApp), isFalse); - - if (additionalExpectations != null) { - additionalExpectations(); - } - }, overrides: { - DoctorValidatorsProvider: () => FakeIosDoctorProvider(), - IMobileDevice: () => mockIMobileDevice, - Platform: () => macPlatform, - ProcessManager: () => mockProcessManager, - }); - } - - testNonPrebuilt('flaky: false', showBuildSettingsFlakes: false); - testNonPrebuilt('flaky: true', showBuildSettingsFlakes: true); - testNonPrebuilt('with concurrent build failiure', - showBuildSettingsFlakes: false, - additionalSetup: () { - int callCount = 0; - when(mockProcessManager.run( - argThat(allOf( - contains('xcodebuild'), - contains('-configuration'), - contains('Debug'), - )), - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'), - )).thenAnswer((Invocation inv) { - // Succeed after 2 calls. - if (++callCount > 2) { - return Future.value(ProcessResult(0, 0, '', '')); - } - // Otherwise fail with the Xcode concurrent error. - return Future.value(ProcessResult( - 0, - 1, - ''' - "/Developer/Xcode/DerivedData/foo/XCBuildData/build.db": - database is locked - Possibly there are two concurrent builds running in the same filesystem location. - ''', - '', - )); - }); - }, - additionalExpectations: () { - expect(testLogger.statusText, contains('will retry in 2 seconds')); - expect(testLogger.statusText, contains('will retry in 4 seconds')); - expect(testLogger.statusText, contains('Xcode build done.')); - }, - ); - }); }); group('pollingGetDevices', () { MockXcdevice mockXcdevice; MockArtifacts mockArtifacts; MockCache mockCache; - MockFileSystem mockFileSystem; FakeProcessManager fakeProcessManager; Logger logger; IOSDeploy iosDeploy; @@ -544,7 +273,6 @@ void main() { mockArtifacts = MockArtifacts(); mockCache = MockCache(); logger = BufferLogger.test(); - mockFileSystem = MockFileSystem(); mockIosWorkflow = MockIOSWorkflow(); fakeProcessManager = FakeProcessManager.any(); iosDeploy = IOSDeploy( @@ -596,7 +324,7 @@ void main() { iMobileDevice: iMobileDevice, logger: logger, platform: macPlatform, - fileSystem: mockFileSystem, + fileSystem: MemoryFileSystem.test(), ); when(mockXcdevice.getAvailableTetheredIOSDevices()) .thenAnswer((Invocation invocation) => Future>.value([device])); @@ -646,48 +374,10 @@ void main() { }); } -class AbsoluteBuildableIOSApp extends BuildableIOSApp { - AbsoluteBuildableIOSApp(IosProject project, String projectBundleId) : - super(project, projectBundleId); - - static Future fromProject(IosProject project) async { - final String projectBundleId = await project.productBundleIdentifier; - return AbsoluteBuildableIOSApp(project, projectBundleId); - } - - @override - String get deviceBundlePath => - globals.fs.path.join(project.parent.directory.path, 'build', 'ios', 'iphoneos', name); - -} - -class FakeIosDoctorProvider implements DoctorValidatorsProvider { - List _workflows; - - @override - List get validators => []; - - @override - List get workflows { - if (_workflows == null) { - _workflows = []; - if (globals.iosWorkflow.appliesToHostPlatform) { - _workflows.add(globals.iosWorkflow); - } - } - return _workflows; - } -} - class MockIOSApp extends Mock implements IOSApp {} class MockArtifacts extends Mock implements Artifacts {} class MockCache extends Mock implements Cache {} -class MockDirectory extends Mock implements Directory {} -class MockFile extends Mock implements File {} -class MockFileSystem extends Mock implements FileSystem {} class MockIMobileDevice extends Mock implements IMobileDevice {} class MockIOSDeploy extends Mock implements IOSDeploy {} class MockIOSWorkflow extends Mock implements IOSWorkflow {} -class MockPlatform extends Mock implements Platform {} -class MockPortForwarder extends Mock implements DevicePortForwarder {} class MockXcdevice extends Mock implements XCDevice {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart new file mode 100644 index 00000000000..e82ed73c580 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -0,0 +1,300 @@ +// 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 '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/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/ios/devices.dart'; +import 'package:flutter_tools/src/ios/ios_deploy.dart'; +import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:quiver/testing/async.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fake_process_manager.dart'; + +const List kRunReleaseArgs = [ + '/usr/bin/env', + 'xcrun', + 'xcodebuild', + '-configuration', + 'Release', + '-quiet', + '-workspace', + 'Runner.xcworkspace', + '-scheme', + 'Runner', + 'BUILD_DIR=/build/ios', + '-sdk', + 'iphoneos', + 'ONLY_ACTIVE_ARCH=YES', + 'ARCHS=arm64', + 'FLUTTER_SUPPRESS_ANALYTICS=true', + 'COMPILER_INDEX_STORE_ENABLE=NO', +]; + +const String kConcurrentBuildErrorMessage = ''' +"/Developer/Xcode/DerivedData/foo/XCBuildData/build.db": +database is locked +Possibly there are two concurrent builds running in the same filesystem location. +'''; + +final FakePlatform macPlatform = FakePlatform( + operatingSystem: 'macos', + environment: {}, +); + +void main() { + FileSystem fileSystem; + FakeProcessManager processManager; + BufferLogger logger; + + setUp(() { + logger = BufferLogger.test(); + fileSystem = MemoryFileSystem.test(); + processManager = FakeProcessManager.list([]); + }); + + testUsingContext('IOSDevice.startApp succeeds in release mode with buildable app', () async { + final IOSDevice iosDevice = setUpIOSDevice( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + setUpIOSProject(fileSystem); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + + processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); + processManager.addCommand(const FakeCommand(command: [...kRunReleaseArgs, '-showBuildSettings'])); + processManager.addCommand(FakeCommand( + command: [ + 'ios-deploy', + '--id', + '123', + '--bundle', + 'build/ios/iphoneos/Runner.app', + '--no-wifi', + '--justlaunch', + '--args', + const [ + '--enable-dart-profiling', + '--enable-service-port-fallback', + '--disable-service-auth-codes', + '--observatory-port=53781', + ].join(' ') + ]) + ); + + final LaunchResult launchResult = await iosDevice.startApp( + buildableIOSApp, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + ProcessManager: () => processManager, + FileSystem: () => fileSystem, + Logger: () => logger, + Platform: () => macPlatform, + }); + + testUsingContext('IOSDevice.startApp succeeds in release mode with buildable ' + 'app with flaky buildSettings call', () async { + LaunchResult launchResult; + FakeAsync().run((FakeAsync time) { + final IOSDevice iosDevice = setUpIOSDevice( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + setUpIOSProject(fileSystem); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + + processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); + // The first showBuildSettings call should timeout. + processManager.addCommand( + const FakeCommand( + command: [...kRunReleaseArgs, '-showBuildSettings'], + duration: Duration(minutes: 5), // this is longer than the timeout of 1 minute. + )); + // The second call succeedes and is made after the first times out. + processManager.addCommand( + const FakeCommand( + command: [...kRunReleaseArgs, '-showBuildSettings'], + exitCode: 0, + )); + processManager.addCommand(FakeCommand( + command: [ + 'ios-deploy', + '--id', + '123', + '--bundle', + 'build/ios/iphoneos/Runner.app', + '--no-wifi', + '--justlaunch', + '--args', + const [ + '--enable-dart-profiling', + '--enable-service-port-fallback', + '--disable-service-auth-codes', + '--observatory-port=53781', + ].join(' ') + ]) + ); + + iosDevice.startApp( + buildableIOSApp, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + platformArgs: {}, + ).then((LaunchResult result) { + launchResult = result; + }); + + // Elapse duration for process timeout. + time.flushMicrotasks(); + time.elapse(const Duration(minutes: 1)); + + // Elapse duration for overall process timer. + time.flushMicrotasks(); + time.elapse(const Duration(minutes: 5)); + + time.flushTimers(); + }); + + expect(launchResult?.started, true); + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + ProcessManager: () => processManager, + FileSystem: () => fileSystem, + Logger: () => logger, + Platform: () => macPlatform, + }); + + testUsingContext('IOSDevice.startApp succeeds in release mode with buildable ' + 'app with concurrent build failure', () async { + final IOSDevice iosDevice = setUpIOSDevice( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + ); + setUpIOSProject(fileSystem); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + + // The first xcrun call should fail with a + // concurrent build exception. + processManager.addCommand( + const FakeCommand( + command: kRunReleaseArgs, + exitCode: 1, + stdout: kConcurrentBuildErrorMessage, + )); + processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); + processManager.addCommand( + const FakeCommand( + command: [...kRunReleaseArgs, '-showBuildSettings'], + exitCode: 0, + )); + processManager.addCommand(FakeCommand( + command: [ + 'ios-deploy', + '--id', + '123', + '--bundle', + 'build/ios/iphoneos/Runner.app', + '--no-wifi', + '--justlaunch', + '--args', + const [ + '--enable-dart-profiling', + '--enable-service-port-fallback', + '--disable-service-auth-codes', + '--observatory-port=53781', + ].join(' ') + ]) + ); + + final LaunchResult launchResult = await iosDevice.startApp( + buildableIOSApp, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + platformArgs: {}, + ); + + expect(logger.statusText, + contains('Xcode build failed due to concurrent builds, will retry in 2 seconds')); + expect(launchResult.started, true); + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + ProcessManager: () => processManager, + FileSystem: () => fileSystem, + Logger: () => logger, + Platform: () => macPlatform, + }); +} + +void setUpIOSProject(FileSystem fileSystem) { + fileSystem.file('pubspec.yaml').createSync(); + fileSystem.file('.packages').writeAsStringSync('\n'); + fileSystem.directory('ios').createSync(); + fileSystem.directory('ios/Runner.xcworkspace').createSync(); + fileSystem.directory('ios/Runner.xcodeproj').createSync(); + fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(); + // This is the expected output directory. + fileSystem.directory('build/ios/iphoneos/Runner.app').createSync(recursive: true); +} + +IOSDevice setUpIOSDevice({ + String sdkVersion = '13.0.1', + FileSystem fileSystem, + Logger logger, + ProcessManager processManager, +}) { + const MapEntry dyldLibraryEntry = MapEntry( + 'DYLD_LIBRARY_PATH', + '/path/to/libraries', + ); + final MockCache cache = MockCache(); + final MockArtifacts artifacts = MockArtifacts(); + logger ??= BufferLogger.test(); + when(cache.dyLdLibEntry).thenReturn(dyldLibraryEntry); + when(artifacts.getArtifactPath(Artifact.iosDeploy, platform: anyNamed('platform'))) + .thenReturn('ios-deploy'); + return IOSDevice('123', + name: 'iPhone 1', + sdkVersion: sdkVersion, + fileSystem: fileSystem ?? MemoryFileSystem.test(), + platform: macPlatform, + artifacts: artifacts, + logger: logger, + iosDeploy: IOSDeploy( + logger: logger, + platform: macPlatform, + processManager: processManager ?? FakeProcessManager.any(), + artifacts: artifacts, + cache: cache, + ), + iMobileDevice: IMobileDevice( + logger: logger, + processManager: processManager ?? FakeProcessManager.any(), + artifacts: artifacts, + cache: cache, + ), + cpuArchitecture: DarwinArch.arm64, + ); +} + +class MockArtifacts extends Mock implements Artifacts {} +class MockCache extends Mock implements Cache {}