From ce220a83116f45e3e1711afa100ae425554796dc Mon Sep 17 00:00:00 2001 From: Max Seelemann Date: Thu, 12 Mar 2026 10:18:26 +0100 Subject: [PATCH 1/4] Update scheme serialization to match Xcode 26.3 format - Reorder TestAction children: MacroExpansion, CommandLineArguments, EnvironmentVariables now precede TestPlans and Testables - Reorder LaunchAction children: CommandLineArguments and EnvironmentVariables now precede LocationScenarioReference - Omit empty Testables and CommandLineArguments elements instead of writing empty XML tags - Only write runPostActionsOnFailure, onlyGenerateCoverageForSpecifiedTargets, and useTestSelectionWhitelist attributes when true (Xcode strips the redundant "NO" default) - Fix useTestSelectionWhitelist parsing to distinguish absent (nil) from explicitly set to "NO" - Add TestPlanReference to attribute ordering (reference before default) - Add customLaunchCommand to LaunchAction attribute ordering (between debugDocumentVersioning and debugServiceExtension) - Update test fixtures to match new serialization output Made-with: Cursor --- .../xcshareddata/xcschemes/iOS.xcscheme | 49 +++++++++---------- .../xcschemes/iOS-debug.xcscheme | 2 - .../xcschemes/iOS-other.xcscheme | 2 - .../xcschemes/iOS-release.xcscheme | 2 - .../xcschemes/iOSTests.xcscheme | 3 +- .../xcschemes/custom-scheme.xcscheme | 2 - .../Extensions/AEXML+XcodeFormat.swift | 5 ++ .../Scheme/XCScheme+BuildAction.swift | 4 +- .../Scheme/XCScheme+LaunchAction.swift | 16 +++--- .../Scheme/XCScheme+TestAction.swift | 32 ++++++------ .../Scheme/XCScheme+TestableReference.swift | 6 +-- .../XcodeProjTests/Scheme/XCSchemeTests.swift | 2 +- 12 files changed, 61 insertions(+), 64 deletions(-) diff --git a/Fixtures/iOS/Project.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme b/Fixtures/iOS/Project.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme index 5ec735651..7cc8ee447 100644 --- a/Fixtures/iOS/Project.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme +++ b/Fixtures/iOS/Project.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme @@ -81,12 +81,6 @@ - - - - - - - - - - + + + + + + + + + + + allowLocationSimulation = "YES"> @@ -180,10 +179,6 @@ ReferencedContainer = "container:Project.xcodeproj"> - - + + diff --git a/Fixtures/iOS/Project.xcodeproj/xcuserdata/username1.xcuserdatad/xcschemes/iOS-debug.xcscheme b/Fixtures/iOS/Project.xcodeproj/xcuserdata/username1.xcuserdatad/xcschemes/iOS-debug.xcscheme index 7cb38d44f..04d801fc8 100644 --- a/Fixtures/iOS/Project.xcodeproj/xcuserdata/username1.xcuserdatad/xcschemes/iOS-debug.xcscheme +++ b/Fixtures/iOS/Project.xcodeproj/xcuserdata/username1.xcuserdatad/xcschemes/iOS-debug.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - + skipped = "NO"> - - Date: Thu, 12 Mar 2026 10:50:49 +0100 Subject: [PATCH 2/4] Update pbxproj serialization to match Xcode 26.3 format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `xctestplan` → `text` to the file type table so `.xctestplan` references get `lastKnownFileType = text` - Skip serializing `explicitFileTypes` and `explicitFolders` on `PBXFileSystemSynchronizedRootGroup` when empty - Omit `name` from `PBXFileSystemSynchronizedRootGroup` when it is identical to `path`, matching Xcode's normalization Made-with: Cursor --- .../Objects/Files/PBXFileSystemSynchronizedRootGroup.swift | 7 +++++-- Sources/XcodeProj/Project/Xcode.swift | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift index 43e090eff..203e7409d 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift @@ -93,14 +93,17 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { .string(CommentedString(exception.reference.value, comment: type(of: exception).isa)) }) } - if let explicitFileTypes { + if let explicitFileTypes, !explicitFileTypes.isEmpty { dictionary["explicitFileTypes"] = .dictionary(Dictionary(uniqueKeysWithValues: explicitFileTypes.map { relativePath, fileType in (CommentedString(relativePath), .string(CommentedString(fileType))) })) } - if let explicitFolders { + if let explicitFolders, !explicitFolders.isEmpty { dictionary["explicitFolders"] = .array(explicitFolders.map { .string(CommentedString($0)) }) } + if name == path { + dictionary["name"] = nil + } return (key: CommentedString(reference, comment: name ?? path), value: .dictionary(dictionary)) diff --git a/Sources/XcodeProj/Project/Xcode.swift b/Sources/XcodeProj/Project/Xcode.swift index 44ab18058..cd0b3e51e 100644 --- a/Sources/XcodeProj/Project/Xcode.swift +++ b/Sources/XcodeProj/Project/Xcode.swift @@ -328,6 +328,7 @@ public enum Xcode { "xcsynspec": "text.plist.xcsynspec", "xctarget": "wrapper.pb-target", "xctest": "wrapper.cfbundle", + "xctestplan": "text", "xctxtmacro": "text.plist.xctxtmacro", "xcworkspace": "wrapper.workspace", "xhtml": "text.xml", From b7cb4cf90916f15f5e66d791beea47860a6faff3 Mon Sep 17 00:00:00 2001 From: Max Seelemann Date: Thu, 12 Mar 2026 11:14:56 +0100 Subject: [PATCH 3/4] Generate descriptive plist comments for PBXFileSystemSynchronizedExceptionSet classes Introduce a `plistComment` property on `PBXFileSystemSynchronizedExceptionSet` (base, returns ISA) and override it in subclasses to produce Xcode 26.3's format: `Exceptions for "" folder in "" target`. The back-reference to the owning root group is established via an `assignParentToChildren()` override, consistent with how `PBXFileElement.parent` is wired up for the group hierarchy. Made-with: Cursor --- .../Objects/BuildPhase/PBXBuildPhase.swift | 23 +++++++++++++++++++ .../Objects/Files/PBXFileElement.swift | 20 ++++++++-------- ...temSynchronizedBuildFileExceptionSet.swift | 7 +++++- ...BXFileSystemSynchronizedExceptionSet.swift | 8 ++++++- ...roupBuildPhaseMembershipExceptionSet.swift | 10 +++++++- .../PBXFileSystemSynchronizedRootGroup.swift | 7 +++++- 6 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXBuildPhase.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXBuildPhase.swift index 953be5c8b..dc2bf7b6c 100644 --- a/Sources/XcodeProj/Objects/BuildPhase/PBXBuildPhase.swift +++ b/Sources/XcodeProj/Objects/BuildPhase/PBXBuildPhase.swift @@ -144,6 +144,29 @@ public extension PBXBuildPhase { buildPhase } + /// Returns the target this build phase belongs to, if any. + /// + /// - Returns: the owning target, or nil if not found. + func target() -> PBXTarget? { + guard let projectObjects = try? objects() else { return nil } + let allTargets: [PBXTarget] = Array(projectObjects.nativeTargets.values) + + Array(projectObjects.legacyTargets.values) + + Array(projectObjects.aggregateTargets.values) + return allTargets.first { $0.buildPhaseReferences.map(\.value).contains(reference.value) } + } + + /// Human-readable display name, used in descriptive comment strings. + /// + /// - Returns: display name matching Xcode's comment format. + func displayName() -> String? { + if let phase = self as? PBXCopyFilesBuildPhase { + return phase.name ?? "Copy Files" + } else if let phase = self as? PBXShellScriptBuildPhase { + return phase.name ?? "Run Script" + } + return name() + } + /// Build phase name. /// /// - Returns: build phase name. diff --git a/Sources/XcodeProj/Objects/Files/PBXFileElement.swift b/Sources/XcodeProj/Objects/Files/PBXFileElement.swift index 5f3e80dca..6d623dc50 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileElement.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileElement.swift @@ -139,6 +139,16 @@ public class PBXFileElement: PBXContainerItem, PlistSerializable { guard let rhs = object as? PBXFileElement else { return false } return isEqual(to: rhs) } + + // This method is needed to recursively set the parent to all elements. + // This allows us to more quickly find the full path to the elements. + func assignParentToChildren() { + guard let group = self as? PBXGroup else { return } + for child in group.children { + child.parent = self + child.assignParentToChildren() + } + } } // MARK: - Helpers @@ -206,14 +216,4 @@ public extension PBXFileElement { .first(where: { $0.name == "Base" }) else { return nil } return baseReference.path } - - // This method is needed to recursively set the parent to all elements. - // This allows us to more quickly find the full path to the elements. - func assignParentToChildren() { - guard let group = self as? PBXGroup else { return } - for child in group.children { - child.parent = self - child.assignParentToChildren() - } - } } diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift index e007bb879..d1623ded4 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift @@ -96,6 +96,11 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr // MARK: - PlistSerializable + override var plistComment: String { + let folder = synchronizedRootGroup?.fileName() ?? "" + return "Exceptions for \"\(folder)\" folder in \"\(target.name)\" target" + } + func plistKeyAndValue(proj _: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { var dictionary: [CommentedString: PlistValue] = [:] dictionary["isa"] = .string(CommentedString(PBXFileSystemSynchronizedBuildFileExceptionSet.isa)) @@ -124,6 +129,6 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr })) } dictionary["target"] = .string(CommentedString(target.reference.value, comment: target.name)) - return (key: CommentedString(reference, comment: "PBXFileSystemSynchronizedBuildFileExceptionSet"), value: .dictionary(dictionary)) + return (key: CommentedString(reference, comment: plistComment), value: .dictionary(dictionary)) } } diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift index 095bd24e9..7b141471a 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift @@ -1,4 +1,10 @@ import Foundation /// Common class for exception sets, such as `PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet` and `PBXFileSystemSynchronizedBuildFileExceptionSet` -public class PBXFileSystemSynchronizedExceptionSet: PBXObject {} +public class PBXFileSystemSynchronizedExceptionSet: PBXObject { + /// The synchronized root group this exception set belongs to. + public internal(set) weak var synchronizedRootGroup: PBXFileSystemSynchronizedRootGroup? + + /// The comment string used when this object is referenced in a plist. + var plistComment: String { type(of: self).isa } +} diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift index b2326893e..d18671fcb 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift @@ -66,6 +66,14 @@ public class PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: PBX // MARK: - PlistSerializable + override var plistComment: String { + guard let group = synchronizedRootGroup else { return type(of: self).isa } + let folder = group.fileName() ?? "" + let phase = buildPhase.displayName() ?? "" + let target = buildPhase.target()?.name ?? "" + return "Exceptions for \"\(folder)\" folder in \"\(phase)\" phase from \"\(target)\" target" + } + func plistKeyAndValue(proj _: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { var dictionary: [CommentedString: PlistValue] = [:] dictionary["isa"] = .string(CommentedString(type(of: self).isa)) @@ -78,6 +86,6 @@ public class PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: PBX })) } dictionary["buildPhase"] = .string(CommentedString(buildPhase.reference.value, comment: buildPhase.name() ?? "CopyFiles")) - return (key: CommentedString(reference, comment: "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet"), value: .dictionary(dictionary)) + return (key: CommentedString(reference, comment: plistComment), value: .dictionary(dictionary)) } } diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift index 203e7409d..260524b1c 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift @@ -85,12 +85,17 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { override var multiline: Bool { (exceptions?.count ?? 0) < 2 } + override func assignParentToChildren() { + super.assignParentToChildren() + exceptions?.forEach { $0.synchronizedRootGroup = self } + } + override func plistKeyAndValue(proj: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { var dictionary: [CommentedString: PlistValue] = try super.plistKeyAndValue(proj: proj, reference: reference).value.dictionary ?? [:] dictionary["isa"] = .string(CommentedString(type(of: self).isa)) if let exceptions, !exceptions.isEmpty { dictionary["exceptions"] = .array(exceptions.map { exception in - .string(CommentedString(exception.reference.value, comment: type(of: exception).isa)) + .string(CommentedString(exception.reference.value, comment: exception.plistComment)) }) } if let explicitFileTypes, !explicitFileTypes.isEmpty { From b81dee1f4bf73d91e2bed8cd2fb4cfdb4d0a42c2 Mon Sep 17 00:00:00 2001 From: Max Seelemann Date: Thu, 12 Mar 2026 12:23:26 +0100 Subject: [PATCH 4/4] Added tests for Xcode project updates --- .../project.pbxproj | 18 +++++--- .../XPCService.xpc/Info.plist | 19 +++++++++ .../PBXFileSystemSynchronizedRootGroup.swift | 2 - ...nchronizedBuildFileExceptionSetTests.swift | 14 +++++++ ...FileSystemSynchronizedRootGroupTests.swift | 42 +++++++++++++++++++ .../Objects/Project/PBXProjEncoderTests.swift | 14 +++++-- 6 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 Fixtures/SynchronizedRootGroups/SynchronizedRootGroups/XPCService.xpc/Info.plist diff --git a/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj index a8db250fe..8c0a8d312 100644 --- a/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj +++ b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 73; + objectVersion = 77; objects = { /* Begin PBXCopyFilesBuildPhase section */ @@ -23,7 +23,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 6CF05BA32C53F97F00EF267F /* Exceptions for "SynchronizedRootGroups" folder in "SynchronizedRootGroups" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Exception/Exception.swift, @@ -33,7 +33,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ - F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { + F841A9D12D63B00A00059ED6 /* Exceptions for "SynchronizedRootGroups" folder in "Copy Files" phase from "SynchronizedRootGroups" target */ = { isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; attributesByRelativePath = { XPCService.xpc = ( @@ -48,7 +48,15 @@ /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SynchronizedRootGroups; sourceTree = ""; }; + 6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 6CF05BA32C53F97F00EF267F /* Exceptions for "SynchronizedRootGroups" folder in "SynchronizedRootGroups" target */, + F841A9D12D63B00A00059ED6 /* Exceptions for "SynchronizedRootGroups" folder in "Copy Files" phase from "SynchronizedRootGroups" target */, + ); + path = SynchronizedRootGroups; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -138,7 +146,7 @@ Base, ); mainGroup = 6CF05B822C53F5F200EF267F; - preferredProjectObjectVersion = 60; + preferredProjectObjectVersion = 77; productRefGroup = 6CF05B8D2C53F5F200EF267F /* Products */; projectDirPath = ""; projectRoot = ""; diff --git a/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups/XPCService.xpc/Info.plist b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups/XPCService.xpc/Info.plist new file mode 100644 index 000000000..adceb8c9b --- /dev/null +++ b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups/XPCService.xpc/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleIdentifier + com.example.XPCService + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XPCService + CFBundlePackageType + XPC! + XPCService + + ServiceType + Application + + + diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift index 260524b1c..64139c078 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift @@ -83,8 +83,6 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { // MARK: - PlistSerializable - override var multiline: Bool { (exceptions?.count ?? 0) < 2 } - override func assignParentToChildren() { super.assignParentToChildren() exceptions?.forEach { $0.synchronizedRootGroup = self } diff --git a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift index 0ef9fda58..fedb22e0a 100644 --- a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift +++ b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift @@ -75,4 +75,18 @@ final class PBXFileSystemSynchronizedBuildFileExceptionSetTests: XCTestCase { XCTAssertNil(plistValue.dictionary?[CommentedString("platformFiltersByRelativePath")]) } + + func test_plistComment_withSynchronizedRootGroup_returnsDescriptiveComment() { + let group = PBXFileSystemSynchronizedRootGroup(sourceTree: .group, path: "Sources", exceptions: [subject]) + XCTAssertEqual(subject.plistComment, "Exceptions for \"Sources\" folder in \"Test\" target") + withExtendedLifetime(group) {} + } + + func test_plistKeyAndValue_keyComment_matchesPlistComment() throws { + let proj = PBXProj() + let group = PBXFileSystemSynchronizedRootGroup(sourceTree: .group, path: "Sources", exceptions: [subject]) + let (key, _) = try subject.plistKeyAndValue(proj: proj, reference: "ref") + XCTAssertEqual(key.comment, subject.plistComment) + withExtendedLifetime(group) {} + } } diff --git a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedRootGroupTests.swift b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedRootGroupTests.swift index 374156104..aae5a178e 100644 --- a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedRootGroupTests.swift +++ b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedRootGroupTests.swift @@ -48,4 +48,46 @@ final class PBXFileSystemSynchronizedRootGroupTests: XCTestCase { explicitFolders: []) XCTAssertEqual(subject, another) } + + // MARK: - plistKeyAndValue + + func test_plistKeyAndValue_explicitFileTypes_serializedOnlyWhenNonEmpty() throws { + let (_, nonEmpty) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertNotNil(nonEmpty.dictionary?[CommentedString("explicitFileTypes")]) + + subject.explicitFileTypes = [:] + let (_, empty) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertNil(empty.dictionary?[CommentedString("explicitFileTypes")]) + } + + func test_plistKeyAndValue_explicitFolders_serializedOnlyWhenNonEmpty() throws { + let (_, empty) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertNil(empty.dictionary?[CommentedString("explicitFolders")]) + + subject.explicitFolders = ["SubFolder"] + let (_, nonEmpty) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertNotNil(nonEmpty.dictionary?[CommentedString("explicitFolders")]) + } + + func test_plistKeyAndValue_name_serializedOnlyWhenDifferentFromPath() throws { + let (_, same) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertNil(same.dictionary?[CommentedString("name")]) + + subject.name = "Display Name" + let (_, different) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + XCTAssertEqual(different.dictionary?[CommentedString("name")], .string(CommentedString("Display Name"))) + } + + func test_plistKeyAndValue_exceptionReference_usesDescriptiveComment() throws { + let (_, value) = try subject.plistKeyAndValue(proj: PBXProj(), reference: "ref") + let exceptionsArray = try XCTUnwrap(value.dictionary?[CommentedString("exceptions")]?.array) + let entry = try XCTUnwrap(exceptionsArray.first?.string) + XCTAssertEqual(entry.comment, "Exceptions for \"synchronized\" folder in \"Test\" target") + } + + // MARK: - assignParentToChildren + + func test_assignParentToChildren_wiresSynchronizedRootGroup() { + XCTAssertIdentical(exception.synchronizedRootGroup, subject) + } } diff --git a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift index af5233ce0..f2a4980a9 100644 --- a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift +++ b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift @@ -294,7 +294,15 @@ let lines = lines(fromFile: encodeProject(settings: settings)) let beginGroup = lines.findLine("/* Begin PBXFileSystemSynchronizedRootGroup section */") - var line = lines.validate(line: "6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SynchronizedRootGroups; sourceTree = \"\"; };", after: beginGroup) + var line = lines.validate(line: "6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {", after: beginGroup) + line = lines.validate(line: "isa = PBXFileSystemSynchronizedRootGroup;", after: line) + line = lines.validate(line: "exceptions = (", after: line) + line = lines.validate(line: "6CF05BA32C53F97F00EF267F /* Exceptions for \"SynchronizedRootGroups\" folder in \"SynchronizedRootGroups\" target */,", after: line) + line = lines.validate(line: "F841A9D12D63B00A00059ED6 /* Exceptions for \"SynchronizedRootGroups\" folder in \"Copy Files\" phase from \"SynchronizedRootGroups\" target */,", after: line) + line = lines.validate(line: ");", after: line) + line = lines.validate(line: "path = SynchronizedRootGroups;", after: line) + line = lines.validate(line: "sourceTree = \"\";", after: line) + line = lines.validate(line: "};", after: line) line = lines.validate(line: "/* End PBXFileSystemSynchronizedRootGroup section */", after: line) } @@ -307,7 +315,7 @@ let lines = lines(fromFile: encodeProject(settings: settings)) let beginGroup = lines.findLine("/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */") - var line = lines.validate(line: "6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {", after: beginGroup) + var line = lines.validate(line: "6CF05BA32C53F97F00EF267F /* Exceptions for \"SynchronizedRootGroups\" folder in \"SynchronizedRootGroups\" target */ = {", after: beginGroup) line = lines.validate(line: "isa = PBXFileSystemSynchronizedBuildFileExceptionSet;", after: line) line = lines.validate(line: "membershipExceptions = (", after: line) line = lines.validate(line: "Exception/Exception.swift,", after: line) @@ -326,7 +334,7 @@ let lines = lines(fromFile: encodeProject(settings: settings)) let beginGroup = lines.findLine("/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */") - var line = lines.validate(line: "F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {", after: beginGroup) + var line = lines.validate(line: "F841A9D12D63B00A00059ED6 /* Exceptions for \"SynchronizedRootGroups\" folder in \"Copy Files\" phase from \"SynchronizedRootGroups\" target */ = {", after: beginGroup) line = lines.validate(line: "isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;", after: line) line = lines.validate(line: "attributesByRelativePath = {", after: line) line = lines.validate(line: "XPCService.xpc = (", after: line)