diff --git a/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift b/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift index b5518349..fd02efa7 100644 --- a/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift @@ -86,12 +86,18 @@ struct PBXProjectMapper: PBXProjectMapping { // Map user and shared schemes let schemeMapper = XCSchemeMapper() let graphType: XcodeMapperGraphType = .project(xcodeProj) - let userSchemes = try xcodeProj.userData.flatMap(\.schemes).map { - try schemeMapper.map($0, shared: false, graphType: graphType) + var userSchemes: [Scheme] = [] + for scheme in try xcodeProj.userData.flatMap(\.schemes) { + userSchemes.append( + try await schemeMapper.map(scheme, shared: false, graphType: graphType) + ) + } + var sharedSchemes: [Scheme] = [] + for scheme in try (xcodeProj.sharedData?.schemes ?? []) { + sharedSchemes.append( + try await schemeMapper.map(scheme, shared: true, graphType: graphType) + ) } - let sharedSchemes = try xcodeProj.sharedData?.schemes.map { - try schemeMapper.map($0, shared: true, graphType: graphType) - } ?? [] let schemes = userSchemes + sharedSchemes // Other project-level metadata diff --git a/Sources/XcodeGraphMapper/Mappers/Schemes/XCSchemeMapper.swift b/Sources/XcodeGraphMapper/Mappers/Schemes/XCSchemeMapper.swift index 3337bad2..fdef8357 100644 --- a/Sources/XcodeGraphMapper/Mappers/Schemes/XCSchemeMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Schemes/XCSchemeMapper.swift @@ -1,3 +1,4 @@ +import FileSystem import Foundation import Path import XcodeGraph @@ -20,7 +21,7 @@ protocol SchemeMapping { _ xcscheme: XCScheme, shared: Bool, graphType: XcodeMapperGraphType - ) throws -> Scheme + ) async throws -> Scheme } /// A mapper responsible for converting an `XCScheme` object into a `Scheme` model. @@ -28,19 +29,28 @@ protocol SchemeMapping { /// `XCSchemeMapper` resolves references to targets, environment variables, and all scheme actions. /// The resulting `Scheme` models enable analysis, code generation, or integration with custom tooling. struct XCSchemeMapper: SchemeMapping { + private let fileSystem: FileSysteming + private let jsonDecoder = JSONDecoder() + + init( + fileSystem: FileSysteming = FileSystem() + ) { + self.fileSystem = fileSystem + } + // MARK: - Public API func map( _ xcscheme: XCScheme, shared: Bool, graphType: XcodeMapperGraphType - ) throws -> Scheme { + ) async throws -> Scheme { Scheme( name: xcscheme.name, shared: shared, hidden: false, buildAction: try mapBuildAction(action: xcscheme.buildAction, graphType: graphType), - testAction: try mapTestAction(action: xcscheme.testAction, graphType: graphType), + testAction: try await mapTestAction(action: xcscheme.testAction, graphType: graphType), runAction: try mapRunAction(action: xcscheme.launchAction, graphType: graphType), archiveAction: try mapArchiveAction(action: xcscheme.archiveAction), profileAction: try mapProfileAction(action: xcscheme.profileAction, graphType: graphType), @@ -74,7 +84,7 @@ struct XCSchemeMapper: SchemeMapping { private func mapTestAction( action: XCScheme.TestAction?, graphType: XcodeMapperGraphType - ) throws -> TestAction? { + ) async throws -> TestAction? { guard let action else { return nil } let testTargets = try action.testables.compactMap { testable in @@ -91,6 +101,16 @@ struct XCSchemeMapper: SchemeMapping { ) let diagnosticsOptions = SchemeDiagnosticsOptions(action: action) + var testPlans: [TestPlan]? + if let actionTestPlans = action.testPlans { + testPlans = [] + for testPlan in actionTestPlans { + testPlans?.append( + try await mapTestPlan(testPlan, graphType: graphType) + ) + } + } + return TestAction( targets: testTargets, arguments: arguments, @@ -103,10 +123,61 @@ struct XCSchemeMapper: SchemeMapping { postActions: [], diagnosticsOptions: diagnosticsOptions, language: action.language, - region: action.region + region: action.region, + testPlans: testPlans ) } + private func mapTestPlan( + _ testPlan: XCScheme.TestPlanReference, + graphType: XcodeMapperGraphType + ) async throws -> TestPlan { + let testPlanPath = try containerPath(from: testPlan.reference, graphType: graphType) + let xctestPlan: XCTestPlan = try await fileSystem.readJSONFile(at: testPlanPath) + + return TestPlan( + path: testPlanPath, + testTargets: try xctestPlan.testTargets.map { + let parallelization: TestableTarget.Parallelization = switch $0.parallelizable { + case .none: + .swiftTestingOnly + case .some(true): + .all + case .some(false): + .none + } + + return TestableTarget( + target: TargetReference( + projectPath: try containerPath( + from: $0.target.containerPath, + graphType: graphType + ).parentDirectory, + name: $0.target.name + ), + parallelization: parallelization + ) + }, + isDefault: testPlan.default + ) + } + + private func containerPath( + from containerReference: String, + graphType: XcodeMapperGraphType + ) throws -> AbsolutePath { + let relativeContainerPath = try RelativePath(validating: containerReference.replacingOccurrences( + of: "container:", + with: "" + )) + switch graphType { + case let .workspace(xcworkspace): + return xcworkspace.workspacePath.parentDirectory.appending(relativeContainerPath) + case let .project(xcodeProj): + return xcodeProj.projectPath.parentDirectory.appending(relativeContainerPath) + } + } + /// Maps the optional run (launch) action into a domain `RunAction`, or returns `nil` if not present. private func mapRunAction( action: XCScheme.LaunchAction?, diff --git a/Sources/XcodeGraphMapper/Mappers/Schemes/XCTestPlan.swift b/Sources/XcodeGraphMapper/Mappers/Schemes/XCTestPlan.swift new file mode 100644 index 00000000..6c709d9e --- /dev/null +++ b/Sources/XcodeGraphMapper/Mappers/Schemes/XCTestPlan.swift @@ -0,0 +1,16 @@ +import Foundation + +struct XCTestPlan: Codable { + struct TestTarget: Codable { + let parallelizable: Bool? + let target: TestTargetReference + } + + struct TestTargetReference: Codable { + let containerPath: String + let identifier: String + let name: String + } + + let testTargets: [TestTarget] +} diff --git a/Sources/XcodeGraphMapper/Mappers/Workspace/XCWorkspaceMapper.swift b/Sources/XcodeGraphMapper/Mappers/Workspace/XCWorkspaceMapper.swift index db6e15be..6d892b36 100644 --- a/Sources/XcodeGraphMapper/Mappers/Workspace/XCWorkspaceMapper.swift +++ b/Sources/XcodeGraphMapper/Mappers/Workspace/XCWorkspaceMapper.swift @@ -46,7 +46,7 @@ struct XCWorkspaceMapper: WorkspaceMapping { ) let workspaceName = xcWorkspacePath.basenameWithoutExt - let schemes = try mapSchemes(from: xcworkspace) + let schemes = try await mapSchemes(from: xcworkspace) let generationOptions = Workspace.GenerationOptions( enableAutomaticXcodeSchemes: nil, @@ -109,7 +109,7 @@ struct XCWorkspaceMapper: WorkspaceMapping { /// /// - Parameter xcworkspace: The workspace whose schemes should be mapped. /// - Returns: An array of `Scheme` instances for all shared schemes in the workspace. - private func mapSchemes(from xcworkspace: XCWorkspace) throws -> [Scheme] { + private func mapSchemes(from xcworkspace: XCWorkspace) async throws -> [Scheme] { let srcPath = xcworkspace.workspacePath.parentDirectory let sharedDataPath = Path(srcPath.pathString) + "xcshareddata/xcschemes" @@ -118,13 +118,17 @@ struct XCWorkspaceMapper: WorkspaceMapping { } let schemePaths = try sharedDataPath.children().filter { $0.extension == "xcscheme" } - return try schemePaths.map { schemePath in + var schemes: [Scheme] = [] + for schemePath in try schemePaths { let xcscheme = try XCScheme(path: schemePath) - return try schemeMapper.map( - xcscheme, - shared: true, - graphType: .workspace(xcworkspace) + schemes.append( + try await schemeMapper.map( + xcscheme, + shared: true, + graphType: .workspace(xcworkspace) + ) ) } + return schemes } } diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift index 6ebc6cdf..8572fef3 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift @@ -1,4 +1,5 @@ import AEXML +import FileSystem import Path import Testing import XcodeGraph @@ -7,10 +8,11 @@ import XcodeGraph @testable import XcodeProj @Suite -struct XCSchemeMapperTests { - let xcodeProj: XcodeProj - let mapper: XCSchemeMapper - let graphType: XcodeMapperGraphType +struct XCSchemeMapperTests: Sendable { + private let xcodeProj: XcodeProj + private let mapper: XCSchemeMapper + private let graphType: XcodeMapperGraphType + private let fileSystem = FileSystem() init() async throws { xcodeProj = try await XcodeProj.test() @@ -24,7 +26,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "SharedScheme") // When - let scheme = try mapper.map(xcscheme, shared: true, graphType: graphType) + let scheme = try await mapper.map(xcscheme, shared: true, graphType: graphType) // Then #expect(scheme.name == "SharedScheme") @@ -37,7 +39,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme") // When - let scheme = try mapper.map(xcscheme, shared: false, graphType: graphType) + let scheme = try await mapper.map(xcscheme, shared: false, graphType: graphType) // Then #expect(scheme.name == "UserScheme") @@ -66,7 +68,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", buildAction: buildAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.buildAction // Then #expect(mappedAction != nil) @@ -111,7 +113,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", testAction: testAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.testAction // Then @@ -126,6 +128,99 @@ struct XCSchemeMapperTests { #expect(mappedAction?.region == "US") } + @Test("Maps a test action with test plans") + func testMapTestActionWithTestPlans() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XCSchemeMapperTests") { temporaryPath in + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "AppTests.xctest", + blueprintName: "AppTests" + ) + let testableEntry = XCScheme.TestableReference.test( + skipped: false, + buildableReference: targetRef + ) + let testPlan = XCTestPlan( + testTargets: [ + XCTestPlan.TestTarget( + parallelizable: nil, + target: XCTestPlan.TestTargetReference( + containerPath: "container:App.xcodeproj", + identifier: "AppTests", + name: "AppTests" + ) + ), + XCTestPlan.TestTarget( + parallelizable: true, + target: XCTestPlan.TestTargetReference( + containerPath: "container:Library", + identifier: "LibraryTests", + name: "LibraryTests" + ) + ), + ] + ) + let testPlanPath = temporaryPath.appending(component: "App.xctestplan") + try await fileSystem.writeAsJSON( + testPlan, + at: testPlanPath + ) + let testAction = XCScheme.TestAction( + buildConfiguration: "Debug", + macroExpansion: nil, + testables: [testableEntry], + testPlans: [ + XCScheme.TestPlanReference( + reference: "container:App.xctestplan", + default: true + ), + ], + codeCoverageEnabled: true, + commandlineArguments: XCScheme.CommandLineArguments(arguments: []), + environmentVariables: [], + language: "en", + region: "US" + ) + let xcscheme = XCScheme.test(name: "UserScheme", testAction: testAction) + + // When + let mapped = try await mapper.map( + xcscheme, + shared: false, + graphType: .project(.test(path: temporaryPath.appending(component: "App.xcodeproj"))) + ) + let mappedAction = mapped.testAction + + // Then + #expect( + mappedAction?.testPlans == [ + TestPlan( + path: testPlanPath, + testTargets: [ + TestableTarget( + target: TargetReference( + projectPath: temporaryPath, + name: "AppTests" + ), + parallelization: .swiftTestingOnly + ), + TestableTarget( + target: TargetReference( + projectPath: temporaryPath, + name: "LibraryTests" + ), + parallelization: .all + ), + ], + isDefault: true + ), + ] + ) + } + } + @Test("Maps a run action with environment variables and launch arguments") func testMapRunAction() async throws { // Given @@ -154,7 +249,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", launchAction: launchAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.runAction // Then @@ -177,7 +272,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", archiveAction: archiveAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.archiveAction // Then @@ -204,7 +299,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", profileAction: profileAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.profileAction // Then @@ -221,7 +316,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", analyzeAction: analyzeAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = mapped.analyzeAction // Then @@ -250,7 +345,7 @@ struct XCSchemeMapperTests { let xcscheme = XCScheme.test(name: "UserScheme", buildAction: buildAction) // When - let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mapped = try await mapper.map(xcscheme, shared: false, graphType: graphType) let mappedAction = try #require(mapped.buildAction) // Then @@ -272,7 +367,7 @@ struct XCSchemeMapperTests { ) // When - let mapped = try mapper.map(scheme, shared: true, graphType: graphType) + let mapped = try await mapper.map(scheme, shared: true, graphType: graphType) // Then #expect(mapped.buildAction == nil) diff --git a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift index 7cf73fbe..cd122b0d 100644 --- a/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift +++ b/Tests/XcodeGraphMapperTests/MapperTests/Target/PBXTargetMapperTests.swift @@ -446,7 +446,7 @@ struct PBXTargetMapperTests: Sendable { let mapper = PBXTargetMapper() // When - let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj, projectNativeTargets: [:]) // Then #expect( @@ -491,7 +491,7 @@ struct PBXTargetMapperTests: Sendable { let mapper = PBXTargetMapper() // When - let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj, projectNativeTargets: [:]) // Then #expect(