diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index 17679b5f793..bbc018f5884 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -26,7 +26,58 @@ import '../../src/pubspec_schema.dart'; const String xcodebuild = '/usr/bin/xcodebuild'; void main() { - mocks.MockProcessManager processManager; + group('MockProcessManager', () { + mocks.MockProcessManager processManager; + XcodeProjectInterpreter xcodeProjectInterpreter; + FakePlatform platform; + BufferLogger logger; + + setUp(() { + processManager = mocks.MockProcessManager(); + platform = FakePlatform(operatingSystem: 'macos'); + final FileSystem fileSystem = MemoryFileSystem(); + fileSystem.file(xcodebuild).createSync(recursive: true); + final AnsiTerminal terminal = MockAnsiTerminal(); + logger = BufferLogger.test( + terminal: terminal + ); + xcodeProjectInterpreter = XcodeProjectInterpreter( + logger: logger, + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + terminal: terminal, + usage: null, + ); + }); + + // Work around https://github.com/flutter/flutter/issues/56415. + testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenThrow(const ProcessException(xcodebuild, ['-version'])); + + expect(xcodeProjectInterpreter.versionText, isNull); + }); + + testWithoutContext('xcodebuild build settings flakes', () async { + const Duration delay = Duration(seconds: 1); + processManager.processFactory = mocks.flakyProcessFactory( + flakes: 1, + delay: delay + const Duration(seconds: 1), + ); + platform.environment = const {}; + + expect(await xcodeProjectInterpreter.getBuildSettings( + '', '', timeout: delay), + const {}); + // build settings times out and is killed once, then succeeds. + verify(processManager.killPid(any)).called(1); + // The verbose logs should tell us something timed out. + expect(logger.traceText, contains('timed out')); + }); + }); + + FakeProcessManager fakeProcessManager; XcodeProjectInterpreter xcodeProjectInterpreter; FakePlatform platform; FileSystem fileSystem; @@ -34,7 +85,7 @@ void main() { AnsiTerminal terminal; setUp(() { - processManager = mocks.MockProcessManager(); + fakeProcessManager = FakeProcessManager.list([]); platform = FakePlatform(operatingSystem: 'macos'); fileSystem = MemoryFileSystem(); fileSystem.file(xcodebuild).createSync(recursive: true); @@ -46,81 +97,89 @@ void main() { logger: logger, fileSystem: fileSystem, platform: platform, - processManager: processManager, + processManager: fakeProcessManager, terminal: terminal, usage: null, ); }); - testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenThrow(const ProcessException(xcodebuild, ['-version'])); - - expect(xcodeProjectInterpreter.versionText, isNull); - }); - testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () { - when(processManager.runSync([xcodebuild, '-version'])).thenReturn( - ProcessResult( - 0, - 1, - "xcode-select: error: tool 'xcodebuild' requires Xcode, " + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, " "but active developer directory '/Library/Developer/CommandLineTools' " 'is a command line tools instance', - '', - ), - ); + exitCode: 1, + )); expect(xcodeProjectInterpreter.versionText, isNull); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild versionText returns formatted version text', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode 8.3.3\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b'); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode Ultra5000\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b'); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild majorVersion returns major version', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 11.4.1\nBuild version 11N111s', '')); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode 11.4.1\nBuild version 11N111s', + )); expect(xcodeProjectInterpreter.majorVersion, 11); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild majorVersion is null when version has unexpected format', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode Ultra5000\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.majorVersion, isNull); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild minorVersion returns minor version', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); - + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode 8.3.3\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.minorVersion, 3); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild minorVersion returns 0 when minor version is unspecified', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', '')); - + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode 8\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.minorVersion, 0); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild minorVersion is null when version has unexpected format', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode Ultra5000\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.minorVersion, isNull); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () { @@ -129,163 +188,154 @@ void main() { logger: logger, fileSystem: fileSystem, platform: platform, - processManager: processManager, + processManager: fakeProcessManager, terminal: terminal, usage: Usage.test(), ); fileSystem.file(xcodebuild).deleteSync(); expect(xcodeProjectInterpreter.isInstalled, isFalse); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild isInstalled is false when xcodebuild does not exist', () { fileSystem.file(xcodebuild).deleteSync(); expect(xcodeProjectInterpreter.isInstalled, isFalse); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild isInstalled is false when Xcode is not fully installed', () { - when(processManager.runSync([xcodebuild, '-version'])).thenReturn( - ProcessResult( - 0, - 1, - "xcode-select: error: tool 'xcodebuild' requires Xcode, " + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: "xcode-select: error: tool 'xcodebuild' requires Xcode, " "but active developer directory '/Library/Developer/CommandLineTools' " 'is a command line tools instance', - '', - ), - ); + exitCode: 1, + )); expect(xcodeProjectInterpreter.isInstalled, isFalse); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode Ultra5000\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.isInstalled, isFalse); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild isInstalled is true when version has expected format', () { - when(processManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-version'], + stdout: 'Xcode 8.3.3\nBuild version 8E3004b', + )); expect(xcodeProjectInterpreter.isInstalled, isTrue); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async { - platform.environment = Map.unmodifiable({}); - when(processManager.runSync( - argThat(contains(xcodebuild)), - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))) - .thenReturn(ProcessResult(0, 1, '', '')); + platform.environment = const {}; - expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); - }); + fakeProcessManager.addCommand(const FakeCommand( + command: [ + '/usr/bin/xcodebuild', + '-project', + '/', + '-target', + 'Runner', + '-showBuildSettings' + ], + exitCode: 1, + )); - testWithoutContext('xcodebuild build settings flakes', () async { - const Duration delay = Duration(seconds: 1); - processManager.processFactory = mocks.flakyProcessFactory( - flakes: 1, - delay: delay + const Duration(seconds: 1), - ); - platform.environment = Map.unmodifiable({}); - - expect(await xcodeProjectInterpreter.getBuildSettings( - '', '', timeout: delay), - const {}); - // build settings times out and is killed once, then succeeds. - verify(processManager.killPid(any)).called(1); - // The verbose logs should tell us something timed out. - expect(logger.traceText, contains('timed out')); + expect(await xcodeProjectInterpreter.getBuildSettings('', 'Runner'), const {}); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async { - platform.environment = Map.unmodifiable({ + platform.environment = const { 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_ARCHS': 'arm64' - }); - when(processManager.runSync([ - xcodebuild, - '-project', - platform.pathSeparator, - '-target', - '', - '-showBuildSettings', - 'CODE_SIGN_STYLE=Manual', - 'ARCHS=arm64' - ], - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))) - .thenReturn(ProcessResult(1, 0, '', '')); - expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); + }; + fakeProcessManager.addCommand(FakeCommand( + command: [ + xcodebuild, + '-project', + fileSystem.path.separator, + '-target', + 'Runner', + '-showBuildSettings', + 'CODE_SIGN_STYLE=Manual', + 'ARCHS=arm64' + ], + )); + expect(await xcodeProjectInterpreter.getBuildSettings('', 'Runner'), const {}); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async { - platform.environment = Map.unmodifiable({ + platform.environment = const { 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_ARCHS': 'arm64' - }); - when(processManager.run( - any, - workingDirectory: anyNamed('workingDirectory'))) - .thenAnswer((_) => Future.value(ProcessResult(1, 0, '', ''))); - await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner'); - final List captured = verify(processManager.run( - captureAny, - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))).captured; + }; - expect(captured.first, [ - xcodebuild, - '-workspace', - 'workspace_path', - '-scheme', - 'Runner', - '-quiet', - 'clean', - 'CODE_SIGN_STYLE=Manual', - 'ARCHS=arm64' - ]); + fakeProcessManager.addCommand(const FakeCommand( + command: [ + xcodebuild, + '-workspace', + 'workspace_path', + '-scheme', + 'Runner', + '-quiet', + 'clean', + 'CODE_SIGN_STYLE=Manual', + 'ARCHS=arm64' + ], + )); + + await xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner'); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async { const String workingDirectory = '/'; - when(processManager.run( - [xcodebuild, '-list'], - environment: anyNamed('environment'), - workingDirectory: workingDirectory), - ).thenAnswer((_) { - return Future.value(ProcessResult(1, 0, '', '')); - }); + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-list'], + )); + final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter( logger: logger, fileSystem: fileSystem, platform: platform, - processManager: processManager, + processManager: fakeProcessManager, terminal: terminal, usage: Usage.test(), ); expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('xcodebuild -list getInfo throws a tool exit when it is unable to find a project', () async { const String workingDirectory = '/'; const String stderr = 'Useful Xcode failure message about missing project.'; - when(processManager.run( - [xcodebuild, '-list'], - environment: anyNamed('environment'), - workingDirectory: workingDirectory), - ).thenAnswer((_) { - return Future.value(ProcessResult(1, 66, '', stderr)); - }); + + fakeProcessManager.addCommand(const FakeCommand( + command: [xcodebuild, '-list'], + exitCode: 66, + stderr: stderr, + )); + final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter( logger: logger, fileSystem: fileSystem, platform: platform, - processManager: processManager, + processManager: fakeProcessManager, terminal: terminal, usage: Usage.test(), ); @@ -293,6 +343,7 @@ void main() { expect( () async => await xcodeProjectInterpreter.getInfo(workingDirectory), throwsToolExit(message: stderr)); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); }); testWithoutContext('Xcode project properties from default project can be parsed', () { @@ -427,12 +478,12 @@ Information about project "Runner": }); testWithoutContext('environment variables as Xcode build settings', () { - platform.environment = Map.unmodifiable({ + platform.environment = const { 'Ignored': 'Bogus', 'FLUTTER_NOT_XCODE': 'Bogus', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_ARCHS': 'arm64' - }); + }; final List environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform); expect(environmentVariablesAsBuildSettings, ['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']); }); @@ -440,14 +491,12 @@ Information about project "Runner": group('updateGeneratedXcodeProperties', () { MockLocalEngineArtifacts mockArtifacts; - MockProcessManager mockProcessManager; FakePlatform macOS; FileSystem fs; setUp(() { fs = MemoryFileSystem(); mockArtifacts = MockLocalEngineArtifacts(); - mockProcessManager = MockProcessManager(); macOS = FakePlatform(operatingSystem: 'macos'); fs.file(xcodebuild).createSync(recursive: true); }); @@ -457,7 +506,7 @@ Information about project "Runner": Artifacts: () => mockArtifacts, Platform: () => macOS, FileSystem: () => fs, - ProcessManager: () => mockProcessManager, + ProcessManager: () => FakeProcessManager.any(), }); } @@ -805,7 +854,6 @@ flutter: } class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {} -class MockProcessManager extends Mock implements ProcessManager {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} class MockAnsiTerminal extends Mock implements AnsiTerminal { @override