Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.6
// swift-tools-version: 6.0

import PackageDescription

Expand Down
13 changes: 6 additions & 7 deletions Plugins/SelectiveTestingPlugin/SelectiveTestingPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}
}

Expand All @@ -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")

Expand Down Expand Up @@ -66,7 +65,7 @@ struct SelectiveTestingPlugin: CommandPlugin {
}
}

try run(tool.path.string, arguments: toolArguments)
try run(tool.url, arguments: toolArguments)
}
}
#endif
Expand Down
13 changes: 10 additions & 3 deletions Sources/DependencyCalculator/DependencyGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,22 @@ extension WorkspaceInfo {
dependencyStructure: resultDependencies,
candidateTestPlans: candidateTestPlans)
if let config {
let additionalBasePath: Path
if path.extension == "xcworkspace" || path.extension == "xcodeproj" {
additionalBasePath = path.parent()
} else {
additionalBasePath = path
}
// Process additional config
return processAdditional(config: config, workspaceInfo: workspaceInfo)
return processAdditional(config: config, workspaceInfo: workspaceInfo, basePath: additionalBasePath)
} else {
return workspaceInfo
}
}

static func processAdditional(config: WorkspaceInfo.AdditionalConfig,
workspaceInfo: WorkspaceInfo) -> WorkspaceInfo
workspaceInfo: WorkspaceInfo,
basePath: Path) -> WorkspaceInfo
{
var files = workspaceInfo.files
var folders = workspaceInfo.folders
Expand Down Expand Up @@ -191,7 +198,7 @@ extension WorkspaceInfo {
}

for filePath in filesToAdd {
let path = Path(filePath).absolute()
let path = (basePath + filePath).absolute()

guard path.exists else {
Logger.error("Config: Path \(path) does not exist")
Expand Down
2 changes: 1 addition & 1 deletion Sources/DependencyCalculator/PackageMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
6 changes: 3 additions & 3 deletions Sources/Git/Git+Changeset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public extension Git {
func changeset(baseBranch: String, verbose: Bool = false) throws -> Set<Path> {
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)")
Expand All @@ -23,7 +23,7 @@ public extension Git {
currentBranch = "HEAD"
}

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

guard !changesTrimmed.isEmpty else {
Expand All @@ -36,7 +36,7 @@ public extension Git {
func localChangeset() throws -> Set<Path> {
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)

Expand Down
4 changes: 2 additions & 2 deletions Sources/Git/Git.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ 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()
}

public func find(pattern: String) throws -> Set<Path> {
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()
Expand Down
13 changes: 8 additions & 5 deletions Sources/SelectiveTestLogger/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ public struct StandardErrorOutputStream: TextOutputStream {
public mutating func write(_ string: String) { fputs(string, stderr) }
}

public var errStream = StandardErrorOutputStream()

public enum Logger {
private static func write(_ message: String) {
var stream = StandardErrorOutputStream()
print(message, to: &stream)
}

public static func message(_ message: String) {
print(message, to: &errStream)
write(message)
}

public static func warning(_ message: String) {
print("[WARN]: \(message)".yellow, to: &errStream)
write("[WARN]: \(message)".yellow)
}

public static func error(_ message: String) {
print("[ERROR]: \(message)".red, to: &errStream)
write("[ERROR]: \(message)".red)
}
}
52 changes: 43 additions & 9 deletions Sources/SelectiveTestingCore/SelectiveTestingTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,40 @@ public final class SelectiveTestingTool {
dryRun: Bool = false,
verbose: Bool = false) throws
{
if let configData = try? (Path.current + Config.defaultConfigName).read(),
let config = try Config.load(from: configData)
{
self.config = config
let suppliedBasePath = basePath.map { Path($0) }
var configCandidates: [Path] = []
if let suppliedBasePath {
let baseDirectory: Path
if let ext = suppliedBasePath.extension,
ext == "xcworkspace" || ext == "xcodeproj" {
baseDirectory = suppliedBasePath.parent()
} else if suppliedBasePath.isDirectory {
baseDirectory = suppliedBasePath
} else {
baseDirectory = suppliedBasePath.parent()
}
configCandidates.append(baseDirectory + Config.defaultConfigName)
}
configCandidates.append(Path.current + Config.defaultConfigName)

if let configPath = configCandidates.first(where: { $0.exists }),
let configData = try? configPath.read(),
let loadedConfig = try Config.load(from: configData) {
self.config = loadedConfig
if verbose {
Logger.message("Loaded config from \(configPath)")
}
} else {
config = nil
}

let finalBasePath = basePath ??
let finalBasePath = Path(basePath ??
config?.basePath ??
Path().glob("*.xcworkspace").first?.string ??
Path().glob("*.xcodeproj").first?.string ?? "."
Path().glob("*.xcodeproj").first?.string ?? ".")

self.baseBranch = baseBranch
self.basePath = Path(finalBasePath)
self.basePath = finalBasePath
self.changedFiles = changedFiles
self.printJSON = printJSON
self.renderDependencyGraph = renderDependencyGraph
Expand All @@ -59,12 +78,21 @@ public final class SelectiveTestingTool {
self.verbose = verbose

// Merge CLI test plans with config test plans
var allTestPlans = config?.allTestPlans ?? []
var allTestPlans: [String] = config?.allTestPlans ?? []
allTestPlans.append(contentsOf: testPlans)
self.testPlans = allTestPlans
}

public func run() async throws -> Set<TargetIdentity> {
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<Path>

Expand Down Expand Up @@ -136,7 +164,13 @@ public final class SelectiveTestingTool {

if !dryRun {
// 4. Configure workspace to test given targets
let plansToUpdate = testPlans.isEmpty ? workspaceInfo.candidateTestPlans : testPlans
let plansToUpdate = testPlans.isEmpty ?
workspaceInfo.candidateTestPlans :
testPlans.map { plan in
let planPath = Path(plan)
let resolved = planPath.isAbsolute ? planPath : workingDirectory + planPath
return resolved.absolute().string
}

if !plansToUpdate.isEmpty {
for testPlan in plansToUpdate {
Expand Down
2 changes: 1 addition & 1 deletion Sources/xcode-selective-test/SelectiveTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Created by Mike Gerasymenko <mike@gera.cx>
//

import ArgumentParser
@preconcurrency import ArgumentParser
import SelectiveTestingCore
import SelectiveTestLogger

Expand Down
29 changes: 12 additions & 17 deletions Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -35,8 +36,8 @@ final class DependencyCalculatorTests: XCTestCase {
return (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests)
}

func testGraphIntegrity_submodule() async throws {
// given
@Test
func graphIntegrity_submodule() async throws {
let (depsGraph, mainApp, module, submodule, mainAppTests, moduleTests, submoduleTests) = depStructure()

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

let affected = graph.affectedTargets(changedFiles: files)

// then
XCTAssertEqual(affected, Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests]))
#expect(affected == Set([mainApp, mainAppTests, module, moduleTests, submodule, submoduleTests]))
}

func testGraphIntegrity_mainApp() async throws {
// given
@Test
func graphIntegrity_mainApp() async throws {
let (depsGraph, mainApp, _, _, mainAppTests, _, _) = depStructure()

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

let affected = graph.affectedTargets(changedFiles: files)

// then
XCTAssertEqual(affected, Set([mainApp, mainAppTests]))
#expect(affected == Set([mainApp, mainAppTests]))
}

func testGraphIntegrity_module() async throws {
// given
@Test
func graphIntegrity_module() async throws {
let (depsGraph, mainApp, module, _, mainAppTests, moduleTests, _) = depStructure()

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

let affected = graph.affectedTargets(changedFiles: files)

// then
XCTAssertEqual(affected, Set([module, moduleTests, mainApp, mainAppTests]))
#expect(affected == Set([module, moduleTests, mainApp, mainAppTests]))
}
}
Loading