diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82956c0..02a4935 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,10 +4,10 @@ on: push: branches: [ "main" ] pull_request: - types: [opened, reopened] + types: [opened, reopened, synchronize] jobs: - test: + test-16-2: runs-on: macos-latest steps: - name: Set Xcode version @@ -16,12 +16,20 @@ jobs: - name: Run tests run: swift test -v - test-linux: - if: false - runs-on: ubuntu-22.04 + test-26: + runs-on: macos-26 + steps: + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_26.0.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 - - uses: swift-actions/setup-swift@v2 + - uses: SwiftyLab/setup-swift@latest with: swift-version: "6.1.0" - name: Get swift version @@ -30,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.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 9c4474b..397262d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 6.0 import PackageDescription @@ -17,40 +17,59 @@ 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", "TestConfigurator", "Git", "PathKit", - "Rainbow", "Yams", - .product(name: "ArgumentParser", package: "swift-argument-parser")]), + .product(name: "ArgumentParser", package: "swift-argument-parser")], + swiftSettings: flags + ), .target(name: "DependencyCalculator", - dependencies: ["Workspace", "PathKit", "SelectiveTestLogger", "Git"]), + dependencies: ["Workspace", "PathKit", "Git", .product(name: "Logging", package: "swift-log")], + swiftSettings: flags + ), .target(name: "TestConfigurator", - dependencies: ["Workspace", "PathKit", "SelectiveTestLogger"]), + dependencies: [ + "Workspace", + "PathKit", + .product(name: "Logging", package: "swift-log"), + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + swiftSettings: flags + ), .target(name: "Workspace", - dependencies: ["XcodeProj", "SelectiveTestLogger"]), + dependencies: ["XcodeProj", .product(name: "Logging", package: "swift-log")], + swiftSettings: flags + ), .target(name: "Git", - dependencies: ["SelectiveTestShell", "SelectiveTestLogger", "PathKit"]), - .target(name: "SelectiveTestLogger", - dependencies: ["Rainbow"]), - .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", @@ -64,7 +83,7 @@ let targets: [PackageDescription.Target] = [ ] ), dependencies: ["xcode-selective-test"] - ), + ) ] let package = Package( @@ -77,8 +96,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/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift b/Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift index e2fc66a..2a73c2d 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") @@ -49,24 +48,28 @@ 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()]) } } } - try run(tool.path.string, arguments: toolArguments) + try run(tool.url, arguments: toolArguments) } } #endif 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 5a0adfb..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 [] } @@ -152,15 +154,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 @@ -169,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 } @@ -186,15 +195,15 @@ 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 } 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") + logger.error("Config: Path \(path) does not exist") continue } @@ -265,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 } @@ -302,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 { @@ -316,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 } @@ -324,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)) } @@ -334,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 3e21461..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 @@ -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..cbd1b0c 100644 --- a/Sources/Git/Git+Changeset.swift +++ b/Sources/Git/Git+Changeset.swift @@ -4,26 +4,28 @@ 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) + 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" } - 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 +38,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 deleted file mode 100644 index cb88b01..0000000 --- a/Sources/SelectiveTestLogger/Logger.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by Mike Gerasymenko -// - -import Foundation -import Rainbow - -public struct StandardErrorOutputStream: TextOutputStream { - public mutating func write(_ string: String) { fputs(string, stderr) } -} - -public var errStream = StandardErrorOutputStream() - -public enum Logger { - public static func message(_ message: String) { - print(message, to: &errStream) - } - - public static func warning(_ message: String) { - print("[WARN]: \(message)".yellow, to: &errStream) - } - - public static func error(_ message: String) { - print("[ERROR]: \(message)".red, to: &errStream) - } -} diff --git a/Sources/SelectiveTestingCore/SelectiveTestingTool.swift b/Sources/SelectiveTestingCore/SelectiveTestingTool.swift index fb5e735..2910077 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 @@ -35,21 +37,39 @@ 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 + var configCandidates: [Path] = [] + if let suppliedBasePath = basePath.map({ Path($0) }) { + 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.info("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,17 +79,26 @@ 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 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 { @@ -80,7 +109,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(), @@ -107,36 +136,42 @@ 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)") } } 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 { @@ -145,23 +180,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/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 } 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 54c7ffb..4c59ead 100644 --- a/Sources/xcode-selective-test/SelectiveTesting.swift +++ b/Sources/xcode-selective-test/SelectiveTesting.swift @@ -2,9 +2,9 @@ // Created by Mike Gerasymenko // -import ArgumentParser +@preconcurrency import ArgumentParser import SelectiveTestingCore -import SelectiveTestLogger +import Logging @main struct SelectiveTesting: AsyncParsableCommand { diff --git a/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift b/Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift index 2cf2920..f3a9109 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,7 +36,8 @@ final class DependencyCalculatorTests: XCTestCase { return (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) } - func testGraphIntegrity_submodule() async throws { + @Test + func graphIntegrity_submodule() async throws { // given let (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) = depStructure() @@ -45,15 +47,16 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when + // 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 { + @Test + func graphIntegrity_mainApp() async throws { // given let (depsGraph, mainApp, _, _, mainAppTests, _, _) = depStructure() @@ -63,15 +66,16 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when + // when let affected = graph.affectedTargets(changedFiles: files) // then - XCTAssertEqual(affected, Set([mainApp, mainAppTests])) + #expect(affected == Set([mainApp, mainAppTests])) } - func testGraphIntegrity_module() async throws { + @Test + func graphIntegrity_module() async throws { // given let (depsGraph, mainApp, module, _, mainAppTests, moduleTests, _) = depStructure() @@ -81,11 +85,11 @@ final class DependencyCalculatorTests: XCTestCase { folders: [:], dependencyStructure: depsGraph, candidateTestPlan: nil) - // when + // 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..9e39e3a 100644 --- a/Tests/DependencyCalculatorTests/PackageMetadataTests.swift +++ b/Tests/DependencyCalculatorTests/PackageMetadataTests.swift @@ -5,26 +5,29 @@ @testable import DependencyCalculator import Foundation import PathKit +import Testing @testable import Workspace -import XCTest -final class PackageMetadataTests: XCTestCase { - func testPackageMetadataParsing_Simple() throws { +@Suite +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 - 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 +35,85 @@ 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 { + @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 - 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 { + + @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 - 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..610906b 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingConfigTests.swift @@ -5,106 +5,118 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingConfigTests: 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 testConfigWorkspacePath() async throws { +@Suite +struct SelectiveTestingConfigTests { + @Test + func configWorkspacePath() async throws { // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + 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 { + @Test + func configTestplanPath() async throws { // given + 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 { + @Test + func configTestplanPath_packageChanged() async throws { // given + 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 { + + @Test + func configTestplanPath_packageResolvedChanged() async throws { // given + 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 { + @Test + func additionalDependency() async throws { // given + 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 +125,22 @@ 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 { + @Test + func additionalFiles() async throws { // given + 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 +149,60 @@ 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 { + @Test + func exclude() async throws { // given + 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 { + @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 - 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 { + + @Test + func dryRun() async throws { // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try SelectiveTestingTool(baseBranch: "main", basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlans: ["ExampleProject.xctestplan"], @@ -184,12 +215,16 @@ final class SelectiveTestingConfigTests: XCTestCase { 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 { + @Test + func multipleTestPlansViaCLI() async throws { // given + 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"], @@ -201,25 +236,28 @@ final class SelectiveTestingConfigTests: XCTestCase { // 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 { + @Test + func multipleTestPlansViaConfig() async throws { // given + 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"], @@ -231,25 +269,28 @@ final class SelectiveTestingConfigTests: XCTestCase { // 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 { + @Test + func multipleTestPlansMixedCliAndConfig() async throws { // given - config has one test plan, CLI adds another + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT( config: Config(basePath: (testTool.projectPath + "ExampleWorkspace.xcworkspace").string, testPlan: "ExampleProject.xctestplan", @@ -263,20 +304,19 @@ final class SelectiveTestingConfigTests: XCTestCase { // 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..9df4bce 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPackagesTests.swift @@ -5,91 +5,102 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingPackagesTests: 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 testProjectLoading_changePackage() async throws { +@Suite +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() - 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 { + @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() - 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 { + @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() - 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 { + @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() - 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 { + @Test + func binaryTargetChange() async throws { // given + let testTool = try IntegrationTestTool() + defer { try? testTool.tearDown() } + let tool = try testTool.createSUT() // when @@ -97,6 +108,6 @@ final class SelectiveTestingPackagesTests: XCTestCase { // 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..8b58282 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingPerformanceTests.swift @@ -5,45 +5,29 @@ 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 { + // 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(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), + ])) } } diff --git a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift index c67481b..9794d18 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingProjectTests.swift @@ -5,122 +5,143 @@ import PathKit @testable import SelectiveTestingCore import SelectiveTestShell +import Testing import Workspace -import XCTest -final class SelectiveTestingProjectTests: 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 testProjectAlone() async throws { +@Suite +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() - 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 { + @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() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget + #expect(result == Set([ + testTool.mainProjectMainTarget() ])) } - func testProjectDeepGroupPathChange() async throws { + @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() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - func testProjectDeepFolderPathChange_turbo() async throws { + @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() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget + #expect(result == Set([ + testTool.mainProjectMainTarget() ])) } - func testProjectDeepFolderPathChange() async throws { + @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() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - func testProjectLocalizedPathChange() async throws { + @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() - XCTAssertEqual(result, Set([ - testTool.mainProjectMainTarget, - testTool.mainProjectTests, - testTool.mainProjectUITests, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), ])) } - - func testPassingChangedFiles() async throws { + + @Test + func passingChangedFiles() async throws { // given & when + 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", @@ -128,10 +149,10 @@ final class SelectiveTestingProjectTests: XCTestCase { // 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..c0e3cdd 100644 --- a/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift +++ b/Tests/SelectiveTestingTests/SelectiveTestingWorkspaceTests.swift @@ -5,142 +5,151 @@ 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 { +@Suite +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 - XCTAssertEqual(result, Set()) + #expect(result == Set()) } } - func testProjectLoading_changeLibrary() async throws { - try await testTool.withTestTool { + @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() - 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 { + @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() - 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 { + @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() - XCTAssertEqual(result, Set()) + #expect(result == Set()) } } - func testProjectLoading_testWorkspaceFileChange() async throws { - try await testTool.withTestTool { + @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() - 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 { + @Test + func projectLoading_testProjectFileChange() async throws { + try await IntegrationTestTool.withTestTool { testTool in // 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, + #expect(result == Set([ + testTool.mainProjectMainTarget(), + testTool.mainProjectTests(), + testTool.mainProjectUITests(), + testTool.mainProjectLibrary(), + testTool.mainProjectLibraryTests(), ])) } } - func testInferTestPlan() async throws { - try await testTool.withTestTool { + @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 - 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) { + + @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 - 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 }