diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 0df783f..8883779 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -24,7 +24,7 @@ jobs: - name: 🛠️ Build with release configuration run: | - swift build --configuration release -skipPackagePluginValidation | xcpretty --utf --color && exit ${PIPESTATUS[0]} + swift build --configuration release | xcpretty --utf --color && exit ${PIPESTATUS[0]} - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/detect-api.changes.yml b/.github/workflows/detect-api.changes.yml new file mode 100644 index 0000000..b0678a6 --- /dev/null +++ b/.github/workflows/detect-api.changes.yml @@ -0,0 +1,65 @@ +name: 👀 Detect public API changes + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + new: + description: 'Branch/tag of the new/updated version' + required: true + old: + description: 'Branch/tag of the old/comparison version' + required: true + +jobs: + + build: + runs-on: macos-14 # Apple Silicon Runner + + steps: + - uses: actions/checkout@v4 + - uses: n1hility/cancel-previous-runs@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Select latest Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + + - name: 🚚 Fetch repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 🔍 Detect Changes + run: | + PROJECT_FOLDER=$PWD + echo $PROJECT_FOLDER + + NEW="${{ env.source }}~${{ env.githubRepo }}" + if [[ '${{ github.head_ref || env.noTargetBranch }}' == 'release/*' ]] + then + LATEST_TAG=$(git describe --tags --abbrev=0) + OLD="$LATEST_TAG~${{ env.githubRepo }}" + else + OLD="${{ env.target }}~${{ env.githubRepo }}" + fi + + swift run public-api-diff project --new "$NEW" --old "$OLD" --platform macos --output "$PROJECT_FOLDER/api_comparison.md" --log-level debug --log-output "$PROJECT_FOLDER/logs.txt" + cat "$PROJECT_FOLDER/logs.txt" + cat "$PROJECT_FOLDER/api_comparison.md" >> $GITHUB_STEP_SUMMARY + env: + source: '${{ github.event.inputs.new || github.head_ref }}' + target: '${{ github.event.inputs.old || github.event.pull_request.base.ref }}' + githubRepo: '${{github.server_url}}/${{github.repository}}.git' + noTargetBranch: 'no target branch' + + - if: ${{ github.event.pull_request.base.ref != '' }} + name: 📝 Comment on PR + uses: thollander/actions-comment-pull-request@v3 + with: + file-path: "${{ github.workspace }}/api_comparison.md" + comment-tag: api_changes + mode: recreate diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index d8459e1..f3162b0 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -3,7 +3,7 @@ # You can adjust the behavior by modifying this file. # For more information, see: # https://github.com/actions/stale -name: Manage stale issues +name: 🏷️ Manage stale issues on: schedule: diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..74ca312 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,36 @@ +disabled_rules: + - trailing_whitespace + - nesting + - todo + - for_where + +excluded: + - Build + - .build + - Tests + - Internal + - Docs + - Demo + - Package.swift + - DerivedData + - Carthage + - TempProject + - Scripts + +line_length: + ignores_function_declarations: true + ignores_urls: true + warning: 140 + +file_length: + warning: 500 + +function_parameter_count: + warning: 10 + error: 15 + +identifier_name: + min_length: + warning: 2 + max_length: + warning: 40 diff --git a/README.md b/README.md index 704601f..b6a19bf 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This method requires an iOS 17.5 Simulator to be installed ``` swift run public-api-diff project + --platform iOS --new "develop~https://github.com/Adyen/adyen-ios.git" --old "5.12.0~https://github.com/Adyen/adyen-ios.git" ``` @@ -28,11 +29,12 @@ swift run public-api-diff
--help: ``` -USAGE: public-api-diff project --new --old [--scheme ] [--swift-interface-type ] [--output ] [--log-output ] [--log-level ] +USAGE: public-api-diff project --new --old --platform [--scheme ] [--swift-interface-type ] [--output ] [--log-output ] [--log-level ] OPTIONS: --new Specify the updated version to compare to --old Specify the old version to compare to + --platform The platform to build the project for (iOS/macOS) --scheme [Optional] Which scheme to build (Needed when comparing 2 xcode projects) --swift-interface-type diff --git a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift index e269abf..77ca052 100644 --- a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift +++ b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift @@ -14,6 +14,7 @@ extension SwiftInterfaceType: ExpressibleByArgument { switch argument { case "public": self = .public case "private": self = .private + case "package": self = .package default: return nil } } diff --git a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift index 94179ca..3002b24 100644 --- a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift @@ -29,6 +29,9 @@ struct ProjectToOutputCommand: AsyncParsableCommand { @Option(help: "Specify the old version to compare to") public var old: String + @Option(help: "The platform to build the project for (iOS/macOS)") + public var platform: ProjectPlatform + /// The (optional) scheme to build /// /// Needed when comparing 2 xcode projects @@ -74,6 +77,7 @@ struct ProjectToOutputCommand: AsyncParsableCommand { oldSource: oldSource, newSource: newSource, projectType: projectType, + platform: platform, swiftInterfaceType: swiftInterfaceType, logger: logger ) @@ -136,12 +140,14 @@ private extension ProjectToOutputCommand { oldSource: ProjectSource, newSource: ProjectSource, projectType: ProjectType, + platform: ProjectPlatform, swiftInterfaceType: SwiftInterfaceType, logger: any Logging ) async throws -> ProjectBuilder.Result { let projectBuilder = ProjectBuilder( projectType: projectType, + platform: platform, swiftInterfaceType: swiftInterfaceType, logger: logger ) @@ -207,3 +213,22 @@ private extension ProjectToOutputCommand { ) } } + +extension ProjectPlatform: ExpressibleByArgument { + + static var mapping: [String: ProjectPlatform] = [ + "iOS": .iOS, + "macOS": .macOS + ] + + public init?(argument: String) { + for (key, value) in Self.mapping { + if argument.compare(key, options: .caseInsensitive) == .orderedSame { + self = value + return + } + } + + return nil + } +} diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift b/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift index 7381ab6..4de7a1f 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift @@ -33,6 +33,7 @@ public struct ProjectBuilder { } private let projectType: ProjectType + private let platform: ProjectPlatform private let swiftInterfaceType: SwiftInterfaceType private let fileHandler: any FileHandling private let shell: any ShellHandling @@ -40,11 +41,13 @@ public struct ProjectBuilder { public init( projectType: ProjectType, + platform: ProjectPlatform, swiftInterfaceType: SwiftInterfaceType, logger: (any Logging)? = nil ) { self.init( projectType: projectType, + platform: platform, swiftInterfaceType: swiftInterfaceType, fileHandler: FileManager.default, shell: Shell(), @@ -54,12 +57,14 @@ public struct ProjectBuilder { init( projectType: ProjectType, + platform: ProjectPlatform, swiftInterfaceType: SwiftInterfaceType, fileHandler: any FileHandling = FileManager.default, shell: any ShellHandling = Shell(), logger: (any Logging)? ) { self.projectType = projectType + self.platform = platform self.swiftInterfaceType = swiftInterfaceType self.fileHandler = fileHandler self.shell = shell @@ -99,6 +104,7 @@ public struct ProjectBuilder { let producer = SwiftInterfaceProducer( workingDirectoryPath: workingDirectoryPath, projectType: projectType, + platform: platform, swiftInterfaceType: swiftInterfaceType, fileHandler: fileHandler, shell: shell, diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectPlatform.swift b/Sources/PublicModules/PADProjectBuilder/ProjectPlatform.swift new file mode 100644 index 0000000..216ef86 --- /dev/null +++ b/Sources/PublicModules/PADProjectBuilder/ProjectPlatform.swift @@ -0,0 +1,12 @@ +// +// Copyright (c) 2024 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +/// The platform to build the project on +public enum ProjectPlatform { + + case macOS + case iOS +} diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift index be8122b..58c2945 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift @@ -18,7 +18,7 @@ internal enum GitError: LocalizedError, Equatable { var errorDescription: String? { switch self { case let .couldNotClone(branchOrTag, repository): - "Could not clone \(repository) @ \(branchOrTag) - Please check the provided information" + "Could not clone \(repository) @ \(branchOrTag) - Please check the debug logs for more information" } } } @@ -50,9 +50,12 @@ internal struct Git { func clone(_ repository: String, at branchOrTag: String, targetDirectoryPath: String) throws { logger?.log("🐱 Cloning \(repository) @ \(branchOrTag) into \(targetDirectoryPath)", from: String(describing: Self.self)) let command = "git clone -b \(branchOrTag) \(repository) \(targetDirectoryPath)" - shell.execute(command) - - guard fileHandler.fileExists(atPath: targetDirectoryPath) else { + + let shellOutput = shell.execute(command) + logger?.debug(shellOutput, from: String(describing: Self.self)) + + let directoryContents = try? fileHandler.contentsOfDirectory(atPath: targetDirectoryPath) + guard let directoryContents, !directoryContents.isEmpty else { throw GitError.couldNotClone(branchOrTag: branchOrTag, repository: repository) } } diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift index 43c0511..d70639a 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift @@ -22,6 +22,7 @@ struct SwiftInterfaceProducer { let workingDirectoryPath: String let projectType: ProjectType + let platform: ProjectPlatform let swiftInterfaceType: SwiftInterfaceType let fileHandler: any FileHandling let shell: any ShellHandling @@ -122,12 +123,14 @@ extension SwiftInterfaceProducer { let newDerivedDataPath = try await xcodeTools.archive( projectDirectoryPath: newProjectDirectoryPath, scheme: scheme, - projectType: projectType + projectType: projectType, + platform: platform ) let oldDerivedDataPath = try await xcodeTools.archive( projectDirectoryPath: oldProjectDirectoryPath, scheme: scheme, - projectType: projectType + projectType: projectType, + platform: platform ) return (newDerivedDataPath, oldDerivedDataPath) diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift index 005ecf7..350fc8a 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift @@ -49,16 +49,28 @@ struct XcodeTools { func archive( projectDirectoryPath: String, scheme: String, - projectType: ProjectType + projectType: ProjectType, + platform: ProjectPlatform ) async throws -> String { + var commandComponents = [ "cd \(projectDirectoryPath);", "xcodebuild clean build -scheme \"\(scheme)\"", - "-destination \"generic/platform=iOS\"", "-derivedDataPath \(Constants.derivedDataPath)", - "-sdk `\(Constants.simulatorSdkCommand)`", "BUILD_LIBRARY_FOR_DISTRIBUTION=YES" ] + + switch platform { + case .iOS: + commandComponents += [ + "-sdk `\(Constants.simulatorSdkCommand)`", + "-destination \"generic/platform=iOS\"" + ] + case .macOS: + commandComponents += [ + "-destination \"generic/platform=macOS\"" + ] + } switch projectType { case .swiftPackage: diff --git a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift index 360af4e..a7209c1 100644 --- a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift +++ b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift @@ -127,14 +127,19 @@ package extension SwiftPackageDescription.Dependency { // TODO: Which other requirements exist? package let exact: [String]? + package let range: [[String: String]]? } } extension SwiftPackageDescription.Dependency.Requirement: CustomStringConvertible { package var description: String { - if let exactVersion = exact?.first { - return "exact: \"\(exactVersion)\"" + if let version = exact?.first { + return "exact: \"\(version)\"" + } + + if let lowerUpper = range?.first, let lower = lowerUpper["lower_bound"], let upper = lowerUpper["upper_bound"] { + return "\"\(lower)\"..<\"\(upper)\"" } return "UNKNOWN_REQUIREMENT" @@ -155,6 +160,7 @@ package extension SwiftPackageDescription { case library = "library" case binary = "binary" case test = "test" + case executable = "executable" } package let name: String @@ -208,6 +214,7 @@ extension SwiftPackageDescription.Target.TargetType: CustomStringConvertible { case .binary: "binaryTarget" case .library: "target" case .test: "testTarget" + case .executable: "executableTarget" } } } diff --git a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift index 9b6d06b..bfe29df 100644 --- a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift +++ b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift @@ -152,9 +152,14 @@ private extension SwiftPackageFileHelper { } func decodePackageDescription(from packageDescriptionData: Data, warnings: [String]) throws -> SwiftPackageDescription { - var packageDescription = try JSONDecoder().decode(SwiftPackageDescription.self, from: packageDescriptionData) - packageDescription.warnings = warnings - return packageDescription + do { + var packageDescription = try JSONDecoder().decode(SwiftPackageDescription.self, from: packageDescriptionData) + packageDescription.warnings = warnings + return packageDescription + } catch { + logger?.log(String(describing: error), from: String(describing: Self.self)) + throw error + } } } diff --git a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift index fa961d1..47cd512 100644 --- a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift +++ b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift @@ -63,14 +63,18 @@ public struct SwiftInterfaceFileLocator { switch type { case .private: swiftInterfacePaths = swiftModuleContent.filter { $0.hasSuffix(".private.swiftinterface") } + case .package: + swiftInterfacePaths = swiftModuleContent.filter { $0.hasSuffix(".package.swiftinterface") } case .public: - swiftInterfacePaths = swiftModuleContent.filter { $0.hasSuffix(".swiftinterface") && !$0.hasSuffix(".private.swiftinterface") } + swiftInterfacePaths = swiftModuleContent.filter { $0.hasSuffix(".swiftinterface") && !$0.hasSuffix(".private.swiftinterface") && !$0.hasSuffix(".package.swiftinterface") } } guard let swiftInterfacePath = swiftInterfacePaths.first else { switch type { case .private: throw FileHandlerError.pathDoesNotExist(path: "'\(completeSwiftModulePath)/\(scheme).private.swiftinterface'") + case .package: + throw FileHandlerError.pathDoesNotExist(path: "'\(completeSwiftModulePath)/\(scheme).package.swiftinterface'") case .public: throw FileHandlerError.pathDoesNotExist(path: "'\(completeSwiftModulePath)/\(scheme).swiftinterface'") } diff --git a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift index 407c997..d6b1dbb 100644 --- a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift +++ b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift @@ -10,11 +10,13 @@ import Foundation public enum SwiftInterfaceType { case `private` case `public` + case package var name: String { switch self { case .private: "private" case .public: "public" + case .package: "package" } } } diff --git a/Tests/IntegrationTests/ReferencePackageTests.swift b/Tests/IntegrationTests/ReferencePackageTests.swift index c794fb8..c94302e 100644 --- a/Tests/IntegrationTests/ReferencePackageTests.swift +++ b/Tests/IntegrationTests/ReferencePackageTests.swift @@ -26,8 +26,18 @@ class ReferencePackageTests: XCTestCase { let xcodeTools = XcodeTools(logger: nil) - _ = try await xcodeTools.archive(projectDirectoryPath: oldReferencePackageDirectory.path(), scheme: "ReferencePackage", projectType: .swiftPackage) - _ = try await xcodeTools.archive(projectDirectoryPath: newReferencePackageDirectory.path(), scheme: "ReferencePackage", projectType: .swiftPackage) + _ = try await xcodeTools.archive( + projectDirectoryPath: oldReferencePackageDirectory.path(), + scheme: "ReferencePackage", + projectType: .swiftPackage, + platform: .iOS + ) + _ = try await xcodeTools.archive( + projectDirectoryPath: newReferencePackageDirectory.path(), + scheme: "ReferencePackage", + projectType: .swiftPackage, + platform: .iOS + ) } override static func tearDown() { diff --git a/Tests/UnitTests/GitTests.swift b/Tests/UnitTests/GitTests.swift index 2aa6e8a..96f0972 100644 --- a/Tests/UnitTests/GitTests.swift +++ b/Tests/UnitTests/GitTests.swift @@ -8,55 +8,141 @@ import XCTest class GitTests: XCTestCase { - + func test_clone_success() throws { - + let repository = "repository" let branch = "branch" let targetDirectoryPath = "targetDirectoryPath" - - let mockShell = MockShell { command in - XCTAssertEqual(command, "git clone -b \(branch) \(repository) \(targetDirectoryPath)") - return "" - } - - let mockFileHandler = MockFileHandler(handleFileExists: { _ in true }) - var mockLogger = MockLogger() - mockLogger.handleLog = { message, subsystem in - XCTAssertEqual(message, "🐱 Cloning repository @ branch into targetDirectoryPath") - XCTAssertEqual(subsystem, "Git") - } - - let git = Git(shell: mockShell, fileHandler: mockFileHandler, logger: mockLogger) + let shellResult = "shell-result" + + let shellSetup = setupShell( + branch: branch, + repository: repository, + targetDirectoryPath: targetDirectoryPath, + result: shellResult + ) + + let fileHandlerSetup = setupFileHandler( + targetDirectoryPath: targetDirectoryPath, + result: ["NonEmpty"] + ) + + let loggerSetup = setupLogger( + shellResult: shellResult + ) + + let allExpectations = [shellSetup.expectation, fileHandlerSetup.expectation] + loggerSetup.expectations + + let git = Git( + shell: shellSetup.shell, + fileHandler: fileHandlerSetup.fileHandler, + logger: loggerSetup.logger + ) + try git.clone(repository, at: branch, targetDirectoryPath: targetDirectoryPath) + + wait(for: allExpectations, timeout: 1) } - + func test_clone_fail() throws { - + let repository = "repository" let branch = "branch" let targetDirectoryPath = "targetDirectoryPath" + let shellResult = "shell-result" + + let shellSetup = setupShell( + branch: branch, + repository: repository, + targetDirectoryPath: targetDirectoryPath, + result: shellResult + ) + + let fileHandlerSetup = setupFileHandler( + targetDirectoryPath: targetDirectoryPath, + result: [] + ) + + let loggerSetup = setupLogger( + shellResult: shellResult + ) + + let allExpectations = [shellSetup.expectation, fileHandlerSetup.expectation] + loggerSetup.expectations + + let git = Git( + shell: shellSetup.shell, + fileHandler: fileHandlerSetup.fileHandler, + logger: loggerSetup.logger + ) + + do { + try git.clone(repository, at: branch, targetDirectoryPath: targetDirectoryPath) + XCTFail("Clone should have thrown an error") + } catch { + let fileHandlerError = try XCTUnwrap(error as? GitError) + XCTAssertEqual(fileHandlerError, GitError.couldNotClone(branchOrTag: branch, repository: repository)) + } + + wait(for: allExpectations, timeout: 1) + } +} +private extension GitTests { + + func setupShell( + branch: String, + repository: String, + targetDirectoryPath: String, + result: String + ) -> (shell: MockShell, expectation: XCTestExpectation) { + + let shellExpectation = expectation(description: "MockShell.execute was called once") + let mockShell = MockShell { command in XCTAssertEqual(command, "git clone -b \(branch) \(repository) \(targetDirectoryPath)") - return "" + shellExpectation.fulfill() + return result } - - let mockFileHandler = MockFileHandler(handleFileExists: { _ in false }) + + return (mockShell, shellExpectation) + } + + func setupFileHandler( + targetDirectoryPath: String, + result: [String] + ) -> (fileHandler: MockFileHandler, expectation: XCTestExpectation) { + + let fileHandlerExpectation = expectation(description: "MockFileHandler.handleContentsOfDirectory was called once") + + var mockFileHandler = MockFileHandler() + mockFileHandler.handleContentsOfDirectory = { directoryPath in + XCTAssertEqual(targetDirectoryPath, directoryPath) + fileHandlerExpectation.fulfill() + return result + } + return (mockFileHandler, fileHandlerExpectation) + } + + func setupLogger( + shellResult: String + ) -> (logger: MockLogger, expectations: [XCTestExpectation]) { + + let loggerLogExpectation = expectation(description: "MockLogger.handleLog was called once") + let loggerDebugExpectation = expectation(description: "MockLogger.handleDebug was called once") + var mockLogger = MockLogger() mockLogger.handleLog = { message, subsystem in XCTAssertEqual(message, "🐱 Cloning repository @ branch into targetDirectoryPath") XCTAssertEqual(subsystem, "Git") + loggerLogExpectation.fulfill() } - - let git = Git(shell: mockShell, fileHandler: mockFileHandler, logger: mockLogger) - - do { - try git.clone(repository, at: branch, targetDirectoryPath: targetDirectoryPath) - XCTFail("Clone should have thrown an error") - } catch { - let fileHandlerError = try XCTUnwrap(error as? GitError) - XCTAssertEqual(fileHandlerError, GitError.couldNotClone(branchOrTag: branch, repository: repository)) + mockLogger.handleDebug = { message, subsystem in + XCTAssertEqual(message, shellResult) + XCTAssertEqual(subsystem, "Git") + loggerDebugExpectation.fulfill() } + + return (mockLogger, [loggerLogExpectation, loggerDebugExpectation]) } } diff --git a/Tests/UnitTests/XcodeToolsTests.swift b/Tests/UnitTests/XcodeToolsTests.swift index d43a31a..4761f4a 100644 --- a/Tests/UnitTests/XcodeToolsTests.swift +++ b/Tests/UnitTests/XcodeToolsTests.swift @@ -9,49 +9,93 @@ import XCTest class XcodeToolsTests: XCTestCase { - func test_archive_swiftPackage() async throws { + func test_archive_swiftPackage_iOS() async throws { let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" - let scheme = "SCHEME" try await testArchiving( projectDirectoryPath: projectDirectoryPath, - scheme: scheme, - projectType: .swiftPackage + projectType: .swiftPackage, + platform: .iOS ) } - func test_archive_xcodeProject() async throws { + func test_archive_xcodeProject_iOS() async throws { let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" - let scheme = "SCHEME" try await testArchiving( projectDirectoryPath: projectDirectoryPath, - scheme: scheme, - projectType: .xcodeProject(scheme: scheme) + projectType: .xcodeProject(scheme: "SCHEME"), + platform: .iOS + ) + } + + func test_archive_swiftPackage_macOS() async throws { + + let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" + + try await testArchiving( + projectDirectoryPath: projectDirectoryPath, + projectType: .swiftPackage, + platform: .macOS + ) + } + + func test_archive_xcodeProject_macOS() async throws { + + let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" + + try await testArchiving( + projectDirectoryPath: projectDirectoryPath, + projectType: .xcodeProject(scheme: "SCHEME"), + platform: .macOS ) } } private extension XcodeToolsTests { + func expectedCommand(projectDirectoryPath: String, scheme: String, projectType: ProjectType, platform: ProjectPlatform) -> String { + + var commandComponents = [ + "cd \(projectDirectoryPath);", + "xcodebuild clean build -scheme \"\(scheme)\"", + "-derivedDataPath .build BUILD_LIBRARY_FOR_DISTRIBUTION=YES" + ] + + switch platform { + case .macOS: + commandComponents += ["-destination \"generic/platform=macOS\""] + case .iOS: + commandComponents += ["-sdk `xcrun --sdk iphonesimulator --show-sdk-path` -destination \"generic/platform=iOS\""] + } + + switch projectType { + case .swiftPackage: + commandComponents += ["-skipPackagePluginValidation"] + case .xcodeProject: + break // Nothing specific to add + } + + return String(commandComponents.joined(separator: " ")) + } + func testArchiving( projectDirectoryPath: String, - scheme: String, - projectType: ProjectType + projectType: ProjectType, + platform: ProjectPlatform ) async throws { + let scheme = "SCHEME" let archiveResult = "ARCHIVE_RESULT" let expectedDerivedDataPath = "\(projectDirectoryPath)/.build" - var expectedHandleExecuteCalls: [String] = { - switch projectType { - case .swiftPackage: - ["cd \(projectDirectoryPath); xcodebuild clean build -scheme \"\(scheme)\" -destination \"generic/platform=iOS\" -derivedDataPath .build -sdk `xcrun --sdk iphonesimulator --show-sdk-path` BUILD_LIBRARY_FOR_DISTRIBUTION=YES -skipPackagePluginValidation"] - case let .xcodeProject(scheme): - ["cd \(projectDirectoryPath); xcodebuild clean build -scheme \"\(scheme)\" -destination \"generic/platform=iOS\" -derivedDataPath .build -sdk `xcrun --sdk iphonesimulator --show-sdk-path` BUILD_LIBRARY_FOR_DISTRIBUTION=YES"] - } - }() + var expectedHandleExecuteCalls: [String] = { [expectedCommand( + projectDirectoryPath: projectDirectoryPath, + scheme: scheme, + projectType: projectType, + platform: platform + )] }() var expectedHandleLogCalls: [(message: String, subsystem: String)] = [ ("📦 Archiving SCHEME from PROJECT_DIRECTORY_PATH", "XcodeTools") ] @@ -93,7 +137,8 @@ private extension XcodeToolsTests { let derivedDataPath = try await xcodeTools.archive( projectDirectoryPath: projectDirectoryPath, scheme: scheme, - projectType: projectType + projectType: projectType, + platform: platform ) XCTAssertEqual(derivedDataPath, expectedDerivedDataPath)