From df41998110c53df009b969473928a6221e3f7c5e Mon Sep 17 00:00:00 2001 From: Mike Gerasymenko Date: Mon, 11 Aug 2025 20:53:19 +0200 Subject: [PATCH 01/12] Re-enable tests on Linux --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82956c0..6351798 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,6 @@ jobs: run: swift test -v test-linux: - if: false runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 From f6135028b96b50d47de0650aadcd0fd0422a6b25 Mon Sep 17 00:00:00 2001 From: Mike Gerasymenko Date: Mon, 11 Aug 2025 20:59:12 +0200 Subject: [PATCH 02/12] Use ubuntu-latest --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6351798..e1cd02b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: run: swift test -v test-linux: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: swift-actions/setup-swift@v2 From fcab5c2edfb4f8f24345e017ee739bc4ed4ab194 Mon Sep 17 00:00:00 2001 From: Mike Gerasymenko Date: Sun, 2 Nov 2025 20:32:12 +0100 Subject: [PATCH 03/12] Update Swift setup action in GitHub workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1cd02b..6093710 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: swift-actions/setup-swift@v2 + - uses: SwiftyLab/setup-swift@latest with: swift-version: "6.1.0" - name: Get swift version From f4719678fc489bd1a5b1d47703d1c6e4fa3cfe57 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 12:28:31 +0100 Subject: [PATCH 04/12] Migrate to Swift Testing --- Package.swift | 2 +- .../SelectiveTestingPlugin.swift | 13 +- .../DependencyGraph.swift | 13 +- .../PackageMetadata.swift | 2 +- Sources/Git/Git+Changeset.swift | 6 +- Sources/Git/Git.swift | 4 +- Sources/SelectiveTestLogger/Logger.swift | 13 +- .../SelectiveTestingTool.swift | 52 +++- .../SelectiveTesting.swift | 2 +- .../DependencyCalculatorTests.swift | 29 +- .../PackageMetadataTests.swift | 92 +++--- .../IntegrationTestTool.swift | 100 ++++--- .../SelectiveTestingConfigTests.swift | 268 +++++++++--------- .../SelectiveTestingPackagesTests.swift | 115 ++++---- .../SelectiveTestingPerformanceTests.swift | 60 ++-- .../SelectiveTestingProjectTests.swift | 133 +++++---- .../SelectiveTestingWorkspaceTests.swift | 155 +++++----- .../TestPlanCodableTests.swift | 45 +-- 18 files changed, 559 insertions(+), 545 deletions(-) diff --git a/Package.swift b/Package.swift index 9c4474b..7d53159 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 6.0 import PackageDescription diff --git a/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift b/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift index e2fc66a..7f0dc29 100644 --- a/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift +++ b/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift @@ -7,11 +7,10 @@ import PackagePlugin @main struct SelectiveTestingPlugin: CommandPlugin { - private func run(_ executable: String, arguments: [String] = []) throws { - let executableURL = URL(fileURLWithPath: executable) + private func run(_ executable: URL, arguments: [String] = []) throws { let process = Process() - process.executableURL = executableURL + process.executableURL = executable process.arguments = arguments try process.run() @@ -24,10 +23,10 @@ struct SelectiveTestingPlugin: CommandPlugin { } func performCommand(context: PluginContext, arguments: [String]) async throws { - FileManager().changeCurrentDirectoryPath(context.package.directory.string) + FileManager.default.changeCurrentDirectoryPath(context.package.directoryURL.path) let tool = try context.tool(named: "xcode-selective-test") - try run(tool.path.string, arguments: arguments) + try run(tool.url, arguments: arguments) } } @@ -36,7 +35,7 @@ struct SelectiveTestingPlugin: CommandPlugin { extension SelectiveTestingPlugin: XcodeCommandPlugin { func performCommand(context: XcodePluginContext, arguments: [String]) throws { - FileManager().changeCurrentDirectoryPath(context.xcodeProject.directory.string) + FileManager.default.changeCurrentDirectoryPath(context.xcodeProject.directoryURL.path) let tool = try context.tool(named: "xcode-selective-test") @@ -66,7 +65,7 @@ struct SelectiveTestingPlugin: CommandPlugin { } } - try run(tool.path.string, arguments: toolArguments) + try run(tool.url, arguments: toolArguments) } } #endif diff --git a/Sources/DependencyCalculator/DependencyGraph.swift b/Sources/DependencyCalculator/DependencyGraph.swift index 5a0adfb..5e60bbe 100644 --- a/Sources/DependencyCalculator/DependencyGraph.swift +++ b/Sources/DependencyCalculator/DependencyGraph.swift @@ -152,15 +152,22 @@ extension WorkspaceInfo { dependencyStructure: resultDependencies, candidateTestPlans: candidateTestPlans) if let config { + let additionalBasePath: Path + if path.extension == "xcworkspace" || path.extension == "xcodeproj" { + additionalBasePath = path.parent() + } else { + additionalBasePath = path + } // Process additional config - return processAdditional(config: config, workspaceInfo: workspaceInfo) + return processAdditional(config: config, workspaceInfo: workspaceInfo, basePath: additionalBasePath) } else { return workspaceInfo } } static func processAdditional(config: WorkspaceInfo.AdditionalConfig, - workspaceInfo: WorkspaceInfo) -> WorkspaceInfo + workspaceInfo: WorkspaceInfo, + basePath: Path) -> WorkspaceInfo { var files = workspaceInfo.files var folders = workspaceInfo.folders @@ -191,7 +198,7 @@ extension WorkspaceInfo { } for filePath in filesToAdd { - let path = Path(filePath).absolute() + let path = (basePath + filePath).absolute() guard path.exists else { Logger.error("Config: Path \(path) does not exist") diff --git a/Sources/DependencyCalculator/PackageMetadata.swift b/Sources/DependencyCalculator/PackageMetadata.swift index 3e21461..9b69f47 100644 --- a/Sources/DependencyCalculator/PackageMetadata.swift +++ b/Sources/DependencyCalculator/PackageMetadata.swift @@ -25,7 +25,7 @@ struct PackageTargetMetadata { flags.append("--ignore-lock") } - let manifest = try Shell.execOrFail("cd \(path) && swift package dump-package \(flags.joined(separator: " "))") + let manifest = try Shell.execOrFail("(cd \(path) && swift package dump-package \(flags.joined(separator: " ")))") .trimmingCharacters(in: .newlines) guard let manifestData = manifest.data(using: .utf8), let manifestJson = try JSONSerialization.jsonObject(with: manifestData, options: []) as? [String: Any], diff --git a/Sources/Git/Git+Changeset.swift b/Sources/Git/Git+Changeset.swift index 4856df3..6263721 100644 --- a/Sources/Git/Git+Changeset.swift +++ b/Sources/Git/Git+Changeset.swift @@ -11,7 +11,7 @@ public extension Git { func changeset(baseBranch: String, verbose: Bool = false) throws -> Set { let gitRoot = try repoRoot() - var currentBranch = try Shell.execOrFail("cd \(gitRoot) && git branch --show-current").trimmingCharacters(in: .newlines) + var currentBranch = try Shell.execOrFail("(cd \(gitRoot) && git branch --show-current)").trimmingCharacters(in: .newlines) if verbose { Logger.message("Current branch: \(currentBranch)") Logger.message("Base branch: \(baseBranch)") @@ -23,7 +23,7 @@ public extension Git { currentBranch = "HEAD" } - let changes = try Shell.execOrFail("cd \(gitRoot) && git diff '\(baseBranch)'..'\(currentBranch)' --name-only") + let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff '\(baseBranch)'..'\(currentBranch)' --name-only)") let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines) guard !changesTrimmed.isEmpty else { @@ -36,7 +36,7 @@ public extension Git { func localChangeset() throws -> Set { let gitRoot = try repoRoot() - let changes = try Shell.execOrFail("cd \(gitRoot) && git diff HEAD --name-only") + let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff HEAD --name-only)") let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Sources/Git/Git.swift b/Sources/Git/Git.swift index 11a4a94..e8894c1 100644 --- a/Sources/Git/Git.swift +++ b/Sources/Git/Git.swift @@ -14,7 +14,7 @@ public struct Git { } public func repoRoot() throws -> Path { - let gitPath = try Shell.execOrFail("cd \(path) && git rev-parse --show-toplevel").trimmingCharacters(in: .newlines) + let gitPath = try Shell.execOrFail("(cd \(path) && git rev-parse --show-toplevel)").trimmingCharacters(in: .newlines) return Path(gitPath).absolute() } @@ -22,7 +22,7 @@ public struct Git { public func find(pattern: String) throws -> Set { let gitRoot = try repoRoot() - let result = try Shell.exec("cd \(gitRoot) && git ls-files | grep \(pattern)").0.trimmingCharacters(in: .newlines) + let result = try Shell.exec("(cd \(gitRoot) && git ls-files | grep \(pattern))").0.trimmingCharacters(in: .newlines) guard !result.isEmpty else { return Set() diff --git a/Sources/SelectiveTestLogger/Logger.swift b/Sources/SelectiveTestLogger/Logger.swift index cb88b01..2dba74e 100644 --- a/Sources/SelectiveTestLogger/Logger.swift +++ b/Sources/SelectiveTestLogger/Logger.swift @@ -9,18 +9,21 @@ public struct StandardErrorOutputStream: TextOutputStream { public mutating func write(_ string: String) { fputs(string, stderr) } } -public var errStream = StandardErrorOutputStream() - public enum Logger { + private static func write(_ message: String) { + var stream = StandardErrorOutputStream() + print(message, to: &stream) + } + public static func message(_ message: String) { - print(message, to: &errStream) + write(message) } public static func warning(_ message: String) { - print("[WARN]: \(message)".yellow, to: &errStream) + write("[WARN]: \(message)".yellow) } public static func error(_ message: String) { - print("[ERROR]: \(message)".red, to: &errStream) + write("[ERROR]: \(message)".red) } } diff --git a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift index fb5e735..10708a7 100644 --- a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift +++ b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift @@ -35,21 +35,40 @@ public final class SelectiveTestingTool { dryRun: Bool = false, verbose: Bool = false) throws { - if let configData = try? (Path.current + Config.defaultConfigName).read(), - let config = try Config.load(from: configData) - { - self.config = config + let suppliedBasePath = basePath.map { Path($0) } + var configCandidates: [Path] = [] + if let suppliedBasePath { + let baseDirectory: Path + if let ext = suppliedBasePath.extension, + ext == "xcworkspace" || ext == "xcodeproj" { + baseDirectory = suppliedBasePath.parent() + } else if suppliedBasePath.isDirectory { + baseDirectory = suppliedBasePath + } else { + baseDirectory = suppliedBasePath.parent() + } + configCandidates.append(baseDirectory + Config.defaultConfigName) + } + configCandidates.append(Path.current + Config.defaultConfigName) + + if let configPath = configCandidates.first(where: { $0.exists }), + let configData = try? configPath.read(), + let loadedConfig = try Config.load(from: configData) { + self.config = loadedConfig + if verbose { + Logger.message("Loaded config from \(configPath)") + } } else { config = nil } - let finalBasePath = basePath ?? + let finalBasePath = Path(basePath ?? config?.basePath ?? Path().glob("*.xcworkspace").first?.string ?? - Path().glob("*.xcodeproj").first?.string ?? "." + Path().glob("*.xcodeproj").first?.string ?? ".") self.baseBranch = baseBranch - self.basePath = Path(finalBasePath) + self.basePath = finalBasePath self.changedFiles = changedFiles self.printJSON = printJSON self.renderDependencyGraph = renderDependencyGraph @@ -59,12 +78,21 @@ public final class SelectiveTestingTool { self.verbose = verbose // Merge CLI test plans with config test plans - var allTestPlans = config?.allTestPlans ?? [] + var allTestPlans: [String] = config?.allTestPlans ?? [] allTestPlans.append(contentsOf: testPlans) self.testPlans = allTestPlans } public func run() async throws -> Set { + let workingDirectory: Path + if let ext = basePath.extension, + ext == "xcworkspace" || ext == "xcodeproj" { + workingDirectory = basePath.parent() + } else if basePath.isDirectory { + workingDirectory = basePath + } else { + workingDirectory = basePath.parent() + } // 1. Identify changed files let changeset: Set @@ -136,7 +164,13 @@ public final class SelectiveTestingTool { if !dryRun { // 4. Configure workspace to test given targets - let plansToUpdate = testPlans.isEmpty ? workspaceInfo.candidateTestPlans : testPlans + let plansToUpdate = testPlans.isEmpty ? + workspaceInfo.candidateTestPlans : + testPlans.map { plan in + let planPath = Path(plan) + let resolved = planPath.isAbsolute ? planPath : workingDirectory + planPath + return resolved.absolute().string + } if !plansToUpdate.isEmpty { for testPlan in plansToUpdate { diff --git a/Sources/xcode-selective-test/SelectiveTesting.swift b/Sources/xcode-selective-test/SelectiveTesting.swift index 54c7ffb..9605ca6 100644 --- a/Sources/xcode-selective-test/SelectiveTesting.swift +++ b/Sources/xcode-selective-test/SelectiveTesting.swift @@ -2,7 +2,7 @@ // Created by Mike Gerasymenko // -import ArgumentParser +@preconcurrency import ArgumentParser import SelectiveTestingCore import SelectiveTestLogger diff --git a/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift b/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift index 2cf2920..0aa7733 100644 --- a/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift +++ b/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift @@ -6,10 +6,11 @@ import Foundation import PathKit import SelectiveTestingCore +import Testing import Workspace -import XCTest -final class DependencyCalculatorTests: XCTestCase { +@Suite +struct DependencyCalculatorTests { func depStructure() -> (DependencyGraph, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity) { let mainApp = TargetIdentity.project(path: "/folder/Project.xcodepoj", targetName: "MainApp", testTarget: false) let mainAppTests = TargetIdentity.project(path: "/folder/Project.xcodepoj", targetName: "MainAppTests", testTarget: true) @@ -35,8 +36,8 @@ final class DependencyCalculatorTests: XCTestCase { return (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) } - func testGraphIntegrity_submodule() async throws { - // given + @Test + func graphIntegrity_submodule() async throws { let (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -45,16 +46,14 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when let affected = graph.affectedTargets(changedFiles: files) - // then - XCTAssertEqual(affected, Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests])) + #expect(affected == Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests])) } - func testGraphIntegrity_mainApp() async throws { - // given + @Test + func graphIntegrity_mainApp() async throws { let (depsGraph, mainApp, _, _, mainAppTests, _, _) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -63,16 +62,14 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when let affected = graph.affectedTargets(changedFiles: files) - // then - XCTAssertEqual(affected, Set([mainApp, mainAppTests])) + #expect(affected == Set([mainApp, mainAppTests])) } - func testGraphIntegrity_module() async throws { - // given + @Test + func graphIntegrity_module() async throws { let (depsGraph, mainApp, module, _, mainAppTests, moduleTests, _) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -81,11 +78,9 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when let affected = graph.affectedTargets(changedFiles: files) - // then - XCTAssertEqual(affected, Set([module, moduleTests, mainApp, mainAppTests])) + #expect(affected == Set([module, moduleTests, mainApp, mainAppTests])) } } diff --git a/Tests/DependencyCalculatorTests/PackageMetadataTests.swift b/Tests/DependencyCalculatorTests/PackageMetadataTests.swift index dc6fdd6..f4eac3b 100644 --- a/Tests/DependencyCalculatorTests/PackageMetadataTests.swift +++ b/Tests/DependencyCalculatorTests/PackageMetadataTests.swift @@ -5,26 +5,26 @@ @testable import DependencyCalculator import Foundation import PathKit +import Testing @testable import Workspace -import XCTest -final class PackageMetadataTests: XCTestCase { - func testPackageMetadataParsing_Simple() throws { - // given +@Suite +struct PackageMetadataTests { + @Test + func packageMetadataParsing_Simple() throws { guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } - // when + let basePath = Path(exampleInBundle) + "Simple" let metadata = try PackageTargetMetadata.parse(at: basePath) - // then - XCTAssertEqual(metadata.count, 2) + #expect(metadata.count == 2) let first = metadata[0] - XCTAssertEqual(first.name, "ExampleSubpackage") - XCTAssertEqual(first.path, basePath) - XCTAssertEqual(first.dependsOn.count, 0) - XCTAssertEqual(first.affectedBy, Set([ + #expect(first.name == "ExampleSubpackage") + #expect(first.path == basePath) + #expect(first.dependsOn.isEmpty) + #expect(first.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Sources" + "ExampleSubpackage", @@ -32,81 +32,79 @@ final class PackageMetadataTests: XCTestCase { ])) let second = metadata[1] - XCTAssertEqual(second.name, "ExampleSubpackageTests") - XCTAssertEqual(second.path, basePath) - XCTAssertEqual(second.dependsOn.count, 1) - XCTAssertEqual(second.affectedBy, Set([ + #expect(second.name == "ExampleSubpackageTests") + #expect(second.path == basePath) + #expect(second.dependsOn.count == 1) + #expect(second.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Tests" + "ExampleSubpackageTests" ])) - let identity = try XCTUnwrap(second.dependsOn.first) + let identity = try #require(second.dependsOn.first) - XCTAssertEqual(identity.type, .package) - XCTAssertEqual(identity.path, basePath) - XCTAssertEqual(identity.name, "ExampleSubpackage") - XCTAssertFalse(identity.isTestTarget) + #expect(identity.type == .package) + #expect(identity.path == basePath) + #expect(identity.name == "ExampleSubpackage") + #expect(!identity.isTestTarget) } - func testPackageMetadataParsing_ExamplePacakge() throws { - // given + @Test + func packageMetadataParsing_ExamplePackage() throws { guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } - // when + let basePath = Path(exampleInBundle) + "CrossDependency" let metadata = try PackageTargetMetadata.parse(at: basePath) - // then - XCTAssertEqual(metadata.count, 10) + #expect(metadata.count == 10) let first = metadata[0] - XCTAssertEqual(first.name, "SelectiveTesting") - XCTAssertEqual(first.path, basePath) - XCTAssertEqual(first.dependsOn, Set([TargetIdentity.package(path: basePath, targetName: "SelectiveTestingCore", testTarget: false)])) - XCTAssertEqual(first.affectedBy, Set([ + #expect(first.name == "SelectiveTesting") + #expect(first.path == basePath) + #expect(first.dependsOn == Set([TargetIdentity.package(path: basePath, targetName: "SelectiveTestingCore", testTarget: false)])) + #expect(first.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Sources" + "SelectiveTesting" ])) let second = metadata[1] - XCTAssertEqual(second.name, "SelectiveTestingCore") - XCTAssertEqual(second.path, basePath) - XCTAssertEqual(second.dependsOn.count, 6) - XCTAssertEqual(second.affectedBy, Set([ + #expect(second.name == "SelectiveTestingCore") + #expect(second.path == basePath) + #expect(second.dependsOn.count == 6) + #expect(second.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Sources" + "SelectiveTestingCore" ])) } - - func testPackageAndWorkspace() async throws { - // given + + @Test + func packageAndWorkspace() throws { guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } - // when + let basePath = Path(exampleInBundle) + "PackageAndWorkspace" let metadata = try PackageTargetMetadata.parse(at: basePath) - // then - XCTAssertEqual(metadata.count, 2) + #expect(metadata.count == 2) let first = metadata[0] - XCTAssertEqual(first.name, "APackage") - XCTAssertEqual(first.path, basePath) - XCTAssertEqual(first.dependsOn, Set([])) - XCTAssertEqual(first.affectedBy, Set([ + #expect(first.name == "APackage") + #expect(first.path == basePath) + #expect(first.dependsOn.isEmpty) + #expect(first.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Sources" + "APackage" ])) let second = metadata[1] - XCTAssertEqual(second.name, "APackageTests") - XCTAssertEqual(second.path, basePath) - XCTAssertEqual(second.dependsOn.count, 1) - XCTAssertEqual(second.affectedBy, Set([ + #expect(second.name == "APackageTests") + #expect(second.path == basePath) + #expect(second.dependsOn.count == 1) + #expect(second.affectedBy == Set([ basePath + "Package.swift", basePath + "Package.resolved", basePath + "Tests" + "APackageTests" diff --git a/Tests/SelectiveTestingTests/IntegrationTestTool.swift b/Tests/SelectiveTestingTests/IntegrationTestTool.swift index f788306..e36b8bc 100644 --- a/Tests/SelectiveTestingTests/IntegrationTestTool.swift +++ b/Tests/SelectiveTestingTests/IntegrationTestTool.swift @@ -6,19 +6,24 @@ import Foundation import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import TestConfigurator import Workspace -import XCTest -final class IntegrationTestTool { +struct IntegrationTestTool { var projectPath: Path = "" - func setUp(subfolder: Bool = false) throws { + private func runInProject(_ command: String) throws { + try Shell.execOrFail("(cd \"\(projectPath.string)\" && \(command))") + } + + init(subfolder: Bool = false) throws { let tmpPath = Path.temporary.absolute() + let uniqueFolder = "ExampleProject-\(UUID().uuidString)" guard let exampleInBundle = Bundle.module.path(forResource: "ExampleProject", ofType: "") else { fatalError("Missing ExampleProject in TestBundle") } - projectPath = tmpPath + "ExampleProject" + projectPath = tmpPath + uniqueFolder try? FileManager.default.removeItem(atPath: projectPath.string) if subfolder { let finalPath = (projectPath + "Subfolder").string @@ -28,23 +33,22 @@ final class IntegrationTestTool { else { try FileManager.default.copyItem(atPath: exampleInBundle, toPath: projectPath.string) } - FileManager.default.changeCurrentDirectoryPath(projectPath.string) - try Shell.execOrFail("git init") - try Shell.execOrFail("git config commit.gpgsign false") - try Shell.execOrFail("git checkout -b main") - try Shell.execOrFail("git add .") - try Shell.execOrFail("git commit -m \"Base\"") - try Shell.execOrFail("git checkout -b feature") + try runInProject("git init") + try runInProject("git config commit.gpgsign false") + try runInProject("git checkout -b main") + try runInProject("git add .") + try runInProject("git commit -m \"Base\"") + try runInProject("git checkout -b feature") } func tearDown() throws { try? FileManager.default.removeItem(atPath: projectPath.string) } - func withTestTool(subfolder: Bool = false, closure: () async throws -> Void) async throws { - try setUp(subfolder: subfolder) - try await closure() - try tearDown() + static func withTestTool(subfolder: Bool = false, closure: (IntegrationTestTool) async throws -> Void) async throws { + let tool = try IntegrationTestTool(subfolder: subfolder) + defer { try? tool.tearDown() } + try await closure(tool) } func changeFile(at path: Path) throws { @@ -53,22 +57,22 @@ final class IntegrationTestTool { try handle.write(contentsOf: "\n \n".data(using: .utf8)!) try handle.close() - try Shell.execOrFail("git add .") - try Shell.execOrFail("git commit -m \"Change\"") + try runInProject("git add .") + try runInProject("git commit -m \"Change\"") } func addFile(at path: Path) throws { FileManager().createFile(atPath: path.string, contents: "\n \n".data(using: .utf8)!) - try Shell.execOrFail("git add .") - try Shell.execOrFail("git commit -m \"Change\"") + try runInProject("git add .") + try runInProject("git commit -m \"Change\"") } func removeFile(at path: Path) throws { try path.delete() - try Shell.execOrFail("git add .") - try Shell.execOrFail("git commit -m \"Change\"") + try runInProject("git add .") + try runInProject("git commit -m \"Change\"") } func createSUT(config: Config? = nil, @@ -79,7 +83,7 @@ final class IntegrationTestTool { { if let config { let configText = try config.save() - let path = Path.current + Config.defaultConfigName + let path = projectPath + Config.defaultConfigName try configText.write(toFile: path.string, atomically: true, encoding: .utf8) } @@ -90,9 +94,27 @@ final class IntegrationTestTool { else { testPlans = [] } + + let resolvedBasePath: String? + if let basePath { + if basePath.isAbsolute { + resolvedBasePath = basePath.string + } else { + resolvedBasePath = (projectPath + basePath).string + } + } else if let configBasePath = config?.basePath { + let configPath = Path(configBasePath) + if configPath.isAbsolute { + resolvedBasePath = configPath.string + } else { + resolvedBasePath = (projectPath + configPath).string + } + } else { + resolvedBasePath = projectPath.string + } return try SelectiveTestingTool(baseBranch: "main", - basePath: basePath?.string, + basePath: resolvedBasePath, testPlans: testPlans, changedFiles: changedFiles, renderDependencyGraph: false, @@ -116,8 +138,8 @@ final class IntegrationTestTool { let container = Path(target.target.containerPath.replacingOccurrences(of: "container:", with: "")) let name = target.target.name - guard target.enabled ?? true else { - XCTFail("Unexpected \(target.target.name): disabled targets must be removed") + guard target.enabled != false else { + Issue.record("Unexpected \(target.target.name): disabled targets must be removed") return nil } @@ -128,7 +150,7 @@ final class IntegrationTestTool { } } - XCTAssertEqual(Set(testPlanTargets), expected) + #expect(Set(testPlanTargets) == expected) } func checkTestPlanUnmodified(at newPath: Path) throws { @@ -139,19 +161,19 @@ final class IntegrationTestTool { let originalContents = try String(contentsOfFile: orignialTestPlanPath.string) let newContents = try String(contentsOfFile: newPath.string) - XCTAssertEqual(originalContents, newContents) + #expect(originalContents == newContents) } - lazy var mainProjectMainTarget = TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProject", testTarget: false) - lazy var mainProjectTests = TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProjectTests", testTarget: true) - lazy var mainProjectLibrary = TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExmapleTargetLibrary", testTarget: false) - lazy var mainProjectLibraryTests = TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExmapleTargetLibraryTests", testTarget: true) - lazy var mainProjectUITests = TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProjectUITests", testTarget: true) - lazy var exampleLibrary = TargetIdentity.project(path: projectPath + "ExampleLibrary/ExampleLibrary.xcodeproj", targetName: "ExampleLibrary", testTarget: false) - lazy var exampleLibraryTests = TargetIdentity.project(path: projectPath + "ExampleLibrary/ExampleLibrary.xcodeproj", targetName: "ExampleLibraryTests", testTarget: true) - lazy var exampleLibraryInGroup = TargetIdentity.project(path: projectPath + "Group/ExampleProjectInGroup.xcodeproj", targetName: "ExampleProjectInGroup", testTarget: false) - lazy var package = TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "ExamplePackage", testTarget: false) - lazy var packageTests = TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "ExamplePackageTests", testTarget: true) - lazy var subtests = TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "Subtests", testTarget: true) - lazy var binary = TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "BinaryTarget", testTarget: false) + func mainProjectMainTarget() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProject", testTarget: false) } + func mainProjectTests() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProjectTests", testTarget: true) } + func mainProjectLibrary() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExmapleTargetLibrary", testTarget: false) } + func mainProjectLibraryTests() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExmapleTargetLibraryTests", testTarget: true) } + func mainProjectUITests() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleProject.xcodeproj", targetName: "ExampleProjectUITests", testTarget: true) } + func exampleLibrary() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleLibrary/ExampleLibrary.xcodeproj", targetName: "ExampleLibrary", testTarget: false) } + func exampleLibraryTests() -> TargetIdentity { TargetIdentity.project(path: projectPath + "ExampleLibrary/ExampleLibrary.xcodeproj", targetName: "ExampleLibraryTests", testTarget: true) } + func exampleLibraryInGroup() -> TargetIdentity { TargetIdentity.project(path: projectPath + "Group/ExampleProjectInGroup.xcodeproj", targetName: "ExampleProjectInGroup", testTarget: false) } + func package() -> TargetIdentity { TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "ExamplePackage", testTarget: false) } + func packageTests() -> TargetIdentity { TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "ExamplePackageTests", testTarget: true) } + func subtests() -> TargetIdentity { TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "Subtests", testTarget: true) } + func binary() -> TargetIdentity { TargetIdentity.package(path: projectPath + "ExamplePackage", targetName: "BinaryTarget", testTarget: false) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift index c765497..79f70b4 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift @@ -5,106 +5,105 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingConfigTests: XCTestCase { - let testTool = IntegrationTestTool() +@Suite +struct SelectiveTestingConfigTests { + @Test + func configWorkspacePath() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } - override func setUp() async throws { - try await super.setUp() - - try testTool.setUp() - } - - override func tearDown() async throws { - try await super.tearDown() - - try testTool.tearDown() - } - - func testConfigWorkspacePath() async throws { - // given let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: nil, testPlans: nil, exclude: nil, extra: nil)) - // when + try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) } - func testConfigTestplanPath() async throws { - // given + @Test + func configTestplanPath() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", testPlans: nil, exclude: nil, extra: nil)) - // when + try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } - func testConfigTestplanPath_packageChanged() async throws { - // given + @Test + func configTestplanPath_packageChanged() async throws { + let testTool = try IntegrationTestTool() + + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", testPlans: nil, exclude: nil, extra: nil)) - // when + try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") - // then - let _ = try await tool.run() + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.packageTests, - testTool.subtests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.packageTests(), + testTool.subtests()])) } - - func testConfigTestplanPath_packageResolvedChanged() async throws { - // given + + @Test + func configTestplanPath_packageResolvedChanged() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", testPlans: nil, exclude: nil, extra: nil)) - // when + try testTool.addFile(at: testTool.projectPath + "ExamplePackage/Package.resolved") - // then - let _ = try await tool.run() + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.packageTests, - testTool.subtests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.packageTests(), + testTool.subtests()])) } - func testAdditionalDependency() async throws { - // given + @Test + func additionalDependency() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let additionalConfig = WorkspaceInfo.AdditionalConfig(targetsFiles: [:], dependencies: ["ExampleProject:ExmapleTargetLibrary": ["ExampleSubpackage:ExampleSubpackage"]]) let fullConfig = Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, @@ -113,17 +112,19 @@ final class SelectiveTestingConfigTests: XCTestCase { exclude: nil, extra: additionalConfig) let tool = try testTool.createSUT(config: fullConfig) - // when + try testTool.changeFile(at: testTool.projectPath + "ExampleSubpackage/Package.swift") - // then let result = try await tool.run() - XCTAssertTrue(result.contains(testTool.mainProjectLibrary)) - XCTAssertTrue(result.contains(testTool.mainProjectLibraryTests)) + #expect(result.contains(testTool.mainProjectLibrary())) + #expect(result.contains(testTool.mainProjectLibraryTests())) } - func testAdditionalFiles() async throws { - // given + @Test + func additionalFiles() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let additionalConfig = WorkspaceInfo.AdditionalConfig(targetsFiles: ["ExampleProject:ExmapleTargetLibrary": ["ExmapleTargetLibrary/SomeFile.swift"]], dependencies: [:]) let fullConfig = Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, @@ -132,46 +133,51 @@ final class SelectiveTestingConfigTests: XCTestCase { exclude: nil, extra: additionalConfig) let tool = try testTool.createSUT(config: fullConfig) - // when + try testTool.addFile(at: testTool.projectPath + "ExmapleTargetLibrary/SomeFile.swift") - // then let result = try await tool.run() - XCTAssertTrue(result.contains(testTool.mainProjectLibrary)) - XCTAssertTrue(result.contains(testTool.mainProjectLibraryTests)) + #expect(result.contains(testTool.mainProjectLibrary())) + #expect(result.contains(testTool.mainProjectLibraryTests())) } - func testExclude() async throws { - // given + @Test + func exclude() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", testPlans: nil, exclude: ["ExamplePackage"], extra: nil)) - // when + try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") - // then - let _ = try await tool.run() + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([])) } - func testPackageChangeInDifferentNamedPackage() async throws { - // given + @Test + func packageChangeInDifferentNamedPackage() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Tests/Subtests/Test.swift") - // then - let _ = try await tool.run() + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.subtests])) + expected: Set([testTool.subtests()])) } - - func testDryRun() async throws { - // given + + @Test + func dryRun() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try SelectiveTestingTool(baseBranch: "main", basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlans: ["ExampleProject.xctestplan"], @@ -180,76 +186,77 @@ final class SelectiveTestingConfigTests: XCTestCase { dryRun: true, verbose: true) - // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Tests/Subtests/Test.swift") - // then - let _ = try await tool.run() + _ = try await tool.run() try testTool.checkTestPlanUnmodified(at: testTool.projectPath + "ExampleProject.xctestplan") } - func testMultipleTestPlansViaCLI() async throws { - // given + @Test + func multipleTestPlansViaCLI() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try SelectiveTestingTool(baseBranch: "main", basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlans: ["ExampleProject.xctestplan", "ExampleProject2.xctestplan"], changedFiles: [], verbose: true) - // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) - // Verify both test plans were updated try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject2.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } - func testMultipleTestPlansViaConfig() async throws { - // given + @Test + func multipleTestPlansViaConfig() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT(config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: nil, testPlans: ["ExampleProject.xctestplan", "ExampleProject2.xctestplan"], exclude: nil, extra: nil)) - // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) - // Verify both test plans were updated try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject2.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } - func testMultipleTestPlansMixedCliAndConfig() async throws { - // given - config has one test plan, CLI adds another + @Test + func multipleTestPlansMixedCliAndConfig() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT( config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", @@ -258,25 +265,22 @@ final class SelectiveTestingConfigTests: XCTestCase { extra: nil), testPlan: "ExampleProject2.xctestplan") - // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) - // Verify both test plans were updated try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject2.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift index 985664e..cd439c6 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift @@ -5,98 +5,89 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingPackagesTests: XCTestCase { - let testTool = IntegrationTestTool() +@Suite +struct SelectiveTestingPackagesTests { + @Test + func projectLoading_changePackage() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } - override func setUp() async throws { - try await super.setUp() - - try testTool.setUp() - } - - override func tearDown() async throws { - try await super.tearDown() - - try testTool.tearDown() - } - - func testProjectLoading_changePackage() async throws { - // given let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackage.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.package, - testTool.packageTests, - testTool.subtests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.package(), + testTool.packageTests(), + testTool.subtests()])) } - func testProjectLoading_changePackageDefintion() async throws { - // given + @Test + func projectLoading_changePackageDefinition() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.package, - testTool.packageTests, - testTool.subtests, - testTool.binary])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.package(), + testTool.packageTests(), + testTool.subtests(), + testTool.binary()])) } - func testProjectLoading_packageAddFile() async throws { - // given + @Test + func projectLoading_packageAddFile() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT() - // when try testTool.addFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackageFile.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.package, - testTool.packageTests, - testTool.subtests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.package(), + testTool.packageTests(), + testTool.subtests()])) } - func testProjectLoading_packageRemoveFile() async throws { - // given + @Test + func projectLoading_packageRemoveFile() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT() - // when try testTool.removeFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackage.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.package, - testTool.packageTests, - testTool.subtests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.package(), + testTool.packageTests(), + testTool.subtests()])) } - func testBinaryTargetChange() async throws { - // given - let tool = try testTool.createSUT() + @Test + func binaryTargetChange() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } - // when + let tool = try testTool.createSUT() try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Binary.xcframework/Info.plist") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.binary])) + #expect(result == Set([testTool.binary()])) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift index e08c3f6..fc2663d 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift @@ -5,45 +5,25 @@ import Foundation import PathKit @testable import SelectiveTestingCore -import XCTest - -final class SelectiveTestingPerformanceTests: XCTestCase { - let testTool = IntegrationTestTool() - - override func setUp() async throws { - try await super.setUp() - - try testTool.setUp() - } - - override func tearDown() async throws { - try await super.tearDown() - - try testTool.tearDown() - } - - func testPerformance() async throws { - measure { - let expecation = expectation(description: "Job is done") - Task { - // given - let tool = try testTool.createSUT() - // when - try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") - - // then - let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.mainProjectLibrary, - testTool.mainProjectLibraryTests, - ])) - expecation.fulfill() - } - - wait(for: [expecation], timeout: 2000) - } +import Testing + +@Suite +struct SelectiveTestingPerformanceTests { + @Test + func performance() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + + let tool = try testTool.createSUT() + try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") + + let result = try await tool.run() + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), + ])) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift index c67481b..a51b995 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift @@ -5,133 +5,128 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingProjectTests: XCTestCase { - let testTool = IntegrationTestTool() +@Suite +struct SelectiveTestingProjectTests { + @Test + func projectAlone() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } - override func setUp() async throws { - try await super.setUp() - - try testTool.setUp() - } - - override func tearDown() async throws { - try await super.tearDown() - - try testTool.tearDown() - } - - func testProjectAlone() async throws { - // given let tool = try testTool.createSUT(config: nil, basePath: "ExampleProject.xcodeproj") - // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.mainProjectLibrary, - testTool.mainProjectLibraryTests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), ])) } - func testProjectDeepGroupPathChange_turbo() async throws { - // given + @Test + func projectDeepGroupPathChange_turbo() async throws { + 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/DeepGroup/Path/GroupContentView.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget + #expect(result == Set([ + testTool.mainProjectMainTarget() ])) } - func testProjectDeepGroupPathChange() async throws { - // given + @Test + func projectDeepGroupPathChange() async throws { + 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/DeepGroup/Path/GroupContentView.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - func testProjectDeepFolderPathChange_turbo() async throws { - // given + @Test + func projectDeepFolderPathChange_turbo() async throws { + 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/FolderContentView.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget + #expect(result == Set([ + testTool.mainProjectMainTarget() ])) } - func testProjectDeepFolderPathChange() async throws { - // given + @Test + func projectDeepFolderPathChange() async throws { + 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/FolderContentView.swift") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - func testProjectLocalizedPathChange() async throws { - // given + @Test + func projectLocalizedPathChange() async throws { + 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/Base.lproj/Example.xib") - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - - func testPassingChangedFiles() async throws { - // given & when + + @Test + func passingChangedFiles() async throws { + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let changedPath = testTool.projectPath + "ExampleProject/Base.lproj/Example.xib" let tool = try testTool.createSUT(config: nil, basePath: "ExampleProject.xcodeproj", changedFiles: [changedPath.string]) - // then let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift index 4f15488..43c51b8 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift @@ -5,142 +5,125 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingWorksapceTests: XCTestCase { - let testTool = IntegrationTestTool() - - func testProjectLoading_empty() async throws { - try await testTool.withTestTool { - // given +@Suite +struct SelectiveTestingWorkspaceTests { + @Test + func projectLoading_empty() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT() - // when let result = try await tool.run() - // then - XCTAssertEqual(result, Set()) + #expect(result == Set()) } } - func testProjectLoading_changeLibrary() async throws { - try await testTool.withTestTool { - // given + @Test + func projectLoading_changeLibrary() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - - // then + let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibrary, - testTool.exampleLibraryTests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibrary(), + testTool.exampleLibraryTests()])) } } - func testProjectLoading_changeAsset() async throws { - try await testTool.withTestTool { - // given + @Test + func projectLoading_changeAsset() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject/Assets.xcassets/Contents.json") - - // then + let result = try await tool.run() - XCTAssertEqual(result, Set([testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests])) + #expect(result == Set([testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests()])) } } - func testProjectLoading_testPlanChange() async throws { - try await testTool.withTestTool { - // given + @Test + func projectLoading_testPlanChange() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xctestplan") - - // then + let result = try await tool.run() - XCTAssertEqual(result, Set()) + #expect(result == Set()) } } - func testProjectLoading_testWorkspaceFileChange() async throws { - try await testTool.withTestTool { - // given + @Test + func projectLoading_testWorkspaceFileChange() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT() - // when try testTool.changeFile(at: testTool.projectPath + "ExampleWorkspace.xcworkspace/contents.xcworkspacedata") - // then + let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.mainProjectLibrary, - testTool.mainProjectLibraryTests, - testTool.exampleLibraryTests, - testTool.exampleLibrary, - testTool.exampleLibraryInGroup, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), + testTool.exampleLibraryTests(), + testTool.exampleLibrary(), + testTool.exampleLibraryInGroup() ])) } } - func testProjectLoading_testProjectFileChange() async throws { - try await testTool.withTestTool { - // given - let tool = try testTool.createSUT() - // when + @Test + func projectLoading_testProjectFileChange() async throws { + try await IntegrationTestTool.withTestTool { testTool in try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") - - // then + let tool = try testTool.createSUT() let result = try await tool.run() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.mainProjectLibrary, - testTool.mainProjectLibraryTests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), ])) } } - func testInferTestPlan() async throws { - try await testTool.withTestTool { - // given + @Test + func inferTestPlan() async throws { + try await IntegrationTestTool.withTestTool { testTool in let tool = try testTool.createSUT(config: nil, testPlan: nil) - // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - - // then - let _ = try await tool.run() + + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } } - - func testInferTestPlanInSubfolder() async throws { - try await testTool.withTestTool(subfolder: true) { - // given + + @Test + func inferTestPlanInSubfolder() async throws { + try await IntegrationTestTool.withTestTool(subfolder: true) { testTool in let tool = try testTool.createSUT( config: nil, basePath: testTool.projectPath + "Subfolder", testPlan: nil) - - // when + try testTool.changeFile(at: testTool.projectPath + "Subfolder/ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") - - // then - let _ = try await tool.run() + + _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "Subfolder/ExampleProject.xctestplan", - expected: Set([testTool.mainProjectTests, - testTool.mainProjectUITests, - testTool.exampleLibraryTests])) + expected: Set([testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.exampleLibraryTests()])) } } } diff --git a/Tests/SelectiveTestingTests/TestPlanCodableTests.swift b/Tests/SelectiveTestingTests/TestPlanCodableTests.swift index 495ffc5..4d959c2 100644 --- a/Tests/SelectiveTestingTests/TestPlanCodableTests.swift +++ b/Tests/SelectiveTestingTests/TestPlanCodableTests.swift @@ -1,8 +1,9 @@ +import Foundation @testable import TestConfigurator -import XCTest +import Testing -class SkippedTestsTests: XCTestCase { - // Sample JSON for skippedTests as an array of strings +@Suite +struct SkippedTestsTests { let jsonWithArray = """ { "skippedTests": [ @@ -12,7 +13,6 @@ class SkippedTestsTests: XCTestCase { } """.data(using: .utf8)! - // Sample JSON for skippedTests as a dictionary let jsonWithDictionary = """ { "skippedTests": { @@ -25,33 +25,36 @@ class SkippedTestsTests: XCTestCase { } """.data(using: .utf8)! - func testDecodeSkippedTestsAsArray() throws { + @Test + func decodeSkippedTestsAsArray() throws { let decoder = JSONDecoder() let container = try decoder.decode(SkippedTestsContainer.self, from: jsonWithArray) if case let .array(skippedTests) = container.skippedTests { - XCTAssertEqual(skippedTests, [ + #expect(skippedTests == [ "DigitalRewardsServiceTests", "LoyaltyCreditCardRewardsPointViewModelTests" ]) } else { - XCTFail("Expected skippedTests to be an array") + Issue.record("Expected skippedTests to be an array") } } - func testDecodeSkippedTestsAsDictionary() throws { + @Test + func decodeSkippedTestsAsDictionary() throws { let decoder = JSONDecoder() let container = try decoder.decode(SkippedTestsContainer.self, from: jsonWithDictionary) if case let .dictionary(suites) = container.skippedTests { - XCTAssertEqual(suites.suites.count, 1) - XCTAssertEqual(suites.suites[0].name, "SparksMissionOfferVisibleTrackingEventTests") + #expect(suites.suites.count == 1) + #expect(suites.suites[0].name == "SparksMissionOfferVisibleTrackingEventTests") } else { - XCTFail("Expected skippedTests to be a dictionary") + Issue.record("Expected skippedTests to be a dictionary") } } - func testEncodeSkippedTestsAsArray() throws { + @Test + func encodeSkippedTestsAsArray() throws { let container = SkippedTestsContainer( skippedTests: .array([ "DigitalRewardsServiceTests", @@ -64,12 +67,13 @@ class SkippedTestsTests: XCTestCase { let encodedData = try encoder.encode(container) let encodedString = String(data: encodedData, encoding: .utf8) - XCTAssertNotNil(encodedString) - XCTAssertTrue(encodedString!.contains("\"skippedTests\" : [")) - XCTAssertTrue(encodedString!.contains("\"DigitalRewardsServiceTests\"")) + #expect(encodedString != nil) + #expect(encodedString?.contains("\"skippedTests\" : [") == true) + #expect(encodedString?.contains("\"DigitalRewardsServiceTests\"") == true) } - func testEncodeSkippedTestsAsDictionary() throws { + @Test + func encodeSkippedTestsAsDictionary() throws { let container = SkippedTestsContainer( skippedTests: .dictionary( Tests.Suites(suites: [ @@ -83,14 +87,13 @@ class SkippedTestsTests: XCTestCase { let encodedData = try encoder.encode(container) let encodedString = String(data: encodedData, encoding: .utf8) - XCTAssertNotNil(encodedString) - XCTAssertTrue(encodedString!.contains("\"skippedTests\" : {")) - XCTAssertTrue(encodedString!.contains("\"suites\" : [")) - XCTAssertTrue(encodedString!.contains("\"name\" : \"SparksMissionOfferVisibleTrackingEventTests\"")) + #expect(encodedString != nil) + #expect(encodedString?.contains("\"skippedTests\" : {") == true) + #expect(encodedString?.contains("\"suites\" : [") == true) + #expect(encodedString?.contains("\"name\" : \"SparksMissionOfferVisibleTrackingEventTests\"") == true) } } -// Container to isolate the "skippedTests" field for testing struct SkippedTestsContainer: Codable { let skippedTests: Tests } From 7e718569929b01a4650cccd3709ccfb7f1d10626 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 18:40:49 +0100 Subject: [PATCH 05/12] Move to SwiftLogger --- Package.resolved | 21 +++++----- Package.swift | 13 +++---- .../DependencyCalculator/ConcurrentMap.swift | 6 +-- .../DependencyCalculator.swift | 4 +- .../DependencyGraph.swift | 28 +++++++------- .../PackageMetadata.swift | 6 +-- Sources/Git/Git+Changeset.swift | 10 +++-- Sources/SelectiveTestLogger/Logger.swift | 29 -------------- .../SelectiveTestingTool.swift | 38 ++++++++++--------- .../TestConfigurator/TestConfigurator.swift | 2 +- .../Core/Helper/TestPlanHelper.swift | 22 ++++++----- Sources/Workspace/WorkspaceInfo.swift | 2 +- .../SelectiveTesting.swift | 2 +- 13 files changed, 80 insertions(+), 103 deletions(-) delete mode 100644 Sources/SelectiveTestLogger/Logger.swift diff --git a/Package.resolved b/Package.resolved index a4da9c9..fa0e4f6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "5afd8be6e36d2f7cf1e00bd71901ab1daeb41f6d4d2bc1a212b879e0e4bde9d8", "pins" : [ { "identity" : "aexml", @@ -18,15 +19,6 @@ "version" : "1.0.1" } }, - { - "identity" : "rainbow", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Rainbow", - "state" : { - "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", - "version" : "4.0.1" - } - }, { "identity" : "spectre", "kind" : "remoteSourceControl", @@ -45,6 +37,15 @@ "version" : "1.2.2" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + }, { "identity" : "xcodeproj", "kind" : "remoteSourceControl", @@ -64,5 +65,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 7d53159..754c662 100644 --- a/Package.swift +++ b/Package.swift @@ -28,19 +28,16 @@ let targets: [PackageDescription.Target] = [ "TestConfigurator", "Git", "PathKit", - "Rainbow", "Yams", .product(name: "ArgumentParser", package: "swift-argument-parser")]), .target(name: "DependencyCalculator", - dependencies: ["Workspace", "PathKit", "SelectiveTestLogger", "Git"]), + dependencies: ["Workspace", "PathKit", "Git", .product(name: "Logging", package: "swift-log")]), .target(name: "TestConfigurator", - dependencies: ["Workspace", "PathKit", "SelectiveTestLogger"]), + dependencies: ["Workspace", "PathKit", .product(name: "Logging", package: "swift-log")]), .target(name: "Workspace", - dependencies: ["XcodeProj", "SelectiveTestLogger"]), + dependencies: ["XcodeProj", .product(name: "Logging", package: "swift-log")]), .target(name: "Git", - dependencies: ["SelectiveTestShell", "SelectiveTestLogger", "PathKit"]), - .target(name: "SelectiveTestLogger", - dependencies: ["Rainbow"]), + dependencies: ["SelectiveTestShell", "PathKit", .product(name: "Logging", package: "swift-log")]), .target(name: "SelectiveTestShell"), .testTarget( name: "SelectiveTestingTests", @@ -77,8 +74,8 @@ let package = Package( .package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "9.0.2")), .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.0")), .package(url: "https://github.com/kylef/PathKit.git", .upToNextMinor(from: "1.0.0")), - .package(url: "https://github.com/onevcat/Rainbow", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"), + .package(url: "https://github.com/apple/swift-log", from: "1.6.0") ], targets: targets ) diff --git a/Sources/DependencyCalculator/ConcurrentMap.swift b/Sources/DependencyCalculator/ConcurrentMap.swift index 68a6710..d2bcdc9 100644 --- a/Sources/DependencyCalculator/ConcurrentMap.swift +++ b/Sources/DependencyCalculator/ConcurrentMap.swift @@ -4,7 +4,7 @@ import Foundation -final class ThreadSafe { +final class ThreadSafe: @unchecked Sendable { private var _value: A private let queue = DispatchQueue(label: "ThreadSafe") init(_ value: A) { @@ -22,8 +22,8 @@ final class ThreadSafe { } } -extension Array { - func concurrentMap(_ transform: @escaping (Element) -> B) -> [B] { +extension Array where Element: Sendable { + func concurrentMap(_ transform: @escaping @Sendable (Element) -> B) -> [B] { let result = ThreadSafe([B?](repeating: nil, count: count)) DispatchQueue.concurrentPerform(iterations: count) { idx in let element = self[idx] diff --git a/Sources/DependencyCalculator/DependencyCalculator.swift b/Sources/DependencyCalculator/DependencyCalculator.swift index b1220cd..5df620d 100644 --- a/Sources/DependencyCalculator/DependencyCalculator.swift +++ b/Sources/DependencyCalculator/DependencyCalculator.swift @@ -4,7 +4,7 @@ import Foundation import PathKit -import SelectiveTestLogger +import Logging import Workspace public extension WorkspaceInfo { @@ -18,7 +18,7 @@ public extension WorkspaceInfo { } else if let targetFromFolder = targetForFolder(path) { result.insert(targetFromFolder) } else { - Logger.message("Changed file at \(path) appears not to belong to any target") + logger.info("Changed file at \(path) appears not to belong to any target") } } if incldueIndirectlyAffected { diff --git a/Sources/DependencyCalculator/DependencyGraph.swift b/Sources/DependencyCalculator/DependencyGraph.swift index 5e60bbe..fadadcc 100644 --- a/Sources/DependencyCalculator/DependencyGraph.swift +++ b/Sources/DependencyCalculator/DependencyGraph.swift @@ -4,16 +4,18 @@ import Foundation import Git -import PathKit -import SelectiveTestLogger +@preconcurrency import PathKit +import Logging import SelectiveTestShell import Workspace import XcodeProj +let logger = Logger(label: "cx.gera.XcodeSelectiveTesting") + extension PBXBuildFile { func paths(projectFolder: Path) -> [Path] { guard let file else { - Logger.warning("PBXBuildFile without file: self=\(self), \n self.product=\(String(describing: product))") + logger.warning("PBXBuildFile without file: self=\(self), \n self.product=\(String(describing: product))") return [] } @@ -29,7 +31,7 @@ extension PBXBuildFile { } guard paths.count > 0 else { - Logger.warning("File without paths: self=\(self), \n self.file=\(String(describing: file)), \n self.product=\(String(describing: product))") + logger.warning("File without paths: self=\(self), \n self.file=\(String(describing: file)), \n self.product=\(String(describing: product))") return [] } @@ -176,12 +178,12 @@ extension WorkspaceInfo { for (targetName, dependOnTargets) in config.dependencies { guard let target = allTargets[targetName] else { - Logger.error("Config: Cannot resolve \(targetName) to any known target") + logger.error("Config: Cannot resolve \(targetName) to any known target") continue } for dependOnTargetName in dependOnTargets { guard let targetDependOn = allTargets[dependOnTargetName] else { - Logger.error("Config: Cannot resolve \(dependOnTargetName) to any known target") + logger.error("Config: Cannot resolve \(dependOnTargetName) to any known target") continue } @@ -193,7 +195,7 @@ extension WorkspaceInfo { for (targetName, filesToAdd) in config.targetsFiles { guard let target = allTargets[targetName] else { - Logger.error("Config: Cannot resolve \(targetName) to any known target") + logger.error("Config: Cannot resolve \(targetName) to any known target") continue } @@ -201,7 +203,7 @@ extension WorkspaceInfo { let path = (basePath + filePath).absolute() guard path.exists else { - Logger.error("Config: Path \(path) does not exist") + logger.error("Config: Path \(path) does not exist") continue } @@ -272,7 +274,7 @@ extension WorkspaceInfo { for affectedByPath in metadata.affectedBy { guard affectedByPath.exists else { - Logger.warning("Path \(affectedByPath) is mentioned from package at \(metadata.path) but does not exist") + logger.warning("Path \(affectedByPath) is mentioned from package at \(metadata.path) but does not exist") continue } @@ -309,7 +311,7 @@ extension WorkspaceInfo { let absolutePath = path.parent() + localPackage.relativePath guard let newPackages = try? PackageTargetMetadata.parse(at: absolutePath) else { - Logger.warning("Cannot find local package at \(absolutePath)") + logger.warning("Cannot find local package at \(absolutePath)") return } for package in newPackages { @@ -323,7 +325,7 @@ extension WorkspaceInfo { // Target dependencies for dependency in target.dependencies { guard let name = dependency.target?.name else { - Logger.warning("Target without name: \(dependency)") + logger.warning("Target without name: \(dependency)") continue } @@ -331,7 +333,7 @@ extension WorkspaceInfo { dependsOn.insert(targetIdentity, dependOn: TargetIdentity.project(path: path, target: dependencyTarget)) } else { - Logger.warning("Unknown target: \(name)") + logger.warning("Unknown target: \(name)") dependsOn.insert(targetIdentity, dependOn: TargetIdentity.project(path: path, targetName: name, testTarget: false)) } @@ -341,7 +343,7 @@ extension WorkspaceInfo { for packageDependency in (target.packageProductDependencies ?? []) { let package = packageDependency.productName guard let packageMetadata = packagesByName[package] else { - Logger.warning("Package \(package) not found") + logger.warning("Package \(package) not found") continue } dependsOn.insert(targetIdentity, diff --git a/Sources/DependencyCalculator/PackageMetadata.swift b/Sources/DependencyCalculator/PackageMetadata.swift index 9b69f47..1dca36a 100644 --- a/Sources/DependencyCalculator/PackageMetadata.swift +++ b/Sources/DependencyCalculator/PackageMetadata.swift @@ -3,12 +3,12 @@ // import Foundation -import PathKit -import SelectiveTestLogger +@preconcurrency import PathKit +import Logging import SelectiveTestShell import Workspace -struct PackageTargetMetadata { +struct PackageTargetMetadata: Sendable { let path: Path let affectedBy: Set let name: String diff --git a/Sources/Git/Git+Changeset.swift b/Sources/Git/Git+Changeset.swift index 6263721..cbd1b0c 100644 --- a/Sources/Git/Git+Changeset.swift +++ b/Sources/Git/Git+Changeset.swift @@ -4,21 +4,23 @@ import Foundation import PathKit -import SelectiveTestLogger +import Logging import SelectiveTestShell +let logger = Logger(label: "cx.gera.XcodeSelectiveTesting") + public extension Git { func changeset(baseBranch: String, verbose: Bool = false) throws -> Set { let gitRoot = try repoRoot() var currentBranch = try Shell.execOrFail("(cd \(gitRoot) && git branch --show-current)").trimmingCharacters(in: .newlines) if verbose { - Logger.message("Current branch: \(currentBranch)") - Logger.message("Base branch: \(baseBranch)") + logger.info("Current branch: \(currentBranch)") + logger.info("Base branch: \(baseBranch)") } if currentBranch.isEmpty { - Logger.warning("Missing current branch at \(path)") + logger.warning("Missing current branch at \(path)") currentBranch = "HEAD" } diff --git a/Sources/SelectiveTestLogger/Logger.swift b/Sources/SelectiveTestLogger/Logger.swift deleted file mode 100644 index 2dba74e..0000000 --- a/Sources/SelectiveTestLogger/Logger.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Created by Mike Gerasymenko -// - -import Foundation -import Rainbow - -public struct StandardErrorOutputStream: TextOutputStream { - public mutating func write(_ string: String) { fputs(string, stderr) } -} - -public enum Logger { - private static func write(_ message: String) { - var stream = StandardErrorOutputStream() - print(message, to: &stream) - } - - public static func message(_ message: String) { - write(message) - } - - public static func warning(_ message: String) { - write("[WARN]: \(message)".yellow) - } - - public static func error(_ message: String) { - write("[ERROR]: \(message)".red) - } -} diff --git a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift index 10708a7..1589279 100644 --- a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift +++ b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift @@ -6,11 +6,13 @@ import DependencyCalculator import Foundation import Git import PathKit -import SelectiveTestLogger +import Logging import SelectiveTestShell import TestConfigurator import Workspace +let logger = Logger(label: "cx.gera.XcodeSelectiveTesting") + public final class SelectiveTestingTool { private let baseBranch: String? private let basePath: Path @@ -56,7 +58,7 @@ public final class SelectiveTestingTool { let loadedConfig = try Config.load(from: configData) { self.config = loadedConfig if verbose { - Logger.message("Loaded config from \(configPath)") + logger.info("Loaded config from \(configPath)") } } else { config = nil @@ -97,7 +99,7 @@ public final class SelectiveTestingTool { let changeset: Set if changedFiles.isEmpty { - Logger.message("Finding changeset for repository at \(basePath)") + logger.info("Finding changeset for repository at \(basePath)") if let baseBranch { changeset = try Git(path: basePath).changeset(baseBranch: baseBranch, verbose: verbose) } else { @@ -108,7 +110,7 @@ public final class SelectiveTestingTool { changeset = Set(changedFiles.map { Path($0).absolute() }) } - if verbose { Logger.message("Changed files: \(changeset)") } + if verbose { logger.info("Changed files: \(changeset)") } // 2. Parse workspace: find which files belong to which targets and target dependencies let workspaceInfo = try WorkspaceInfo.parseWorkspace(at: basePath.absolute(), @@ -135,30 +137,30 @@ public final class SelectiveTestingTool { .sorted(by: { $0.description < $1.description }).forEach { target in switch target.type { case .package: - Logger.message("Package target at \(target.path): \(target.name) depends on:") + logger.info("Package target at \(target.path): \(target.name) depends on:") case .project: - Logger.message("Project target at \(target.path): \(target.name) depends on:") + logger.info("Project target at \(target.path): \(target.name) depends on:") } workspaceInfo.dependencyStructure .dependencies(for: target) .sorted(by: { $0.description < $1.description }).forEach { dependency in - Logger.message(" ﹂\(dependency)") + logger.info(" ﹂\(dependency)") } } - Logger.message("Files for targets:") + logger.info("Files for targets:") for key in workspaceInfo.files.keys.sorted(by: { $0.description < $1.description }) { - Logger.message("\(key.description): ") + logger.info("\(key.description): ") workspaceInfo.files[key]?.forEach { filePath in - Logger.message("\t\(filePath)") + logger.info("\t\(filePath)") } } - Logger.message("Folders for targets:") + logger.info("Folders for targets:") for (key, folder) in workspaceInfo.folders.sorted(by: { $0.key < $1.key }) { - Logger.message("\t\(folder): \(key)") + logger.info("\t\(folder): \(key)") } } @@ -179,23 +181,23 @@ public final class SelectiveTestingTool { } } else if !printJSON { if affectedTargets.isEmpty { - if verbose { Logger.message("No targets affected") } + if verbose { logger.info("No targets affected") } } else { - if verbose { Logger.message("Targets to test:") } + if verbose { logger.info("Targets to test:") } for target in affectedTargets { - Logger.message(target.description) + logger.info(Logger.Message(stringLiteral: target.description)) } } } } else if !printJSON { if affectedTargets.isEmpty { - if verbose { Logger.message("No targets affected") } + if verbose { logger.info("No targets affected") } } else { - if verbose { Logger.message("Targets to test:") } + if verbose { logger.info("Targets to test:") } for target in affectedTargets { - Logger.message(target.description) + logger.info(Logger.Message(stringLiteral: target.description)) } } } diff --git a/Sources/TestConfigurator/TestConfigurator.swift b/Sources/TestConfigurator/TestConfigurator.swift index 61ff367..927508e 100644 --- a/Sources/TestConfigurator/TestConfigurator.swift +++ b/Sources/TestConfigurator/TestConfigurator.swift @@ -4,7 +4,7 @@ import Foundation import PathKit -import SelectiveTestLogger +import Logging import Workspace extension TestPlanHelper { diff --git a/Sources/TestConfigurator/xctestplanner/Core/Helper/TestPlanHelper.swift b/Sources/TestConfigurator/xctestplanner/Core/Helper/TestPlanHelper.swift index 542fc28..d1c097a 100644 --- a/Sources/TestConfigurator/xctestplanner/Core/Helper/TestPlanHelper.swift +++ b/Sources/TestConfigurator/xctestplanner/Core/Helper/TestPlanHelper.swift @@ -7,11 +7,13 @@ import ArgumentParser import Foundation -import SelectiveTestLogger +import Logging + +let logger = Logger(label: "cx.gera.XcodeSelectiveTesting") public class TestPlanHelper { public static func readTestPlan(filePath: String) throws -> TestPlanModel { - Logger.message("Reading test plan from file: \(filePath)") + logger.info("Reading test plan from file: \(filePath)") let url = URL(fileURLWithPath: filePath) let data = try Data(contentsOf: url) @@ -20,7 +22,7 @@ public class TestPlanHelper { } static func writeTestPlan(_ testPlan: TestPlanModel, filePath: String) throws { - Logger.message("Writing updated test plan to file: \(filePath)") + logger.info("Writing updated test plan to file: \(filePath)") let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let updatedData = try encoder.encode(testPlan) @@ -30,7 +32,7 @@ public class TestPlanHelper { } static func updateRerunCount(testPlan: inout TestPlanModel, to count: Int) { - Logger.message("Updating rerun count in test plan to: \(count)") + logger.info("Updating rerun count in test plan to: \(count)") if testPlan.defaultOptions.testRepetitionMode == nil { testPlan.defaultOptions.testRepetitionMode = TestPlanValue.retryOnFailure.rawValue } @@ -38,17 +40,17 @@ public class TestPlanHelper { } static func updateLanguage(testPlan: inout TestPlanModel, to language: String) { - Logger.message("Updating language in test plan to: \(language)") + logger.info("Updating language in test plan to: \(language)") testPlan.defaultOptions.language = language.lowercased() } static func updateRegion(testPlan: inout TestPlanModel, to region: String) { - Logger.message("Updating region in test plan to: \(region)") + logger.info("Updating region in test plan to: \(region)") testPlan.defaultOptions.region = region.uppercased() } static func setEnvironmentVariable(testPlan: inout TestPlanModel, key: String, value: String, enabled: Bool? = true) { - Logger.message("Setting environment variable with key '\(key)' and value '\(value)' in test plan") + logger.info("Setting environment variable with key '\(key)' and value '\(value)' in test plan") if testPlan.defaultOptions.environmentVariableEntries == nil { testPlan.defaultOptions.environmentVariableEntries = [] } @@ -60,17 +62,17 @@ public class TestPlanHelper { testPlan.defaultOptions.commandLineArgumentEntries = [] } if disabled { - Logger.message("Setting command line argument with key '\(key)' in test plan as disabled") + logger.info("Setting command line argument with key '\(key)' in test plan as disabled") testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: !disabled)) } else { - Logger.message("Setting command line argument with key '\(key)', enabled by default") + logger.info("Setting command line argument with key '\(key)', enabled by default") testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: nil)) } } static func checkForTestTargets(testPlan: TestPlanModel) { if testPlan.testTargets.isEmpty { - Logger.error("Test plan does not have any test targets. Add a test target before attempting to update the selected or skipped tests.") + logger.error("Test plan does not have any test targets. Add a test target before attempting to update the selected or skipped tests.") exit(1) } } diff --git a/Sources/Workspace/WorkspaceInfo.swift b/Sources/Workspace/WorkspaceInfo.swift index c557e94..aaef492 100644 --- a/Sources/Workspace/WorkspaceInfo.swift +++ b/Sources/Workspace/WorkspaceInfo.swift @@ -4,7 +4,7 @@ import Foundation import PathKit -import SelectiveTestLogger +import Logging import XcodeProj public extension Dictionary where Key == TargetIdentity, Value == Set { diff --git a/Sources/xcode-selective-test/SelectiveTesting.swift b/Sources/xcode-selective-test/SelectiveTesting.swift index 9605ca6..4c59ead 100644 --- a/Sources/xcode-selective-test/SelectiveTesting.swift +++ b/Sources/xcode-selective-test/SelectiveTesting.swift @@ -4,7 +4,7 @@ @preconcurrency import ArgumentParser import SelectiveTestingCore -import SelectiveTestLogger +import Logging @main struct SelectiveTesting: AsyncParsableCommand { From 94e10515e4e8b4b2a526572a19a8947995af8716 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:02:15 +0100 Subject: [PATCH 06/12] Enable strict concurrency --- Package.swift | 40 ++++++++++++++----- .../SelectiveTestingPlugin.swift | 12 ++++-- Sources/Workspace/Target.swift | 6 +-- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/Package.swift b/Package.swift index 754c662..2f3f80b 100644 --- a/Package.swift +++ b/Package.swift @@ -17,11 +17,14 @@ let products: [PackageDescription.Product] = [ ) ] +let flags: [PackageDescription.SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency")] + let targets: [PackageDescription.Target] = [ .executableTarget( name: "xcode-selective-test", dependencies: ["SelectiveTestingCore", - .product(name: "ArgumentParser", package: "swift-argument-parser")] + .product(name: "ArgumentParser", package: "swift-argument-parser")], + swiftSettings: flags ), .target(name: "SelectiveTestingCore", dependencies: ["DependencyCalculator", @@ -29,25 +32,44 @@ let targets: [PackageDescription.Target] = [ "Git", "PathKit", "Yams", - .product(name: "ArgumentParser", package: "swift-argument-parser")]), + .product(name: "ArgumentParser", package: "swift-argument-parser")], + swiftSettings: flags, + ), .target(name: "DependencyCalculator", - dependencies: ["Workspace", "PathKit", "Git", .product(name: "Logging", package: "swift-log")]), + dependencies: ["Workspace", "PathKit", "Git", .product(name: "Logging", package: "swift-log")], + swiftSettings: flags, + ), .target(name: "TestConfigurator", - dependencies: ["Workspace", "PathKit", .product(name: "Logging", package: "swift-log")]), + dependencies: [ + "Workspace", + "PathKit", + .product(name: "Logging", package: "swift-log"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + swiftSettings: flags, + ), .target(name: "Workspace", - dependencies: ["XcodeProj", .product(name: "Logging", package: "swift-log")]), + dependencies: ["XcodeProj", .product(name: "Logging", package: "swift-log")], + swiftSettings: flags, + ), .target(name: "Git", - dependencies: ["SelectiveTestShell", "PathKit", .product(name: "Logging", package: "swift-log")]), - .target(name: "SelectiveTestShell"), + dependencies: ["SelectiveTestShell", "PathKit", .product(name: "Logging", package: "swift-log")], + swiftSettings: flags, + ), + .target(name: "SelectiveTestShell", + swiftSettings: flags, + ), .testTarget( name: "SelectiveTestingTests", dependencies: ["xcode-selective-test", "PathKit"], - resources: [.copy("ExampleProject")] + resources: [.copy("ExampleProject")], + swiftSettings: flags ), .testTarget( name: "DependencyCalculatorTests", dependencies: ["DependencyCalculator", "Workspace", "PathKit", "SelectiveTestingCore"], - resources: [.copy("ExamplePackages")] + resources: [.copy("ExamplePackages")], + swiftSettings: flags ), .plugin( name: "SelectiveTestingPlugin", diff --git a/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift b/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift index 7f0dc29..2a73c2d 100644 --- a/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift +++ b/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift @@ -48,19 +48,23 @@ struct SelectiveTestingPlugin: CommandPlugin { } if !toolArguments.contains(where: { $0 == "--test-plan" }) { - let testPlans = context.xcodeProject.filePaths.filter { - $0.extension == "xctestplan" + let allFiles = context.xcodeProject.targets.reduce([]) { partialResult, target in + partialResult + target.inputFiles + } + + let testPlans = allFiles.filter { + $0.url.pathExtension == "xctestplan" } if !testPlans.isEmpty { if testPlans.count == 1 { - print("Using \(testPlans[0].string) test plan") + print("Using \(testPlans[0].url.path()) test plan") } else { print("Using \(testPlans.count) test plans") } for testPlan in testPlans { - toolArguments.append(contentsOf: ["--test-plan", testPlan.string]) + toolArguments.append(contentsOf: ["--test-plan", testPlan.url.path()]) } } } diff --git a/Sources/Workspace/Target.swift b/Sources/Workspace/Target.swift index 0c59a4f..e3cca85 100644 --- a/Sources/Workspace/Target.swift +++ b/Sources/Workspace/Target.swift @@ -3,7 +3,7 @@ // import Foundation -import PathKit +@preconcurrency import PathKit import XcodeProj extension PBXNativeTarget { @@ -17,8 +17,8 @@ extension PBXNativeTarget { } } -public struct TargetIdentity: Hashable { - public enum TargetType { +public struct TargetIdentity: Hashable, Sendable { + public enum TargetType: Sendable { case project case package } From 2465ac43f395f67541b54ef2a529e275c4a3c230 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:16:03 +0100 Subject: [PATCH 07/12] Branch out test setups --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++-- Package.swift | 14 +++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6093710..db861d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: types: [opened, reopened] jobs: - test: + test-16-2: runs-on: macos-latest steps: - name: Set Xcode version @@ -16,7 +16,16 @@ jobs: - name: Run tests run: swift test -v - test-linux: + test-26: + runs-on: macos-latest + steps: + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_26.app + - uses: actions/checkout@v3 + - name: Run tests + run: swift test -v + + test-linux-6-1: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -29,3 +38,17 @@ jobs: run: git config --global user.email "test@example.com" && git config --global user.name "Test User" - name: Run tests run: swift test -v + + test-linux-6-2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: SwiftyLab/setup-swift@latest + with: + swift-version: "6.2.0" + - name: Get swift version + run: swift --version + - name: Prepare Git + run: git config --global user.email "test@example.com" && git config --global user.name "Test User" + - name: Run tests + run: swift test -v \ No newline at end of file diff --git a/Package.swift b/Package.swift index 2f3f80b..397262d 100644 --- a/Package.swift +++ b/Package.swift @@ -33,11 +33,11 @@ let targets: [PackageDescription.Target] = [ "PathKit", "Yams", .product(name: "ArgumentParser", package: "swift-argument-parser")], - swiftSettings: flags, + swiftSettings: flags ), .target(name: "DependencyCalculator", dependencies: ["Workspace", "PathKit", "Git", .product(name: "Logging", package: "swift-log")], - swiftSettings: flags, + swiftSettings: flags ), .target(name: "TestConfigurator", dependencies: [ @@ -46,18 +46,18 @@ let targets: [PackageDescription.Target] = [ .product(name: "Logging", package: "swift-log"), .product(name: "ArgumentParser", package: "swift-argument-parser") ], - swiftSettings: flags, + swiftSettings: flags ), .target(name: "Workspace", dependencies: ["XcodeProj", .product(name: "Logging", package: "swift-log")], - swiftSettings: flags, + swiftSettings: flags ), .target(name: "Git", dependencies: ["SelectiveTestShell", "PathKit", .product(name: "Logging", package: "swift-log")], - swiftSettings: flags, + swiftSettings: flags ), .target(name: "SelectiveTestShell", - swiftSettings: flags, + swiftSettings: flags ), .testTarget( name: "SelectiveTestingTests", @@ -83,7 +83,7 @@ let targets: [PackageDescription.Target] = [ ] ), dependencies: ["xcode-selective-test"] - ), + ) ] let package = Package( From 3eb0309950de18de369dada736daa1ff93a6f5eb Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:23:14 +0100 Subject: [PATCH 08/12] Use macOS 26 image --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db861d9..8f65e9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: run: swift test -v test-26: - runs-on: macos-latest + runs-on: macos-26 steps: - name: Set Xcode version run: sudo xcode-select -s /Applications/Xcode_26.app From 2192c0b867653568b61a7541cae0be863461182e Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:25:15 +0100 Subject: [PATCH 09/12] Run CI on push --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f65e9a..6dfa589 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main" ] pull_request: - types: [opened, reopened] + types: [opened, reopened, synchronize] jobs: test-16-2: From 6fdb39b45ab9005f8bd24de2c930f7ca37d99951 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:26:43 +0100 Subject: [PATCH 10/12] Xcode 26 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dfa589..02a4935 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: runs-on: macos-26 steps: - name: Set Xcode version - run: sudo xcode-select -s /Applications/Xcode_26.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app - uses: actions/checkout@v3 - name: Run tests run: swift test -v From 87dae95b5090551e9e39f4e8f5bb17883c56270c Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:42:40 +0100 Subject: [PATCH 11/12] Bring back given/when/then --- .../DependencyCalculatorTests.swift | 9 +++++ .../PackageMetadataTests.swift | 9 +++++ .../SelectiveTestingConfigTests.swift | 36 +++++++++++++++++++ .../SelectiveTestingPackagesTests.swift | 20 +++++++++++ .../SelectiveTestingPerformanceTests.swift | 4 +++ .../SelectiveTestingProjectTests.swift | 26 ++++++++++++++ .../SelectiveTestingWorkspaceTests.swift | 28 ++++++++++++++- 7 files changed, 131 insertions(+), 1 deletion(-) diff --git a/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift b/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift index 0aa7733..f3a9109 100644 --- a/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift +++ b/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift @@ -38,6 +38,7 @@ struct DependencyCalculatorTests { @Test func graphIntegrity_submodule() async throws { + // given let (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -47,13 +48,16 @@ struct DependencyCalculatorTests { dependencyStructure: depsGraph, candidateTestPlan: nil) + // when let affected = graph.affectedTargets(changedFiles: files) + // then #expect(affected == Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests])) } @Test func graphIntegrity_mainApp() async throws { + // given let (depsGraph, mainApp, _, _, mainAppTests, _, _) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -63,13 +67,16 @@ struct DependencyCalculatorTests { dependencyStructure: depsGraph, candidateTestPlan: nil) + // when let affected = graph.affectedTargets(changedFiles: files) + // then #expect(affected == Set([mainApp, mainAppTests])) } @Test func graphIntegrity_module() async throws { + // given let (depsGraph, mainApp, module, _, mainAppTests, moduleTests, _) = depStructure() let files = Set([Path("/folder/submodule/file.swift")]) @@ -79,8 +86,10 @@ struct DependencyCalculatorTests { dependencyStructure: depsGraph, candidateTestPlan: nil) + // when let affected = graph.affectedTargets(changedFiles: files) + // then #expect(affected == Set([module, moduleTests, mainApp, mainAppTests])) } } diff --git a/Tests/DependencyCalculatorTests/PackageMetadataTests.swift b/Tests/DependencyCalculatorTests/PackageMetadataTests.swift index f4eac3b..9e39e3a 100644 --- a/Tests/DependencyCalculatorTests/PackageMetadataTests.swift +++ b/Tests/DependencyCalculatorTests/PackageMetadataTests.swift @@ -12,13 +12,16 @@ import Testing struct PackageMetadataTests { @Test func packageMetadataParsing_Simple() throws { + // given guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } + // when let basePath = Path(exampleInBundle) + "Simple" let metadata = try PackageTargetMetadata.parse(at: basePath) + // then #expect(metadata.count == 2) let first = metadata[0] #expect(first.name == "ExampleSubpackage") @@ -51,13 +54,16 @@ struct PackageMetadataTests { @Test func packageMetadataParsing_ExamplePackage() throws { + // given guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } + // when let basePath = Path(exampleInBundle) + "CrossDependency" let metadata = try PackageTargetMetadata.parse(at: basePath) + // then #expect(metadata.count == 10) let first = metadata[0] #expect(first.name == "SelectiveTesting") @@ -82,13 +88,16 @@ struct PackageMetadataTests { @Test func packageAndWorkspace() throws { + // given guard let exampleInBundle = Bundle.module.path(forResource: "ExamplePackages", ofType: "") else { fatalError("Missing ExamplePackages in TestBundle") } + // when let basePath = Path(exampleInBundle) + "PackageAndWorkspace" let metadata = try PackageTargetMetadata.parse(at: basePath) + // then #expect(metadata.count == 2) let first = metadata[0] #expect(first.name == "APackage") diff --git a/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift index 79f70b4..610906b 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift @@ -12,6 +12,7 @@ import Workspace struct SelectiveTestingConfigTests { @Test func configWorkspacePath() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -21,8 +22,10 @@ struct SelectiveTestingConfigTests { exclude: nil, extra: nil)) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -33,6 +36,7 @@ struct SelectiveTestingConfigTests { @Test func configTestplanPath() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -42,8 +46,10 @@ struct SelectiveTestingConfigTests { exclude: nil, extra: nil)) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -58,6 +64,7 @@ struct SelectiveTestingConfigTests { @Test func configTestplanPath_packageChanged() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -68,8 +75,10 @@ struct SelectiveTestingConfigTests { exclude: nil, extra: nil)) + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([testTool.mainProjectTests(), @@ -80,6 +89,7 @@ struct SelectiveTestingConfigTests { @Test func configTestplanPath_packageResolvedChanged() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -89,8 +99,10 @@ struct SelectiveTestingConfigTests { exclude: nil, extra: nil)) + // when try testTool.addFile(at: testTool.projectPath + "ExamplePackage/Package.resolved") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([testTool.mainProjectTests(), @@ -101,6 +113,7 @@ struct SelectiveTestingConfigTests { @Test func additionalDependency() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -113,8 +126,10 @@ struct SelectiveTestingConfigTests { extra: additionalConfig) let tool = try testTool.createSUT(config: fullConfig) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleSubpackage/Package.swift") + // then let result = try await tool.run() #expect(result.contains(testTool.mainProjectLibrary())) #expect(result.contains(testTool.mainProjectLibraryTests())) @@ -122,6 +137,7 @@ struct SelectiveTestingConfigTests { @Test func additionalFiles() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -134,8 +150,10 @@ struct SelectiveTestingConfigTests { extra: additionalConfig) let tool = try testTool.createSUT(config: fullConfig) + // when try testTool.addFile(at: testTool.projectPath + "ExmapleTargetLibrary/SomeFile.swift") + // then let result = try await tool.run() #expect(result.contains(testTool.mainProjectLibrary())) #expect(result.contains(testTool.mainProjectLibraryTests())) @@ -143,6 +161,7 @@ struct SelectiveTestingConfigTests { @Test func exclude() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -152,8 +171,10 @@ struct SelectiveTestingConfigTests { exclude: ["ExamplePackage"], extra: nil)) + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([])) @@ -161,13 +182,16 @@ struct SelectiveTestingConfigTests { @Test func packageChangeInDifferentNamedPackage() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Tests/Subtests/Test.swift") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([testTool.subtests()])) @@ -175,6 +199,7 @@ struct SelectiveTestingConfigTests { @Test func dryRun() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -186,14 +211,17 @@ struct SelectiveTestingConfigTests { dryRun: true, verbose: true) + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Tests/Subtests/Test.swift") + // then _ = try await tool.run() try testTool.checkTestPlanUnmodified(at: testTool.projectPath + "ExampleProject.xctestplan") } @Test func multipleTestPlansViaCLI() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -203,8 +231,10 @@ struct SelectiveTestingConfigTests { changedFiles: [], verbose: true) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -224,6 +254,7 @@ struct SelectiveTestingConfigTests { @Test func multipleTestPlansViaConfig() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -233,8 +264,10 @@ struct SelectiveTestingConfigTests { exclude: nil, extra: nil)) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -254,6 +287,7 @@ struct SelectiveTestingConfigTests { @Test func multipleTestPlansMixedCliAndConfig() async throws { + // given - config has one test plan, CLI adds another let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -265,8 +299,10 @@ struct SelectiveTestingConfigTests { extra: nil), testPlan: "ExampleProject2.xctestplan") + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), diff --git a/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift index cd439c6..9df4bce 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift @@ -12,12 +12,16 @@ import Workspace struct SelectiveTestingPackagesTests { @Test func projectLoading_changePackage() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackage.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -29,12 +33,16 @@ struct SelectiveTestingPackagesTests { @Test func projectLoading_changePackageDefinition() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Package.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -47,12 +55,16 @@ struct SelectiveTestingPackagesTests { @Test func projectLoading_packageAddFile() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.addFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackageFile.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -64,12 +76,16 @@ struct SelectiveTestingPackagesTests { @Test func projectLoading_packageRemoveFile() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.removeFile(at: testTool.projectPath + "ExamplePackage/Sources/ExamplePackage/ExamplePackage.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -81,12 +97,16 @@ struct SelectiveTestingPackagesTests { @Test func binaryTargetChange() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.changeFile(at: testTool.projectPath + "ExamplePackage/Binary.xcframework/Info.plist") + // then let result = try await tool.run() #expect(result == Set([testTool.binary()])) } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift index fc2663d..8b58282 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift @@ -11,12 +11,16 @@ import Testing struct SelectiveTestingPerformanceTests { @Test func performance() async throws { + // given let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } let tool = try testTool.createSUT() + + // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), diff --git a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift index a51b995..9794d18 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift @@ -12,13 +12,17 @@ import Workspace struct SelectiveTestingProjectTests { @Test func projectAlone() 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.xcodeproj/project.pbxproj") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -31,14 +35,18 @@ struct SelectiveTestingProjectTests { @Test func projectDeepGroupPathChange_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/DeepGroup/Path/GroupContentView.swift") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget() @@ -47,13 +55,17 @@ struct SelectiveTestingProjectTests { @Test func projectDeepGroupPathChange() 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/DeepGroup/Path/GroupContentView.swift") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -64,14 +76,18 @@ struct SelectiveTestingProjectTests { @Test func projectDeepFolderPathChange_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/FolderContentView.swift") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget() @@ -80,13 +96,17 @@ struct SelectiveTestingProjectTests { @Test func projectDeepFolderPathChange() 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/FolderContentView.swift") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -97,13 +117,17 @@ struct SelectiveTestingProjectTests { @Test func projectLocalizedPathChange() 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/Base.lproj/Example.xib") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -114,6 +138,7 @@ struct SelectiveTestingProjectTests { @Test func passingChangedFiles() async throws { + // given & when let testTool = try IntegrationTestTool() defer { try? testTool.tearDown() } @@ -122,6 +147,7 @@ struct SelectiveTestingProjectTests { basePath: "ExampleProject.xcodeproj", changedFiles: [changedPath.string]) + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), diff --git a/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift index 43c51b8..c0e3cdd 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift @@ -13,8 +13,13 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_empty() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT() + + // when let result = try await tool.run() + + // then #expect(result == Set()) } } @@ -22,9 +27,12 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_changeLibrary() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT() + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -37,9 +45,12 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_changeAsset() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT() + // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject/Assets.xcassets/Contents.json") + // then let result = try await tool.run() #expect(result == Set([testTool.mainProjectMainTarget(), testTool.mainProjectTests(), @@ -50,9 +61,12 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_testPlanChange() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT() + // when try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xctestplan") + // then let result = try await tool.run() #expect(result == Set()) } @@ -61,9 +75,12 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_testWorkspaceFileChange() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT() + // when try testTool.changeFile(at: testTool.projectPath + "ExampleWorkspace.xcworkspace/contents.xcworkspacedata") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -81,8 +98,11 @@ struct SelectiveTestingWorkspaceTests { @Test func projectLoading_testProjectFileChange() async throws { try await IntegrationTestTool.withTestTool { testTool in - try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") + // given let tool = try testTool.createSUT() + // when + try testTool.changeFile(at: testTool.projectPath + "ExampleProject.xcodeproj/project.pbxproj") + // then let result = try await tool.run() #expect(result == Set([ testTool.mainProjectMainTarget(), @@ -97,10 +117,13 @@ struct SelectiveTestingWorkspaceTests { @Test func inferTestPlan() async throws { try await IntegrationTestTool.withTestTool { testTool in + // given let tool = try testTool.createSUT(config: nil, testPlan: nil) + // when try testTool.changeFile(at: testTool.projectPath + "ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "ExampleProject.xctestplan", expected: Set([testTool.mainProjectTests(), @@ -112,13 +135,16 @@ struct SelectiveTestingWorkspaceTests { @Test func inferTestPlanInSubfolder() async throws { try await IntegrationTestTool.withTestTool(subfolder: true) { testTool in + // given let tool = try testTool.createSUT( config: nil, basePath: testTool.projectPath + "Subfolder", testPlan: nil) + // when try testTool.changeFile(at: testTool.projectPath + "Subfolder/ExampleLibrary/ExampleLibrary/ExampleLibrary.swift") + // then _ = try await tool.run() try testTool.validateTestPlan(testPlanPath: testTool.projectPath + "Subfolder/ExampleProject.xctestplan", expected: Set([testTool.mainProjectTests(), From 6408b44d98d91f19539778a1746fd64e811d1010 Mon Sep 17 00:00:00 2001 From: Mike Gerasimenko Date: Mon, 3 Nov 2025 21:42:52 +0100 Subject: [PATCH 12/12] Cleanup --- Sources/SelectiveTestingCore/SelectiveTestingTool.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift index 1589279..2910077 100644 --- a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift +++ b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift @@ -37,9 +37,8 @@ public final class SelectiveTestingTool { dryRun: Bool = false, verbose: Bool = false) throws { - let suppliedBasePath = basePath.map { Path($0) } var configCandidates: [Path] = [] - if let suppliedBasePath { + if let suppliedBasePath = basePath.map({ Path($0) }) { let baseDirectory: Path if let ext = suppliedBasePath.extension, ext == "xcworkspace" || ext == "xcodeproj" {