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:
Alfredo Delli Bovi 2022-01-18 12:03:29 +01:00 committed by GitHub
parent 433480dbd4
commit 2e270bfe2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 564 additions and 833 deletions

View File

@ -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"
}
},
{

View File

@ -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",

View File

@ -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> {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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 {}
})
}
}

View File

@ -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
}

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCore
import TuistGraph

View File

@ -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
}
}
}
}

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import struct TSCUtility.Version
import TuistCache

View File

@ -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)
}
}

View File

@ -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

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
/// Async queue dispatcher.
public protocol AsyncQueueDispatching {

View File

@ -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)
}

View File

@ -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!)
}
}
}

View File

@ -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()

View File

@ -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()

View File

@ -0,0 +1,5 @@
import ArgumentParser
public protocol AsyncParsableCommand: ParsableCommand {
mutating func runAsync() async throws
}

View File

@ -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,

View File

@ -1,7 +1,5 @@
import ArgumentParser
import Foundation
import RxBlocking
import RxSwift
import TSCBasic
import TuistCache
import TuistCore

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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:

View File

@ -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() }
}
}
}

View File

@ -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)
}
}
}

View File

@ -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.

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
/// It represents an event sent by a running process.

View File

@ -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
{

View File

@ -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()
}
}
}
}

View File

@ -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)
{

View File

@ -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, ())

View File

@ -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)
}
}

View File

@ -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"))
}
}
}

View File

@ -0,0 +1 @@
// This empty file is needed in order to compile `@main` attribute with swift-tools-version:5.4.0

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)

View File

@ -1,6 +1,5 @@
import Foundation
import Queuer
import RxBlocking
import RxSwift
import TuistCore
import TuistSupport

View File

@ -1,4 +1,3 @@
import RxSwift
import TSCBasic
import struct TSCUtility.Version
import TuistCore

View File

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

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCore
import TuistCoreTesting

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCache
import TuistGraph

View File

@ -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)

View File

@ -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("")
)
}

View File

@ -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)

View File

@ -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] {
[]
}

View File

@ -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,

View File

@ -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
}

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCore
import TuistGraph

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCore
import TuistSupport

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import struct TSCUtility.Version
import TuistCore

View File

@ -1,5 +1,4 @@
import Foundation
import RxSwift
import TSCBasic
import TuistAutomation
import TuistCore

View File

@ -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!)")
}
}

View File

@ -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")),

View File

@ -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"
}
},
{