Skip to content

Commit 56d5a67

Browse files
Ckitakishiclaude
andcommitted
Add group parent membership regression tests
The first two tests reproduce multiple parents: a source pointing at the spec directory, and a directory reached first through a file source. The third preserves a skipped subdirectory at the project root when a later source populates it. `expectSingleParents` lives in TestSupport so the fixtures are covered too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 366592b commit 56d5a67

3 files changed

Lines changed: 98 additions & 0 deletions

File tree

‎Sources/TestSupport/TestHelpers.swift‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@ public func unwrap<T>(_ value: T?, file: String = #file, line: Int = #line) thro
2222
}
2323
}
2424

25+
public func expectSingleParents(_ pbxProj: PBXProj, function: String = #function, file: String = #file, line: Int = #line) throws {
26+
func label(_ element: PBXFileElement) -> String {
27+
element.name ?? element.path ?? ""
28+
}
29+
30+
var parentsByElement: [ObjectIdentifier: (element: PBXFileElement, groups: [String])] = [:]
31+
let allGroups: [PBXGroup] = pbxProj.groups + pbxProj.variantGroups + pbxProj.versionGroups
32+
for group in allGroups {
33+
for child in group.children {
34+
parentsByElement[ObjectIdentifier(child), default: (child, [])].groups.append(label(group))
35+
}
36+
}
37+
let duplicates = parentsByElement.values.filter { $0.groups.count > 1 }
38+
if !duplicates.isEmpty {
39+
let description = duplicates
40+
.map { "\(label($0.element).quoted) is a child of \($0.groups.map(\.quoted).joined(separator: " and "))" }
41+
.sorted()
42+
.joined(separator: "\n")
43+
throw failure("Elements with more than one parent group:\n\(description)", function: function, file: file, line: line)
44+
}
45+
}
46+
2547
public func expectError<T: Error>(_ expectedError: T, function: String = #function, file: String = #file, line: Int = #line, _ closure: () throws -> Void) throws where T: CustomStringConvertible {
2648
do {
2749
try closure()

‎Tests/FixtureTests/FixtureTests.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ private func generateXcodeProject(specPath: Path, file: String = #file, line: In
3030
let generator = ProjectGenerator(project: project)
3131
let writer = FileWriter(project: project)
3232
let xcodeProject = try generator.generateXcodeProject(userName: "someUser")
33+
try expectSingleParents(xcodeProject.pbxproj, file: file, line: line)
3334
try writer.writeXcodeProject(xcodeProject)
3435
try writer.writePlists()
3536
}

‎Tests/XcodeGenKitTests/SourceGeneratorTests.swift‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,81 @@ class SourceGeneratorTests: XCTestCase {
8484
try pbxProj.expectFile(paths: ["Sources", "A", "C2.0", "c.swift"], buildPhase: .sources)
8585
}
8686

87+
$0.it("generates a single parent for groups when a source is the base path") {
88+
let directories = """
89+
Module:
90+
- Extension:
91+
- a.swift
92+
- Model:
93+
- b.swift
94+
- c.swift
95+
"""
96+
try createDirectories(directories)
97+
98+
let target = Target(name: "Module", type: .framework, platform: .iOS, sources: ["../Module"])
99+
let project = Project(basePath: directoryPath + "Module", name: "Module", targets: [target])
100+
101+
let generator = PBXProjGenerator(project: project, projectDirectory: directoryPath)
102+
let pbxProj = try generator.generate()
103+
104+
try pbxProj.expectFile(paths: ["Module", "Extension", "a.swift"], buildPhase: .sources)
105+
try pbxProj.expectFile(paths: ["Module", "Model", "b.swift"], buildPhase: .sources)
106+
try pbxProj.expectFile(paths: ["Module", "c.swift"], buildPhase: .sources)
107+
try expectSingleParents(pbxProj)
108+
109+
let mainGroupChildren = try pbxProj.getMainGroup().children.map(\.nameOrPath)
110+
try expect(mainGroupChildren.contains("Extension")) == false
111+
try expect(mainGroupChildren.contains("Model")) == false
112+
}
113+
114+
$0.it("generates a single parent for a group first referenced as a top level file source") {
115+
let directories = """
116+
Sources:
117+
- Pages:
118+
- a.swift
119+
- b.swift
120+
"""
121+
try createDirectories(directories)
122+
123+
// The file source creates Pages at the top level before the directory source reparents it.
124+
let fileTarget = Target(name: "A", type: .application, platform: .iOS, sources: ["Sources/Pages/a.swift"])
125+
let directoryTarget = Target(name: "B", type: .application, platform: .iOS, sources: ["Sources"])
126+
let project = Project(basePath: directoryPath, name: "Test", targets: [fileTarget, directoryTarget])
127+
128+
let pbxProj = try project.generatePbxProj()
129+
130+
try pbxProj.expectFile(paths: ["Sources", "Pages", "a.swift"], buildPhase: .sources)
131+
try pbxProj.expectFile(paths: ["Sources", "b.swift"], buildPhase: .sources)
132+
try expectSingleParents(pbxProj)
133+
134+
let mainGroupChildren = try pbxProj.getMainGroup().children.map(\.nameOrPath)
135+
try expect(mainGroupChildren.contains("Sources/Pages")) == false
136+
}
137+
138+
$0.it("keeps a subdirectory reachable when the base source excludes it and another source adds it") {
139+
let directories = """
140+
Module:
141+
- Resources:
142+
- a.json
143+
- b.swift
144+
"""
145+
try createDirectories(directories)
146+
147+
// The excluded directory is skipped by its parent, then populated through a later source.
148+
let target = Target(name: "Module", type: .framework, platform: .iOS, sources: [
149+
TargetSource(path: "../Module", excludes: ["Resources/**"]),
150+
TargetSource(path: "Resources", buildPhase: .resources),
151+
])
152+
let project = Project(basePath: directoryPath + "Module", name: "Module", targets: [target])
153+
154+
let generator = PBXProjGenerator(project: project, projectDirectory: directoryPath)
155+
let pbxProj = try generator.generate()
156+
157+
try pbxProj.expectFile(paths: ["Module", "b.swift"], buildPhase: .sources)
158+
try pbxProj.expectFile(paths: ["Module/Resources", "a.json"], names: ["Resources", "a.json"], buildPhase: .resources)
159+
try expectSingleParents(pbxProj)
160+
}
161+
87162
$0.it("generates synced folder") {
88163
let directories = """
89164
Sources:

0 commit comments

Comments
 (0)