From 09a7508b6d94fbf47dbb7a955b9172f576dc3e4d Mon Sep 17 00:00:00 2001 From: Econa77 Date: Fri, 5 Jun 2026 23:38:02 +0900 Subject: [PATCH 1/4] Add failing test for synchronized group membership exceptions --- .../ExampleProject.xcodeproj/project.pbxproj | 12 ++++++- .../DeepFolder/Path/ExceptionView.swift | 7 ++++ .../SelectiveTestingProjectTests.swift | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExceptionView.swift diff --git a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj index c3b31f5..1457d32 100644 --- a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj +++ b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj @@ -94,8 +94,18 @@ B24BBDBF2CAD7244005E6DAC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Example.strings; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 095D76502FD30A2E007174DE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Path/ExceptionView.swift, + ); + target = 276DB5BA29B144C900E5C615 /* ExampleProject */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ - 095EE0952DB8E35400EACE2E /* DeepFolder */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DeepFolder; sourceTree = ""; }; + 095EE0952DB8E35400EACE2E /* DeepFolder */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (095D76502FD30A2E007174DE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DeepFolder; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExceptionView.swift b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExceptionView.swift new file mode 100644 index 0000000..4f226ea --- /dev/null +++ b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExceptionView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct ExceptionView: View { + var body: some View { + EmptyView() + } +} diff --git a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift index 1b5ea3a..3b8080f 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift @@ -133,6 +133,24 @@ struct SelectiveTestingProjectTests { ])) } + @Test + func projectDeepFolderMembershipExceptionPathChange_turbo() async throws { + // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + + let tool = try testTool.createSUT(config: nil, + basePath: "ExampleProject.xcodeproj", + turbo: true) + + // when + try testTool.changeFile(at: testTool.projectPath + "ExampleProject/DeepFolder/Path/ExceptionView.swift") + + // then + let result = try await tool.run() + #expect(result == Set()) + } + @Test func projectTargetDependencyChange_turbo() async throws { // given @@ -176,6 +194,23 @@ struct SelectiveTestingProjectTests { ])) } + @Test + func projectDeepFolderMembershipExceptionPathChange() async throws { + // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + + let tool = try testTool.createSUT(config: nil, + basePath: "ExampleProject.xcodeproj") + + // when + try testTool.changeFile(at: testTool.projectPath + "ExampleProject/DeepFolder/Path/ExceptionView.swift") + + // then + let result = try await tool.run() + #expect(result == Set()) + } + @Test func projectLocalizedPathChange() async throws { // given From 11919c1f9b11508174c2a6ee6d37be87354926ef Mon Sep 17 00:00:00 2001 From: Econa77 Date: Fri, 5 Jun 2026 23:49:24 +0900 Subject: [PATCH 2/4] Support synchronized group membership exceptions --- Sources/DependencyCalculator/DependencyGraph.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/DependencyCalculator/DependencyGraph.swift b/Sources/DependencyCalculator/DependencyGraph.swift index c515078..d6205e4 100644 --- a/Sources/DependencyCalculator/DependencyGraph.swift +++ b/Sources/DependencyCalculator/DependencyGraph.swift @@ -429,8 +429,7 @@ extension WorkspaceInfo { } /// Search all files specified in fileSystemSynchronizedGroups. - /// Currently, file extensions are note considered at all, so all files in the folder are subject to the search. - /// NOTE: FileSystemSynchronizedFileExceptionSet is not suppored yet. + /// Currently, file extensions are not considered at all, so all files in the folder are subject to the search. /// /// ref: https://github.com/tuist/XcodeGraph/pull/108 /// The implementation of `XcodeGraph` only considers cases where the root is a folder. @@ -450,7 +449,16 @@ extension WorkspaceInfo { folderPath = group.path.map { Path($0) } } guard let folderPath else { return } - paths.append(contentsOf: (try? folderPath.recursiveChildren()) ?? []) + + let membershipExceptionPaths = (group.exceptions ?? []) + .compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet } + .filter { $0.target.uuid == target.uuid } + .flatMap { $0.membershipExceptions ?? [] } + .map { folderPath + Path($0) } + let recursiveChildrenFilePaths = ((try? folderPath.recursiveChildren()) ?? []) + .filter { $0.isFile } + + paths.append(contentsOf: recursiveChildrenFilePaths.filter { !membershipExceptionPaths.contains($0) }) } return paths } From 61bd6be9dfb46e785d2d06bd59bcef91f7a5ed02 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Mon, 8 Jun 2026 09:42:44 +0900 Subject: [PATCH 3/4] Add failing test for synchronized group folder exceptions --- .../ExampleProject.xcodeproj/project.pbxproj | 3 +- .../ExcludedFolder/ExcludedFolderView.swift | 7 ++++ .../SelectiveTestingProjectTests.swift | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift diff --git a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj index 1457d32..6e7045f 100644 --- a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj +++ b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject.xcodeproj/project.pbxproj @@ -99,13 +99,14 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Path/ExceptionView.swift, + Path/ExcludedFolder, ); target = 276DB5BA29B144C900E5C615 /* ExampleProject */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 095EE0952DB8E35400EACE2E /* DeepFolder */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (095D76502FD30A2E007174DE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DeepFolder; sourceTree = ""; }; + 095EE0952DB8E35400EACE2E /* DeepFolder */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (095D76502FD30A2E007174DE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (Path/ExcludedFolder, ); path = DeepFolder; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift new file mode 100644 index 0000000..c283149 --- /dev/null +++ b/Tests/SelectiveTestingTests/ExampleProject/ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct ExcludedFolderView: View { + var body: some View { + EmptyView() + } +} diff --git a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift index 3b8080f..d4d67d9 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift @@ -151,6 +151,24 @@ struct SelectiveTestingProjectTests { #expect(result == Set()) } + @Test + func projectDeepFolderMembershipExceptionFolderChange_turbo() async throws { + // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + + let tool = try testTool.createSUT(config: nil, + basePath: "ExampleProject.xcodeproj", + turbo: true) + + // when + try testTool.changeFile(at: testTool.projectPath + "ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift") + + // then + let result = try await tool.run() + #expect(result == Set()) + } + @Test func projectTargetDependencyChange_turbo() async throws { // given @@ -211,6 +229,23 @@ struct SelectiveTestingProjectTests { #expect(result == Set()) } + @Test + func projectDeepFolderMembershipExceptionFolderChange() async throws { + // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + + let tool = try testTool.createSUT(config: nil, + basePath: "ExampleProject.xcodeproj") + + // when + try testTool.changeFile(at: testTool.projectPath + "ExampleProject/DeepFolder/Path/ExcludedFolder/ExcludedFolderView.swift") + + // then + let result = try await tool.run() + #expect(result == Set()) + } + @Test func projectLocalizedPathChange() async throws { // given From 9e179855ab404ade578e6700cc6c735bb262ccc6 Mon Sep 17 00:00:00 2001 From: Econa77 Date: Mon, 8 Jun 2026 10:03:23 +0900 Subject: [PATCH 4/4] Support folder membership exceptions in synchronized groups --- Sources/DependencyCalculator/DependencyGraph.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/DependencyCalculator/DependencyGraph.swift b/Sources/DependencyCalculator/DependencyGraph.swift index d6205e4..cc36151 100644 --- a/Sources/DependencyCalculator/DependencyGraph.swift +++ b/Sources/DependencyCalculator/DependencyGraph.swift @@ -458,8 +458,16 @@ extension WorkspaceInfo { let recursiveChildrenFilePaths = ((try? folderPath.recursiveChildren()) ?? []) .filter { $0.isFile } - paths.append(contentsOf: recursiveChildrenFilePaths.filter { !membershipExceptionPaths.contains($0) }) + paths.append(contentsOf: recursiveChildrenFilePaths.filter { filePath in + !membershipExceptionPaths.contains { filePath.isDescendantOrSelf(of: $0) } + }) } return paths } } + +private extension Path { + func isDescendantOrSelf(of path: Path) -> Bool { + self == path || string.hasPrefix(path.string + Path.separator) + } +}