Test the CacheController

This commit is contained in:
Pedro Piñera 2020-02-28 09:47:29 +01:00
parent 1218c461d4
commit 5e0af454d1
6 changed files with 175 additions and 49 deletions

View File

@ -6,6 +6,9 @@ import TuistCore
public final class MockCacheStorage: CacheStoraging {
var existsStub: ((String) -> Bool)?
public init() {}
public func exists(hash: String) -> Single<Bool> {
if let existsStub = existsStub {
return Single.just(existsStub(hash))

View File

@ -5,15 +5,22 @@ import TuistCache
import TuistCore
public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
var buildProjectArgs: [(projectPath: AbsolutePath, target: Target)] = []
var buildWorkspaceArgs: [(workspacePath: AbsolutePath, target: Target)] = []
var buildProjectStub: AbsolutePath?
var buildWorkspaceStub: AbsolutePath?
public var buildProjectArgs: [(projectPath: AbsolutePath, target: Target)] = []
public var buildWorkspaceArgs: [(workspacePath: AbsolutePath, target: Target)] = []
public var buildProjectStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
public var buildWorkspaceStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
public init() {}
public func build(projectPath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
buildProjectArgs.append((projectPath: projectPath, target: target))
if let buildProjectStub = buildProjectStub {
return Observable.just(buildProjectStub)
switch buildProjectStub(projectPath, target) {
case let .failure(error):
return Observable.error(error)
case let .success(path):
return Observable.just(path)
}
} else {
return Observable.just(AbsolutePath.root)
}
@ -22,7 +29,12 @@ public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
public func build(workspacePath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
buildWorkspaceArgs.append((workspacePath: workspacePath, target: target))
if let buildWorkspaceStub = buildWorkspaceStub {
return Observable.just(buildWorkspaceStub)
switch buildWorkspaceStub(workspacePath, target) {
case let .failure(error):
return Observable.error(error)
case let .success(path):
return Observable.just(path)
}
} else {
return Observable.just(AbsolutePath.root)
}

View File

@ -0,0 +1,13 @@
import Foundation
import TuistCore
@testable import TuistCache
public final class MockGraphContentHasher: GraphContentHashing {
public var contentHashesStub: [TargetNode: String]?
public init() {}
public func contentHashes(for _: Graphing) throws -> [TargetNode: String] {
contentHashesStub ?? [:]
}
}

View File

@ -44,12 +44,21 @@ final class CacheController: CacheControlling {
}
func cache(path: AbsolutePath) throws {
// Generate the project.
let (path, graph) = try generator.generate(at: path, manifestLoader: manifestLoader, projectOnly: false)
let (path, graph) = try generator.generateWorkspace(at: path, manifestLoader: manifestLoader)
// Getting the hash
Printer.shared.print(section: "Hashing cacheable frameworks")
let targets: [TargetNode: String] = try graphContentHasher.contentHashes(for: graph)
let cacheableTargets = try self.cacheableTargets(graph: graph)
let completables = try cacheableTargets.map { try buildAndCacheXCFramework(path: path, target: $0.key, hash: $0.value) }
_ = try Completable.zip(completables).toBlocking().last()
Printer.shared.print(success: "All cacheable frameworks have been cached successfully")
}
/// Returns all the targets that are cacheable and their hashes.
/// - Parameter graph: Graph that contains all the dependency graph nodes.
fileprivate func cacheableTargets(graph: Graphing) throws -> [TargetNode: String] {
try graphContentHasher.contentHashes(for: graph)
.filter { target, hash in
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
Printer.shared.print("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
@ -57,28 +66,36 @@ final class CacheController: CacheControlling {
}
return true
}
}
var completables: [Completable] = []
try targets.forEach { target, hash in
// Build targets sequentially
let xcframeworkPath: AbsolutePath!
if path.extension == "xcworkspace" {
xcframeworkPath = try self.xcframeworkBuilder.build(workspacePath: path, target: target.target).toBlocking().single()
} else {
xcframeworkPath = try self.xcframeworkBuilder.build(projectPath: path, target: target.target).toBlocking().single()
}
/// Builds the .xcframework for the given target and returns an obervable to store them in the cache.
/// - Parameters:
/// - path: Path to either the .xcodeproj or .xcworkspace that contains the framework to be cached.
/// - target: Target whose .xcframework will be built and cached.
/// - hash: Hash of the target.
fileprivate func buildAndCacheXCFramework(path: AbsolutePath, target: TargetNode, hash: String) throws -> Completable {
// Build targets sequentially
let xcframeworkPath: AbsolutePath!
// Create tasks to cache and delete the xcframeworks asynchronously
let deleteXCFrameworkCompletable = Completable.create(subscribe: { completed in
try? FileHandler.shared.delete(xcframeworkPath)
completed(.completed)
return Disposables.create()
})
completables.append(cache.store(hash: hash, xcframeworkPath: xcframeworkPath).concat(deleteXCFrameworkCompletable))
// Note: Since building XCFrameworks involves calling xcodebuild, we run the building process sequentially.
if path.extension == "xcworkspace" {
xcframeworkPath = try xcframeworkBuilder.build(workspacePath: path, target: target.target).toBlocking().single()
} else {
xcframeworkPath = try xcframeworkBuilder.build(projectPath: path, target: target.target).toBlocking().single()
}
_ = try Completable.zip(completables).toBlocking().last()
Printer.shared.print(success: "All cacheable frameworks have been cached successfully")
// Create tasks to cache and delete the xcframeworks asynchronously
let deleteXCFrameworkCompletable = Completable.create(subscribe: { completed in
try? FileHandler.shared.delete(xcframeworkPath)
completed(.completed)
return Disposables.create()
})
return cache
.store(hash: hash, xcframeworkPath: xcframeworkPath)
.concat(deleteXCFrameworkCompletable)
.catchError { error in
// We propagate the error downstream
deleteXCFrameworkCompletable.concat(Completable.error(error))
}
}
}

View File

@ -3,34 +3,36 @@ import Foundation
import ProjectDescription
@testable import TuistLoader
final class MockManifestLoader: ManifestLoading {
var loadProjectCount: UInt = 0
var loadProjectStub: ((AbsolutePath) throws -> ProjectDescription.Project)?
public final class MockManifestLoader: ManifestLoading {
public var loadProjectCount: UInt = 0
public var loadProjectStub: ((AbsolutePath) throws -> ProjectDescription.Project)?
var loadWorkspaceCount: UInt = 0
var loadWorkspaceStub: ((AbsolutePath) throws -> ProjectDescription.Workspace)?
public var loadWorkspaceCount: UInt = 0
public var loadWorkspaceStub: ((AbsolutePath) throws -> ProjectDescription.Workspace)?
var manifestsAtCount: UInt = 0
var manifestsAtStub: ((AbsolutePath) -> Set<Manifest>)?
public var manifestsAtCount: UInt = 0
public var manifestsAtStub: ((AbsolutePath) -> Set<Manifest>)?
var manifestPathCount: UInt = 0
var manifestPathStub: ((AbsolutePath, Manifest) throws -> AbsolutePath)?
public var manifestPathCount: UInt = 0
public var manifestPathStub: ((AbsolutePath, Manifest) throws -> AbsolutePath)?
var loadSetupCount: UInt = 0
var loadSetupStub: ((AbsolutePath) throws -> [Upping])?
public var loadSetupCount: UInt = 0
public var loadSetupStub: ((AbsolutePath) throws -> [Upping])?
var loadTuistConfigCount: UInt = 0
var loadTuistConfigStub: ((AbsolutePath) throws -> ProjectDescription.TuistConfig)?
public var loadTuistConfigCount: UInt = 0
public var loadTuistConfigStub: ((AbsolutePath) throws -> ProjectDescription.TuistConfig)?
func loadProject(at path: AbsolutePath) throws -> ProjectDescription.Project {
public init() {}
public func loadProject(at path: AbsolutePath) throws -> ProjectDescription.Project {
try loadProjectStub?(path) ?? ProjectDescription.Project.test()
}
func loadWorkspace(at path: AbsolutePath) throws -> ProjectDescription.Workspace {
public func loadWorkspace(at path: AbsolutePath) throws -> ProjectDescription.Workspace {
try loadWorkspaceStub?(path) ?? ProjectDescription.Workspace.test()
}
func manifests(at path: AbsolutePath) -> Set<Manifest> {
public func manifests(at path: AbsolutePath) -> Set<Manifest> {
manifestsAtCount += 1
return manifestsAtStub?(path) ?? Set()
}
@ -40,12 +42,12 @@ final class MockManifestLoader: ManifestLoading {
return try manifestPathStub?(path, manifest) ?? TemporaryDirectory(removeTreeOnDeinit: true).path
}
func loadSetup(at path: AbsolutePath) throws -> [Upping] {
public func loadSetup(at path: AbsolutePath) throws -> [Upping] {
loadSetupCount += 1
return try loadSetupStub?(path) ?? []
}
func loadTuistConfig(at path: AbsolutePath) throws -> TuistConfig {
public func loadTuistConfig(at path: AbsolutePath) throws -> TuistConfig {
loadTuistConfigCount += 1
return try loadTuistConfigStub?(path) ?? ProjectDescription.TuistConfig.test()
}

View File

@ -1,15 +1,94 @@
import Basic
import Foundation
import TuistCacheTesting
import TuistCore
import TuistCoreTesting
import TuistLoader
import TuistLoaderTesting
import TuistSupport
import TuistSupportTesting
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
final class CacheControllerTests: XCTestCase {
final class CacheControllerTests: TuistUnitTestCase {
var generator: MockGenerator!
var graphContentHasher: MockGraphContentHasher!
var xcframeworkBuilder: MockXCFrameworkBuilder!
var manifestLoader: MockManifestLoader!
var cache: MockCacheStorage!
var subject: CacheController!
override func setUp() {
generator = MockGenerator()
xcframeworkBuilder = MockXCFrameworkBuilder()
cache = MockCacheStorage()
manifestLoader = MockManifestLoader()
graphContentHasher = MockGraphContentHasher()
subject = CacheController(generator: generator,
manifestLoader: manifestLoader,
xcframeworkBuilder: xcframeworkBuilder,
cache: cache,
graphContentHasher: graphContentHasher)
super.setUp()
}
override func tearDown() {
super.tearDown()
generator = nil
xcframeworkBuilder = nil
graphContentHasher = nil
manifestLoader = nil
cache = nil
subject = nil
}
func test_cache_builds_and_caches_the_frameworks() throws {
// Given
let path = try temporaryPath()
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
let cache = GraphLoaderCache()
let graph = Graph.test(cache: cache)
let project = Project.test(path: path, name: "Cache")
let aTarget = Target.test(name: "A")
let bTarget = Target.test(name: "B")
let axcframeworkPath = path.appending(component: "A.xcframework")
let bxcframeworkPath = path.appending(component: "B.xcframework")
try FileHandler.shared.createFolder(axcframeworkPath)
try FileHandler.shared.createFolder(bxcframeworkPath)
let nodeWithHashes = [
TargetNode.test(project: project, target: aTarget): "A_HASH",
TargetNode.test(project: project, target: bTarget): "B_HASH",
]
manifestLoader.manifestsAtStub = { (loadPath: AbsolutePath) -> Set<Manifest> in
XCTAssertEqual(loadPath, path)
return Set(arrayLiteral: .project)
}
generator.generateProjectWorkspaceStub = { (loadPath, _) -> (AbsolutePath, Graphing) in
XCTAssertEqual(loadPath, path)
return (xcworkspacePath, graph)
}
graphContentHasher.contentHashesStub = nodeWithHashes
xcframeworkBuilder.buildWorkspaceStub = { _xcworkspacePath, target in
switch (_xcworkspacePath, target) {
case (xcworkspacePath, aTarget): return .success(axcframeworkPath)
case (xcworkspacePath, bTarget): return .success(bxcframeworkPath)
default: return .failure(TestError("Received invalid Xcode project path or target"))
}
}
try subject.cache(path: path)
// Then
XCTAssertPrinterOutputContains("""
Hashing cacheable frameworks
All cacheable frameworks have been cached successfully
""")
XCTAssertFalse(FileHandler.shared.exists(axcframeworkPath))
XCTAssertFalse(FileHandler.shared.exists(bxcframeworkPath))
}
}