From 003a46065cb479c57cac7c229dce646dd7a7dbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fo=C5=99t?= Date: Mon, 10 Feb 2025 17:50:02 +0100 Subject: [PATCH] Add support for selective testing for Xcode non-generated projects (#7287) * Add support for selective testing for Xcode projects not generated with Tuist * Update XcodeGraph * Update reference from xctest-dynamic-overlay to swift-issue-reporting in Package.resolved * Add xcode_project_with_tests to a list of fixtures * Add log for which test modules are skipped * Add Tuist.swift for the xcode_project_with_tests fixture * Override acceptance test case arguments for XcodeBuildCommand * Add logs for which targets were skippped only when some were --- Package.resolved | 31 +- Package.swift | 11 +- .../TuistAcceptanceFixtures.swift | 3 + .../TuistAcceptanceTestCase.swift | 9 + .../XcodeBuild/XcodeBuildController.swift | 96 +-- .../SelectiveTestingServicing.swift | 15 + .../Automation/XcodeBuildControlling.swift | 4 + Sources/TuistCore/Graph/GraphTraverser.swift | 1 - .../Graph/MockGraphLoader.swift | 12 - .../Generator/ConfigGenerator.swift | 2 +- .../TuistGenerator/Linter/GraphLinter.swift | 2 +- .../TuistGenerator/Linter/TargetLinter.swift | 4 +- .../GenerateEntitlementsProjectMapper.swift | 2 +- .../GenerateInfoPlistProjectMapper.swift | 4 +- Sources/TuistHasher/GraphContentHasher.swift | 1 - Sources/TuistHasher/PlistContentHasher.swift | 14 +- .../SelectiveTestingGraphHashing.swift | 15 + Sources/TuistKit/Commands/TuistCommand.swift | 1 + .../TuistKit/Commands/XcodeBuildCommand.swift | 55 ++ Sources/TuistKit/Services/TestService.swift | 2 - .../TuistKit/Services/XcodeBuildService.swift | 266 ++++++ .../Services/XcodeBuildServiceTests.swift | 497 ++++++++++++ Tuist/ProjectDescriptionHelpers/Module.swift | 1 + fixtures/AppFramework.xctestplan | 25 + .../Tests/Framework1FileTests.swift | 1 + fixtures/xcode_project_with_tests/.gitignore | 1 + .../App.xcodeproj/project.pbxproj | 767 ++++++++++++++++++ .../xcshareddata/xcschemes/App.xcscheme | 102 +++ .../xcschemes/AppFramework.xcscheme | 71 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../App/Assets.xcassets/Contents.json | 6 + .../App/ContentView.swift | 17 + .../xcode_project_with_tests/App/MyApp.swift | 10 + .../Preview Assets.xcassets/Contents.json | 6 + .../AppFramework/AppFramework.swift | 9 + .../AppFrameworkTests/AppFrameworkTests.swift | 8 + .../AppTests/AppTests.swift | 9 + fixtures/xcode_project_with_tests/Tuist.swift | 9 + 39 files changed, 2042 insertions(+), 93 deletions(-) create mode 100644 Sources/TuistCache/SelectiveTestingServicing.swift delete mode 100644 Sources/TuistCoreTesting/Graph/MockGraphLoader.swift create mode 100644 Sources/TuistHasher/SelectiveTestingGraphHashing.swift create mode 100644 Sources/TuistKit/Commands/XcodeBuildCommand.swift create mode 100644 Sources/TuistKit/Services/XcodeBuildService.swift create mode 100644 Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift create mode 100644 fixtures/AppFramework.xctestplan create mode 100644 fixtures/xcode_project_with_tests/.gitignore create mode 100644 fixtures/xcode_project_with_tests/App.xcodeproj/project.pbxproj create mode 100644 fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme create mode 100644 fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/AppFramework.xcscheme create mode 100644 fixtures/xcode_project_with_tests/App/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 fixtures/xcode_project_with_tests/App/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 fixtures/xcode_project_with_tests/App/Assets.xcassets/Contents.json create mode 100644 fixtures/xcode_project_with_tests/App/ContentView.swift create mode 100644 fixtures/xcode_project_with_tests/App/MyApp.swift create mode 100644 fixtures/xcode_project_with_tests/App/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 fixtures/xcode_project_with_tests/AppFramework/AppFramework.swift create mode 100644 fixtures/xcode_project_with_tests/AppFrameworkTests/AppFrameworkTests.swift create mode 100644 fixtures/xcode_project_with_tests/AppTests/AppTests.swift create mode 100644 fixtures/xcode_project_with_tests/Tuist.swift diff --git a/Package.resolved b/Package.resolved index f850418533e..c99e4ce4b59 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b0472b9536122cb752e6688cb17b4063f73451c1c52c46a6c61d7a40a6d51677", + "originHash" : "1e4d907dfd7f7c760087efd4410e7861f779556f8a1990ea6836b33ba13a4f54", "pins" : [ { "identity" : "aexml", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/Command.git", "state" : { - "revision" : "27270402bfb9cd65f6a8b83bdf59941a8c9ae368", - "version" : "0.8.0" + "revision" : "9d03a95faa94b961edc1cf2c5f4379b0108ee97a", + "version" : "0.12.1" } }, { @@ -91,13 +91,22 @@ "version" : "1.1.3" } }, + { + "identity" : "machokit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/p-x9/MachOKit", + "state" : { + "revision" : "518e8e1aca7ee64b87b08ecec5f7cad2a63b8efd", + "version" : "0.28.0" + } + }, { "identity" : "mockable", "kind" : "remoteSourceControl", "location" : "https://github.com/Kolos65/Mockable.git", "state" : { - "revision" : "a9e5e1d222035567069ed6fff8429c327229b5f6", - "version" : "0.0.11" + "revision" : "203336d0ccb7ff03a8a03db54a4fa18fc2b0c771", + "version" : "0.3.0" } }, { @@ -276,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-service-context", "state" : { - "revision" : "0c62c5b4601d6c125050b5c3a97f20cce881d32b", - "version" : "1.1.0" + "revision" : "8946c930cae601452149e45d31d8ddfac973c3c7", + "version" : "1.2.0" } }, { @@ -330,8 +339,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeGraph.git", "state" : { - "revision" : "c533cf2543dc41428f0118d4f0df5c890307918a", - "version" : "1.3.2" + "revision" : "decf6cc311e8dd238c87501c369b05827bca6a48", + "version" : "1.5.14" } }, { @@ -339,8 +348,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj", "state" : { - "revision" : "6e971133653f069b7699d5fb081e5db1e5f81559", - "version" : "8.26.1" + "revision" : "6f90427e172da66336739801c84b9cef3e17367b", + "version" : "8.26.6" } }, { diff --git a/Package.swift b/Package.swift index a633c3b5561..3a489acdcae 100644 --- a/Package.swift +++ b/Package.swift @@ -85,6 +85,7 @@ let targets: [Target] = [ "TuistCache", .product(name: "Command", package: "Command"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "XcodeGraphMapper", package: "XcodeGraph"), .byName(name: "AnyCodable"), ], swiftSettings: [ @@ -497,10 +498,10 @@ let package = Package( .package(url: "https://github.com/tuist/GraphViz.git", exact: "0.4.2"), .package(url: "https://github.com/SwiftGen/StencilSwiftKit", exact: "2.10.1"), .package(url: "https://github.com/SwiftGen/SwiftGen", exact: "6.6.2"), - .package(url: "https://github.com/tuist/XcodeProj", exact: "8.26.1"), + .package(url: "https://github.com/tuist/XcodeProj", .upToNextMajor(from: "8.26.1")), .package(url: "https://github.com/cpisciotta/xcbeautify", .upToNextMajor(from: "2.20.0")), .package(url: "https://github.com/krzysztofzablocki/Difference.git", from: "1.0.2"), - .package(url: "https://github.com/Kolos65/Mockable.git", exact: "0.0.11"), + .package(url: "https://github.com/Kolos65/Mockable.git", .upToNextMajor(from: "0.0.11")), .package( url: "https://github.com/apple/swift-openapi-runtime", .upToNextMajor(from: "1.5.0") ), @@ -511,11 +512,9 @@ let package = Package( url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMajor(from: "1.0.2") ), .package(url: "https://github.com/tuist/Path", .upToNextMajor(from: "0.3.0")), - .package( - url: "https://github.com/tuist/XcodeGraph.git", exact: "1.3.2" - ), + .package(url: "https://github.com/tuist/XcodeGraph.git", exact: "1.5.14"), .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.7.0")), - .package(url: "https://github.com/tuist/Command.git", exact: "0.8.0"), + .package(url: "https://github.com/tuist/Command.git", .upToNextMajor(from: "0.8.0")), .package(url: "https://github.com/sparkle-project/Sparkle.git", from: "2.6.4"), .package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.1.4")), .package(url: "https://github.com/apple/swift-service-context", .upToNextMajor(from: "1.0.0")), diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift index b90d17d604d..c6d720e4301 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift @@ -100,6 +100,7 @@ public enum TuistAcceptanceFixtures { case workspaceWithInlineFileHeaderTemplate case xcodeApp case xcodeProjectWithRegistryAndAlamofire + case xcodeProjectWithTests case appWithExecutableNonLocalDependencies case appWithGeneratedSources case custom(String) @@ -302,6 +303,8 @@ public enum TuistAcceptanceFixtures { return "xcode_app" case .xcodeProjectWithRegistryAndAlamofire: return "xcode_project_with_registry_and_alamofire" + case .xcodeProjectWithTests: + return "xcode_project_with_tests" case .appWithExecutableNonLocalDependencies: return "app_with_executable_non_local_dependencies" case .appWithGeneratedSources: diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift index 4e04de16006..458069a0e55 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift @@ -198,6 +198,15 @@ open class TuistAcceptanceTestCase: XCTestCase { .first(where: { $0.extension == "xcworkspace" }) } + public func run(_ command: XcodeBuildCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + + public func run(_ command: XcodeBuildCommand.Type, _ arguments: [String] = []) async throws { + let parsedCommand = try command.parse(arguments) + try await parsedCommand.run() + } + public func run(_ command: (some AsyncParsableCommand).Type, _ arguments: String...) async throws { try await run(command, Array(arguments)) } diff --git a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift index 45be3a8e86d..159c984d635 100644 --- a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift +++ b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift @@ -45,25 +45,26 @@ public final class XcodeBuildController: XcodeBuildControlling { arguments: [XcodeBuildArgument], passthroughXcodeBuildArguments: [String] ) async throws { - var command = ["/usr/bin/xcrun", "xcodebuild"] + let extraArguments = arguments.flatMap(\.arguments) + var arguments: [String] = [] // Action if clean { - command.append("clean") + arguments.append("clean") } - command.append("build") + arguments.append("build") // Scheme - command.append(contentsOf: ["-scheme", scheme]) + arguments.append(contentsOf: ["-scheme", scheme]) // Target - command.append(contentsOf: target.xcodebuildArguments) + arguments.append(contentsOf: target.xcodebuildArguments) // Arguments - command.append(contentsOf: arguments.flatMap(\.arguments)) + arguments.append(contentsOf: extraArguments) // Passthrough arguments - command.append(contentsOf: passthroughXcodeBuildArguments) + arguments.append(contentsOf: passthroughXcodeBuildArguments) // Destination switch destination { @@ -72,21 +73,21 @@ public final class XcodeBuildController: XcodeBuildControlling { if rosetta { value += ["arch=x86_64"] } - command.append(contentsOf: ["-destination", value.joined(separator: ",")]) + arguments.append(contentsOf: ["-destination", value.joined(separator: ",")]) case .mac: - command.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) + arguments.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) case .macCatalyst: - command.append(contentsOf: ["-destination", simulatorController.macOSDestination(catalyst: true)]) + arguments.append(contentsOf: ["-destination", simulatorController.macOSDestination(catalyst: true)]) case nil: break } // Derived data path if let derivedDataPath { - command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) + arguments.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } - try await run(command: command) + try await run(arguments: arguments) } public func test( @@ -104,29 +105,30 @@ public final class XcodeBuildController: XcodeBuildControlling { testPlanConfiguration: TestPlanConfiguration?, passthroughXcodeBuildArguments: [String] ) async throws { - var command = ["/usr/bin/xcrun", "xcodebuild"] + let extraArguments = arguments.flatMap(\.arguments) + var arguments: [String] = [] // Action if clean { - command.append("clean") + arguments.append("clean") } - command.append("test") + arguments.append("test") // Scheme - command.append(contentsOf: ["-scheme", scheme]) + arguments.append(contentsOf: ["-scheme", scheme]) // Target - command.append(contentsOf: target.xcodebuildArguments) + arguments.append(contentsOf: target.xcodebuildArguments) // Arguments - command.append(contentsOf: arguments.flatMap(\.arguments)) + arguments.append(contentsOf: extraArguments) // Passthrough arguments - command.append(contentsOf: passthroughXcodeBuildArguments) + arguments.append(contentsOf: passthroughXcodeBuildArguments) // Retry On Failure if retryCount > 0 { - command.append(contentsOf: XcodeBuildArgument.retryCount(retryCount).arguments) + arguments.append(contentsOf: XcodeBuildArgument.retryCount(retryCount).arguments) } // Destination @@ -136,45 +138,45 @@ public final class XcodeBuildController: XcodeBuildControlling { if rosetta { value += ["arch=x86_64"] } - command.append(contentsOf: ["-destination", value.joined(separator: ",")]) + arguments.append(contentsOf: ["-destination", value.joined(separator: ",")]) case .mac: - command.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) + arguments.append(contentsOf: ["-destination", simulatorController.macOSDestination()]) case .macCatalyst: - command.append(contentsOf: ["-destination", simulatorController.macOSDestination(catalyst: true)]) + arguments.append(contentsOf: ["-destination", simulatorController.macOSDestination(catalyst: true)]) case nil: break } // Derived data path if let derivedDataPath { - command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) + arguments.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } // Result bundle path if let resultBundlePath { - command.append(contentsOf: ["-resultBundlePath", resultBundlePath.pathString]) + arguments.append(contentsOf: ["-resultBundlePath", resultBundlePath.pathString]) } for test in testTargets { - command.append(contentsOf: ["-only-testing", test.description]) + arguments.append(contentsOf: ["-only-testing", test.description]) } for test in skipTestTargets { - command.append(contentsOf: ["-skip-testing", test.description]) + arguments.append(contentsOf: ["-skip-testing", test.description]) } if let testPlanConfiguration { - command.append(contentsOf: ["-testPlan", testPlanConfiguration.testPlan]) + arguments.append(contentsOf: ["-testPlan", testPlanConfiguration.testPlan]) for configuration in testPlanConfiguration.configurations { - command.append(contentsOf: ["-only-test-configuration", configuration]) + arguments.append(contentsOf: ["-only-test-configuration", configuration]) } for configuration in testPlanConfiguration.skipConfigurations { - command.append(contentsOf: ["-skip-test-configuration", configuration]) + arguments.append(contentsOf: ["-skip-test-configuration", configuration]) } } - try await run(command: command) + try await run(arguments: arguments) } public func archive( @@ -185,44 +187,44 @@ public final class XcodeBuildController: XcodeBuildControlling { arguments: [XcodeBuildArgument], derivedDataPath: AbsolutePath? ) async throws { - var command = ["/usr/bin/xcrun", "xcodebuild"] + let extraArguments = arguments.flatMap(\.arguments) + var arguments: [String] = [] // Action if clean { - command.append("clean") + arguments.append("clean") } - command.append("archive") + arguments.append("archive") // Scheme - command.append(contentsOf: ["-scheme", scheme]) + arguments.append(contentsOf: ["-scheme", scheme]) // Target - command.append(contentsOf: target.xcodebuildArguments) + arguments.append(contentsOf: target.xcodebuildArguments) // Archive path - command.append(contentsOf: ["-archivePath", archivePath.pathString]) + arguments.append(contentsOf: ["-archivePath", archivePath.pathString]) // Derived data path if let derivedDataPath { - command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) + arguments.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } // Arguments - command.append(contentsOf: arguments.flatMap(\.arguments)) + arguments.append(contentsOf: extraArguments) - try await run(command: command) + try await run(arguments: arguments) } public func createXCFramework( arguments: [String], output: AbsolutePath ) async throws { - var command = ["/usr/bin/xcrun", "xcodebuild", "-create-xcframework"] - command.append(contentsOf: arguments) - command.append(contentsOf: ["-output", output.pathString]) - command.append("-allow-internal-distribution") + var arguments = ["-create-xcframework"] + arguments + arguments.append(contentsOf: ["-output", output.pathString]) + arguments.append("-allow-internal-distribution") - try await run(command: command) + try await run(arguments: arguments) } enum ShowBuildSettingsError: Error { @@ -298,7 +300,7 @@ public final class XcodeBuildController: XcodeBuildControlling { return buildSettingsByTargetName } - fileprivate func run(command: [String]) async throws { + public func run(arguments: [String]) async throws { let logger = ServiceContext.current?.logger func format(_ bytes: [UInt8]) -> String { @@ -321,6 +323,8 @@ public final class XcodeBuildController: XcodeBuildControlling { } } + let command = ["/usr/bin/xcrun", "xcodebuild"] + arguments + logger?.debug("Running xcodebuild command: \(command.joined(separator: " "))") try system.run(command, diff --git a/Sources/TuistCache/SelectiveTestingServicing.swift b/Sources/TuistCache/SelectiveTestingServicing.swift new file mode 100644 index 00000000000..800f87fa15b --- /dev/null +++ b/Sources/TuistCache/SelectiveTestingServicing.swift @@ -0,0 +1,15 @@ +import Foundation +import Mockable +import TuistCore +import XcodeGraph + +@Mockable +public protocol SelectiveTestingServicing { + /// - Returns: Tests that are cached. + func cachedTests( + scheme: Scheme, + graph: Graph, + selectiveTestingHashes: [GraphTarget: String], + selectiveTestingCacheItems: [CacheItem] + ) async throws -> [TestIdentifier] +} diff --git a/Sources/TuistCore/Automation/XcodeBuildControlling.swift b/Sources/TuistCore/Automation/XcodeBuildControlling.swift index 44e932a3075..a95e5d2bb52 100644 --- a/Sources/TuistCore/Automation/XcodeBuildControlling.swift +++ b/Sources/TuistCore/Automation/XcodeBuildControlling.swift @@ -96,4 +96,8 @@ public protocol XcodeBuildControlling { configuration: String, derivedDataPath: AbsolutePath? ) async throws -> [String: XcodeBuildSettings] + + /// Runs `xcodebuild` with passed `arguments` and formats the output + /// - arguments: Arguments to pass to `xcodebuild` + func run(arguments: [String]) async throws } diff --git a/Sources/TuistCore/Graph/GraphTraverser.swift b/Sources/TuistCore/Graph/GraphTraverser.swift index 409da5c6fad..0da1b55f80e 100644 --- a/Sources/TuistCore/Graph/GraphTraverser.swift +++ b/Sources/TuistCore/Graph/GraphTraverser.swift @@ -163,7 +163,6 @@ public class GraphTraverser: GraphTraversing { let target = GraphDependency.target(name: name, path: path) guard let dependencies = graph.dependencies[target] else { return [] } - let targetDependencies = dependencies .compactMap(\.targetDependency) diff --git a/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift b/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift deleted file mode 100644 index f7b4d144bcc..00000000000 --- a/Sources/TuistCoreTesting/Graph/MockGraphLoader.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import TuistCore -import XcodeGraph - -public final class MockGraphLoader: GraphLoading { - public init() {} - - public var loadWorkspaceStub: ((Workspace, [Project]) throws -> (Graph))? - public func loadWorkspace(workspace: Workspace, projects: [Project]) throws -> Graph { - try loadWorkspaceStub?(workspace, projects) ?? Graph.test() - } -} diff --git a/Sources/TuistGenerator/Generator/ConfigGenerator.swift b/Sources/TuistGenerator/Generator/ConfigGenerator.swift index 41739ad8d07..6638dbc7b44 100644 --- a/Sources/TuistGenerator/Generator/ConfigGenerator.swift +++ b/Sources/TuistGenerator/Generator/ConfigGenerator.swift @@ -256,7 +256,7 @@ final class ConfigGenerator: ConfigGenerating { } else { settings["CODE_SIGN_ENTITLEMENTS"] = .string("$(SRCROOT)/\(relativePath)") } - } else if case let .variable(configName) = entitlements { + } else if case let .variable(configName, configuration: _) = entitlements { settings["CODE_SIGN_ENTITLEMENTS"] = .string(configName) } } diff --git a/Sources/TuistGenerator/Linter/GraphLinter.swift b/Sources/TuistGenerator/Linter/GraphLinter.swift index 60d90e2a9d4..ee8e20a8ba5 100644 --- a/Sources/TuistGenerator/Linter/GraphLinter.swift +++ b/Sources/TuistGenerator/Linter/GraphLinter.swift @@ -414,7 +414,7 @@ public class GraphLinter: GraphLinting { } if let entitlements = appClip.target.entitlements { - if case let .file(path: path) = entitlements, try await !fileSystem.exists(path) { + if case let .file(path: path, configuration: _) = entitlements, try await !fileSystem.exists(path) { foundIssues .append(LintingIssue( reason: "The entitlements at path '\(path.pathString)' referenced by target does not exist", diff --git a/Sources/TuistGenerator/Linter/TargetLinter.swift b/Sources/TuistGenerator/Linter/TargetLinter.swift index b29b729f995..f3d635480ca 100644 --- a/Sources/TuistGenerator/Linter/TargetLinter.swift +++ b/Sources/TuistGenerator/Linter/TargetLinter.swift @@ -203,7 +203,7 @@ class TargetLinter: TargetLinting { private func lintInfoplistExists(target: Target) async throws -> [LintingIssue] { var issues: [LintingIssue] = [] if let infoPlist = target.infoPlist, - case let InfoPlist.file(path: path) = infoPlist, + case let InfoPlist.file(path: path, configuration: _) = infoPlist, try await !fileSystem.exists(path) { issues @@ -215,7 +215,7 @@ class TargetLinter: TargetLinting { private func lintEntitlementsExist(target: Target) async throws -> [LintingIssue] { var issues: [LintingIssue] = [] if let entitlements = target.entitlements, - case let Entitlements.file(path: path) = entitlements, + case let Entitlements.file(path: path, configuration: _) = entitlements, try await !fileSystem.exists(path) { issues diff --git a/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift b/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift index 86a2fe96171..3d28d5fb835 100644 --- a/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/GenerateEntitlementsProjectMapper.swift @@ -71,7 +71,7 @@ public final class GenerateEntitlementsProjectMapper: ProjectMapping { entitlements: Entitlements ) -> [String: Any]? { switch entitlements { - case let .dictionary(content): + case let .dictionary(content, _): return content.mapValues { $0.value } default: return nil diff --git a/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift b/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift index 86f7e00e998..d81ce23df71 100644 --- a/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/GenerateInfoPlistProjectMapper.swift @@ -88,9 +88,9 @@ public final class GenerateInfoPlistProjectMapper: ProjectMapping { target: Target ) -> [String: Any]? { switch infoPlist { - case let .dictionary(content): + case let .dictionary(content, _): return content.mapValues { $0.value } - case let .extendingDefault(extended): + case let .extendingDefault(extended, _): if let content = infoPlistContentProvider.content( project: project, target: target, diff --git a/Sources/TuistHasher/GraphContentHasher.swift b/Sources/TuistHasher/GraphContentHasher.swift index cab8f82ce8a..81560b167b4 100644 --- a/Sources/TuistHasher/GraphContentHasher.swift +++ b/Sources/TuistHasher/GraphContentHasher.swift @@ -49,7 +49,6 @@ public struct GraphContentHasher: GraphContentHashing { let hashedPaths: ThreadSafe<[AbsolutePath: String]> = ThreadSafe([:]) let sortedCacheableTargets = try graphTraverser.allTargetsTopologicalSorted() - let hashableTargets = sortedCacheableTargets.compactMap { target -> GraphTarget? in if isHashable( target, diff --git a/Sources/TuistHasher/PlistContentHasher.swift b/Sources/TuistHasher/PlistContentHasher.swift index 07762a35dbd..874f3491b96 100644 --- a/Sources/TuistHasher/PlistContentHasher.swift +++ b/Sources/TuistHasher/PlistContentHasher.swift @@ -25,32 +25,32 @@ public final class PlistContentHasher: PlistContentHashing { switch plist { case let .infoPlist(infoPlist): switch infoPlist { - case let .file(path): + case let .file(path: path, configuration: _): return try await contentHasher.hash(path: path) - case let .dictionary(dictionary), let .extendingDefault(dictionary): + case let .dictionary(dictionary, configuration: _), let .extendingDefault(dictionary, configuration: _): var dictionaryString = "" for key in dictionary.keys.sorted() { let value = dictionary[key, default: "nil"] dictionaryString += "\(key)=\(value);" } return try contentHasher.hash(dictionaryString) - case let .generatedFile(_, data): + case let .generatedFile(_, data, _): return try contentHasher.hash(data) } case let .entitlements(entitlements): switch entitlements { - case let .variable(variable): + case let .variable(variable, configuration: _): return try contentHasher.hash(variable) - case let .file(path): + case let .file(path, configuration: _): return try await contentHasher.hash(path: path) - case let .dictionary(dictionary): + case let .dictionary(dictionary, configuration: _): var dictionaryString = "" for key in dictionary.keys.sorted() { let value = dictionary[key, default: "nil"] dictionaryString += "\(key)=\(value);" } return try contentHasher.hash(dictionaryString) - case let .generatedFile(_, data): + case let .generatedFile(_, data, _): return try contentHasher.hash(data) } } diff --git a/Sources/TuistHasher/SelectiveTestingGraphHashing.swift b/Sources/TuistHasher/SelectiveTestingGraphHashing.swift new file mode 100644 index 00000000000..1578328ec86 --- /dev/null +++ b/Sources/TuistHasher/SelectiveTestingGraphHashing.swift @@ -0,0 +1,15 @@ +import Foundation +import Mockable +import XcodeGraph + +@Mockable +public protocol SelectiveTestingGraphHashing { + /// - Parameters: + /// - graph: Graph to hash. + /// - additionalStrings: Additional strings that should be added to each target hash. + /// - Returns: A dictionary where key is a `GraphTarget` and a value is its hash + func hash( + graph: Graph, + additionalStrings: [String] + ) async throws -> [GraphTarget: String] +} diff --git a/Sources/TuistKit/Commands/TuistCommand.swift b/Sources/TuistKit/Commands/TuistCommand.swift index f4a08af0f3e..c3261f55f3c 100644 --- a/Sources/TuistKit/Commands/TuistCommand.swift +++ b/Sources/TuistKit/Commands/TuistCommand.swift @@ -42,6 +42,7 @@ public struct TuistCommand: AsyncParsableCommand { ScaffoldCommand.self, TestCommand.self, InspectCommand.self, + XcodeBuildCommand.self, ] ), CommandGroup( diff --git a/Sources/TuistKit/Commands/XcodeBuildCommand.swift b/Sources/TuistKit/Commands/XcodeBuildCommand.swift new file mode 100644 index 00000000000..6441c9049ee --- /dev/null +++ b/Sources/TuistKit/Commands/XcodeBuildCommand.swift @@ -0,0 +1,55 @@ +import ArgumentParser +import Foundation +import TuistCache +import TuistCore +import TuistHasher +import TuistServer +import XcodeGraph + +public struct XcodeBuildCommand: AsyncParsableCommand { + public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() + public static var selectiveTestingGraphHasher: SelectiveTestingGraphHashing = EmptySelectiveTestingGraphHasher() + public static var selectiveTestingService: SelectiveTestingServicing = EmptySelectiveTestingService() + + public static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "xcodebuild", + abstract: "tuist xcodebuild extends the xcodebuild CLI with server capabilities such as selective testing or analytics." + ) + } + + public init() {} + + @Argument( + parsing: .captureForPassthrough, + help: "xcodebuild arguments that will be passed through to the xcodebuild CLI." + ) + public var passthroughXcodebuildArguments: [String] = [] + + public func run() async throws { + try await XcodeBuildService( + cacheStorageFactory: Self.cacheStorageFactory, + selectiveTestingGraphHasher: Self.selectiveTestingGraphHasher, + selectiveTestingService: Self.selectiveTestingService + ) + .run( + passthroughXcodebuildArguments: passthroughXcodebuildArguments + ) + } +} + +struct EmptySelectiveTestingGraphHasher: SelectiveTestingGraphHashing { + func hash(graph _: Graph, additionalStrings _: [String]) async throws -> [GraphTarget: String] { + [:] + } +} + +struct EmptySelectiveTestingService: SelectiveTestingServicing { + func cachedTests( + scheme _: Scheme, + graph _: Graph, + selectiveTestingHashes _: [GraphTarget: String], selectiveTestingCacheItems _: [CacheItem] + ) async throws -> [TestIdentifier] { + [] + } +} diff --git a/Sources/TuistKit/Services/TestService.swift b/Sources/TuistKit/Services/TestService.swift index d72705cb6b3..f33b94debe5 100644 --- a/Sources/TuistKit/Services/TestService.swift +++ b/Sources/TuistKit/Services/TestService.swift @@ -418,8 +418,6 @@ final class TestService { // swiftlint:disable:this type_body_length schemes: schemes, testPlanConfiguration: testPlanConfiguration ) - let testTargets = initialTestTargets - .map(\.target.name) await ServiceContext.current?.runMetadataStorage?.update( selectiveTestingCacheItems: initialTestTargets.reduce(into: [:]) { result, element in diff --git a/Sources/TuistKit/Services/XcodeBuildService.swift b/Sources/TuistKit/Services/XcodeBuildService.swift new file mode 100644 index 00000000000..8fbefb444ae --- /dev/null +++ b/Sources/TuistKit/Services/XcodeBuildService.swift @@ -0,0 +1,266 @@ +import FileSystem +import Path +import ServiceContextModule +import TuistAutomation +import TuistCache +import TuistCore +import TuistHasher +import TuistLoader +import TuistServer +import TuistSupport +import XcodeGraph +import XcodeGraphMapper + +enum XcodeBuildServiceError: FatalError, Equatable { + case actionNotSupported + case schemeNotFound(String) + case schemeNotPassed + case testPlanNotFound(testPlan: String, scheme: String) + + var description: String { + switch self { + case .actionNotSupported: + return "The used 'xcodebuild' action is currently not supported. Supported actions are: test, test-without-building." + case let .schemeNotFound(scheme): + return "The scheme \(scheme) was not found in the Xcode project. Make sure it's present." + case .schemeNotPassed: + return "The xcodebuild invocation contains no scheme. Specify one by adding '-scheme MyScheme'." + case let .testPlanNotFound(testPlan: testPlan, scheme: scheme): + return "Test plan \(testPlan) for scheme \(scheme) was not found. Make sure it's present." + } + } + + var type: ErrorType { + switch self { + case .actionNotSupported, .schemeNotFound, .schemeNotPassed, .testPlanNotFound: + return .abort + } + } +} + +struct XcodeBuildService { + private let fileSystem: FileSysteming + private let xcodeGraphMapper: XcodeGraphMapping + private let xcodeBuildController: XcodeBuildControlling + private let configLoader: ConfigLoading + private let cacheStorageFactory: CacheStorageFactorying + private let selectiveTestingGraphHasher: SelectiveTestingGraphHashing + private let selectiveTestingService: SelectiveTestingServicing + + init( + fileSystem: FileSysteming = FileSystem(), + xcodeGraphMapper: XcodeGraphMapping = XcodeGraphMapper(), + xcodeBuildController: XcodeBuildControlling = XcodeBuildController(), + configLoader: ConfigLoading = ConfigLoader(warningController: WarningController.shared), + cacheStorageFactory: CacheStorageFactorying, + selectiveTestingGraphHasher: SelectiveTestingGraphHashing, + selectiveTestingService: SelectiveTestingServicing + ) { + self.fileSystem = fileSystem + self.xcodeGraphMapper = xcodeGraphMapper + self.xcodeBuildController = xcodeBuildController + self.configLoader = configLoader + self.cacheStorageFactory = cacheStorageFactory + self.selectiveTestingGraphHasher = selectiveTestingGraphHasher + self.selectiveTestingService = selectiveTestingService + } + + func run( + passthroughXcodebuildArguments: [String] + ) async throws { + let path = try await path(passthroughXcodebuildArguments: passthroughXcodebuildArguments) + let config = try await configLoader.loadConfig(path: path) + if passthroughXcodebuildArguments.contains("test") || passthroughXcodebuildArguments.contains("test-without-building") { + try await runTests( + passthroughXcodebuildArguments: passthroughXcodebuildArguments, + path: path, + config: config + ) + } else { + throw XcodeBuildServiceError.actionNotSupported + } + } + + private func runTests( + passthroughXcodebuildArguments: [String], + path: AbsolutePath, + config: Config + ) async throws { + let cacheStorage = try await cacheStorageFactory.cacheStorage(config: config) + guard let schemeName = passedValue(for: "-scheme", arguments: passthroughXcodebuildArguments) + else { + throw XcodeBuildServiceError.schemeNotPassed + } + let graph = try await xcodeGraphMapper.map(at: path) + let graphTraverser = GraphTraverser(graph: graph) + guard let scheme = graphTraverser.schemes().first(where: { + $0.name == schemeName + }) else { + throw XcodeBuildServiceError.schemeNotFound(schemeName) + } + let additionalStrings = [ + "-configuration", + "-xcconfig", + "-sdk", + "-skip-test-configuration", + "-only-test-configuration", + "-toolchain", + "-testPlan", + // We can be smarter about these and match those with targets to be tested. + // For now, this is a safe way to ensure we accidentally don't store selective test results for tests that were _not_ + // tested. + "-skip-testing", + "-only-testing", + ] + .compactMap { passedValue(for: $0, arguments: passthroughXcodebuildArguments) } + let selectiveTestingHashes = try await selectiveTestingGraphHasher.hash( + graph: graph, + additionalStrings: additionalStrings + ) + let selectiveTestingCacheItems = try await cacheStorage.fetch( + Set(selectiveTestingHashes.map { CacheStorableItem(name: $0.key.target.name, hash: $0.value) }), + cacheCategory: .selectiveTests + ) + .keys + .map { $0 } + + let skipTestTargets = try await selectiveTestingService.cachedTests( + scheme: scheme, + graph: graph, + selectiveTestingHashes: selectiveTestingHashes, + selectiveTestingCacheItems: selectiveTestingCacheItems + ) + + let testableTargets: [TestableTarget] + if let testPlanName = passedValue(for: "-testPlan", arguments: passthroughXcodebuildArguments) { + guard let testPlan = scheme.testAction?.testPlans?.first(where: { $0.name == testPlanName }) else { + throw XcodeBuildServiceError.testPlanNotFound(testPlan: testPlanName, scheme: scheme.name) + } + testableTargets = testPlan.testTargets + } else { + testableTargets = scheme.testAction?.targets ?? [] + } + let testableGraphTargets = testableGraphTargets(for: testableTargets, graphTraverser: graphTraverser) + let targetTestCacheItems: [AbsolutePath: [String: CacheItem]] = selectiveTestingHashes + .reduce(into: [:]) { result, element in + if let cacheItem = selectiveTestingCacheItems.first(where: { $0.hash == element.value }) { + result[element.key.path, default: [:]][element.key.target.name] = cacheItem + } + } + + if testableTargets + .filter({ testableTarget in !skipTestTargets.contains(where: { $0.target == testableTarget.target.name }) }) + .isEmpty + { + ServiceContext.current?.logger?.info("There are no tests to run, exiting early...") + await updateRunMetadataStorage( + with: testableGraphTargets, + selectiveTestingHashes: selectiveTestingHashes, + targetTestCacheItems: targetTestCacheItems + ) + return + } + + if !skipTestTargets.isEmpty { + ServiceContext.current?.logger? + .info( + "The following targets have not changed since the last successful run and will be skipped: \(Set(skipTestTargets.compactMap(\.target)).joined(separator: ", "))" + ) + } + + let skipTestingArguments = skipTestTargets.map { + "-skip-testing:\($0.target)" + } + + try await xcodeBuildController + .run(arguments: passthroughXcodebuildArguments + skipTestingArguments) + + try await storeTestableGraphTargets( + testableGraphTargets, + selectiveTestingHashes: selectiveTestingHashes, + targetTestCacheItems: targetTestCacheItems, + cacheStorage: cacheStorage + ) + await updateRunMetadataStorage( + with: testableGraphTargets, + selectiveTestingHashes: selectiveTestingHashes, + targetTestCacheItems: targetTestCacheItems + ) + } + + private func updateRunMetadataStorage( + with testableGraphTargets: [GraphTarget], + selectiveTestingHashes: [GraphTarget: String], + targetTestCacheItems: [AbsolutePath: [String: CacheItem]] + ) async { + await ServiceContext.current?.runMetadataStorage?.update( + selectiveTestingCacheItems: testableGraphTargets.reduce(into: [:]) { result, element in + guard let hash = selectiveTestingHashes[element] else { return } + let cacheItem = targetTestCacheItems[element.path]?[element.target.name] ?? CacheItem( + name: element.target.name, + hash: hash, + source: .miss, + cacheCategory: .selectiveTests + ) + result[element.path, default: [:]][element.target.name] = cacheItem + } + ) + } + + private func storeTestableGraphTargets( + _ testableGraphTargets: [GraphTarget], + selectiveTestingHashes: [GraphTarget: String], + targetTestCacheItems: [AbsolutePath: [String: CacheItem]], + cacheStorage: CacheStoring + ) async throws { + let cacheableItems: [CacheStorableItem: [AbsolutePath]] = testableGraphTargets + .filter { + return targetTestCacheItems[$0.path]?[$0.target.name] == nil + } + .compactMap { graphTarget -> (target: Target, hash: String)? in + guard let hash = selectiveTestingHashes[graphTarget] + else { return nil } + return (target: graphTarget.target, hash: hash) + } + .reduce(into: [:]) { acc, element in + acc[CacheStorableItem(name: element.target.name, hash: element.hash)] = [AbsolutePath]() + } + try await cacheStorage.store(cacheableItems, cacheCategory: .selectiveTests) + } + + private func testableGraphTargets( + for testableTargets: [TestableTarget], + graphTraverser: GraphTraversing + ) -> [GraphTarget] { + testableTargets + .compactMap { target in + guard let graphTarget = graphTraverser.targets(at: target.target.projectPath) + .first(where: { $0.target.name == target.target.name }) + else { return nil } + return graphTarget + } + } + + private func path( + passthroughXcodebuildArguments: [String] + ) async throws -> AbsolutePath { + let currentWorkingDirectory = try await fileSystem.currentWorkingDirectory() + if let workspaceOrProjectPath = passedValue(for: "-workspace", arguments: passthroughXcodebuildArguments) ?? + passedValue(for: "-project", arguments: passthroughXcodebuildArguments) + { + return try AbsolutePath(validating: workspaceOrProjectPath, relativeTo: currentWorkingDirectory) + } else { + return currentWorkingDirectory + } + } + + private func passedValue( + for option: String, + arguments: [String] + ) -> String? { + guard let optionIndex = arguments.firstIndex(of: option) else { return nil } + let valueIndex = arguments.index(after: optionIndex) + guard arguments.endIndex > valueIndex else { return nil } + return arguments[valueIndex] + } +} diff --git a/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift b/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift new file mode 100644 index 00000000000..1db26ff655a --- /dev/null +++ b/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift @@ -0,0 +1,497 @@ +import FileSystem +import Foundation +import Logging +import Mockable +import Path +import ServiceContextModule +import Testing +import TuistCache +import TuistCore +import TuistHasher +import TuistLoader +import TuistServer +import XcodeGraph +import protocol XcodeGraphMapper.XcodeGraphMapping + +@testable import TuistKit + +@Suite +struct XcodeBuildServiceTests { + private let fileSystem = FileSystem() + private let xcodeGraphMapper = MockXcodeGraphMapping() + private let xcodeBuildController = MockXcodeBuildControlling() + private let configLoader = MockConfigLoading() + private let cacheStorage = MockCacheStoring() + private let selectiveTestingGraphHasher = MockSelectiveTestingGraphHashing() + private let selectiveTestingService = MockSelectiveTestingServicing() + private let subject: XcodeBuildService + init() { + let cacheStorageFactory = MockCacheStorageFactorying() + given(cacheStorageFactory) + .cacheStorage(config: .any) + .willReturn(cacheStorage) + given(configLoader) + .loadConfig(path: .any) + .willReturn(.test()) + given(xcodeBuildController) + .run(arguments: .any) + .willReturn() + given(cacheStorage) + .store(.any, cacheCategory: .any) + .willReturn() + + subject = XcodeBuildService( + fileSystem: fileSystem, + xcodeGraphMapper: xcodeGraphMapper, + xcodeBuildController: xcodeBuildController, + cacheStorageFactory: cacheStorageFactory, + selectiveTestingGraphHasher: selectiveTestingGraphHasher, + selectiveTestingService: selectiveTestingService + ) + } + + @Test func throwsErrorWhenSchemeNotFound() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { temporaryPath in + // Given + let project: Project = .test( + schemes: [ + .test(name: "DifferentScheme"), + ] + ) + given(xcodeGraphMapper) + .map(at: .any) + .willReturn( + .test( + projects: [ + temporaryPath: project, + ] + ) + ) + + // When / Then + await #expect(throws: XcodeBuildServiceError.schemeNotFound("MyScheme")) { + try await subject.run(passthroughXcodebuildArguments: ["test", "-scheme", "MyScheme"]) + } + } + } + + @Test func throwsErrorWhenSchemeNotPassed() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { _ in + // When / Then + await #expect(throws: XcodeBuildServiceError.schemeNotPassed) { + try await subject.run(passthroughXcodebuildArguments: ["test"]) + } + } + } + + @Test func existsEarlyIfAllTestsAreCached() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { temporaryPath in + var context = ServiceContext.current ?? ServiceContext.topLevel + let runMetadataStorage = RunMetadataStorage() + context.runMetadataStorage = runMetadataStorage + try await ServiceContext.withValue(context) { + // Given + let aUnitTestsTarget: Target = .test(name: "AUnitTests") + let bUnitTestsTarget: Target = .test(name: "BUnitTests") + let project: Project = .test( + path: temporaryPath, + targets: [ + aUnitTestsTarget, + bUnitTestsTarget, + ], + schemes: [ + .test( + name: "App", + testAction: .test( + targets: [ + .test( + target: TargetReference( + projectPath: temporaryPath, + name: "AUnitTests" + ) + ), + .test( + target: TargetReference( + projectPath: temporaryPath, + name: "BUnitTests" + ) + ), + ] + ) + ), + ] + ) + + given(xcodeGraphMapper) + .map(at: .any) + .willReturn( + .test( + projects: [ + temporaryPath: project, + ] + ) + ) + + given(selectiveTestingGraphHasher) + .hash( + graph: .any, + additionalStrings: .any + ) + .willReturn( + [ + GraphTarget( + path: project.path, + target: aUnitTestsTarget, + project: project + ): "hash-a-unit-tests", + GraphTarget( + path: project.path, + target: bUnitTestsTarget, + project: project + ): "hash-b-unit-tests", + ] + ) + given(selectiveTestingService) + .cachedTests( + scheme: .any, + graph: .any, + selectiveTestingHashes: .any, + selectiveTestingCacheItems: .any + ) + .willReturn( + [ + try TestIdentifier(string: "AUnitTests"), + try TestIdentifier(string: "BUnitTests"), + ] + ) + + given(cacheStorage) + .fetch(.any, cacheCategory: .any) + .willReturn( + [ + CacheItem( + name: "AUnitTests", + hash: "hash-a-unit-tests", + source: .local, + cacheCategory: .selectiveTests + ): temporaryPath, + CacheItem( + name: "BUnitTests", + hash: "hash-b-unit-tests", + source: .remote, + cacheCategory: .selectiveTests + ): temporaryPath, + ] + ) + + // When + try await subject.run( + passthroughXcodebuildArguments: [ + "test", + "-scheme", "App", + ] + ) + + // Then + verify(xcodeBuildController) + .run( + arguments: .any + ) + .called(0) + verify(cacheStorage) + .store( + .any, + cacheCategory: .value(.selectiveTests) + ) + .called(0) + await #expect( + runMetadataStorage.selectiveTestingCacheItems == [ + temporaryPath: [ + "AUnitTests": CacheItem( + name: "AUnitTests", + hash: "hash-a-unit-tests", + source: .local, + cacheCategory: .selectiveTests + ), + "BUnitTests": CacheItem( + name: "BUnitTests", + hash: "hash-b-unit-tests", + source: .remote, + cacheCategory: .selectiveTests + ), + ], + ] + ) + } + } + } + + @Test func skipsCachedTests() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { temporaryPath in + var context = ServiceContext.current ?? ServiceContext.topLevel + let runMetadataStorage = RunMetadataStorage() + context.runMetadataStorage = runMetadataStorage + try await ServiceContext.withValue(context) { + // Given + let aUnitTestsTarget: Target = .test(name: "AUnitTests") + let bUnitTestsTarget: Target = .test(name: "BUnitTests") + let project: Project = .test( + path: temporaryPath, + targets: [ + aUnitTestsTarget, + bUnitTestsTarget, + ], + schemes: [ + .test( + name: "App", + testAction: .test( + targets: [ + .test( + target: TargetReference( + projectPath: temporaryPath, + name: "AUnitTests" + ) + ), + .test( + target: TargetReference( + projectPath: temporaryPath, + name: "BUnitTests" + ) + ), + ] + ) + ), + ] + ) + + given(xcodeGraphMapper) + .map(at: .any) + .willReturn( + .test( + projects: [ + temporaryPath: project, + ] + ) + ) + + given(selectiveTestingGraphHasher) + .hash( + graph: .any, + additionalStrings: .any + ) + .willReturn( + [ + GraphTarget( + path: project.path, + target: aUnitTestsTarget, + project: project + ): "hash-a-unit-tests", + GraphTarget( + path: project.path, + target: bUnitTestsTarget, + project: project + ): "hash-b-unit-tests", + ] + ) + given(selectiveTestingService) + .cachedTests( + scheme: .any, + graph: .any, + selectiveTestingHashes: .any, + selectiveTestingCacheItems: .any + ) + .willReturn( + [ + try TestIdentifier(string: "AUnitTests"), + ] + ) + + given(cacheStorage) + .fetch(.any, cacheCategory: .any) + .willReturn( + [ + CacheItem( + name: "AUnitTests", + hash: "hash-a-unit-tests", + source: .local, + cacheCategory: .selectiveTests + ): temporaryPath, + ] + ) + + // When + try await subject.run( + passthroughXcodebuildArguments: [ + "test", + "-scheme", "App", + ] + ) + + // Then + verify(xcodeBuildController) + .run( + arguments: .value( + [ + "test", + "-scheme", "App", + "-skip-testing:AUnitTests", + ] + ) + ) + .called(1) + verify(cacheStorage) + .store( + .value( + [ + CacheStorableItem(name: "BUnitTests", hash: "hash-b-unit-tests"): [AbsolutePath](), + ] + ), + cacheCategory: .value(.selectiveTests) + ) + .called(1) + await #expect( + runMetadataStorage.selectiveTestingCacheItems == [ + temporaryPath: [ + "AUnitTests": CacheItem( + name: "AUnitTests", + hash: "hash-a-unit-tests", + source: .local, + cacheCategory: .selectiveTests + ), + "BUnitTests": CacheItem( + name: "BUnitTests", + hash: "hash-b-unit-tests", + source: .miss, + cacheCategory: .selectiveTests + ), + ], + ] + ) + } + } + } + + @Test func skipsCachedTestsOfCustomTestPlan() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { temporaryPath in + // Given + let aUnitTestsTarget: Target = .test(name: "AUnitTests") + let bUnitTestsTarget: Target = .test(name: "BUnitTests") + let project: Project = .test( + targets: [ + aUnitTestsTarget, + bUnitTestsTarget, + ], + schemes: [ + .test( + name: "App", + testAction: .test( + testPlans: [ + TestPlan( + path: temporaryPath.appending(component: "MyTestPlan.xctestplan"), + testTargets: [ + TestableTarget( + target: TargetReference( + projectPath: temporaryPath, + name: "AUnitTests" + ) + ), + TestableTarget( + target: TargetReference( + projectPath: temporaryPath, + name: "BUnitTests" + ) + ), + ], + isDefault: false + ), + ] + ) + ), + ] + ) + + given(xcodeGraphMapper) + .map(at: .any) + .willReturn( + .test( + projects: [ + temporaryPath: project, + ] + ) + ) + + given(selectiveTestingGraphHasher) + .hash( + graph: .any, + additionalStrings: .any + ) + .willReturn( + [ + GraphTarget( + path: project.path, + target: aUnitTestsTarget, + project: project + ): "hash-a-unit-tests", + GraphTarget( + path: project.path, + target: bUnitTestsTarget, + project: project + ): "hash-b-unit-tests", + ] + ) + given(selectiveTestingService) + .cachedTests( + scheme: .any, + graph: .any, + selectiveTestingHashes: .any, + selectiveTestingCacheItems: .any + ) + .willReturn( + [ + try TestIdentifier(string: "AUnitTests"), + ] + ) + + given(cacheStorage) + .fetch(.any, cacheCategory: .any) + .willReturn( + [ + CacheItem( + name: "AUnitTests", + hash: "hash-a-unit-tests", + source: .local, + cacheCategory: .selectiveTests + ): temporaryPath, + ] + ) + + // When + try await subject.run( + passthroughXcodebuildArguments: [ + "test", + "-scheme", "App", + "-testPlan", "MyTestPlan", + ] + ) + + // Then + verify(xcodeBuildController) + .run( + arguments: .value( + [ + "test", + "-scheme", "App", + "-testPlan", "MyTestPlan", + "-skip-testing:AUnitTests", + ] + ) + ) + .called(1) + } + } +} + +@Mockable +protocol XcodeGraphMapping: XcodeGraphMapper.XcodeGraphMapping { + func map(at path: AbsolutePath) async throws -> Graph +} diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift index 31778f63a8b..b60ea4c9240 100644 --- a/Tuist/ProjectDescriptionHelpers/Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -305,6 +305,7 @@ public enum Module: String, CaseIterable { .external(name: "FileSystem"), .external(name: "SwiftToolsSupport"), .external(name: "XcodeGraph"), + .external(name: "XcodeGraphMapper"), .external(name: "ArgumentParser"), .external(name: "GraphViz"), .external(name: "AnyCodable"), diff --git a/fixtures/AppFramework.xctestplan b/fixtures/AppFramework.xctestplan new file mode 100644 index 00000000000..4d15717f7c7 --- /dev/null +++ b/fixtures/AppFramework.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "B71F83E5-7053-4E26-8B91-EF505CEABD91", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:App.xcodeproj", + "identifier" : "F806E1092D54DD7B0058A673", + "name" : "AppFrameworkTests" + } + } + ], + "version" : 1 +} diff --git a/fixtures/ios_app_with_frameworks/Framework1/Tests/Framework1FileTests.swift b/fixtures/ios_app_with_frameworks/Framework1/Tests/Framework1FileTests.swift index f44e3adcca1..8086614a9e5 100644 --- a/fixtures/ios_app_with_frameworks/Framework1/Tests/Framework1FileTests.swift +++ b/fixtures/ios_app_with_frameworks/Framework1/Tests/Framework1FileTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import Framework1 class Framework1Tests: XCTestCase { diff --git a/fixtures/xcode_project_with_tests/.gitignore b/fixtures/xcode_project_with_tests/.gitignore new file mode 100644 index 00000000000..730b97c271c --- /dev/null +++ b/fixtures/xcode_project_with_tests/.gitignore @@ -0,0 +1 @@ +!App.xcodeproj diff --git a/fixtures/xcode_project_with_tests/App.xcodeproj/project.pbxproj b/fixtures/xcode_project_with_tests/App.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..40535f45d1d --- /dev/null +++ b/fixtures/xcode_project_with_tests/App.xcodeproj/project.pbxproj @@ -0,0 +1,767 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + F806E10B2D54DD7C0058A673 /* AppFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F806E1012D54DD7B0058A673 /* AppFramework.framework */; }; + F806E1162D54DD7C0058A673 /* AppFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F806E1012D54DD7B0058A673 /* AppFramework.framework */; }; + F806E1172D54DD7C0058A673 /* AppFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F806E1012D54DD7B0058A673 /* AppFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F806E0D52D54DD3C0058A673 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F806E0BC2D54DD3B0058A673 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F806E0C32D54DD3B0058A673; + remoteInfo = App; + }; + F806E10C2D54DD7C0058A673 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F806E0BC2D54DD3B0058A673 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F806E1002D54DD7B0058A673; + remoteInfo = AppFramework; + }; + F806E10E2D54DD7C0058A673 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F806E0BC2D54DD3B0058A673 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F806E0C32D54DD3B0058A673; + remoteInfo = App; + }; + F806E1142D54DD7C0058A673 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F806E0BC2D54DD3B0058A673 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F806E1002D54DD7B0058A673; + remoteInfo = AppFramework; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + F806E11C2D54DD7C0058A673 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + F806E1172D54DD7C0058A673 /* AppFramework.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + F806E0C42D54DD3B0058A673 /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F806E0D42D54DD3C0058A673 /* AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F806E1012D54DD7B0058A673 /* AppFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F806E10A2D54DD7B0058A673 /* AppFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F806E1A82D563A0E0058A673 /* AppFramework.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = AppFramework.xctestplan; path = ../AppFramework.xctestplan; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + F806E0C62D54DD3B0058A673 /* App */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = App; + sourceTree = ""; + }; + F806E0D72D54DD3C0058A673 /* AppTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AppTests; + sourceTree = ""; + }; + F806E1022D54DD7B0058A673 /* AppFramework */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AppFramework; + sourceTree = ""; + }; + F806E1102D54DD7C0058A673 /* AppFrameworkTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AppFrameworkTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + F806E0C12D54DD3B0058A673 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F806E1162D54DD7C0058A673 /* AppFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0D12D54DD3C0058A673 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0FE2D54DD7B0058A673 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E1072D54DD7B0058A673 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F806E10B2D54DD7C0058A673 /* AppFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F806E0BB2D54DD3B0058A673 = { + isa = PBXGroup; + children = ( + F806E1A82D563A0E0058A673 /* AppFramework.xctestplan */, + F806E0C62D54DD3B0058A673 /* App */, + F806E0D72D54DD3C0058A673 /* AppTests */, + F806E1022D54DD7B0058A673 /* AppFramework */, + F806E1102D54DD7C0058A673 /* AppFrameworkTests */, + F806E16E2D5511510058A673 /* Frameworks */, + F806E0C52D54DD3B0058A673 /* Products */, + ); + sourceTree = ""; + }; + F806E0C52D54DD3B0058A673 /* Products */ = { + isa = PBXGroup; + children = ( + F806E0C42D54DD3B0058A673 /* App.app */, + F806E0D42D54DD3C0058A673 /* AppTests.xctest */, + F806E1012D54DD7B0058A673 /* AppFramework.framework */, + F806E10A2D54DD7B0058A673 /* AppFrameworkTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F806E16E2D5511510058A673 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + F806E0FC2D54DD7B0058A673 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + F806E0C32D54DD3B0058A673 /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = F806E0E82D54DD3C0058A673 /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + F806E0C02D54DD3B0058A673 /* Sources */, + F806E0C12D54DD3B0058A673 /* Frameworks */, + F806E0C22D54DD3B0058A673 /* Resources */, + F806E11C2D54DD7C0058A673 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + F806E1152D54DD7C0058A673 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + F806E0C62D54DD3B0058A673 /* App */, + ); + name = App; + packageProductDependencies = ( + ); + productName = App; + productReference = F806E0C42D54DD3B0058A673 /* App.app */; + productType = "com.apple.product-type.application"; + }; + F806E0D32D54DD3C0058A673 /* AppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F806E0EB2D54DD3C0058A673 /* Build configuration list for PBXNativeTarget "AppTests" */; + buildPhases = ( + F806E0D02D54DD3C0058A673 /* Sources */, + F806E0D12D54DD3C0058A673 /* Frameworks */, + F806E0D22D54DD3C0058A673 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F806E0D62D54DD3C0058A673 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + F806E0D72D54DD3C0058A673 /* AppTests */, + ); + name = AppTests; + packageProductDependencies = ( + ); + productName = AppTests; + productReference = F806E0D42D54DD3C0058A673 /* AppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F806E1002D54DD7B0058A673 /* AppFramework */ = { + isa = PBXNativeTarget; + buildConfigurationList = F806E1192D54DD7C0058A673 /* Build configuration list for PBXNativeTarget "AppFramework" */; + buildPhases = ( + F806E0FC2D54DD7B0058A673 /* Headers */, + F806E0FD2D54DD7B0058A673 /* Sources */, + F806E0FE2D54DD7B0058A673 /* Frameworks */, + F806E0FF2D54DD7B0058A673 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + F806E1022D54DD7B0058A673 /* AppFramework */, + ); + name = AppFramework; + packageProductDependencies = ( + ); + productName = AppFramework; + productReference = F806E1012D54DD7B0058A673 /* AppFramework.framework */; + productType = "com.apple.product-type.framework"; + }; + F806E1092D54DD7B0058A673 /* AppFrameworkTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F806E11D2D54DD7C0058A673 /* Build configuration list for PBXNativeTarget "AppFrameworkTests" */; + buildPhases = ( + F806E1062D54DD7B0058A673 /* Sources */, + F806E1072D54DD7B0058A673 /* Frameworks */, + F806E1082D54DD7B0058A673 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F806E10D2D54DD7C0058A673 /* PBXTargetDependency */, + F806E10F2D54DD7C0058A673 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + F806E1102D54DD7C0058A673 /* AppFrameworkTests */, + ); + name = AppFrameworkTests; + packageProductDependencies = ( + ); + productName = AppFrameworkTests; + productReference = F806E10A2D54DD7B0058A673 /* AppFrameworkTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F806E0BC2D54DD3B0058A673 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + F806E0C32D54DD3B0058A673 = { + CreatedOnToolsVersion = 16.2; + }; + F806E0D32D54DD3C0058A673 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = F806E0C32D54DD3B0058A673; + }; + F806E1002D54DD7B0058A673 = { + CreatedOnToolsVersion = 16.2; + }; + F806E1092D54DD7B0058A673 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = F806E0C32D54DD3B0058A673; + }; + }; + }; + buildConfigurationList = F806E0BF2D54DD3B0058A673 /* Build configuration list for PBXProject "App" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F806E0BB2D54DD3B0058A673; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = F806E0C52D54DD3B0058A673 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F806E0C32D54DD3B0058A673 /* App */, + F806E0D32D54DD3C0058A673 /* AppTests */, + F806E1002D54DD7B0058A673 /* AppFramework */, + F806E1092D54DD7B0058A673 /* AppFrameworkTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F806E0C22D54DD3B0058A673 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0D22D54DD3C0058A673 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0FF2D54DD7B0058A673 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E1082D54DD7B0058A673 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F806E0C02D54DD3B0058A673 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0D02D54DD3C0058A673 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E0FD2D54DD7B0058A673 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F806E1062D54DD7B0058A673 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F806E0D62D54DD3C0058A673 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F806E0C32D54DD3B0058A673 /* App */; + targetProxy = F806E0D52D54DD3C0058A673 /* PBXContainerItemProxy */; + }; + F806E10D2D54DD7C0058A673 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F806E1002D54DD7B0058A673 /* AppFramework */; + targetProxy = F806E10C2D54DD7C0058A673 /* PBXContainerItemProxy */; + }; + F806E10F2D54DD7C0058A673 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F806E0C32D54DD3B0058A673 /* App */; + targetProxy = F806E10E2D54DD7C0058A673 /* PBXContainerItemProxy */; + }; + F806E1152D54DD7C0058A673 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F806E1002D54DD7B0058A673 /* AppFramework */; + targetProxy = F806E1142D54DD7C0058A673 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + F806E0E62D54DD3C0058A673 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F806E0E72D54DD3C0058A673 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F806E0E92D54DD3C0058A673 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/App", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.App; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F806E0EA2D54DD3C0058A673 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"App/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/App", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.App; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + F806E0EC2D54DD3C0058A673 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/App"; + }; + name = Debug; + }; + F806E0ED2D54DD3C0058A673 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/App"; + }; + name = Release; + }; + F806E11A2D54DD7C0058A673 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppFramework; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + F806E11B2D54DD7C0058A673 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppFramework; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + F806E11E2D54DD7C0058A673 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppFrameworkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/App"; + }; + name = Debug; + }; + F806E11F2D54DD7C0058A673 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = tuist.io.AppFrameworkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/App.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/App"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F806E0BF2D54DD3B0058A673 /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F806E0E62D54DD3C0058A673 /* Debug */, + F806E0E72D54DD3C0058A673 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F806E0E82D54DD3C0058A673 /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F806E0E92D54DD3C0058A673 /* Debug */, + F806E0EA2D54DD3C0058A673 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F806E0EB2D54DD3C0058A673 /* Build configuration list for PBXNativeTarget "AppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F806E0EC2D54DD3C0058A673 /* Debug */, + F806E0ED2D54DD3C0058A673 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F806E1192D54DD7C0058A673 /* Build configuration list for PBXNativeTarget "AppFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F806E11A2D54DD7C0058A673 /* Debug */, + F806E11B2D54DD7C0058A673 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F806E11D2D54DD7C0058A673 /* Build configuration list for PBXNativeTarget "AppFrameworkTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F806E11E2D54DD7C0058A673 /* Debug */, + F806E11F2D54DD7C0058A673 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F806E0BC2D54DD3B0058A673 /* Project object */; +} diff --git a/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme new file mode 100644 index 00000000000..61ec1d9be7c --- /dev/null +++ b/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/AppFramework.xcscheme b/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/AppFramework.xcscheme new file mode 100644 index 00000000000..1ad718c3ee3 --- /dev/null +++ b/fixtures/xcode_project_with_tests/App.xcodeproj/xcshareddata/xcschemes/AppFramework.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fixtures/xcode_project_with_tests/App/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/xcode_project_with_tests/App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_project_with_tests/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/xcode_project_with_tests/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..2305880107d --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_project_with_tests/App/Assets.xcassets/Contents.json b/fixtures/xcode_project_with_tests/App/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_project_with_tests/App/ContentView.swift b/fixtures/xcode_project_with_tests/App/ContentView.swift new file mode 100644 index 00000000000..b000a7e4618 --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/ContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/fixtures/xcode_project_with_tests/App/MyApp.swift b/fixtures/xcode_project_with_tests/App/MyApp.swift new file mode 100644 index 00000000000..7cb2ea61064 --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/MyApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/fixtures/xcode_project_with_tests/App/Preview Content/Preview Assets.xcassets/Contents.json b/fixtures/xcode_project_with_tests/App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/xcode_project_with_tests/App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/xcode_project_with_tests/AppFramework/AppFramework.swift b/fixtures/xcode_project_with_tests/AppFramework/AppFramework.swift new file mode 100644 index 00000000000..3610956dec6 --- /dev/null +++ b/fixtures/xcode_project_with_tests/AppFramework/AppFramework.swift @@ -0,0 +1,9 @@ +import Foundation + +public final class AppFramework { + public init() {} + + public func hello() -> String { + "AppFramework.hello()" + } +} diff --git a/fixtures/xcode_project_with_tests/AppFrameworkTests/AppFrameworkTests.swift b/fixtures/xcode_project_with_tests/AppFrameworkTests/AppFrameworkTests.swift new file mode 100644 index 00000000000..3ac3305601e --- /dev/null +++ b/fixtures/xcode_project_with_tests/AppFrameworkTests/AppFrameworkTests.swift @@ -0,0 +1,8 @@ +import Testing +@testable import AppFramework + +struct AppFrameworkTests { + @Test func example() async throws { + #expect(AppFramework().hello() == "AppFramework.hello()") + } +} diff --git a/fixtures/xcode_project_with_tests/AppTests/AppTests.swift b/fixtures/xcode_project_with_tests/AppTests/AppTests.swift new file mode 100644 index 00000000000..a2a1d092c35 --- /dev/null +++ b/fixtures/xcode_project_with_tests/AppTests/AppTests.swift @@ -0,0 +1,9 @@ +import AppFramework +import Testing +@testable import App + +struct AppTests { + @Test func example() async throws { + #expect(AppFramework().hello() == "AppFramework.hello()") + } +} diff --git a/fixtures/xcode_project_with_tests/Tuist.swift b/fixtures/xcode_project_with_tests/Tuist.swift new file mode 100644 index 00000000000..6c2d16f76be --- /dev/null +++ b/fixtures/xcode_project_with_tests/Tuist.swift @@ -0,0 +1,9 @@ +import ProjectDescription + +let config = Config( + fullHandle: "tuist/xcode_project_with_tests", + url: "https://canary.tuist.dev", + generationOptions: .options( + optionalAuthentication: true + ) +)