Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Sources/XcodeGraphMapper/Mappers/Project/PBXProjectMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 76 additions & 5 deletions Sources/XcodeGraphMapper/Mappers/Schemes/XCSchemeMapper.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FileSystem
import Foundation
import Path
import XcodeGraph
Expand All @@ -20,27 +21,36 @@ 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.
///
/// `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),
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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?,
Expand Down
16 changes: 16 additions & 0 deletions Sources/XcodeGraphMapper/Mappers/Schemes/XCTestPlan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

struct XCTestPlan: Codable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this go in the XcodeGraph module along with the other models?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this model is just a helper to decode the .xctestplan file which we then convert into the XcodeGraph.TestPlan primitive.

struct TestTarget: Codable {
let parallelizable: Bool?
let target: TestTargetReference
}

struct TestTargetReference: Codable {
let containerPath: String
let identifier: String
let name: String
}

let testTargets: [TestTarget]
}
18 changes: 11 additions & 7 deletions Sources/XcodeGraphMapper/Mappers/Workspace/XCWorkspaceMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"

Expand All @@ -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
}
}
Loading