Skip to content

Commit 8238877

Browse files
authored
Migrate to Swift Testing (#74)
1 parent 85b71b8 commit 8238877

18 files changed

Lines changed: 559 additions & 545 deletions

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.6
1+
// swift-tools-version: 6.0
22

33
import PackageDescription
44

Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import PackagePlugin
77

88
@main
99
struct SelectiveTestingPlugin: CommandPlugin {
10-
private func run(_ executable: String, arguments: [String] = []) throws {
11-
let executableURL = URL(fileURLWithPath: executable)
10+
private func run(_ executable: URL, arguments: [String] = []) throws {
1211

1312
let process = Process()
14-
process.executableURL = executableURL
13+
process.executableURL = executable
1514
process.arguments = arguments
1615

1716
try process.run()
@@ -24,10 +23,10 @@ struct SelectiveTestingPlugin: CommandPlugin {
2423
}
2524

2625
func performCommand(context: PluginContext, arguments: [String]) async throws {
27-
FileManager().changeCurrentDirectoryPath(context.package.directory.string)
26+
FileManager.default.changeCurrentDirectoryPath(context.package.directoryURL.path)
2827
let tool = try context.tool(named: "xcode-selective-test")
2928

30-
try run(tool.path.string, arguments: arguments)
29+
try run(tool.url, arguments: arguments)
3130
}
3231
}
3332

@@ -36,7 +35,7 @@ struct SelectiveTestingPlugin: CommandPlugin {
3635

3736
extension SelectiveTestingPlugin: XcodeCommandPlugin {
3837
func performCommand(context: XcodePluginContext, arguments: [String]) throws {
39-
FileManager().changeCurrentDirectoryPath(context.xcodeProject.directory.string)
38+
FileManager.default.changeCurrentDirectoryPath(context.xcodeProject.directoryURL.path)
4039

4140
let tool = try context.tool(named: "xcode-selective-test")
4241

@@ -66,7 +65,7 @@ struct SelectiveTestingPlugin: CommandPlugin {
6665
}
6766
}
6867

69-
try run(tool.path.string, arguments: toolArguments)
68+
try run(tool.url, arguments: toolArguments)
7069
}
7170
}
7271
#endif

Sources/DependencyCalculator/DependencyGraph.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,22 @@ extension WorkspaceInfo {
152152
dependencyStructure: resultDependencies,
153153
candidateTestPlans: candidateTestPlans)
154154
if let config {
155+
let additionalBasePath: Path
156+
if path.extension == "xcworkspace" || path.extension == "xcodeproj" {
157+
additionalBasePath = path.parent()
158+
} else {
159+
additionalBasePath = path
160+
}
155161
// Process additional config
156-
return processAdditional(config: config, workspaceInfo: workspaceInfo)
162+
return processAdditional(config: config, workspaceInfo: workspaceInfo, basePath: additionalBasePath)
157163
} else {
158164
return workspaceInfo
159165
}
160166
}
161167

162168
static func processAdditional(config: WorkspaceInfo.AdditionalConfig,
163-
workspaceInfo: WorkspaceInfo) -> WorkspaceInfo
169+
workspaceInfo: WorkspaceInfo,
170+
basePath: Path) -> WorkspaceInfo
164171
{
165172
var files = workspaceInfo.files
166173
var folders = workspaceInfo.folders
@@ -191,7 +198,7 @@ extension WorkspaceInfo {
191198
}
192199

193200
for filePath in filesToAdd {
194-
let path = Path(filePath).absolute()
201+
let path = (basePath + filePath).absolute()
195202

196203
guard path.exists else {
197204
Logger.error("Config: Path \(path) does not exist")

Sources/DependencyCalculator/PackageMetadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct PackageTargetMetadata {
2525
flags.append("--ignore-lock")
2626
}
2727

28-
let manifest = try Shell.execOrFail("cd \(path) && swift package dump-package \(flags.joined(separator: " "))")
28+
let manifest = try Shell.execOrFail("(cd \(path) && swift package dump-package \(flags.joined(separator: " ")))")
2929
.trimmingCharacters(in: .newlines)
3030
guard let manifestData = manifest.data(using: .utf8),
3131
let manifestJson = try JSONSerialization.jsonObject(with: manifestData, options: []) as? [String: Any],

Sources/Git/Git+Changeset.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public extension Git {
1111
func changeset(baseBranch: String, verbose: Bool = false) throws -> Set<Path> {
1212
let gitRoot = try repoRoot()
1313

14-
var currentBranch = try Shell.execOrFail("cd \(gitRoot) && git branch --show-current").trimmingCharacters(in: .newlines)
14+
var currentBranch = try Shell.execOrFail("(cd \(gitRoot) && git branch --show-current)").trimmingCharacters(in: .newlines)
1515
if verbose {
1616
Logger.message("Current branch: \(currentBranch)")
1717
Logger.message("Base branch: \(baseBranch)")
@@ -23,7 +23,7 @@ public extension Git {
2323
currentBranch = "HEAD"
2424
}
2525

26-
let changes = try Shell.execOrFail("cd \(gitRoot) && git diff '\(baseBranch)'..'\(currentBranch)' --name-only")
26+
let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff '\(baseBranch)'..'\(currentBranch)' --name-only)")
2727
let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines)
2828

2929
guard !changesTrimmed.isEmpty else {
@@ -36,7 +36,7 @@ public extension Git {
3636
func localChangeset() throws -> Set<Path> {
3737
let gitRoot = try repoRoot()
3838

39-
let changes = try Shell.execOrFail("cd \(gitRoot) && git diff HEAD --name-only")
39+
let changes = try Shell.execOrFail("(cd \(gitRoot) && git diff HEAD --name-only)")
4040

4141
let changesTrimmed = changes.trimmingCharacters(in: .whitespacesAndNewlines)
4242

Sources/Git/Git.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ public struct Git {
1414
}
1515

1616
public func repoRoot() throws -> Path {
17-
let gitPath = try Shell.execOrFail("cd \(path) && git rev-parse --show-toplevel").trimmingCharacters(in: .newlines)
17+
let gitPath = try Shell.execOrFail("(cd \(path) && git rev-parse --show-toplevel)").trimmingCharacters(in: .newlines)
1818

1919
return Path(gitPath).absolute()
2020
}
2121

2222
public func find(pattern: String) throws -> Set<Path> {
2323
let gitRoot = try repoRoot()
2424

25-
let result = try Shell.exec("cd \(gitRoot) && git ls-files | grep \(pattern)").0.trimmingCharacters(in: .newlines)
25+
let result = try Shell.exec("(cd \(gitRoot) && git ls-files | grep \(pattern))").0.trimmingCharacters(in: .newlines)
2626

2727
guard !result.isEmpty else {
2828
return Set()

Sources/SelectiveTestLogger/Logger.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@ public struct StandardErrorOutputStream: TextOutputStream {
99
public mutating func write(_ string: String) { fputs(string, stderr) }
1010
}
1111

12-
public var errStream = StandardErrorOutputStream()
13-
1412
public enum Logger {
13+
private static func write(_ message: String) {
14+
var stream = StandardErrorOutputStream()
15+
print(message, to: &stream)
16+
}
17+
1518
public static func message(_ message: String) {
16-
print(message, to: &errStream)
19+
write(message)
1720
}
1821

1922
public static func warning(_ message: String) {
20-
print("[WARN]: \(message)".yellow, to: &errStream)
23+
write("[WARN]: \(message)".yellow)
2124
}
2225

2326
public static func error(_ message: String) {
24-
print("[ERROR]: \(message)".red, to: &errStream)
27+
write("[ERROR]: \(message)".red)
2528
}
2629
}

Sources/SelectiveTestingCore/SelectiveTestingTool.swift

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,40 @@ public final class SelectiveTestingTool {
3535
dryRun: Bool = false,
3636
verbose: Bool = false) throws
3737
{
38-
if let configData = try? (Path.current + Config.defaultConfigName).read(),
39-
let config = try Config.load(from: configData)
40-
{
41-
self.config = config
38+
let suppliedBasePath = basePath.map { Path($0) }
39+
var configCandidates: [Path] = []
40+
if let suppliedBasePath {
41+
let baseDirectory: Path
42+
if let ext = suppliedBasePath.extension,
43+
ext == "xcworkspace" || ext == "xcodeproj" {
44+
baseDirectory = suppliedBasePath.parent()
45+
} else if suppliedBasePath.isDirectory {
46+
baseDirectory = suppliedBasePath
47+
} else {
48+
baseDirectory = suppliedBasePath.parent()
49+
}
50+
configCandidates.append(baseDirectory + Config.defaultConfigName)
51+
}
52+
configCandidates.append(Path.current + Config.defaultConfigName)
53+
54+
if let configPath = configCandidates.first(where: { $0.exists }),
55+
let configData = try? configPath.read(),
56+
let loadedConfig = try Config.load(from: configData) {
57+
self.config = loadedConfig
58+
if verbose {
59+
Logger.message("Loaded config from \(configPath)")
60+
}
4261
} else {
4362
config = nil
4463
}
4564

46-
let finalBasePath = basePath ??
65+
let finalBasePath = Path(basePath ??
4766
config?.basePath ??
4867
Path().glob("*.xcworkspace").first?.string ??
49-
Path().glob("*.xcodeproj").first?.string ?? "."
68+
Path().glob("*.xcodeproj").first?.string ?? ".")
5069

5170
self.baseBranch = baseBranch
52-
self.basePath = Path(finalBasePath)
71+
self.basePath = finalBasePath
5372
self.changedFiles = changedFiles
5473
self.printJSON = printJSON
5574
self.renderDependencyGraph = renderDependencyGraph
@@ -59,12 +78,21 @@ public final class SelectiveTestingTool {
5978
self.verbose = verbose
6079

6180
// Merge CLI test plans with config test plans
62-
var allTestPlans = config?.allTestPlans ?? []
81+
var allTestPlans: [String] = config?.allTestPlans ?? []
6382
allTestPlans.append(contentsOf: testPlans)
6483
self.testPlans = allTestPlans
6584
}
6685

6786
public func run() async throws -> Set<TargetIdentity> {
87+
let workingDirectory: Path
88+
if let ext = basePath.extension,
89+
ext == "xcworkspace" || ext == "xcodeproj" {
90+
workingDirectory = basePath.parent()
91+
} else if basePath.isDirectory {
92+
workingDirectory = basePath
93+
} else {
94+
workingDirectory = basePath.parent()
95+
}
6896
// 1. Identify changed files
6997
let changeset: Set<Path>
7098

@@ -136,7 +164,13 @@ public final class SelectiveTestingTool {
136164

137165
if !dryRun {
138166
// 4. Configure workspace to test given targets
139-
let plansToUpdate = testPlans.isEmpty ? workspaceInfo.candidateTestPlans : testPlans
167+
let plansToUpdate = testPlans.isEmpty ?
168+
workspaceInfo.candidateTestPlans :
169+
testPlans.map { plan in
170+
let planPath = Path(plan)
171+
let resolved = planPath.isAbsolute ? planPath : workingDirectory + planPath
172+
return resolved.absolute().string
173+
}
140174

141175
if !plansToUpdate.isEmpty {
142176
for testPlan in plansToUpdate {

Sources/xcode-selective-test/SelectiveTesting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Created by Mike Gerasymenko <mike@gera.cx>
33
//
44

5-
import ArgumentParser
5+
@preconcurrency import ArgumentParser
66
import SelectiveTestingCore
77
import SelectiveTestLogger
88

Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import Foundation
77
import PathKit
88
import SelectiveTestingCore
9+
import Testing
910
import Workspace
10-
import XCTest
1111

12-
final class DependencyCalculatorTests: XCTestCase {
12+
@Suite
13+
struct DependencyCalculatorTests {
1314
func depStructure() -> (DependencyGraph, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity, TargetIdentity) {
1415
let mainApp = TargetIdentity.project(path: "/folder/Project.xcodepoj", targetName: "MainApp", testTarget: false)
1516
let mainAppTests = TargetIdentity.project(path: "/folder/Project.xcodepoj", targetName: "MainAppTests", testTarget: true)
@@ -35,8 +36,8 @@ final class DependencyCalculatorTests: XCTestCase {
3536
return (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests)
3637
}
3738

38-
func testGraphIntegrity_submodule() async throws {
39-
// given
39+
@Test
40+
func graphIntegrity_submodule() async throws {
4041
let (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) = depStructure()
4142

4243
let files = Set([Path("/folder/submodule/file.swift")])
@@ -45,16 +46,14 @@ final class DependencyCalculatorTests: XCTestCase {
4546
folders: [:],
4647
dependencyStructure: depsGraph,
4748
candidateTestPlan: nil)
48-
// when
4949

5050
let affected = graph.affectedTargets(changedFiles: files)
5151

52-
// then
53-
XCTAssertEqual(affected, Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests]))
52+
#expect(affected == Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests]))
5453
}
5554

56-
func testGraphIntegrity_mainApp() async throws {
57-
// given
55+
@Test
56+
func graphIntegrity_mainApp() async throws {
5857
let (depsGraph, mainApp, _, _, mainAppTests, _, _) = depStructure()
5958

6059
let files = Set([Path("/folder/submodule/file.swift")])
@@ -63,16 +62,14 @@ final class DependencyCalculatorTests: XCTestCase {
6362
folders: [:],
6463
dependencyStructure: depsGraph,
6564
candidateTestPlan: nil)
66-
// when
6765

6866
let affected = graph.affectedTargets(changedFiles: files)
6967

70-
// then
71-
XCTAssertEqual(affected, Set([mainApp, mainAppTests]))
68+
#expect(affected == Set([mainApp, mainAppTests]))
7269
}
7370

74-
func testGraphIntegrity_module() async throws {
75-
// given
71+
@Test
72+
func graphIntegrity_module() async throws {
7673
let (depsGraph, mainApp, module, _, mainAppTests, moduleTests, _) = depStructure()
7774

7875
let files = Set([Path("/folder/submodule/file.swift")])
@@ -81,11 +78,9 @@ final class DependencyCalculatorTests: XCTestCase {
8178
folders: [:],
8279
dependencyStructure: depsGraph,
8380
candidateTestPlan: nil)
84-
// when
8581

8682
let affected = graph.affectedTargets(changedFiles: files)
8783

88-
// then
89-
XCTAssertEqual(affected, Set([module, moduleTests, mainApp, mainAppTests]))
84+
#expect(affected == Set([module, moduleTests, mainApp, mainAppTests]))
9085
}
9186
}

0 commit comments

Comments
 (0)