Skip to content

Commit

Permalink
Add support for selective testing for Xcode projects with local packa…
Browse files Browse the repository at this point in the history
…ges (tuist#7317)

* Add support for selective testing for Xcode projects with local packages

* Update XcodeGraph

* Apply PR feedback
  • Loading branch information
fortmarek authored Feb 18, 2025
1 parent 1134fc4 commit 35e7310
Show file tree
Hide file tree
Showing 43 changed files with 2,736 additions and 2,334 deletions.
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "df248465b2e90fbda1d3eb2b299c28f9d8ecf0f165219a83012a438b3ff3c5f5",
"originHash" : "af740149e032b45c3f83b4c6bc2c77a872c7e30191618f94e278ea873e4bdddc",
"pins" : [
{
"identity" : "aexml",
Expand Down Expand Up @@ -348,8 +348,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeGraph.git",
"state" : {
"revision" : "ce94b777dda9c9109c9e95e911f5bac907188cbc",
"version" : "1.5.21"
"revision" : "8fbb1c6554cf95bcbeab0bd64a23a29d842a3f39",
"version" : "1.6.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ let package = Package(
url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMajor(from: "1.0.2")
),
.package(url: "https://github.com/tuist/Path", .upToNextMajor(from: "0.3.0")),
.package(url: "https://github.com/tuist/XcodeGraph.git", exact: "1.5.21"),
.package(url: "https://github.com/tuist/XcodeGraph.git", exact: "1.6.0"),
.package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.7.0")),
.package(url: "https://github.com/tuist/Command.git", .upToNextMajor(from: "0.8.0")),
.package(url: "https://github.com/sparkle-project/Sparkle.git", from: "2.6.4"),
Expand Down
3 changes: 3 additions & 0 deletions Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public enum TuistAcceptanceFixtures {
case xcodeApp
case xcodeProjectWithRegistryAndAlamofire
case xcodeProjectWithTests
case xcodeProjectWithPackagesAndTests
case appWithExecutableNonLocalDependencies
case appWithGeneratedSources
case custom(String)
Expand Down Expand Up @@ -308,6 +309,8 @@ public enum TuistAcceptanceFixtures {
return "xcode_project_with_registry_and_alamofire"
case .xcodeProjectWithTests:
return "xcode_project_with_tests"
case .xcodeProjectWithPackagesAndTests:
return "xcode_project_with_packages_and_tests"
case .appWithExecutableNonLocalDependencies:
return "app_with_executable_non_local_dependencies"
case .appWithGeneratedSources:
Expand Down
3 changes: 1 addition & 2 deletions Sources/TuistCache/SelectiveTestingServicing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import XcodeGraph
public protocol SelectiveTestingServicing {
/// - Returns: Tests that are cached.
func cachedTests(
scheme: Scheme,
graph: Graph,
testableGraphTargets: [GraphTarget],
selectiveTestingHashes: [GraphTarget: String],
selectiveTestingCacheItems: [CacheItem]
) async throws -> [TestIdentifier]
Expand Down
57 changes: 55 additions & 2 deletions Sources/TuistHasher/GraphContentHasher.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FileSystem
import Foundation
import Mockable
import Path
Expand All @@ -23,17 +24,31 @@ public protocol GraphContentHashing {
/// is responsible for computing an hash that uniquely identifies a Tuist `Graph`.
/// It considers only targets that are considered cacheable: frameworks without dependencies on XCTest or on non-cacheable targets
public struct GraphContentHasher: GraphContentHashing {
private let contentHasher: ContentHashing
private let targetContentHasher: TargetContentHashing
private let fileSystem: FileSysteming
private let rootDirectoryLocator: RootDirectoryLocating

// MARK: - Init

public init(contentHasher: ContentHashing) {
let targetContentHasher = TargetContentHasher(contentHasher: contentHasher)
self.init(targetContentHasher: targetContentHasher)
self.init(
contentHasher: contentHasher,
targetContentHasher: targetContentHasher
)
}

public init(targetContentHasher: TargetContentHashing) {
public init(
contentHasher: ContentHashing,
targetContentHasher: TargetContentHashing,
fileSystem: FileSysteming = FileSystem(),
rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator()
) {
self.contentHasher = contentHasher
self.targetContentHasher = targetContentHasher
self.fileSystem = fileSystem
self.rootDirectoryLocator = rootDirectoryLocator
}

// MARK: - GraphContentHashing
Expand All @@ -48,6 +63,12 @@ public struct GraphContentHasher: GraphContentHashing {
let hashedTargets: ThreadSafe<[GraphHashedTarget: String]> = ThreadSafe([:])
let hashedPaths: ThreadSafe<[AbsolutePath: String]> = ThreadSafe([:])

var additionalStrings = additionalStrings

if let lockFileHash = try await lockFileHash(for: graph) {
additionalStrings.append(lockFileHash)
}

let sortedCacheableTargets = try graphTraverser.allTargetsTopologicalSorted()
let hashableTargets = sortedCacheableTargets.compactMap { target -> GraphTarget? in
if isHashable(
Expand Down Expand Up @@ -111,4 +132,36 @@ public struct GraphContentHasher: GraphContentHashing {
visited[target] = allTargetDependenciesAreHashable
return allTargetDependenciesAreHashable
}

private func lockFileHash(
for graph: Graph
) async throws -> String? {
if let lockFilePath = try await rootDirectoryLocator.locate(from: graph.path)
.map({ $0.appending(component: ".package.resolved") }),
try await fileSystem.exists(lockFilePath)
{
return try await contentHasher.hash(
path: lockFilePath
)
}
if let workspacePath = try await fileSystem.glob(directory: graph.path, include: ["*.xcworkspace"]).collect().first {
let lockFilePath = workspacePath.appending(components: "xcshareddata", "swiftpm", "Package.resolved")
if try await fileSystem.exists(lockFilePath) {
return try await contentHasher.hash(path: lockFilePath)
}
} else if let projectPath = try await fileSystem.glob(directory: graph.path, include: ["*.xcodeproj"]).collect().first {
let lockFilePath = projectPath.appending(
components: [
"project.xcworkspace",
"xcshareddata",
"swiftpm",
"Package.resolved",
]
)
if try await fileSystem.exists(lockFilePath) {
return try await contentHasher.hash(path: lockFilePath)
}
}
return nil
}
}
2 changes: 2 additions & 0 deletions Sources/TuistHasher/TargetContentHasher.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Foundation
import Mockable
import Path
import TuistCore
import TuistSupport
import XcodeGraph

@Mockable
public protocol TargetContentHashing {
func contentHash(
for target: GraphTarget,
Expand Down
3 changes: 1 addition & 2 deletions Sources/TuistKit/Commands/XcodeBuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ struct EmptySelectiveTestingGraphHasher: SelectiveTestingGraphHashing {

struct EmptySelectiveTestingService: SelectiveTestingServicing {
func cachedTests(
scheme _: Scheme,
graph _: Graph,
testableGraphTargets _: [GraphTarget],
selectiveTestingHashes _: [GraphTarget: String], selectiveTestingCacheItems _: [CacheItem]
) async throws -> [TestIdentifier] {
[]
Expand Down
15 changes: 8 additions & 7 deletions Sources/TuistKit/Services/XcodeBuildService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,23 +132,24 @@ struct XcodeBuildService {
.keys
.map { $0 }

let skipTestTargets = try await selectiveTestingService.cachedTests(
scheme: scheme,
graph: graph,
selectiveTestingHashes: selectiveTestingHashes,
selectiveTestingCacheItems: selectiveTestingCacheItems
)

let testableTargets: [TestableTarget]
if let testPlanName = passedValue(for: "-testPlan", arguments: passthroughXcodebuildArguments) {
guard let testPlan = scheme.testAction?.testPlans?.first(where: { $0.name == testPlanName }) else {
throw XcodeBuildServiceError.testPlanNotFound(testPlan: testPlanName, scheme: scheme.name)
}
testableTargets = testPlan.testTargets
} else if let defaultTestPlan = scheme.testAction?.testPlans?.first(where: { $0.isDefault }) {
testableTargets = defaultTestPlan.testTargets
} else {
testableTargets = scheme.testAction?.targets ?? []
}
let testableGraphTargets = testableGraphTargets(for: testableTargets, graphTraverser: graphTraverser)
let skipTestTargets = try await selectiveTestingService.cachedTests(
testableGraphTargets: testableGraphTargets,
selectiveTestingHashes: selectiveTestingHashes,
selectiveTestingCacheItems: selectiveTestingCacheItems
)

let targetTestCacheItems: [AbsolutePath: [String: CacheItem]] = selectiveTestingHashes
.reduce(into: [:]) { result, element in
if let cacheItem = selectiveTestingCacheItems.first(where: { $0.hash == element.value }) {
Expand Down
5 changes: 3 additions & 2 deletions Sources/TuistLoader/Loaders/CachedManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProjectDescription
import ServiceContextModule
import TuistCore
import TuistSupport
import XcodeGraph

/// Cached Manifest Loader
///
Expand Down Expand Up @@ -68,13 +69,13 @@ public class CachedManifestLoader: ManifestLoading {
}
}

public func loadProject(at path: AbsolutePath) async throws -> Project {
public func loadProject(at path: AbsolutePath) async throws -> ProjectDescription.Project {
try await load(manifest: .project, at: path) {
try await manifestLoader.loadProject(at: path)
}
}

public func loadWorkspace(at path: AbsolutePath) async throws -> Workspace {
public func loadWorkspace(at path: AbsolutePath) async throws -> ProjectDescription.Workspace {
try await load(manifest: .workspace, at: path) {
try await manifestLoader.loadWorkspace(at: path)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/TuistLoader/Loaders/ManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProjectDescription
import ServiceContextModule
import TuistCore
import TuistSupport
import XcodeGraph

public enum ManifestLoaderError: FatalError, Equatable {
case projectDescriptionNotFound(AbsolutePath)
Expand Down
5 changes: 3 additions & 2 deletions Sources/TuistLoader/Loaders/PackageInfoLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Mockable
import Path
import TSCUtility
import TuistSupport
import XcodeGraph

/// Protocol that defines an interface to interact with the Swift Package Manager.
@Mockable
Expand Down Expand Up @@ -41,15 +42,15 @@ public final class PackageInfoLoader: PackageInfoLoading {
try system.run(command)
}

public func setToolsVersion(at path: AbsolutePath, to version: Version) throws {
public func setToolsVersion(at path: AbsolutePath, to version: TSCUtility.Version) throws {
let extraArguments = ["tools-version", "--set", "\(version.major).\(version.minor)"]

let command = buildSwiftPackageCommand(packagePath: path, extraArguments: extraArguments)

try system.run(command)
}

public func getToolsVersion(at path: AbsolutePath) throws -> Version {
public func getToolsVersion(at path: AbsolutePath) throws -> TSCUtility.Version {
let extraArguments = ["tools-version"]

let command = buildSwiftPackageCommand(packagePath: path, extraArguments: extraArguments)
Expand Down
Loading

0 comments on commit 35e7310

Please sign in to comment.