Replace RxSwift for Cache command to async/await (#4003)
* Replace RxSwift for Cache command to async/await * Update Sources/TuistCacheTesting/Cache/Mocks/MockCacheStorage.swift Co-authored-by: Daniele Formichelli <df@bendingspoons.com> * Update Sources/TuistCacheTesting/Cache/Mocks/MockCacheStorage.swift Co-authored-by: Daniele Formichelli <df@bendingspoons.com> * Update Sources/TuistCache/Cache/CacheLocalStorage.swift Co-authored-by: Daniele Formichelli <df@bendingspoons.com> * Apply code review * Update concurrent async map and compactMap to keep execution order * Replace RxSwift for TuistAnalytics (#4005) Co-authored-by: Daniele Formichelli <df@bendingspoons.com>
This commit is contained in:
parent
433480dbd4
commit
2e270bfe2f
|
@ -132,8 +132,8 @@
|
|||
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "254617dd7fae0c45319ba5fbea435bf4d0e15b5d",
|
||||
"version": "5.1.2"
|
||||
"revision": "b4307ba0b6425c0ba4178e138799946c3da594f8",
|
||||
"version": "6.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// swift-tools-version:5.2.0
|
||||
// swift-tools-version:5.4.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
|
@ -53,7 +53,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "8.7.1")),
|
||||
.package(name: "Signals", url: "https://github.com/tuist/BlueSignals.git", .upToNextMajor(from: "1.0.21")),
|
||||
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.1.1")),
|
||||
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.5.0")),
|
||||
.package(url: "https://github.com/rnine/Checksum.git", .upToNextMajor(from: "1.0.2")),
|
||||
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")),
|
||||
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.4.1")),
|
||||
|
@ -181,7 +181,7 @@ let package = Package(
|
|||
"TuistGraphTesting",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "tuist",
|
||||
dependencies: [
|
||||
"TuistKit",
|
||||
|
@ -206,7 +206,7 @@ let package = Package(
|
|||
"TuistSupportTesting",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "tuistenv",
|
||||
dependencies: [
|
||||
"TuistEnvKit",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistAsyncQueue
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
@ -11,10 +10,8 @@ class TuistAnalyticsBackboneBackend: TuistAnalyticsBackend {
|
|||
self.requestDispatcher = requestDispatcher
|
||||
}
|
||||
|
||||
func send(commandEvent: CommandEvent) throws -> Single<Void> {
|
||||
requestDispatcher
|
||||
.dispatch(resource: try resource(commandEvent))
|
||||
.flatMap { _, _ in .just(()) }
|
||||
func send(commandEvent: CommandEvent) async throws {
|
||||
_ = try await requestDispatcher.dispatch(resource: resource(commandEvent))
|
||||
}
|
||||
|
||||
func resource(_ commandEvent: CommandEvent) throws -> HTTPResource<Void, CloudEmptyResponseError> {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
/// An analytics backend an entity (e.g. an HTTP server)
|
||||
/// that can process analytics events generated by the Tuist CLI.
|
||||
protocol TuistAnalyticsBackend: AnyObject {
|
||||
/// Sends a command event to the backend.
|
||||
/// - Parameter commandEvent: Command event to be delivered.
|
||||
func send(commandEvent: CommandEvent) throws -> Single<Void>
|
||||
func send(commandEvent: CommandEvent) async throws
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistAsyncQueue
|
||||
import TuistCloud
|
||||
import TuistCore
|
||||
|
@ -28,12 +27,10 @@ class TuistAnalyticsCloudBackend: TuistAnalyticsBackend {
|
|||
self.client = client
|
||||
}
|
||||
|
||||
func send(commandEvent: CommandEvent) throws -> Single<Void> {
|
||||
guard config.options.contains(.analytics) else { return .just(()) }
|
||||
func send(commandEvent: CommandEvent) async throws {
|
||||
guard config.options.contains(.analytics) else { return }
|
||||
|
||||
let resource = try resourceFactory.create(commandEvent: commandEvent)
|
||||
return client
|
||||
.request(resource)
|
||||
.flatMap { _, _ in .just(()) }
|
||||
_ = try await client.request(resource)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistAsyncQueue
|
||||
import TuistCloud
|
||||
import TuistCore
|
||||
|
@ -11,7 +10,6 @@ public struct TuistAnalyticsDispatcher: AsyncQueueDispatching {
|
|||
public static let dispatcherId = "TuistAnalytics"
|
||||
|
||||
private let backends: [TuistAnalyticsBackend]
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
public init(
|
||||
cloud: Cloud?,
|
||||
|
@ -40,11 +38,10 @@ public struct TuistAnalyticsDispatcher: AsyncQueueDispatching {
|
|||
public func dispatch(event: AsyncQueueEvent, completion: @escaping () -> Void) throws {
|
||||
guard let commandEvent = event as? CommandEvent else { return }
|
||||
|
||||
Single
|
||||
.zip(try backends.map { try $0.send(commandEvent: commandEvent) })
|
||||
.asObservable()
|
||||
.subscribe(onNext: { _ in completion() })
|
||||
.disposed(by: disposeBag)
|
||||
Task.detached {
|
||||
_ = try await backends.concurrentMap { try? await $0.send(commandEvent: commandEvent) }
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchPersisted(data: Data, completion: @escaping () -> Void) throws {
|
||||
|
|
|
@ -119,10 +119,10 @@ public class AsyncQueue: AsyncQueuing {
|
|||
private func loadEvents() {
|
||||
persistor
|
||||
.readAll()
|
||||
.subscribeOn(persistedEventsSchedulerType)
|
||||
.subscribe(on: persistedEventsSchedulerType)
|
||||
.subscribe(onSuccess: { events in
|
||||
events.forEach(self.dispatchPersisted)
|
||||
}, onError: { error in
|
||||
}, onFailure: { error in
|
||||
logger.debug("Error loading persisted events: \(error)")
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
|
||||
|
@ -23,30 +22,31 @@ public final class Cache: CacheStoring {
|
|||
|
||||
// MARK: - CacheStoring
|
||||
|
||||
public func exists(name: String, hash: String) -> Single<Bool> {
|
||||
/// It calls exists sequentially until one of the storages returns true.
|
||||
storages.reduce(Single.just(false)) { result, next -> Single<Bool> in
|
||||
result.flatMap { exists in
|
||||
guard !exists else { return result }
|
||||
return next.exists(name: name, hash: hash)
|
||||
}.catchError { _ -> Single<Bool> in
|
||||
next.exists(name: name, hash: hash)
|
||||
public func exists(name: String, hash: String) async throws -> Bool {
|
||||
for storage in storages {
|
||||
if try await storage.exists(name: name, hash: hash) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func fetch(name: String, hash: String) -> Single<AbsolutePath> {
|
||||
storages
|
||||
.reduce(nil) { result, next -> Single<AbsolutePath> in
|
||||
if let result = result {
|
||||
return result.catchError { _ in next.fetch(name: name, hash: hash) }
|
||||
} else {
|
||||
return next.fetch(name: name, hash: hash)
|
||||
}
|
||||
}!
|
||||
public func fetch(name: String, hash: String) async throws -> AbsolutePath {
|
||||
var throwingError: Error = CacheLocalStorageError.compiledArtifactNotFound(hash: hash)
|
||||
for storage in storages {
|
||||
do {
|
||||
return try await storage.fetch(name: name, hash: hash)
|
||||
} catch {
|
||||
throwingError = error
|
||||
continue
|
||||
}
|
||||
}
|
||||
throw throwingError
|
||||
}
|
||||
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
Completable.zip(storages.map { $0.store(name: name, hash: hash, paths: paths) })
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) async throws {
|
||||
_ = try await storages.concurrentMap { storage in
|
||||
try await storage.store(name: name, hash: hash, paths: paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
@ -38,49 +37,37 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
|
||||
// MARK: - CacheStoring
|
||||
|
||||
public func exists(name _: String, hash: String) -> Single<Bool> {
|
||||
Single.create { completed -> Disposable in
|
||||
completed(.success(self.lookupCompiledArtifact(directory: self.cacheDirectory.appending(component: hash)) != nil))
|
||||
return Disposables.create()
|
||||
}
|
||||
public func exists(name _: String, hash: String) throws -> Bool {
|
||||
let hashFolder = cacheDirectory.appending(component: hash)
|
||||
return lookupCompiledArtifact(directory: hashFolder) != nil
|
||||
}
|
||||
|
||||
public func fetch(name _: String, hash: String) -> Single<AbsolutePath> {
|
||||
Single.create { completed -> Disposable in
|
||||
if let path = self.lookupCompiledArtifact(directory: self.cacheDirectory.appending(component: hash)) {
|
||||
completed(.success(path))
|
||||
} else {
|
||||
completed(.error(CacheLocalStorageError.compiledArtifactNotFound(hash: hash)))
|
||||
}
|
||||
return Disposables.create()
|
||||
public func fetch(name _: String, hash: String) throws -> AbsolutePath {
|
||||
let hashFolder = cacheDirectory.appending(component: hash)
|
||||
guard let path = lookupCompiledArtifact(directory: hashFolder) else {
|
||||
throw CacheLocalStorageError.compiledArtifactNotFound(hash: hash)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
public func store(name _: String, hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
let copy = Completable.create { completed -> Disposable in
|
||||
let hashFolder = self.cacheDirectory.appending(component: hash)
|
||||
|
||||
do {
|
||||
if !FileHandler.shared.exists(hashFolder) {
|
||||
try FileHandler.shared.createFolder(hashFolder)
|
||||
}
|
||||
try paths.forEach { sourcePath in
|
||||
let destinationPath = hashFolder.appending(component: sourcePath.basename)
|
||||
if FileHandler.shared.exists(destinationPath) {
|
||||
try FileHandler.shared.delete(destinationPath)
|
||||
}
|
||||
|
||||
try FileHandler.shared.copy(from: sourcePath, to: destinationPath)
|
||||
}
|
||||
} catch {
|
||||
completed(.error(error))
|
||||
return Disposables.create()
|
||||
}
|
||||
completed(.completed)
|
||||
return Disposables.create()
|
||||
public func store(name _: String, hash: String, paths: [AbsolutePath]) throws {
|
||||
if !FileHandler.shared.exists(cacheDirectory) {
|
||||
try FileHandler.shared.createFolder(cacheDirectory)
|
||||
}
|
||||
|
||||
return createCacheDirectory().concat(copy)
|
||||
let hashFolder = cacheDirectory.appending(component: hash)
|
||||
|
||||
if !FileHandler.shared.exists(hashFolder) {
|
||||
try FileHandler.shared.createFolder(hashFolder)
|
||||
}
|
||||
try paths.forEach { sourcePath in
|
||||
let destinationPath = hashFolder.appending(component: sourcePath.basename)
|
||||
if FileHandler.shared.exists(destinationPath) {
|
||||
try FileHandler.shared.delete(destinationPath)
|
||||
}
|
||||
try FileHandler.shared.copy(from: sourcePath, to: destinationPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
@ -92,19 +79,4 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func createCacheDirectory() -> Completable {
|
||||
Completable.create { completed -> Disposable in
|
||||
do {
|
||||
if !FileHandler.shared.exists(self.cacheDirectory) {
|
||||
try FileHandler.shared.createFolder(self.cacheDirectory)
|
||||
}
|
||||
} catch {
|
||||
completed(.error(error))
|
||||
return Disposables.create()
|
||||
}
|
||||
completed(.completed)
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
|
@ -61,52 +60,33 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
|
||||
// MARK: - CacheStoring
|
||||
|
||||
public func exists(name: String, hash: String) -> Single<Bool> {
|
||||
public func exists(name: String, hash: String) async throws -> Bool {
|
||||
do {
|
||||
let successRange = 200 ..< 300
|
||||
let resource = try cloudCacheResourceFactory.existsResource(name: name, hash: hash)
|
||||
return cloudClient.request(resource)
|
||||
.flatMap { _, response in
|
||||
.just(successRange.contains(response.statusCode))
|
||||
}
|
||||
.catchError { error in
|
||||
if case let HTTPRequestDispatcherError.serverSideError(_, response) = error, response.statusCode == 404 {
|
||||
return .just(false)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
let (_, response) = try await cloudClient.request(resource)
|
||||
return successRange.contains(response.statusCode)
|
||||
} catch {
|
||||
return Single.error(error)
|
||||
if case let HTTPRequestDispatcherError.serverSideError(_, response) = error, response.statusCode == 404 {
|
||||
return false
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetch(name: String, hash: String) -> Single<AbsolutePath> {
|
||||
do {
|
||||
let resource = try cloudCacheResourceFactory.fetchResource(name: name, hash: hash)
|
||||
return cloudClient
|
||||
.request(resource)
|
||||
.map(\.object.data.url)
|
||||
.flatMap { (url: URL) in
|
||||
self.fileClient.download(url: url)
|
||||
.do(onSubscribed: { logger.info("Downloading cache artifact with hash \(hash).") })
|
||||
}
|
||||
.flatMap { (filePath: AbsolutePath) in
|
||||
do {
|
||||
let archiveContentPath = try self.unzip(downloadedArchive: filePath, hash: hash)
|
||||
return Single.just(archiveContentPath)
|
||||
} catch {
|
||||
return Single.error(error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return Single.error(error)
|
||||
}
|
||||
public func fetch(name: String, hash: String) async throws -> AbsolutePath {
|
||||
let resource = try cloudCacheResourceFactory.fetchResource(name: name, hash: hash)
|
||||
let url = try await cloudClient.request(resource).object.data.url
|
||||
|
||||
logger.info("Downloading cache artifact with hash \(hash).")
|
||||
let filePath = try await fileClient.download(url: url)
|
||||
return try unzip(downloadedArchive: filePath, hash: hash)
|
||||
}
|
||||
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) async throws {
|
||||
let archiver = try fileArchiverFactory.makeFileArchiver(for: paths)
|
||||
do {
|
||||
let archiver = try fileArchiverFactory.makeFileArchiver(for: paths)
|
||||
let destinationZipPath = try archiver.zip(name: hash)
|
||||
let md5 = try FileHandler.shared.urlSafeBase64MD5(path: destinationZipPath)
|
||||
let storeResource = try cloudCacheResourceFactory.storeResource(
|
||||
|
@ -115,41 +95,25 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
contentMD5: md5
|
||||
)
|
||||
|
||||
return cloudClient
|
||||
.request(storeResource)
|
||||
.map { responseTuple -> URL in responseTuple.object.data.url }
|
||||
.flatMapCompletable { (url: URL) in
|
||||
let deleteCompletable = self.deleteZipArchiveCompletable(archiver: archiver)
|
||||
return self.fileClient.upload(file: destinationZipPath, hash: hash, to: url)
|
||||
.flatMapCompletable { _ in
|
||||
self.verify(name: name, hash: hash, contentMD5: md5)
|
||||
}
|
||||
.catchError {
|
||||
deleteCompletable.concat(.error($0))
|
||||
}
|
||||
}
|
||||
let url = try await cloudClient.request(storeResource).object.data.url
|
||||
|
||||
_ = try await fileClient.upload(file: destinationZipPath, hash: hash, to: url)
|
||||
|
||||
let verifyUploadResource = try cloudCacheResourceFactory.verifyUploadResource(
|
||||
name: name,
|
||||
hash: hash,
|
||||
contentMD5: md5
|
||||
)
|
||||
|
||||
_ = try await cloudClient.request(verifyUploadResource)
|
||||
} catch {
|
||||
return Completable.error(error)
|
||||
try archiver.delete()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func verify(name: String, hash: String, contentMD5: String) -> Completable {
|
||||
do {
|
||||
let verifyUploadResource = try cloudCacheResourceFactory.verifyUploadResource(
|
||||
name: name,
|
||||
hash: hash,
|
||||
contentMD5: contentMD5
|
||||
)
|
||||
|
||||
return cloudClient
|
||||
.request(verifyUploadResource).asCompletable()
|
||||
} catch {
|
||||
return Completable.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func artifactPath(in archive: AbsolutePath) -> AbsolutePath? {
|
||||
if let xcframeworkPath = FileHandler.shared.glob(archive, glob: "*.xcframework").first {
|
||||
return xcframeworkPath
|
||||
|
@ -178,16 +142,4 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
try FileHandler.shared.move(from: unarchivedDirectory, to: archiveDestination)
|
||||
return artifactPath(in: archiveDestination)!
|
||||
}
|
||||
|
||||
private func deleteZipArchiveCompletable(archiver: FileArchiving) -> Completable {
|
||||
Completable.create(subscribe: { observer in
|
||||
do {
|
||||
try archiver.delete()
|
||||
observer(.completed)
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
}
|
||||
return Disposables.create {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
|
||||
|
@ -9,7 +8,7 @@ public protocol CacheStoring {
|
|||
/// - name: Target's name.
|
||||
/// - hash: Target's hash.
|
||||
/// - Returns: An observable that returns a boolean indicating whether the target is cached.
|
||||
func exists(name: String, hash: String) -> Single<Bool>
|
||||
func exists(name: String, hash: String) async throws -> Bool
|
||||
|
||||
/// For the target with the given hash, it fetches it from the cache and returns a path
|
||||
/// pointint to the .xcframework that represents it.
|
||||
|
@ -18,12 +17,12 @@ public protocol CacheStoring {
|
|||
/// - name: Target's name.
|
||||
/// - hash: Target's hash.
|
||||
/// - Returns: An observable that returns a boolean indicating whether the target is cached.
|
||||
func fetch(name: String, hash: String) -> Single<AbsolutePath>
|
||||
func fetch(name: String, hash: String) async throws -> AbsolutePath
|
||||
|
||||
/// It stores the xcframework at the given path in the cache.
|
||||
/// - Parameters:
|
||||
/// - name: Target's name.
|
||||
/// - hash: Hash of the target the xcframework belongs to.
|
||||
/// - paths: Path to the files that will be stored.
|
||||
func store(name: String, hash: String, paths: [AbsolutePath]) -> Completable
|
||||
func store(name: String, hash: String, paths: [AbsolutePath]) async throws
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
|
|
|
@ -42,9 +42,6 @@ public final class TargetsToCacheBinariesGraphMapper: GraphMapping {
|
|||
/// List of targets that will be generated as sources instead of pre-compiled targets from the cache.
|
||||
private let sources: Set<String>
|
||||
|
||||
/// Dispatch queue.
|
||||
private let queue: DispatchQueue
|
||||
|
||||
/// The type of artifact that the hasher is configured with.
|
||||
private let cacheOutputType: CacheOutputType
|
||||
|
||||
|
@ -76,13 +73,11 @@ public final class TargetsToCacheBinariesGraphMapper: GraphMapping {
|
|||
sources: Set<String>,
|
||||
cacheProfile: TuistGraph.Cache.Profile,
|
||||
cacheOutputType: CacheOutputType,
|
||||
cacheGraphMutator: CacheGraphMutating = CacheGraphMutator(),
|
||||
queue: DispatchQueue = TargetsToCacheBinariesGraphMapper.dispatchQueue())
|
||||
cacheGraphMutator: CacheGraphMutating = CacheGraphMutator())
|
||||
{
|
||||
self.config = config
|
||||
self.cache = cache
|
||||
self.cacheGraphContentHasher = cacheGraphContentHasher
|
||||
self.queue = queue
|
||||
self.cacheGraphMutator = cacheGraphMutator
|
||||
self.sources = sources
|
||||
self.cacheProfile = cacheProfile
|
||||
|
@ -105,61 +100,43 @@ public final class TargetsToCacheBinariesGraphMapper: GraphMapping {
|
|||
availableTargets: availableTargets.sorted()
|
||||
)
|
||||
}
|
||||
let single = hashes(graph: graph).flatMap { self.map(graph: graph, hashes: $0, sources: self.sources) }
|
||||
let single = AsyncThrowingStream<Graph, Error> { continuation in
|
||||
Task.detached {
|
||||
do {
|
||||
let hashes = try self.cacheGraphContentHasher.contentHashes(
|
||||
for: graph,
|
||||
cacheProfile: self.cacheProfile,
|
||||
cacheOutputType: self.cacheOutputType,
|
||||
excludedTargets: self.sources
|
||||
)
|
||||
let result = try self.cacheGraphMutator.map(
|
||||
graph: graph,
|
||||
precompiledArtifacts: await self.fetch(hashes: hashes),
|
||||
sources: self.sources
|
||||
)
|
||||
continuation.yield(result)
|
||||
continuation.finish()
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
}
|
||||
}
|
||||
}.asObservable().asSingle()
|
||||
return try (single.toBlocking().single(), [])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func dispatchQueue() -> DispatchQueue {
|
||||
let qos: DispatchQoS = .userInitiated
|
||||
return DispatchQueue(label: "io.tuist.generator-cache-mapper.\(qos)", qos: qos, attributes: [], target: nil)
|
||||
}
|
||||
|
||||
private func hashes(graph: Graph) -> Single<[GraphTarget: String]> {
|
||||
Single.create { observer -> Disposable in
|
||||
do {
|
||||
let hashes = try self.cacheGraphContentHasher.contentHashes(
|
||||
for: graph,
|
||||
cacheProfile: self.cacheProfile,
|
||||
cacheOutputType: self.cacheOutputType,
|
||||
excludedTargets: self.sources
|
||||
)
|
||||
observer(.success(hashes))
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
private func fetch(hashes: [GraphTarget: String]) async throws -> [GraphTarget: AbsolutePath] {
|
||||
try await hashes.concurrentMap { target, hash -> (GraphTarget, AbsolutePath?) in
|
||||
if try await self.cache.exists(name: target.target.name, hash: hash) {
|
||||
let path = try await self.cache.fetch(name: target.target.name, hash: hash)
|
||||
return (target, path)
|
||||
} else {
|
||||
return (target, nil)
|
||||
}
|
||||
return Disposables.create {}
|
||||
}.reduce(into: [GraphTarget: AbsolutePath]()) { acc, next in
|
||||
guard let path = next.1 else { return }
|
||||
acc[next.0] = path
|
||||
}
|
||||
.subscribeOn(ConcurrentDispatchQueueScheduler(queue: queue))
|
||||
}
|
||||
|
||||
private func map(graph: Graph, hashes: [GraphTarget: String], sources: Set<String>) -> Single<Graph> {
|
||||
fetch(hashes: hashes).map { xcframeworkPaths in
|
||||
try self.cacheGraphMutator.map(
|
||||
graph: graph,
|
||||
precompiledArtifacts: xcframeworkPaths,
|
||||
sources: sources
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetch(hashes: [GraphTarget: String]) -> Single<[GraphTarget: AbsolutePath]> {
|
||||
Single
|
||||
.zip(
|
||||
hashes.map(context: .concurrent) { target, hash in
|
||||
self.cache.exists(name: target.target.name, hash: hash)
|
||||
.flatMap { exists -> Single<(target: GraphTarget, path: AbsolutePath?)> in
|
||||
guard exists else { return Single.just((target: target, path: nil)) }
|
||||
return self.cache.fetch(name: target.target.name, hash: hash).map { (target: target, path: $0) }
|
||||
}
|
||||
}
|
||||
)
|
||||
.map { result in
|
||||
result.reduce(into: [GraphTarget: AbsolutePath]()) { acc, next in
|
||||
guard let path = next.path else { return }
|
||||
acc[next.target] = path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import struct TSCUtility.Version
|
||||
import TuistCache
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
|
@ -9,36 +8,17 @@ public final class MockCacheStorage: CacheStoring {
|
|||
|
||||
public init() {}
|
||||
|
||||
public func exists(name: String, hash: String) -> Single<Bool> {
|
||||
do {
|
||||
if let existsStub = existsStub {
|
||||
return Single.just(try existsStub(name, hash))
|
||||
} else {
|
||||
return Single.just(false)
|
||||
}
|
||||
} catch {
|
||||
return Single.error(error)
|
||||
}
|
||||
public func exists(name: String, hash: String) async throws -> Bool {
|
||||
try existsStub?(name, hash) ?? false
|
||||
}
|
||||
|
||||
var fetchStub: ((String, String) throws -> AbsolutePath)?
|
||||
public func fetch(name: String, hash: String) -> Single<AbsolutePath> {
|
||||
if let fetchStub = fetchStub {
|
||||
do {
|
||||
return Single.just(try fetchStub(name, hash))
|
||||
} catch {
|
||||
return Single.error(error)
|
||||
}
|
||||
} else {
|
||||
return Single.just(AbsolutePath.root)
|
||||
}
|
||||
public func fetch(name: String, hash: String) async throws -> AbsolutePath {
|
||||
try fetchStub?(name, hash) ?? .root
|
||||
}
|
||||
|
||||
var storeStub: ((String, String, [AbsolutePath]) -> Void)?
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
if let storeStub = storeStub {
|
||||
storeStub(name, hash, paths)
|
||||
}
|
||||
return Completable.empty()
|
||||
public func store(name: String, hash: String, paths: [AbsolutePath]) async throws {
|
||||
storeStub?(name, hash, paths)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
|
@ -28,15 +27,8 @@ public class CloudClient: CloudClienting {
|
|||
|
||||
// MARK: - Public
|
||||
|
||||
public func request<T, E>(_ resource: HTTPResource<T, E>) -> Single<(object: T, response: HTTPURLResponse)> {
|
||||
Single<HTTPResource<T, E>>.create { observer -> Disposable in
|
||||
do {
|
||||
observer(.success(try self.resourceWithHeaders(resource)))
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
}
|
||||
return Disposables.create()
|
||||
}.flatMap(requestDispatcher.dispatch)
|
||||
public func request<T, E>(_ resource: HTTPResource<T, E>) async throws -> (object: T, response: HTTPURLResponse) {
|
||||
try await requestDispatcher.dispatch(resource: resourceWithHeaders(resource))
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
/// Async queue dispatcher.
|
||||
public protocol AsyncQueueDispatching {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
public protocol CloudClienting {
|
||||
func request<T, E>(_ resource: HTTPResource<T, E>) -> Single<(object: T, response: HTTPURLResponse)>
|
||||
func request<T, E>(_ resource: HTTPResource<T, E>) async throws -> (object: T, response: HTTPURLResponse)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
@testable import TuistCore
|
||||
|
@ -52,7 +51,7 @@ public final class MockCloudClient: CloudClienting {
|
|||
|
||||
// MARK: Public Interface
|
||||
|
||||
public func request<T, Err: Error>(_ resource: HTTPResource<T, Err>) -> Single<(object: T, response: HTTPURLResponse)> {
|
||||
public func request<T, Err: Error>(_ resource: HTTPResource<T, Err>) async throws -> (object: T, response: HTTPURLResponse) {
|
||||
invokedRequest = true
|
||||
invokedRequestCount += 1
|
||||
invokedRequestParameterList.append(resource)
|
||||
|
@ -60,7 +59,7 @@ public final class MockCloudClient: CloudClienting {
|
|||
let urlRequest = resource.request()
|
||||
let errorCandidate = stubbedErrorPerURLRequest[urlRequest] ?? stubbedError
|
||||
if let error = errorCandidate {
|
||||
return Single.error(error)
|
||||
throw error
|
||||
} else {
|
||||
let objectCandidate = stubbedObjectPerURLRequest[urlRequest] ?? stubbedObject
|
||||
guard let object = objectCandidate as? T
|
||||
|
@ -70,7 +69,7 @@ public final class MockCloudClient: CloudClienting {
|
|||
)
|
||||
}
|
||||
let responseCandidate = stubbedResponsePerURLRequest[urlRequest] ?? stubbedResponse
|
||||
return Single.just((object, responseCandidate!))
|
||||
return (object, responseCandidate!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public struct TuistCommand: ParsableCommand {
|
|||
)
|
||||
}
|
||||
|
||||
public static func main(_: [String]? = nil) -> Never {
|
||||
public static func main(_: [String]? = nil) async {
|
||||
let errorHandler = ErrorHandler()
|
||||
let processedArguments = processArguments()
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistAutomation
|
||||
import TuistCache
|
||||
|
@ -23,7 +21,7 @@ protocol CacheControlling {
|
|||
path: AbsolutePath,
|
||||
cacheProfile: TuistGraph.Cache.Profile,
|
||||
includedTargets: Set<String>,
|
||||
dependenciesOnly: Bool) throws
|
||||
dependenciesOnly: Bool) async throws
|
||||
}
|
||||
|
||||
final class CacheController: CacheControlling {
|
||||
|
@ -82,7 +80,7 @@ final class CacheController: CacheControlling {
|
|||
cacheProfile: TuistGraph.Cache.Profile,
|
||||
includedTargets: Set<String>,
|
||||
dependenciesOnly: Bool
|
||||
) throws {
|
||||
) async throws {
|
||||
let xcframeworks = artifactBuilder.cacheOutputType == .xcframework
|
||||
let generator = generatorFactory.cache(
|
||||
config: config,
|
||||
|
@ -99,7 +97,7 @@ final class CacheController: CacheControlling {
|
|||
// Hash
|
||||
logger.notice("Hashing cacheable targets")
|
||||
|
||||
let hashesByTargetToBeCached = try makeHashesByTargetToBeCached(
|
||||
let hashesByTargetToBeCached = try await makeHashesByTargetToBeCached(
|
||||
for: graph,
|
||||
cacheProfile: cacheProfile,
|
||||
cacheOutputType: artifactBuilder.cacheOutputType,
|
||||
|
@ -129,7 +127,7 @@ final class CacheController: CacheControlling {
|
|||
|
||||
logger.notice("Building cacheable targets")
|
||||
|
||||
try archive(updatedGraph, projectPath: projectPath, cacheProfile: cacheProfile, hashesByTargetToBeCached)
|
||||
try await archive(updatedGraph, projectPath: projectPath, cacheProfile: cacheProfile, hashesByTargetToBeCached)
|
||||
|
||||
logger.notice(
|
||||
"All cacheable targets have been cached successfully as \(artifactBuilder.cacheOutputType.description)s",
|
||||
|
@ -143,7 +141,7 @@ final class CacheController: CacheControlling {
|
|||
projectPath: AbsolutePath,
|
||||
cacheProfile: TuistGraph.Cache.Profile,
|
||||
_ hashesByCacheableTarget: [(GraphTarget, String)]
|
||||
) throws {
|
||||
) async throws {
|
||||
let binariesSchemes = graph.workspace.schemes
|
||||
.filter { $0.name.contains(Constants.AutogeneratedScheme.binariesSchemeNamePrefix) }
|
||||
.filter { !($0.buildAction?.targets ?? []).isEmpty }
|
||||
|
@ -151,11 +149,11 @@ final class CacheController: CacheControlling {
|
|||
.filter { $0.name.contains(Constants.AutogeneratedScheme.bundlesSchemeNamePrefix) }
|
||||
.filter { !($0.buildAction?.targets ?? []).isEmpty }
|
||||
|
||||
try FileHandler.shared.inTemporaryDirectory { outputDirectory in
|
||||
try await FileHandler.shared.inTemporaryDirectory { outputDirectory in
|
||||
for scheme in binariesSchemes {
|
||||
let outputDirectory = outputDirectory.appending(component: scheme.name)
|
||||
try FileHandler.shared.createFolder(outputDirectory)
|
||||
try artifactBuilder.build(
|
||||
try self.artifactBuilder.build(
|
||||
scheme: scheme,
|
||||
projectTarget: XcodeBuildTarget(with: projectPath),
|
||||
configuration: cacheProfile.configuration,
|
||||
|
@ -168,7 +166,7 @@ final class CacheController: CacheControlling {
|
|||
for scheme in bundlesSchemes {
|
||||
let outputDirectory = outputDirectory.appending(component: scheme.name)
|
||||
try FileHandler.shared.createFolder(outputDirectory)
|
||||
try bundleArtifactBuilder.build(
|
||||
try self.bundleArtifactBuilder.build(
|
||||
scheme: scheme,
|
||||
projectTarget: XcodeBuildTarget(with: projectPath),
|
||||
configuration: cacheProfile.configuration,
|
||||
|
@ -180,21 +178,32 @@ final class CacheController: CacheControlling {
|
|||
|
||||
let targetsToStore = hashesByCacheableTarget.map(\.0.target.name).sorted().joined(separator: ", ")
|
||||
logger.notice("Storing \(hashesByCacheableTarget.count) cacheable targets: \(targetsToStore)")
|
||||
try hashesByCacheableTarget.forEach(context: .concurrent) { target, hash in
|
||||
try await self.store(hashesByCacheableTarget, outputDirectory: outputDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func store(
|
||||
_ hashesByCacheableTarget: [(GraphTarget, String)],
|
||||
outputDirectory: AbsolutePath
|
||||
) async throws {
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
for (target, hash) in hashesByCacheableTarget {
|
||||
let isBinary = target.target.product.isFramework
|
||||
let suffix =
|
||||
"\(isBinary ? Constants.AutogeneratedScheme.binariesSchemeNamePrefix : Constants.AutogeneratedScheme.bundlesSchemeNamePrefix)-\(target.target.platform.caseValue)"
|
||||
|
||||
let productNameWithExtension = target.target.productName
|
||||
_ = try cache.store(
|
||||
name: target.target.name,
|
||||
hash: hash,
|
||||
paths: FileHandler.shared.glob(
|
||||
outputDirectory.appending(component: suffix),
|
||||
glob: "\(productNameWithExtension).*"
|
||||
group.addTask {
|
||||
try await self.cache.store(
|
||||
name: target.target.name,
|
||||
hash: hash,
|
||||
paths: FileHandler.shared.glob(
|
||||
outputDirectory.appending(component: suffix),
|
||||
glob: "\(productNameWithExtension).*"
|
||||
)
|
||||
)
|
||||
).toBlocking().last()
|
||||
}
|
||||
}
|
||||
try await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,7 +213,7 @@ final class CacheController: CacheControlling {
|
|||
cacheOutputType: CacheOutputType,
|
||||
includedTargets: Set<String>,
|
||||
dependenciesOnly: Bool
|
||||
) throws -> [(GraphTarget, String)] {
|
||||
) async throws -> [(GraphTarget, String)] {
|
||||
// When `dependenciesOnly` is true, there is no need to compute `includedTargets` hashes
|
||||
let excludedTargets = dependenciesOnly ? includedTargets : []
|
||||
let hashesByCacheableTarget = try cacheGraphContentHasher.contentHashes(
|
||||
|
@ -222,16 +231,14 @@ final class CacheController: CacheControlling {
|
|||
Array(graphTraverser.directTargetDependencies(path: $0.path, name: $0.target.name))
|
||||
}
|
||||
)
|
||||
|
||||
return try graph.compactMap(context: .concurrent) { target throws -> (GraphTarget, String)? in
|
||||
return try await graph.concurrentCompactMap { target in
|
||||
guard
|
||||
let hash = hashesByCacheableTarget[target],
|
||||
// if cache already exists, no need to build
|
||||
try !self.cache.exists(name: target.target.name, hash: hash).toBlocking().single()
|
||||
try await !self.cache.exists(name: target.target.name, hash: hash)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (target, hash)
|
||||
}
|
||||
.reversed()
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import ArgumentParser
|
||||
|
||||
public protocol AsyncParsableCommand: ParsableCommand {
|
||||
mutating func runAsync() async throws
|
||||
}
|
|
@ -4,7 +4,7 @@ import TSCBasic
|
|||
import TuistSupport
|
||||
|
||||
/// Command to cache targets as `.(xc)framework`s and speed up your and your peers' build times.
|
||||
struct CacheWarmCommand: ParsableCommand {
|
||||
struct CacheWarmCommand: AsyncParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(
|
||||
commandName: "warm",
|
||||
|
@ -28,8 +28,8 @@ struct CacheWarmCommand: ParsableCommand {
|
|||
)
|
||||
var dependenciesOnly: Bool = false
|
||||
|
||||
func run() throws {
|
||||
try CacheWarmService().run(
|
||||
func runAsync() async throws {
|
||||
try await CacheWarmService().run(
|
||||
path: options.path,
|
||||
profile: options.profile,
|
||||
xcframeworks: options.xcframeworks,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
|
|
|
@ -32,12 +32,16 @@ public class TrackableCommand: TrackableParametersDelegate {
|
|||
self.asyncQueue = asyncQueue
|
||||
}
|
||||
|
||||
func run() throws -> Future<Void, Never> {
|
||||
func run() async throws -> Future<Void, Never> {
|
||||
let timer = clock.startTimer()
|
||||
if let command = command as? HasTrackableParameters {
|
||||
type(of: command).analyticsDelegate = self
|
||||
}
|
||||
try command.run()
|
||||
if var asyncCommand = command as? AsyncParsableCommand {
|
||||
try await asyncCommand.runAsync()
|
||||
} else {
|
||||
try command.run()
|
||||
}
|
||||
let durationInSeconds = timer.stop()
|
||||
let durationInMs = Int(durationInSeconds * 1000)
|
||||
let configuration = type(of: command).configuration
|
||||
|
|
|
@ -40,7 +40,7 @@ public struct TuistCommand: ParsableCommand {
|
|||
)
|
||||
var isTuistEnvHelp: Bool = false
|
||||
|
||||
public static func main(_ arguments: [String]? = nil) -> Never {
|
||||
public static func main(_ arguments: [String]? = nil) async {
|
||||
let errorHandler = ErrorHandler()
|
||||
var command: ParsableCommand
|
||||
do {
|
||||
|
@ -65,7 +65,7 @@ public struct TuistCommand: ParsableCommand {
|
|||
_exit(exitCode)
|
||||
}
|
||||
do {
|
||||
try execute(command)
|
||||
try await execute(command)
|
||||
TuistProcess.shared.asyncExit()
|
||||
} catch let error as FatalError {
|
||||
errorHandler.fatal(error: error)
|
||||
|
@ -81,12 +81,19 @@ public struct TuistCommand: ParsableCommand {
|
|||
}
|
||||
}
|
||||
|
||||
private static func execute(_ command: ParsableCommand) throws {
|
||||
private static func execute(_ command: ParsableCommand) async throws {
|
||||
var command = command
|
||||
guard Environment.shared.isStatsEnabled else { try command.run(); return }
|
||||
let trackableCommand = TrackableCommand(command: command)
|
||||
let future = try trackableCommand.run()
|
||||
TuistProcess.shared.add(futureTask: future)
|
||||
if Environment.shared.isStatsEnabled {
|
||||
let trackableCommand = TrackableCommand(command: command)
|
||||
let future = try await trackableCommand.run()
|
||||
TuistProcess.shared.add(futureTask: future)
|
||||
} else {
|
||||
if var asyncCommand = command as? AsyncParsableCommand {
|
||||
try await asyncCommand.runAsync()
|
||||
} else {
|
||||
try command.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
|
|
@ -19,7 +19,7 @@ final class CacheWarmService {
|
|||
pluginService = PluginService()
|
||||
}
|
||||
|
||||
func run(path: String?, profile: String?, xcframeworks: Bool, targets: Set<String>, dependenciesOnly: Bool) throws {
|
||||
func run(path: String?, profile: String?, xcframeworks: Bool, targets: Set<String>, dependenciesOnly: Bool) async throws {
|
||||
let path = self.path(path)
|
||||
let config = try configLoader.loadConfig(path: path)
|
||||
let cache = Cache(storageProvider: CacheStorageProvider(config: config))
|
||||
|
@ -32,7 +32,7 @@ final class CacheWarmService {
|
|||
}
|
||||
|
||||
let profile = try CacheProfileResolver().resolveCacheProfile(named: profile, from: config)
|
||||
try cacheController.cache(
|
||||
try await cacheController.cache(
|
||||
config: config,
|
||||
path: path,
|
||||
cacheProfile: profile,
|
||||
|
|
|
@ -15,6 +15,23 @@ extension Array {
|
|||
}
|
||||
}
|
||||
|
||||
/// Async concurrent map
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transform: The transformation closure to apply to the array
|
||||
public func concurrentMap<B>(_ transform: @escaping (Element) async throws -> B) async throws -> [B] {
|
||||
let tasks = map { element in
|
||||
Task {
|
||||
try await transform(element)
|
||||
}
|
||||
}
|
||||
var values = [B]()
|
||||
for element in tasks {
|
||||
try await values.append(element.value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/// Compact map (with execution context)
|
||||
///
|
||||
/// - Parameters:
|
||||
|
@ -29,6 +46,25 @@ extension Array {
|
|||
}
|
||||
}
|
||||
|
||||
/// Async concurrent compact map
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transform: The transformation closure to apply to the array
|
||||
public func concurrentCompactMap<B>(_ transform: @escaping (Element) async throws -> B?) async throws -> [B] {
|
||||
let tasks = map { element in
|
||||
Task {
|
||||
try await transform(element)
|
||||
}
|
||||
}
|
||||
var values = [B]()
|
||||
for element in tasks {
|
||||
if let element = try await element.value {
|
||||
values.append(element)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/// For Each (with execution context)
|
||||
///
|
||||
/// - Parameters:
|
||||
|
|
|
@ -9,6 +9,15 @@ extension Dictionary {
|
|||
.map(context: context, transform)
|
||||
}
|
||||
|
||||
/// Async concurrent map
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transform: The transformation closure to apply to the dictionary
|
||||
public func concurrentMap<B>(_ transform: @escaping (Key, Value) async throws -> B) async throws -> [B] {
|
||||
try await map { ($0.key, $0.value) }
|
||||
.concurrentMap(transform)
|
||||
}
|
||||
|
||||
/// Compact map (with execution context)
|
||||
///
|
||||
/// - Parameters:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
|
||||
enum FileClientError: LocalizedError, FatalError {
|
||||
|
@ -54,8 +53,8 @@ enum FileClientError: LocalizedError, FatalError {
|
|||
}
|
||||
|
||||
public protocol FileClienting {
|
||||
func upload(file: AbsolutePath, hash: String, to url: URL) -> Single<Bool>
|
||||
func download(url: URL) -> Single<AbsolutePath>
|
||||
func upload(file: AbsolutePath, hash: String, to url: URL) async throws -> Bool
|
||||
func download(url: URL) async throws -> AbsolutePath
|
||||
}
|
||||
|
||||
public class FileClient: FileClienting {
|
||||
|
@ -72,36 +71,47 @@ public class FileClient: FileClienting {
|
|||
|
||||
// MARK: - Public
|
||||
|
||||
public func download(url: URL) -> Single<AbsolutePath> {
|
||||
dispatchDownload(request: URLRequest(url: url)).map { AbsolutePath($0.path) }
|
||||
public func download(url: URL) async throws -> AbsolutePath {
|
||||
let request = URLRequest(url: url)
|
||||
do {
|
||||
let (url, response) = try await session.download(for: request)
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw FileClientError.invalidResponse(request, nil)
|
||||
}
|
||||
if successStatusCodeRange.contains(response.statusCode) {
|
||||
return AbsolutePath(url.path)
|
||||
} else {
|
||||
throw FileClientError.invalidResponse(request, nil)
|
||||
}
|
||||
} catch {
|
||||
if error is FileClientError {
|
||||
throw error
|
||||
} else {
|
||||
throw FileClientError.urlSessionError(error, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func upload(file: AbsolutePath, hash _: String, to url: URL) -> Single<Bool> {
|
||||
Single<Bool>.create { observer -> Disposable in
|
||||
do {
|
||||
let fileSize = try FileHandler.shared.fileSize(path: file)
|
||||
let fileData = try Data(contentsOf: file.url)
|
||||
|
||||
let request = self.uploadRequest(url: url, fileSize: fileSize, data: fileData)
|
||||
let uploadTask = self.session.dataTask(with: request) { _, response, error in
|
||||
if let error = error {
|
||||
observer(.error(FileClientError.urlSessionError(error, file)))
|
||||
} else if let response = response as? HTTPURLResponse {
|
||||
if self.successStatusCodeRange.contains(response.statusCode) {
|
||||
observer(.success(true))
|
||||
} else {
|
||||
observer(.error(FileClientError.serverSideError(request, response, file)))
|
||||
}
|
||||
} else {
|
||||
observer(.error(FileClientError.invalidResponse(request, file)))
|
||||
}
|
||||
}
|
||||
uploadTask.resume()
|
||||
return Disposables.create { uploadTask.cancel() }
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
public func upload(file: AbsolutePath, hash _: String, to url: URL) async throws -> Bool {
|
||||
let fileSize = try FileHandler.shared.fileSize(path: file)
|
||||
let fileData = try Data(contentsOf: file.url)
|
||||
let request = uploadRequest(url: url, fileSize: fileSize, data: fileData)
|
||||
do {
|
||||
let (_, response) = try await session.data(for: request)
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw FileClientError.invalidResponse(request, file)
|
||||
}
|
||||
if successStatusCodeRange.contains(response.statusCode) {
|
||||
return true
|
||||
} else {
|
||||
throw FileClientError.serverSideError(request, response, file)
|
||||
}
|
||||
} catch {
|
||||
if error is FileClientError {
|
||||
throw error
|
||||
} else {
|
||||
throw FileClientError.urlSessionError(error, file)
|
||||
}
|
||||
return Disposables.create {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,30 +126,44 @@ public class FileClient: FileClienting {
|
|||
request.httpBody = data
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
private func dispatchDownload(request: URLRequest) -> Single<URL> {
|
||||
Single.create { observer in
|
||||
let task = self.session.downloadTask(with: request) { localURL, response, networkError in
|
||||
if let networkError = networkError {
|
||||
observer(.error(FileClientError.urlSessionError(networkError, nil)))
|
||||
} else if let response = response as? HTTPURLResponse {
|
||||
guard let localURL = localURL else {
|
||||
observer(.error(FileClientError.noLocalURL(request)))
|
||||
return
|
||||
}
|
||||
|
||||
if self.successStatusCodeRange.contains(response.statusCode) {
|
||||
observer(.success(localURL))
|
||||
} else {
|
||||
observer(.error(FileClientError.invalidResponse(request, nil)))
|
||||
}
|
||||
} else {
|
||||
observer(.error(FileClientError.invalidResponse(request, nil)))
|
||||
extension URLSession {
|
||||
/// Convenience method to load data using an URLRequest, creates and resumes an URLSessionDataTask internally.
|
||||
///
|
||||
/// - Parameter request: The URLRequest for which to load data.
|
||||
/// - Returns: Data and response.
|
||||
@available(macOS, deprecated: 12.0, message: "This extension is no longer necessary.")
|
||||
public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let task = self.dataTask(with: request) { data, response, error in
|
||||
guard let data = data, let response = response else {
|
||||
let error = error ?? URLError(.badServerResponse)
|
||||
return continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
|
||||
continuation.resume(returning: (data, response))
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method to download using an URLRequest, creates and resumes an URLSessionDownloadTask internally.
|
||||
///
|
||||
/// - Parameter request: The URLRequest for which to download.
|
||||
/// - Returns: Downloaded file URL and response. The file will not be removed automatically.
|
||||
@available(macOS, deprecated: 12.0, message: "This extension is no longer necessary.")
|
||||
public func download(for request: URLRequest, delegate _: URLSessionTaskDelegate? = nil) async throws -> (URL, URLResponse) {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let task = self.downloadTask(with: request) { url, response, error in
|
||||
guard let url = url, let response = response else {
|
||||
let error = error ?? URLError(.badServerResponse)
|
||||
return continuation.resume(throwing: error)
|
||||
}
|
||||
|
||||
continuation.resume(returning: (url, response))
|
||||
}
|
||||
task.resume()
|
||||
return Disposables.create { task.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Combine
|
||||
import CombineExt
|
||||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
public enum HTTPRequestDispatcherError: LocalizedError, FatalError {
|
||||
case urlSessionError(Error)
|
||||
|
@ -60,8 +59,7 @@ public enum HTTPRequestDispatcherError: LocalizedError, FatalError {
|
|||
}
|
||||
|
||||
public protocol HTTPRequestDispatching {
|
||||
func dispatch<T, E: Error>(resource: HTTPResource<T, E>) -> Single<(object: T, response: HTTPURLResponse)>
|
||||
func dispatch<T, E: Error>(resource: HTTPResource<T, E>) -> AnyPublisher<(object: T, response: HTTPURLResponse), Error>
|
||||
func dispatch<T, E: Error>(resource: HTTPResource<T, E>) async throws -> (object: T, response: HTTPURLResponse)
|
||||
}
|
||||
|
||||
public final class HTTPRequestDispatcher: HTTPRequestDispatching {
|
||||
|
@ -71,53 +69,33 @@ public final class HTTPRequestDispatcher: HTTPRequestDispatching {
|
|||
self.session = session
|
||||
}
|
||||
|
||||
public func dispatch<T, E: Error>(resource: HTTPResource<T, E>) -> Single<(object: T, response: HTTPURLResponse)> {
|
||||
Single.create { observer in
|
||||
let task = self.session.dataTask(with: resource.request(), completionHandler: { data, response, error in
|
||||
if let error = error {
|
||||
observer(.error(HTTPRequestDispatcherError.urlSessionError(error)))
|
||||
} else if let data = data, let response = response as? HTTPURLResponse {
|
||||
switch response.statusCode {
|
||||
case 200 ..< 300:
|
||||
do {
|
||||
let object = try resource.parse(data, response)
|
||||
observer(.success((object: object, response: response)))
|
||||
} catch {
|
||||
observer(.error(HTTPRequestDispatcherError.parseError(error)))
|
||||
}
|
||||
default: // Error
|
||||
do {
|
||||
let error = try resource.parseError(data, response)
|
||||
observer(.error(HTTPRequestDispatcherError.serverSideError(error, response)))
|
||||
} catch {
|
||||
observer(.error(HTTPRequestDispatcherError.parseError(error)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
observer(.error(HTTPRequestDispatcherError.invalidResponse))
|
||||
}
|
||||
})
|
||||
task.resume()
|
||||
|
||||
return Disposables.create {
|
||||
task.cancel()
|
||||
public func dispatch<T, E: Error>(resource: HTTPResource<T, E>) async throws -> (object: T, response: HTTPURLResponse) {
|
||||
do {
|
||||
let (data, response) = try await session.data(for: resource.request())
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw HTTPRequestDispatcherError.invalidResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch<T, E>(resource: HTTPResource<T, E>) -> AnyPublisher<(object: T, response: HTTPURLResponse), Error>
|
||||
where E: Error
|
||||
{
|
||||
AnyPublisher.create { subscriber in
|
||||
let disposable = self.dispatch(resource: resource)
|
||||
.subscribe(onSuccess: { value in
|
||||
subscriber.send(value)
|
||||
subscriber.send(completion: .finished)
|
||||
}, onError: { error in
|
||||
subscriber.send(completion: .failure(error))
|
||||
})
|
||||
return AnyCancellable {
|
||||
disposable.dispose()
|
||||
switch response.statusCode {
|
||||
case 200 ..< 300:
|
||||
do {
|
||||
let object = try resource.parse(data, response)
|
||||
return (object: object, response: response)
|
||||
} catch {
|
||||
throw HTTPRequestDispatcherError.parseError(error)
|
||||
}
|
||||
default: // Error
|
||||
do {
|
||||
let error = try resource.parseError(data, response)
|
||||
throw HTTPRequestDispatcherError.serverSideError(error, response)
|
||||
} catch {
|
||||
throw HTTPRequestDispatcherError.parseError(error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if error is HTTPRequestDispatcherError {
|
||||
throw error
|
||||
} else {
|
||||
throw HTTPRequestDispatcherError.urlSessionError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -510,7 +510,7 @@ public final class System: Systeming {
|
|||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
|
||||
.subscribe(on: ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
|
||||
}
|
||||
|
||||
public func observable(_ arguments: [String], pipedToArguments: [String]) -> Observable<SystemEvent<Data>> {
|
||||
|
@ -576,7 +576,7 @@ public final class System: Systeming {
|
|||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
|
||||
.subscribe(on: ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global()))
|
||||
}
|
||||
|
||||
/// Runs a command in the shell asynchronously.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
|
||||
/// It represents an event sent by a running process.
|
||||
|
|
|
@ -53,6 +53,7 @@ public protocol FileHandling: AnyObject {
|
|||
/// Determine temporary directory either default for user or specified by ENV variable
|
||||
func determineTemporaryDirectory() throws -> AbsolutePath
|
||||
func temporaryDirectory() throws -> AbsolutePath
|
||||
func inTemporaryDirectory(_ closure: @escaping (AbsolutePath) async throws -> Void) async throws
|
||||
func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws
|
||||
func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Void) throws
|
||||
func inTemporaryDirectory<Result>(_ closure: (AbsolutePath) throws -> Result) throws -> Result
|
||||
|
@ -140,6 +141,18 @@ public class FileHandler: FileHandling {
|
|||
try withTemporaryDirectory(removeTreeOnDeinit: true, closure)
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory(_ closure: @escaping (AbsolutePath) async throws -> Void) async throws {
|
||||
try withTemporaryDirectory(removeTreeOnDeinit: true) { path in
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
Task.detached {
|
||||
try await closure(path)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory<Result>(removeOnCompletion: Bool,
|
||||
_ closure: (AbsolutePath) throws -> Result) throws -> Result
|
||||
{
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
public enum URLSessionSchedulerError: FatalError {
|
||||
case httpError(status: HTTPStatusCode, response: URLResponse, request: URLRequest)
|
||||
|
||||
public var type: ErrorType {
|
||||
switch self {
|
||||
case .httpError: return .abort
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .httpError(status, response, request):
|
||||
return "We got an error \(status) from the request \(response.url!) \(request.httpMethod!)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol URLSessionScheduling: AnyObject {
|
||||
/// Schedules an URLSession request and returns the result synchronously.
|
||||
///
|
||||
/// - Parameter request: request to be executed.
|
||||
/// - Returns: request's response.
|
||||
func schedule(request: URLRequest) -> (error: Error?, data: Data?)
|
||||
|
||||
/// Returns an observable that runs the given request and completes with either the data or an error.
|
||||
/// - Parameter request: URL request to be sent.
|
||||
/// - Returns: A Single instance to trigger the request.
|
||||
func single(request: URLRequest) -> Single<Data>
|
||||
}
|
||||
|
||||
public final class URLSessionScheduler: URLSessionScheduling {
|
||||
// MARK: - Constants
|
||||
|
||||
/// The default request timeout.
|
||||
public static let defaultRequestTimeout: Double = 3
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Session.
|
||||
private let session: URLSession
|
||||
|
||||
/// Initializes the client with the session.
|
||||
///
|
||||
/// - Parameter session: url session.
|
||||
/// - Parameter requestTimeout: request timeout.
|
||||
public init(requestTimeout: Double = URLSessionScheduler.defaultRequestTimeout) {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = requestTimeout
|
||||
session = URLSession(configuration: configuration)
|
||||
}
|
||||
|
||||
public func schedule(request: URLRequest) -> (error: Error?, data: Data?) {
|
||||
var data: Data?
|
||||
var error: Error?
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
session.dataTask(with: request) { sessionData, _, sessionError in
|
||||
data = sessionData
|
||||
error = sessionError
|
||||
semaphore.signal()
|
||||
}.resume()
|
||||
semaphore.wait()
|
||||
return (error: error, data: data)
|
||||
}
|
||||
|
||||
public func single(request: URLRequest) -> Single<Data> {
|
||||
Single.create { subscriber -> Disposable in
|
||||
let task = self.session.dataTask(with: request) { data, response, error in
|
||||
let statusCode = (response as? HTTPURLResponse)?.statusCodeValue
|
||||
|
||||
if let error = error {
|
||||
subscriber(.error(error))
|
||||
} else if let statusCode = statusCode {
|
||||
if !statusCode.isClientError, !statusCode.isServerError {
|
||||
subscriber(.success(data ?? Data()))
|
||||
} else {
|
||||
subscriber(.error(URLSessionSchedulerError.httpError(
|
||||
status: statusCode,
|
||||
response: response!,
|
||||
request: request
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return Disposables.create {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -93,6 +93,24 @@ extension XCTestCase {
|
|||
XCTFail("No error was thrown", file: file, line: line)
|
||||
}
|
||||
|
||||
public func XCTAssertThrowsSpecific<Error: Swift.Error & Equatable, T>(
|
||||
_ closure: @autoclosure () async throws -> T,
|
||||
_ error: Error,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
_ = try await closure()
|
||||
} catch let closureError as Error {
|
||||
XCTAssertEqual(error, closureError, file: file, line: line)
|
||||
return
|
||||
} catch let closureError {
|
||||
XCTFail("\(error) is not equal to: \(closureError)", file: file, line: line)
|
||||
return
|
||||
}
|
||||
XCTFail("No error was thrown", file: file, line: line)
|
||||
}
|
||||
|
||||
public func XCTAssertCodableEqualToJson<C: Codable>(_ subject: C, _ json: String, file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
|
@ -12,13 +11,17 @@ public final class MockFileClient: FileClienting {
|
|||
public var invokedUploadCount = 0
|
||||
public var invokedUploadParameters: (file: AbsolutePath, hash: String, url: URL)?
|
||||
public var invokedUploadParametersList = [(file: AbsolutePath, hash: String, url: URL)]()
|
||||
public var stubbedUploadResult: Single<Bool> = Single.just(true)
|
||||
public var stubbedUploadResult = true
|
||||
public var stubbedUploadError: Error!
|
||||
|
||||
public func upload(file: AbsolutePath, hash: String, to url: URL) -> Single<Bool> {
|
||||
public func upload(file: AbsolutePath, hash: String, to url: URL) async throws -> Bool {
|
||||
invokedUpload = true
|
||||
invokedUploadCount += 1
|
||||
invokedUploadParameters = (file, hash, url)
|
||||
invokedUploadParametersList.append((file, hash, url))
|
||||
if let stubbedUploadError = stubbedUploadError {
|
||||
throw stubbedUploadError
|
||||
}
|
||||
return stubbedUploadResult
|
||||
}
|
||||
|
||||
|
@ -26,9 +29,9 @@ public final class MockFileClient: FileClienting {
|
|||
public var invokedDownloadCount = 0
|
||||
public var invokedDownloadParameters: (url: URL, Void)?
|
||||
public var invokedDownloadParametersList = [(url: URL, Void)]()
|
||||
public var stubbedDownloadResult: Single<AbsolutePath>!
|
||||
public var stubbedDownloadResult: AbsolutePath!
|
||||
|
||||
public func download(url: URL) -> Single<AbsolutePath> {
|
||||
public func download(url: URL) async throws -> AbsolutePath {
|
||||
invokedDownload = true
|
||||
invokedDownloadCount += 1
|
||||
invokedDownloadParameters = (url, ())
|
||||
|
|
|
@ -1,47 +1,22 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
public class MockHTTPRequestDispatcher: HTTPRequestDispatching {
|
||||
public var requests: [URLRequest] = []
|
||||
|
||||
public func dispatch<T, E>(resource: HTTPResource<T, E>) -> Single<(object: T, response: HTTPURLResponse)> where E: Error {
|
||||
Single.create { observer in
|
||||
if T.self != Void.self {
|
||||
fatalError(
|
||||
"""
|
||||
MockHTTPRequestDispatcher only supports resources with Void as its generic value. \
|
||||
Use HTTPResource.noop from TuistSupportTesting.
|
||||
"""
|
||||
)
|
||||
}
|
||||
self.requests.append(resource.request())
|
||||
let response = HTTPURLResponse()
|
||||
// swiftlint:disable:next force_cast
|
||||
observer(.success((object: () as! T, response: response)))
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch<T, E>(resource: HTTPResource<T, E>) -> AnyPublisher<(object: T, response: HTTPURLResponse), Error>
|
||||
where E: Error
|
||||
{
|
||||
AnyPublisher.create { subscriber in
|
||||
if T.self != Void.self {
|
||||
fatalError(
|
||||
"""
|
||||
MockHTTPRequestDispatcher only supports resources with Void as its generic value. \
|
||||
Use HTTPResource.noop from TuistSupportTesting.
|
||||
"""
|
||||
)
|
||||
}
|
||||
self.requests.append(resource.request())
|
||||
let response = HTTPURLResponse()
|
||||
// swiftlint:disable:next force_cast
|
||||
subscriber.send((object: () as! T, response: response))
|
||||
subscriber.send(completion: .finished)
|
||||
return AnyCancellable {}
|
||||
public func dispatch<T, E: Error>(resource: HTTPResource<T, E>) async throws -> (object: T, response: HTTPURLResponse) {
|
||||
if T.self != Void.self {
|
||||
fatalError(
|
||||
"""
|
||||
MockHTTPRequestDispatcher only supports resources with Void as its generic value. \
|
||||
Use HTTPResource.noop from TuistSupportTesting.
|
||||
"""
|
||||
)
|
||||
}
|
||||
requests.append(resource.request())
|
||||
let response = HTTPURLResponse()
|
||||
// swiftlint:disable:next force_cast
|
||||
return (object: () as! T, response: response)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
public final class MockURLSessionScheduler: TuistSupport.URLSessionScheduling {
|
||||
private var stubs: [URLRequest: (error: URLError?, data: Data?)] = [:]
|
||||
|
||||
public init() {}
|
||||
|
||||
public func stub(request: URLRequest, error: URLError?) {
|
||||
stubs[request] = (error: error, data: nil)
|
||||
}
|
||||
|
||||
public func stub(request: URLRequest, data: Data?) {
|
||||
stubs[request] = (error: nil, data: data)
|
||||
}
|
||||
|
||||
public func schedule(request: URLRequest) -> (error: Error?, data: Data?) {
|
||||
guard let stub = stubs[request] else {
|
||||
return (error: nil, data: nil)
|
||||
}
|
||||
return stub
|
||||
}
|
||||
|
||||
public func single(request: URLRequest) -> Single<Data> {
|
||||
guard let stub = stubs[request] else {
|
||||
return Single.error(TestError("the sent request was not stubbed: \(request)"))
|
||||
}
|
||||
if let error = stub.error {
|
||||
return Single.error(error)
|
||||
} else if let data = stub.data {
|
||||
return Single.just(data)
|
||||
} else {
|
||||
return Single.error(TestError("the s"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// This empty file is needed in order to compile `@main` attribute with swift-tools-version:5.4.0
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistAnalytics
|
||||
import TuistKit
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
@main
|
||||
enum TuistApp {
|
||||
static func main() async throws {
|
||||
if CommandLine.arguments
|
||||
.contains("--verbose") { try? ProcessEnv.setVar(Constants.EnvironmentVariables.verbose, value: "true") }
|
||||
if CommandLine.arguments.contains("--generate-completion-script") {
|
||||
try? ProcessEnv.unsetVar(Constants.EnvironmentVariables.silent)
|
||||
}
|
||||
|
||||
TuistSupport.LogOutput.bootstrap()
|
||||
|
||||
let path: AbsolutePath
|
||||
if let argumentIndex = CommandLine.arguments.firstIndex(of: "--path") {
|
||||
path = AbsolutePath(CommandLine.arguments[argumentIndex + 1], relativeTo: .current)
|
||||
} else {
|
||||
path = .current
|
||||
}
|
||||
|
||||
try TuistSupport.Environment.shared.bootstrap()
|
||||
try TuistAnalytics.bootstrap(config: ConfigLoader().loadConfig(path: path))
|
||||
|
||||
await TuistCommand.main()
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistAnalytics
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar(Constants.EnvironmentVariables.verbose, value: "true") }
|
||||
if CommandLine.arguments.contains("--generate-completion-script") {
|
||||
try? ProcessEnv.unsetVar(Constants.EnvironmentVariables.silent)
|
||||
}
|
||||
|
||||
TuistSupport.LogOutput.bootstrap()
|
||||
|
||||
let path: AbsolutePath
|
||||
if let argumentIndex = CommandLine.arguments.firstIndex(of: "--path") {
|
||||
path = AbsolutePath(CommandLine.arguments[argumentIndex + 1], relativeTo: .current)
|
||||
} else {
|
||||
path = .current
|
||||
}
|
||||
|
||||
try TuistSupport.Environment.shared.bootstrap()
|
||||
try TuistAnalytics.bootstrap(config: ConfigLoader().loadConfig(path: path))
|
||||
|
||||
import TuistKit
|
||||
|
||||
TuistCommand.main()
|
|
@ -1,4 +1,3 @@
|
|||
import RxBlocking
|
||||
import TuistCloud
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
|
@ -36,11 +35,11 @@ final class TuistAnalyticsBackboneBackendTests: TuistUnitTestCase {
|
|||
XCTAssertHTTPResourceContainsHeader(got, header: "Content-Type", value: "application/json")
|
||||
}
|
||||
|
||||
func test_send() throws {
|
||||
func test_send() async throws {
|
||||
// Given
|
||||
let commandEvent = CommandEvent.test()
|
||||
|
||||
// When
|
||||
try subject.send(commandEvent: commandEvent).toBlocking()
|
||||
try await subject.send(commandEvent: commandEvent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import RxBlocking
|
||||
import TuistCloud
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
|
@ -35,7 +34,7 @@ final class TuistAnalyticsCloudBackendTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_send_when_analytics_is_not_enabled() throws {
|
||||
func test_send_when_analytics_is_not_enabled() async throws {
|
||||
// Given
|
||||
config = Cloud.test(options: [])
|
||||
subject = TuistAnalyticsCloudBackend(
|
||||
|
@ -46,13 +45,13 @@ final class TuistAnalyticsCloudBackendTests: TuistUnitTestCase {
|
|||
let event = CommandEvent.test()
|
||||
|
||||
// When
|
||||
try subject.send(commandEvent: event).toBlocking().last()
|
||||
try await subject.send(commandEvent: event)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(resourceFactory.invokedCreateCount, 0)
|
||||
}
|
||||
|
||||
func test_send_when_analytics_is_enabled() throws {
|
||||
func test_send_when_analytics_is_enabled() async throws {
|
||||
// Given
|
||||
config = Cloud.test(options: [.analytics])
|
||||
subject = TuistAnalyticsCloudBackend(
|
||||
|
@ -67,7 +66,7 @@ final class TuistAnalyticsCloudBackendTests: TuistUnitTestCase {
|
|||
client.stubbedResponsePerURLRequest[resource.request()] = HTTPURLResponse.test()
|
||||
|
||||
// When
|
||||
try subject.send(commandEvent: event).toBlocking().last()
|
||||
try await subject.send(commandEvent: event)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(resourceFactory.invokedCreateCount, 1)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Foundation
|
||||
import Queuer
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import RxSwift
|
||||
import TSCBasic
|
||||
import struct TSCUtility.Version
|
||||
import TuistCore
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
@ -22,7 +21,7 @@ final class CacheLocalStorageIntegrationTests: TuistTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_exists_when_a_cached_xcframework_exists() throws {
|
||||
func test_exists_when_a_cached_xcframework_exists() async throws {
|
||||
// Given
|
||||
let cacheDirectory = try temporaryPath()
|
||||
let hash = "abcde"
|
||||
|
@ -32,23 +31,23 @@ final class CacheLocalStorageIntegrationTests: TuistTestCase {
|
|||
try FileHandler.shared.createFolder(xcframeworkPath)
|
||||
|
||||
// When
|
||||
let got = try subject.exists(name: "ignored", hash: hash).toBlocking().first()
|
||||
let got = try await subject.exists(name: "ignored", hash: hash)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(got == true)
|
||||
}
|
||||
|
||||
func test_exists_when_a_cached_xcframework_does_not_exist() throws {
|
||||
func test_exists_when_a_cached_xcframework_does_not_exist() async throws {
|
||||
// When
|
||||
let hash = "abcde"
|
||||
|
||||
let got = try subject.exists(name: "ignored", hash: hash).toBlocking().first()
|
||||
let got = try await subject.exists(name: "ignored", hash: hash)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(got == false)
|
||||
}
|
||||
|
||||
func test_fetch_when_a_cached_xcframework_exists() throws {
|
||||
func test_fetch_when_a_cached_xcframework_exists() async throws {
|
||||
// Given
|
||||
let cacheDirectory = try temporaryPath()
|
||||
let hash = "abcde"
|
||||
|
@ -58,22 +57,22 @@ final class CacheLocalStorageIntegrationTests: TuistTestCase {
|
|||
try FileHandler.shared.createFolder(xcframeworkPath)
|
||||
|
||||
// When
|
||||
let got = try subject.fetch(name: "ignored", hash: hash).toBlocking().first()
|
||||
let got = try await subject.fetch(name: "ignored", hash: hash)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(got == xcframeworkPath)
|
||||
}
|
||||
|
||||
func test_fetch_when_a_cached_xcframework_does_not_exist() throws {
|
||||
func test_fetch_when_a_cached_xcframework_does_not_exist() async throws {
|
||||
let hash = "abcde"
|
||||
|
||||
XCTAssertThrowsSpecific(
|
||||
try subject.fetch(name: "ignored", hash: hash).toBlocking().first(),
|
||||
await XCTAssertThrowsSpecific(
|
||||
try await subject.fetch(name: "ignored", hash: hash),
|
||||
CacheLocalStorageError.compiledArtifactNotFound(hash: hash)
|
||||
)
|
||||
}
|
||||
|
||||
func test_store() throws {
|
||||
func test_store() async throws {
|
||||
// Given
|
||||
let hash = "abcde"
|
||||
let cacheDirectory = try temporaryPath()
|
||||
|
@ -81,7 +80,7 @@ final class CacheLocalStorageIntegrationTests: TuistTestCase {
|
|||
try FileHandler.shared.createFolder(xcframeworkPath)
|
||||
|
||||
// When
|
||||
_ = try subject.store(name: "ignored", hash: hash, paths: [xcframeworkPath]).toBlocking().first()
|
||||
_ = try await subject.store(name: "ignored", hash: hash, paths: [xcframeworkPath])
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(cacheDirectory.appending(RelativePath("\(hash)/framework.xcframework"))))
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistCoreTesting
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistGraph
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCacheTesting
|
||||
import TuistCloud
|
||||
|
@ -38,7 +37,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
fileArchiverFactory.stubbedMakeFileArchiverResult = fileArchiver
|
||||
fileArchiverFactory.stubbedMakeFileUnarchiverResult = fileUnarchiver
|
||||
fileClient = MockFileClient()
|
||||
fileClient.stubbedDownloadResult = Single.just(zipPath)
|
||||
fileClient.stubbedDownloadResult = zipPath
|
||||
cloudClient = MockCloudClient()
|
||||
|
||||
cacheDirectoriesProvider = try MockCacheDirectoriesProvider()
|
||||
|
@ -67,66 +66,58 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
|
||||
// - exists
|
||||
|
||||
func test_exists_whenClientReturnsAnError() throws {
|
||||
func test_exists_whenClientReturnsAnError() async throws {
|
||||
// Given
|
||||
cloudClient.mock(error: CloudEmptyResponseError())
|
||||
|
||||
// When
|
||||
let result = subject.exists(name: "targetName", hash: "acho tio")
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
do {
|
||||
_ = try await subject.exists(name: "targetName", hash: "acho tio")
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CloudEmptyResponseError:
|
||||
XCTAssertEqual(error as! CloudEmptyResponseError, CloudEmptyResponseError())
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
} catch {
|
||||
// Then
|
||||
if error is CloudEmptyResponseError {
|
||||
XCTAssertEqual(error as! CloudEmptyResponseError, CloudEmptyResponseError())
|
||||
} else {
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test_exists_whenClientReturnsAnHTTPError() throws {
|
||||
func test_exists_whenClientReturnsAnHTTPError() async throws {
|
||||
// Given
|
||||
let cloudResponse: CloudResponse<CloudEmptyResponse> = CloudResponse(status: "shaki", data: CloudEmptyResponse())
|
||||
let httpResponse: HTTPURLResponse = .test(statusCode: 500)
|
||||
cloudClient.mock(object: cloudResponse, response: httpResponse)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(name: "targetName", hash: "acho tio")
|
||||
.toBlocking()
|
||||
.single()
|
||||
let result = try await subject.exists(name: "targetName", hash: "acho tio")
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
func test_exists_whenClientReturnsASuccess() throws {
|
||||
func test_exists_whenClientReturnsASuccess() async throws {
|
||||
// Given
|
||||
let cloudResponse = CloudResponse<CloudEmptyResponse>(status: "shaki", data: CloudEmptyResponse())
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
cloudClient.mock(object: cloudResponse, response: httpResponse)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(name: "targetName", hash: "acho tio")
|
||||
.toBlocking()
|
||||
.single()
|
||||
let result = try await subject.exists(name: "targetName", hash: "acho tio")
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(result)
|
||||
}
|
||||
|
||||
func test_exists_whenClientReturnsA202() throws {
|
||||
func test_exists_whenClientReturnsA202() async throws {
|
||||
// Given
|
||||
let cloudResponse = CloudResponse<CloudEmptyResponse>(status: "shaki", data: CloudEmptyResponse())
|
||||
let httpResponse: HTTPURLResponse = .test(statusCode: 202)
|
||||
cloudClient.mock(object: cloudResponse, response: httpResponse)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(name: "targetName", hash: "acho tio")
|
||||
.toBlocking()
|
||||
.single()
|
||||
let result = try await subject.exists(name: "targetName", hash: "acho tio")
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(result)
|
||||
|
@ -134,28 +125,26 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
|
||||
// - fetch
|
||||
|
||||
func test_fetch_whenClientReturnsAnError() throws {
|
||||
func test_fetch_whenClientReturnsAnError() async throws {
|
||||
// Given
|
||||
let expectedError: CloudResponseError = .test()
|
||||
cloudClient.mock(error: expectedError)
|
||||
|
||||
// When
|
||||
let result = subject.fetch(name: "targetName", hash: "acho tio")
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
do {
|
||||
// When
|
||||
_ = try await subject.fetch(name: "targetName", hash: "acho tio")
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CloudResponseError:
|
||||
XCTAssertEqual(error as! CloudResponseError, expectedError)
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
} catch {
|
||||
// Then
|
||||
if error is CloudResponseError {
|
||||
XCTAssertEqual(error as! CloudResponseError, expectedError)
|
||||
} else {
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectErrorThrown() throws {
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectErrorThrown() async throws {
|
||||
// Given
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
|
@ -166,23 +155,21 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let paths = try createFolders(["Unarchived/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
fileUnarchiver.stubbedUnzipResult = paths.first
|
||||
|
||||
// When
|
||||
let result = subject.fetch(name: "targetName", hash: hash)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
do {
|
||||
// When
|
||||
_ = try await subject.fetch(name: "targetName", hash: hash)
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CacheRemoteStorageError:
|
||||
XCTAssertEqual(error as! CacheRemoteStorageError, CacheRemoteStorageError.artifactNotFound(hash: hash))
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
} catch {
|
||||
// Then
|
||||
if error is CacheRemoteStorageError {
|
||||
XCTAssertEqual(error as! CacheRemoteStorageError, CacheRemoteStorageError.artifactNotFound(hash: hash))
|
||||
} else {
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_returnsCorrectRootFolderAfterUnzipping() throws {
|
||||
func test_fetch_whenClientReturnsASuccess_returnsCorrectRootFolderAfterUnzipping() async throws {
|
||||
// Given
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
|
@ -194,9 +181,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
fileUnarchiver.stubbedUnzipResult = paths.first?.parentDirectory
|
||||
|
||||
// When
|
||||
let result = try subject.fetch(name: "targetName", hash: hash)
|
||||
.toBlocking()
|
||||
.single()
|
||||
let result = try await subject.fetch(name: "targetName", hash: hash)
|
||||
|
||||
// Then
|
||||
let expectedPath = cacheDirectoriesProvider.cacheDirectory(for: .builds)
|
||||
|
@ -204,7 +189,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(result, expectedPath)
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileClientTheCorrectURL() throws {
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileClientTheCorrectURL() async throws {
|
||||
// Given
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let url = URL(string: "https://tuist.io/acho/tio")!
|
||||
|
@ -217,15 +202,13 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
fileUnarchiver.stubbedUnzipResult = paths.first!.parentDirectory
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(name: "targetName", hash: hash)
|
||||
.toBlocking()
|
||||
.single()
|
||||
_ = try await subject.fetch(name: "targetName", hash: hash)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileClient.invokedDownloadParameters?.url, url)
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileArchiverTheCorrectDestinationPath() throws {
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileArchiverTheCorrectDestinationPath() async throws {
|
||||
// Given
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
|
@ -238,9 +221,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let hash = "foo_bar"
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(name: "targetName", hash: hash)
|
||||
.toBlocking()
|
||||
.single()
|
||||
_ = try await subject.fetch(name: "targetName", hash: hash)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(fileUnarchiver.invokedUnzip)
|
||||
|
@ -248,35 +229,31 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
|
||||
// - store
|
||||
|
||||
func test_store_whenClientReturnsAnError() throws {
|
||||
func test_store_whenClientReturnsAnError() async throws {
|
||||
// Given
|
||||
let expectedError = CloudResponseError.test()
|
||||
cloudClient.mock(error: expectedError)
|
||||
|
||||
// When
|
||||
let result = subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
do {
|
||||
// When
|
||||
_ = try await subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CloudResponseError:
|
||||
XCTAssertEqual(error as! CloudResponseError, expectedError)
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
} catch {
|
||||
// Then
|
||||
if error is CloudResponseError {
|
||||
XCTAssertEqual(error as! CloudResponseError, expectedError)
|
||||
} else {
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test_store_whenClientReturnsASuccess_returnsURLToUpload() throws {
|
||||
func test_store_whenClientReturnsASuccess_returnsURLToUpload() async throws {
|
||||
// Given
|
||||
configureCloudClientForSuccessfulUpload()
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: "foo_bar", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try await subject.store(name: "targetName", hash: "foo_bar", paths: [.root])
|
||||
|
||||
// Then
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
|
@ -286,15 +263,13 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_store_whenClientReturnsASuccess_usesTheRightHashToUpload() throws {
|
||||
func test_store_whenClientReturnsASuccess_usesTheRightHashToUpload() async throws {
|
||||
// Given
|
||||
let hash = "foo_bar"
|
||||
configureCloudClientForSuccessfulUpload()
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: hash, paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try await subject.store(name: "targetName", hash: hash, paths: [.root])
|
||||
|
||||
// Then
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
|
@ -304,16 +279,14 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_store_whenClientReturnsASuccess_usesTheRightZipPathToUpload() throws {
|
||||
func test_store_whenClientReturnsASuccess_usesTheRightZipPathToUpload() async throws {
|
||||
// Given
|
||||
let hash = "foo_bar"
|
||||
configureCloudClientForSuccessfulUpload()
|
||||
fileArchiver.stubbedZipResult = zipPath
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: hash, paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try await subject.store(name: "targetName", hash: hash, paths: [.root])
|
||||
|
||||
// Then
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
|
@ -323,56 +296,48 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_store_whenClientReturnsAnUploadErrorVerifyIsNotCalled() throws {
|
||||
func test_store_whenClientReturnsAnUploadErrorVerifyIsNotCalled() async throws {
|
||||
// Given
|
||||
let expectedError = CloudResponseError.test()
|
||||
cloudClient.mock(error: expectedError)
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try? await subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(mockCloudCacheResourceFactory.invokedVerifyUploadResource)
|
||||
}
|
||||
|
||||
func test_store_whenFileUploaderReturnsAnErrorFileArchiverIsCalled() throws {
|
||||
func test_store_whenFileUploaderReturnsAnErrorFileArchiverIsCalled() async throws {
|
||||
// Given
|
||||
configureCloudClientForSuccessfulUpload()
|
||||
fileClient.stubbedUploadResult = Single.error(TestError("Error uploading file"))
|
||||
fileClient.stubbedUploadError = TestError("Error uploading file")
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try? await subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileArchiver.invokedDeleteCount, 1)
|
||||
}
|
||||
|
||||
func test_store_whenFileUploaderReturnsAnErrorVerifyIsNotCalled() throws {
|
||||
func test_store_whenFileUploaderReturnsAnErrorVerifyIsNotCalled() async throws {
|
||||
// Given
|
||||
configureCloudClientForSuccessfulUpload()
|
||||
fileClient.stubbedUploadResult = Single.error(TestError("Error uploading file"))
|
||||
fileClient.stubbedUploadError = TestError("Error uploading file")
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try? await subject.store(name: "targetName", hash: "acho tio", paths: [.root])
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(mockCloudCacheResourceFactory.invokedVerifyUploadResource)
|
||||
}
|
||||
|
||||
func test_store_whenVerifyFailsTheZipArchiveIsDeleted() throws {
|
||||
func test_store_whenVerifyFailsTheZipArchiveIsDeleted() async throws {
|
||||
// Given
|
||||
configureCloudClientForSuccessfulUploadAndFailedVerify()
|
||||
|
||||
// When
|
||||
_ = subject.store(name: "targetName", hash: "verify fails hash", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
_ = try? await subject.store(name: "targetName", hash: "verify fails hash", paths: [.root])
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileArchiver.invokedDeleteCount, 1)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TuistGraph
|
||||
import XCTest
|
||||
|
||||
|
@ -29,7 +28,7 @@ final class CacheTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_exists_when_in_first_cache_does_not_check_second_and_returns_true() {
|
||||
func test_exists_when_in_first_cache_does_not_check_second_and_returns_true() async throws {
|
||||
firstCache.existsStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -39,10 +38,11 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTFail("Second cache should not be checked if first hits")
|
||||
return false
|
||||
}
|
||||
XCTAssertTrue(try subject.exists(name: "targetName", hash: "1234").toBlocking().single())
|
||||
let result = try await subject.exists(name: "targetName", hash: "1234")
|
||||
XCTAssertTrue(result)
|
||||
}
|
||||
|
||||
func test_exists_when_in_second_cache_checks_both_and_returns_true() {
|
||||
func test_exists_when_in_second_cache_checks_both_and_returns_true() async throws {
|
||||
firstCache.existsStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -53,10 +53,11 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(hash, "1234")
|
||||
return true
|
||||
}
|
||||
XCTAssertTrue(try subject.exists(name: "targetName", hash: "1234").toBlocking().single())
|
||||
let result = try await subject.exists(name: "targetName", hash: "1234")
|
||||
XCTAssertTrue(result)
|
||||
}
|
||||
|
||||
func test_exists_when_not_in_cache_checks_both_and_returns_false() {
|
||||
func test_exists_when_not_in_cache_checks_both_and_returns_false() async throws {
|
||||
firstCache.existsStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -67,10 +68,11 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(hash, "1234")
|
||||
return false
|
||||
}
|
||||
XCTAssertFalse(try subject.exists(name: "targetName", hash: "1234").toBlocking().single())
|
||||
let result = try await subject.exists(name: "targetName", hash: "1234")
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
func test_fetch_when_in_first_cache_does_not_check_second_and_returns_path() {
|
||||
func test_fetch_when_in_first_cache_does_not_check_second_and_returns_path() async throws {
|
||||
firstCache.fetchStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -80,10 +82,11 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTFail("Second cache should not be checked if first hits")
|
||||
throw TestError("")
|
||||
}
|
||||
XCTAssertEqual(try subject.fetch(name: "targetName", hash: "1234").toBlocking().single(), "/Absolute/Path")
|
||||
let result = try await subject.fetch(name: "targetName", hash: "1234")
|
||||
XCTAssertEqual(result, "/Absolute/Path")
|
||||
}
|
||||
|
||||
func test_fetch_when_in_second_cache_checks_both_and_returns_path() {
|
||||
func test_fetch_when_in_second_cache_checks_both_and_returns_path() async throws {
|
||||
firstCache.fetchStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -94,10 +97,11 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(hash, "1234")
|
||||
return "/Absolute/Path"
|
||||
}
|
||||
XCTAssertEqual(try subject.fetch(name: "targetName", hash: "1234").toBlocking().single(), "/Absolute/Path")
|
||||
let result = try await subject.fetch(name: "targetName", hash: "1234")
|
||||
XCTAssertEqual(result, "/Absolute/Path")
|
||||
}
|
||||
|
||||
func test_fetch_when_not_in_cache_checks_both_and_throws() {
|
||||
func test_fetch_when_not_in_cache_checks_both_and_throws() async {
|
||||
firstCache.fetchStub = { name, hash in
|
||||
XCTAssertEqual(name, "targetName")
|
||||
XCTAssertEqual(hash, "1234")
|
||||
|
@ -108,8 +112,8 @@ final class CacheTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(hash, "1234")
|
||||
throw TestError("")
|
||||
}
|
||||
XCTAssertThrowsSpecific(
|
||||
try subject.fetch(name: "targetName", hash: "1234").toBlocking().single(),
|
||||
await XCTAssertThrowsSpecific(
|
||||
try await subject.fetch(name: "targetName", hash: "1234"),
|
||||
TestError("")
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
import XCTest
|
||||
|
@ -31,8 +29,7 @@ final class TargetsToCacheBinariesGraphMapperTests: TuistUnitTestCase {
|
|||
sources: [],
|
||||
cacheProfile: .test(),
|
||||
cacheOutputType: .framework,
|
||||
cacheGraphMutator: cacheGraphMutator,
|
||||
queue: DispatchQueue.main
|
||||
cacheGraphMutator: cacheGraphMutator
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -54,8 +51,7 @@ final class TargetsToCacheBinariesGraphMapperTests: TuistUnitTestCase {
|
|||
sources: ["B", "C", "D"],
|
||||
cacheProfile: .test(),
|
||||
cacheOutputType: .framework,
|
||||
cacheGraphMutator: cacheGraphMutator,
|
||||
queue: DispatchQueue.main
|
||||
cacheGraphMutator: cacheGraphMutator
|
||||
)
|
||||
let projectPath = try temporaryPath()
|
||||
let graph = Graph.test(
|
||||
|
@ -251,8 +247,7 @@ final class TargetsToCacheBinariesGraphMapperTests: TuistUnitTestCase {
|
|||
sources: [],
|
||||
cacheProfile: .test(),
|
||||
cacheOutputType: .xcframework,
|
||||
cacheGraphMutator: cacheGraphMutator,
|
||||
queue: DispatchQueue.main
|
||||
cacheGraphMutator: cacheGraphMutator
|
||||
)
|
||||
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
|
|
|
@ -358,6 +358,8 @@ final class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
try closure(currentPath)
|
||||
}
|
||||
|
||||
func inTemporaryDirectory(_: @escaping (AbsolutePath) async throws -> Void) async throws {}
|
||||
|
||||
func glob(_: AbsolutePath, glob _: String) -> [AbsolutePath] {
|
||||
[]
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_cache_builds_and_caches_the_frameworks() throws {
|
||||
func test_cache_builds_and_caches_the_frameworks() async throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
|
@ -123,7 +123,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
artifactBuilder.stubbedCacheOutputType = .xcframework
|
||||
|
||||
// When
|
||||
try subject.cache(
|
||||
try await subject.cache(
|
||||
config: .test(),
|
||||
path: path,
|
||||
cacheProfile: .test(configuration: "Debug"),
|
||||
|
@ -146,7 +146,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(artifactBuilder.invokedBuildSchemeProjectParameters?.scheme, scheme)
|
||||
}
|
||||
|
||||
func test_cache_when_cache_fails_throws() throws {
|
||||
func test_cache_when_cache_fails_throws() async throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
|
@ -204,8 +204,8 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
let remoteCacheError = TestError("remote cache error")
|
||||
cache.existsStub = { _, _ in throw remoteCacheError }
|
||||
// When / Then
|
||||
XCTAssertThrowsSpecific(
|
||||
try subject.cache(
|
||||
await XCTAssertThrowsSpecific(
|
||||
try await subject.cache(
|
||||
config: .test(),
|
||||
path: path,
|
||||
cacheProfile: .test(configuration: "Debug"),
|
||||
|
@ -216,7 +216,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func test_cache_early_exit_if_nothing_to_cache() throws {
|
||||
func test_cache_early_exit_if_nothing_to_cache() async throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
|
@ -275,7 +275,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
artifactBuilder.stubbedCacheOutputType = .xcframework
|
||||
|
||||
// When
|
||||
try subject.cache(
|
||||
try await subject.cache(
|
||||
config: .test(),
|
||||
path: path,
|
||||
cacheProfile: .test(configuration: "Debug"),
|
||||
|
@ -288,7 +288,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(cacheGraphLinter.invokedLintCount, 1)
|
||||
}
|
||||
|
||||
func test_filtered_cache_builds_and_caches_the_frameworks() throws {
|
||||
func test_filtered_cache_builds_and_caches_the_frameworks() async throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
|
@ -348,7 +348,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
artifactBuilder.stubbedCacheOutputType = .xcframework
|
||||
|
||||
// When
|
||||
try subject.cache(
|
||||
try await subject.cache(
|
||||
config: .test(),
|
||||
path: path,
|
||||
cacheProfile: .test(configuration: "Debug"),
|
||||
|
@ -371,7 +371,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(artifactBuilder.invokedBuildSchemeProjectParameters?.scheme, scheme)
|
||||
}
|
||||
|
||||
func test_filtered_cache_builds_with_dependencies_only_and_caches_the_frameworks() throws {
|
||||
func test_filtered_cache_builds_with_dependencies_only_and_caches_the_frameworks() async throws {
|
||||
// Given
|
||||
let project = Project.test()
|
||||
let aTarget = Target.test(name: "a")
|
||||
|
@ -409,7 +409,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
artifactBuilder.stubbedCacheOutputType = .xcframework
|
||||
|
||||
// When
|
||||
let results = try subject.makeHashesByTargetToBeCached(
|
||||
let results = try await subject.makeHashesByTargetToBeCached(
|
||||
for: graph,
|
||||
cacheProfile: .test(),
|
||||
cacheOutputType: .framework,
|
||||
|
@ -424,7 +424,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(first.1, nodeWithHashes[cGraphTarget])
|
||||
}
|
||||
|
||||
func test_given_target_to_filter_is_not_cacheable_should_cache_its_depedendencies() throws {
|
||||
func test_given_target_to_filter_is_not_cacheable_should_cache_its_depedendencies() async throws {
|
||||
// Given
|
||||
let project = Project.test()
|
||||
let aTarget = Target.test(name: "a")
|
||||
|
@ -452,7 +452,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// When
|
||||
let results = try subject.makeHashesByTargetToBeCached(
|
||||
let results = try await subject.makeHashesByTargetToBeCached(
|
||||
for: graph,
|
||||
cacheProfile: .test(),
|
||||
cacheOutputType: .xcframework,
|
||||
|
|
|
@ -34,14 +34,14 @@ final class TrackableCommandTests: TuistTestCase {
|
|||
|
||||
// MARK: - Tests
|
||||
|
||||
func test_whenParamsHaveFlagTrue_dispatchesEventWithExpectedParameters() throws {
|
||||
func test_whenParamsHaveFlagTrue_dispatchesEventWithExpectedParameters() async throws {
|
||||
// Given
|
||||
makeSubject(flag: true)
|
||||
let expectedParams = ["flag": "true"]
|
||||
var didPersisteEvent = false
|
||||
|
||||
// When
|
||||
let future = try subject.run()
|
||||
let future = try await subject.run()
|
||||
_ = future.sink {
|
||||
didPersisteEvent = true
|
||||
}
|
||||
|
@ -54,13 +54,13 @@ final class TrackableCommandTests: TuistTestCase {
|
|||
XCTAssertTrue(didPersisteEvent)
|
||||
}
|
||||
|
||||
func test_whenParamsHaveFlagFalse_dispatchesEventWithExpectedParameters() throws {
|
||||
func test_whenParamsHaveFlagFalse_dispatchesEventWithExpectedParameters() async throws {
|
||||
// Given
|
||||
makeSubject(flag: false)
|
||||
let expectedParams = ["flag": "false"]
|
||||
var didPersisteEvent = false
|
||||
// When
|
||||
let future = try subject.run()
|
||||
let future = try await subject.run()
|
||||
_ = future.sink {
|
||||
didPersisteEvent = true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import struct TSCUtility.Version
|
||||
import TuistCore
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistAutomation
|
||||
import TuistCore
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import XCTest
|
||||
@testable import TuistSupport
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class URLSessionSchedulerErrorTests: TuistUnitTestCase {
|
||||
func test_type_when_noData() {
|
||||
// Given
|
||||
let url = URL.test()
|
||||
let request = URLRequest(url: url)
|
||||
let status = HTTPStatusCode.notFound
|
||||
let response = HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: [:])!
|
||||
|
||||
// When
|
||||
let got = URLSessionSchedulerError.httpError(status: status, response: response, request: request).type
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, .abort)
|
||||
}
|
||||
|
||||
func test_description_when_noData() {
|
||||
// Given
|
||||
let url = URL.test()
|
||||
let request = URLRequest(url: url)
|
||||
let status = HTTPStatusCode.notFound
|
||||
let response = HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: [:])!
|
||||
|
||||
// When
|
||||
let got = URLSessionSchedulerError.httpError(status: status, response: response, request: request).description
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, "We got an error \(status) from the request \(response.url!) \(request.httpMethod!)")
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ let dependencies = Dependencies(
|
|||
.package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "8.5.0")),
|
||||
.package(url: "https://github.com/CombineCommunity/CombineExt.git", .upToNextMajor(from: "1.3.0")),
|
||||
.package(url: "https://github.com/apple/swift-tools-support-core.git", .upToNextMinor(from: "0.2.0")),
|
||||
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.1.1")),
|
||||
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.5.0")),
|
||||
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")),
|
||||
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2")),
|
||||
.package(url: "https://github.com/fortmarek/swifter.git", .branch("stable")),
|
||||
|
|
|
@ -132,8 +132,8 @@
|
|||
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "254617dd7fae0c45319ba5fbea435bf4d0e15b5d",
|
||||
"version": "5.1.2"
|
||||
"revision": "b4307ba0b6425c0ba4178e138799946c3da594f8",
|
||||
"version": "6.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue