[Cache] Implement remote fetch logic (#1498)
* [cache-download] Implement remote fetch logic * [cache-download] Add tests in CacheRemoteStorageTests to cover the fetch part * [cache-download] Moving FileClient to TuistSupport * [cache-download] Rename zipFlow to unzip * [cache-download] Force cast in test * [cache-download] Streamlined errors output * [cache-download] Expect an xcframework inside the unzipped archive or throw an error
This commit is contained in:
parent
c03381ebfa
commit
223395c469
|
@ -4,12 +4,29 @@ import TSCBasic
|
|||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
enum CacheRemoteStorageError: FatalError, Equatable {
|
||||
case archiveDoesNotContainXCFramework(AbsolutePath)
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .archiveDoesNotContainXCFramework: return .abort
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .archiveDoesNotContainXCFramework(path):
|
||||
return "Unzipped archive at path \(path.pathString) does not contain any xcframework."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Later, add a warmup function to check if it's correctly authenticated ONCE
|
||||
final class CacheRemoteStorage: CacheStoring {
|
||||
// MARK: - Attributes
|
||||
|
||||
private let cloudClient: CloudClienting
|
||||
private let fileUploader: FileUploading
|
||||
private let fileClient: FileClienting
|
||||
private let fileArchiverFactory: FileArchiverManufacturing
|
||||
private var fileArchiverMap: [AbsolutePath: FileArchiving] = [:]
|
||||
|
||||
|
@ -17,10 +34,10 @@ final class CacheRemoteStorage: CacheStoring {
|
|||
|
||||
init(cloudClient: CloudClienting,
|
||||
fileArchiverFactory: FileArchiverManufacturing = FileArchiverFactory(),
|
||||
fileUploader: FileUploading = FileUploader()) {
|
||||
fileClient: FileClienting = FileClient()) {
|
||||
self.cloudClient = cloudClient
|
||||
self.fileArchiverFactory = fileArchiverFactory
|
||||
self.fileUploader = fileUploader
|
||||
self.fileClient = fileClient
|
||||
}
|
||||
|
||||
// MARK: - CacheStoring
|
||||
|
@ -48,9 +65,18 @@ final class CacheRemoteStorage: CacheStoring {
|
|||
func fetch(hash: String, config: Config) -> Single<AbsolutePath> {
|
||||
do {
|
||||
let resource = try CloudCacheResponse.fetchResource(hash: hash, config: config)
|
||||
return cloudClient.request(resource).map { _ in
|
||||
AbsolutePath.root // TODO:
|
||||
}
|
||||
return cloudClient
|
||||
.request(resource)
|
||||
.map { $0.object.data.url }
|
||||
.flatMap { (url: URL) in self.fileClient.download(url: url) }
|
||||
.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)
|
||||
}
|
||||
|
@ -71,7 +97,7 @@ final class CacheRemoteStorage: CacheStoring {
|
|||
.map { (responseTuple) -> URL in responseTuple.object.data.url }
|
||||
.flatMapCompletable { (url: URL) in
|
||||
let deleteCompletable = self.deleteZipArchiveCompletable(archiver: archiver)
|
||||
return self.fileUploader.upload(file: destinationZipPath, hash: hash, to: url).asCompletable()
|
||||
return self.fileClient.upload(file: destinationZipPath, hash: hash, to: url).asCompletable()
|
||||
.andThen(deleteCompletable)
|
||||
.catchError { deleteCompletable.concat(.error($0)) }
|
||||
}
|
||||
|
@ -82,6 +108,22 @@ final class CacheRemoteStorage: CacheStoring {
|
|||
|
||||
// MARK: - Private
|
||||
|
||||
private func xcframeworkPath(in archive: AbsolutePath) throws -> AbsolutePath? {
|
||||
let folderContent = try FileHandler.shared.contentsOfDirectory(archive)
|
||||
return folderContent.filter { FileHandler.shared.isFolder($0) && $0.extension == "xcframework" }.first
|
||||
}
|
||||
|
||||
private func unzip(downloadedArchive: AbsolutePath, hash: String) throws -> AbsolutePath {
|
||||
let zipPath = try FileHandler.shared.changeExtension(path: downloadedArchive, to: "zip")
|
||||
let archiveDestination = Environment.shared.xcframeworksCacheDirectory.appending(component: hash)
|
||||
try fileArchiver(for: zipPath).unzip(to: archiveDestination)
|
||||
guard let xcframework = try xcframeworkPath(in: archiveDestination) else {
|
||||
try FileHandler.shared.delete(archiveDestination)
|
||||
throw CacheRemoteStorageError.archiveDoesNotContainXCFramework(archiveDestination)
|
||||
}
|
||||
return xcframework
|
||||
}
|
||||
|
||||
private func fileArchiver(for path: AbsolutePath) -> FileArchiving {
|
||||
let fileArchiver = fileArchiverMap[path] ?? fileArchiverFactory.makeFileArchiver(for: path)
|
||||
fileArchiverMap[path] = fileArchiver
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
enum FileUploaderError: LocalizedError, FatalError {
|
||||
case urlSessionError(AbsolutePath, Error)
|
||||
case serverSideError(AbsolutePath, HTTPURLResponse)
|
||||
case invalidResponse(AbsolutePath)
|
||||
|
||||
// MARK: - FatalError
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .urlSessionError(path, error):
|
||||
let output = "Received a session error while uploading file at path \(path.pathString)"
|
||||
if let error = error as? LocalizedError {
|
||||
return "\(output). Error: \(error.localizedDescription)"
|
||||
} else {
|
||||
return output
|
||||
}
|
||||
case let .invalidResponse(path): return "Received unexpected response from the network while uploading file at path \(path.pathString)"
|
||||
case let .serverSideError(path, response):
|
||||
return "Got error code: \(response.statusCode) returned by the server, when uploading file at path \(path.pathString). (String, HTTPURLResponse: \(response.description)"
|
||||
}
|
||||
}
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .urlSessionError: return .bug
|
||||
case .serverSideError: return .bug
|
||||
case .invalidResponse: return .bug
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocalizedError
|
||||
|
||||
public var errorDescription: String? { description }
|
||||
}
|
||||
|
||||
public protocol FileUploading {
|
||||
func upload(file: AbsolutePath, hash: String, to url: URL) -> Single<Bool>
|
||||
}
|
||||
|
||||
public class FileUploader: FileUploading {
|
||||
// MARK: - Attributes
|
||||
|
||||
let session: URLSession
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(session: URLSession = URLSession.shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
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) { data, response, error in
|
||||
if let error = error {
|
||||
observer(.error(FileUploaderError.urlSessionError(file, error)))
|
||||
} else if let data = data, let response = response as? HTTPURLResponse {
|
||||
print(response)
|
||||
print("data: " + (String(data: data, encoding: .utf8) ?? ""))
|
||||
|
||||
switch response.statusCode {
|
||||
case 200 ..< 300:
|
||||
observer(.success(true))
|
||||
default: // Error
|
||||
observer(.error(FileUploaderError.serverSideError(file, response)))
|
||||
}
|
||||
} else {
|
||||
observer(.error(FileUploaderError.invalidResponse(file)))
|
||||
}
|
||||
}
|
||||
uploadTask.resume()
|
||||
return Disposables.create { uploadTask.cancel() }
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
}
|
||||
return Disposables.create {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func uploadRequest(url: URL, fileSize: UInt64, data: Data) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("application/zip", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(String(fileSize), forHTTPHeaderField: "Content-Length")
|
||||
request.setValue("zip", forHTTPHeaderField: "Content-Encoding")
|
||||
request.httpBody = data
|
||||
return request
|
||||
}
|
||||
}
|
|
@ -162,6 +162,7 @@ class ProjectGenerator: ProjectGenerating {
|
|||
|
||||
// MARK: -
|
||||
|
||||
// swiftlint:disable:next large_tuple
|
||||
private func loadProject(path: AbsolutePath) throws -> (Project, Graph, [SideEffectDescriptor]) {
|
||||
// Load all manifests
|
||||
let manifests = try recursiveManifestLoader.loadProject(at: path)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
|
||||
public extension URLRequest {
|
||||
var descriptionForError: String {
|
||||
guard let url = url, let httpMethod = httpMethod else { return "url request without any http method nor url set" }
|
||||
return "an url request that sends a \(httpMethod) request to url '\(url)'"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
|
||||
enum FileClientError: LocalizedError, FatalError {
|
||||
case urlSessionError(Error, AbsolutePath?)
|
||||
case serverSideError(URLRequest, HTTPURLResponse, AbsolutePath?)
|
||||
case invalidResponse(URLRequest, AbsolutePath?)
|
||||
case noLocalURL(URLRequest)
|
||||
|
||||
// MARK: - FatalError
|
||||
|
||||
public var description: String {
|
||||
var output: String
|
||||
|
||||
switch self {
|
||||
case let .urlSessionError(error, path):
|
||||
output = "Received a session error"
|
||||
output.append(pathSubstring(path))
|
||||
if let error = error as? LocalizedError {
|
||||
output.append(": \(error.localizedDescription)")
|
||||
}
|
||||
case let .invalidResponse(urlRequest, path):
|
||||
output = "Received unexpected response from the network with \(urlRequest.descriptionForError)"
|
||||
output.append(pathSubstring(path))
|
||||
case let .serverSideError(request, response, path):
|
||||
output = "Got error code: \(response.statusCode) returned by the server after performing \(request.descriptionForError)"
|
||||
output.append(pathSubstring(path))
|
||||
case let .noLocalURL(request):
|
||||
output = "Could not locate file on disk the downloaded file after performing \(request.descriptionForError)"
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .urlSessionError: return .bug
|
||||
case .serverSideError: return .bug
|
||||
case .invalidResponse: return .bug
|
||||
case .noLocalURL: return .bug
|
||||
}
|
||||
}
|
||||
|
||||
private func pathSubstring(_ path: AbsolutePath?) -> String {
|
||||
guard let path = path else { return "" }
|
||||
return " for file at path \(path.pathString)"
|
||||
}
|
||||
|
||||
// MARK: - LocalizedError
|
||||
|
||||
public var errorDescription: String? { description }
|
||||
}
|
||||
|
||||
public protocol FileClienting {
|
||||
func upload(file: AbsolutePath, hash: String, to url: URL) -> Single<Bool>
|
||||
func download(url: URL) -> Single<AbsolutePath>
|
||||
}
|
||||
|
||||
public class FileClient: FileClienting {
|
||||
// MARK: - Attributes
|
||||
|
||||
let session: URLSession
|
||||
private let successStatusCodeRange = 200 ..< 300
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(session: URLSession = URLSession.shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public func download(url: URL) -> Single<AbsolutePath> {
|
||||
dispatchDownload(request: URLRequest(url: url)).map { AbsolutePath($0.path) }
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
return Disposables.create {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func uploadRequest(url: URL, fileSize: UInt64, data: Data) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("application/zip", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(String(fileSize), forHTTPHeaderField: "Content-Length")
|
||||
request.setValue("zip", forHTTPHeaderField: "Content-Encoding")
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
return Disposables.create { task.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,25 +4,32 @@ import Zip
|
|||
|
||||
public protocol FileArchiving {
|
||||
func zip() throws -> AbsolutePath
|
||||
func unzip(to: AbsolutePath) throws
|
||||
func delete() throws
|
||||
}
|
||||
|
||||
public class FileArchiver: FileArchiving {
|
||||
private let path: AbsolutePath
|
||||
private var temporaryDirectory: TemporaryDirectory!
|
||||
private var temporaryArtefact: AbsolutePath!
|
||||
|
||||
init(path: AbsolutePath) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
public func zip() throws -> AbsolutePath {
|
||||
temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
|
||||
let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
|
||||
temporaryArtefact = temporaryDirectory.path
|
||||
let destinationZipPath = temporaryDirectory.path.appending(component: "\(path.basenameWithoutExt).zip")
|
||||
try Zip.zipFiles(paths: [path.url], zipFilePath: destinationZipPath.url, password: nil, progress: nil)
|
||||
return destinationZipPath
|
||||
}
|
||||
|
||||
public func unzip(to: AbsolutePath) throws {
|
||||
temporaryArtefact = path
|
||||
try Zip.unzipFile(path.url, destination: to.url, overwrite: true, password: nil)
|
||||
}
|
||||
|
||||
public func delete() throws {
|
||||
try FileHandler.shared.delete(temporaryDirectory.path)
|
||||
try FileHandler.shared.delete(temporaryArtefact)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ public enum FileHandlerError: FatalError, Equatable {
|
|||
case fileNotFound(AbsolutePath)
|
||||
case unreachableFileSize(AbsolutePath)
|
||||
case unconvertibleToData(AbsolutePath)
|
||||
case expectedAFile(AbsolutePath)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
|
@ -20,6 +21,8 @@ public enum FileHandlerError: FatalError, Equatable {
|
|||
return "Could not get the file size at path \(path.pathString)"
|
||||
case let .unconvertibleToData(path):
|
||||
return "Could not convert to Data the file content (at path \(path.pathString))"
|
||||
case let .expectedAFile(path):
|
||||
return "Could not find a file at path \(path.pathString))"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +30,7 @@ public enum FileHandlerError: FatalError, Equatable {
|
|||
switch self {
|
||||
case .invalidTextEncoding:
|
||||
return .bug
|
||||
case .writingError, .fileNotFound, .unreachableFileSize, .unconvertibleToData:
|
||||
case .writingError, .fileNotFound, .unreachableFileSize, .unconvertibleToData, .expectedAFile:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
@ -153,6 +156,8 @@ public protocol FileHandling: AnyObject {
|
|||
/// - Returns: The file’s size in bytes.
|
||||
/// - Throws: An error if path's file size can't be retrieved.
|
||||
func fileSize(path: AbsolutePath) throws -> UInt64
|
||||
|
||||
func changeExtension(path: AbsolutePath, to newExtension: String) throws -> AbsolutePath
|
||||
}
|
||||
|
||||
public class FileHandler: FileHandling {
|
||||
|
@ -333,4 +338,16 @@ public class FileHandler: FileHandling {
|
|||
guard let size = attr[FileAttributeKey.size] as? UInt64 else { throw FileHandlerError.unreachableFileSize(path) }
|
||||
return size
|
||||
}
|
||||
|
||||
// MARK: - Extension
|
||||
|
||||
public func changeExtension(path: AbsolutePath, to fileExtension: String) throws -> AbsolutePath {
|
||||
guard isFolder(path) == false else { throw FileHandlerError.expectedAFile(path) }
|
||||
let sanitizedExtension = fileExtension.starts(with: ".") ? String(fileExtension.dropFirst()) : fileExtension
|
||||
guard path.extension != sanitizedExtension else { return path }
|
||||
|
||||
let newPath = path.removingLastComponent().appending(component: "\(path.basenameWithoutExt).\(sanitizedExtension)")
|
||||
try move(from: path, to: newPath)
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistSupport
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
public final class MockFileUploader: FileUploading {
|
||||
public final class MockFileClient: FileClienting {
|
||||
public init() {}
|
||||
|
||||
public var invokedUpload = false
|
||||
|
@ -21,6 +21,20 @@ public final class MockFileUploader: FileUploading {
|
|||
invokedUploadParametersList.append((file, hash, url))
|
||||
return stubbedUploadResult
|
||||
}
|
||||
|
||||
public var invokedDownload = false
|
||||
public var invokedDownloadCount = 0
|
||||
public var invokedDownloadParameters: (url: URL, Void)?
|
||||
public var invokedDownloadParametersList = [(url: URL, Void)]()
|
||||
public var stubbedDownloadResult: Single<AbsolutePath>!
|
||||
|
||||
public func download(url: URL) -> Single<AbsolutePath> {
|
||||
invokedDownload = true
|
||||
invokedDownloadCount += 1
|
||||
invokedDownloadParameters = (url, ())
|
||||
invokedDownloadParametersList.append((url, ()))
|
||||
return stubbedDownloadResult
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable large_tuple
|
|
@ -15,6 +15,7 @@ public final class MockFileHandler: FileHandler {
|
|||
}
|
||||
|
||||
public var homeDirectoryStub: AbsolutePath?
|
||||
public var cacheDirectoryStub: AbsolutePath?
|
||||
|
||||
// swiftlint:disable:next force_try
|
||||
override public var homeDirectory: AbsolutePath { homeDirectoryStub ?? (try! temporaryDirectory()) }
|
||||
|
|
|
@ -15,6 +15,8 @@ public class MockEnvironment: Environmenting {
|
|||
attributes: nil)
|
||||
}
|
||||
|
||||
public var cacheDirectoryStub: AbsolutePath?
|
||||
|
||||
public var shouldOutputBeColoured: Bool = false
|
||||
public var isStandardOutputInteractive: Bool = false
|
||||
public var tuistVariables: [String: String] = [:]
|
||||
|
@ -28,7 +30,7 @@ public class MockEnvironment: Environmenting {
|
|||
}
|
||||
|
||||
public var cacheDirectory: AbsolutePath {
|
||||
directory.path.appending(component: "Cache")
|
||||
cacheDirectoryStub ?? directory.path.appending(component: "Cache")
|
||||
}
|
||||
|
||||
public var projectDescriptionHelpersCacheDirectory: AbsolutePath {
|
||||
|
|
|
@ -49,6 +49,22 @@ public class MockFileArchiver: FileArchiving {
|
|||
return stubbedZipResult
|
||||
}
|
||||
|
||||
public var invokedUnzip = false
|
||||
public var invokedUnzipCount = 0
|
||||
public var invokedUnzipParameters: (to: AbsolutePath, Void)?
|
||||
public var invokedUnzipParametersList = [(to: AbsolutePath, Void)]()
|
||||
public var stubbedUnzipError: Error?
|
||||
|
||||
public func unzip(to: AbsolutePath) throws {
|
||||
invokedUnzip = true
|
||||
invokedUnzipCount += 1
|
||||
invokedUnzipParameters = (to, ())
|
||||
invokedUnzipParametersList.append((to, ()))
|
||||
if let error = stubbedUnzipError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public var invokedDelete = false
|
||||
public var invokedDeleteCount = 0
|
||||
public var stubbedDeleteError: Error?
|
||||
|
|
|
@ -2,6 +2,8 @@ import Foundation
|
|||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
|
||||
public final class MockOpener: Opening {
|
||||
var openStub: Error?
|
||||
var openArgs: [(String, Bool, AbsolutePath?)] = []
|
||||
|
@ -31,3 +33,5 @@ public final class MockOpener: Opening {
|
|||
if let openStub = openStub { throw openStub }
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable large_tuple
|
||||
|
|
|
@ -4,6 +4,7 @@ import TuistCacheTesting
|
|||
import TuistCloud
|
||||
import TuistCore
|
||||
import TuistCoreTesting
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
@testable import TuistCache
|
||||
|
@ -15,16 +16,24 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
var config: Config!
|
||||
var fileArchiverFactory: MockFileArchiverFactory!
|
||||
var fileArchiver: MockFileArchiver!
|
||||
var mockFileUploader: MockFileUploader!
|
||||
var fileClient: MockFileClient!
|
||||
var zipPath: AbsolutePath!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
config = TuistCore.Config.test()
|
||||
zipPath = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||
|
||||
fileArchiverFactory = MockFileArchiverFactory()
|
||||
fileArchiver = MockFileArchiver()
|
||||
fileArchiver.stubbedZipResult = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||
fileArchiver.stubbedZipResult = zipPath
|
||||
fileArchiverFactory.stubbedMakeFileArchiverResult = fileArchiver
|
||||
mockFileUploader = MockFileUploader()
|
||||
fileClient = MockFileClient()
|
||||
fileClient.stubbedDownloadResult = Single.just(zipPath)
|
||||
|
||||
let env = Environment.shared as! MockEnvironment
|
||||
env.cacheDirectoryStub = FileHandler.shared.currentPath.appending(component: "Cache")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
@ -33,6 +42,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
cloudClient = nil
|
||||
fileArchiver = nil
|
||||
fileArchiverFactory = nil
|
||||
fileClient = nil
|
||||
zipPath = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
|
@ -43,7 +54,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ResponseType = CloudResponse<CloudHEADResponse>
|
||||
typealias ErrorType = CloudHEADResponseError
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: CloudHEADResponseError())
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = subject.exists(hash: "acho tio", config: config)
|
||||
|
@ -68,7 +79,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||
let httpResponse: HTTPURLResponse = .test(statusCode: 500)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(hash: "acho tio", config: config)
|
||||
|
@ -86,7 +97,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(hash: "acho tio", config: config)
|
||||
|
@ -105,7 +116,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||
let httpResponse: HTTPURLResponse = .test(statusCode: 202)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = try subject.exists(hash: "acho tio", config: config)
|
||||
|
@ -124,7 +135,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ErrorType = CloudResponseError
|
||||
let expectedError: ErrorType = .test()
|
||||
let cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: expectedError)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = subject.fetch(hash: "acho tio", config: config)
|
||||
|
@ -142,7 +153,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess() throws {
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectArchiveDeleted() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
@ -151,15 +162,125 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
let expectedDeletedPath = AbsolutePath(paths.first!.dirname)
|
||||
|
||||
// When
|
||||
let result = try subject.fetch(hash: "acho tio", config: config)
|
||||
let result = subject.fetch(hash: hash, config: config)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case .failed:
|
||||
XCTAssertFalse(FileHandler.shared.exists(expectedDeletedPath))
|
||||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectErrorThrown() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
let expectedPath = AbsolutePath(paths.first!.dirname)
|
||||
|
||||
// When
|
||||
let result = subject.fetch(hash: hash, config: config)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CacheRemoteStorageError:
|
||||
XCTAssertEqual(error as! CacheRemoteStorageError, CacheRemoteStorageError.archiveDoesNotContainXCFramework(expectedPath))
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_returnsCorrectRootFolderAfterUnzipping() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
|
||||
// When
|
||||
let result = try subject.fetch(hash: hash, config: config)
|
||||
.toBlocking()
|
||||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, AbsolutePath("/"))
|
||||
XCTAssertEqual(result, paths.first!)
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileClientTheCorrectURL() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let url: URL = URL(string: "https://shaki.ra/acho/tio")!
|
||||
let cacheResponse = CloudCacheResponse(url: url, expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
_ = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(hash: hash, config: config)
|
||||
.toBlocking()
|
||||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileClient.invokedDownloadParameters?.url, url)
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileArchiverTheCorrectDestinationPath() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(hash: hash, config: config)
|
||||
.toBlocking()
|
||||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileArchiver.invokedUnzipParameters?.to, paths.first!.parentDirectory)
|
||||
}
|
||||
|
||||
// - store
|
||||
|
@ -171,7 +292,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let expectedError = CloudResponseError.test()
|
||||
let cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: expectedError)
|
||||
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
||||
|
@ -201,7 +322,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
object: cloudResponse,
|
||||
response: .test()
|
||||
)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
||||
|
@ -209,7 +330,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
.materialize()
|
||||
|
||||
// Then
|
||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
XCTAssertEqual(tuple.url, url)
|
||||
} else {
|
||||
XCTFail("Could not unwrap the file uploader input tuple")
|
||||
|
@ -228,7 +349,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
object: cloudResponse,
|
||||
response: .test()
|
||||
)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
||||
|
@ -236,7 +357,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
.materialize()
|
||||
|
||||
// Then
|
||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
XCTAssertEqual(tuple.hash, hash)
|
||||
} else {
|
||||
XCTFail("Could not unwrap the file uploader input tuple")
|
||||
|
@ -256,9 +377,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
response: .test()
|
||||
)
|
||||
|
||||
let path = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||
fileArchiver.stubbedZipResult = path
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
||||
fileArchiver.stubbedZipResult = zipPath
|
||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
||||
|
@ -266,8 +386,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
.materialize()
|
||||
|
||||
// Then
|
||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
||||
XCTAssertEqual(tuple.file, path)
|
||||
if let tuple = fileClient.invokedUploadParameters {
|
||||
XCTAssertEqual(tuple.file, zipPath)
|
||||
} else {
|
||||
XCTFail("Could not unwrap the file uploader input tuple")
|
||||
}
|
||||
|
|
|
@ -307,7 +307,7 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
}
|
||||
|
||||
func readPlistFile<T>(_: AbsolutePath) throws -> T where T: Decodable {
|
||||
return try JSONDecoder().decode(T.self, from: Data())
|
||||
try JSONDecoder().decode(T.self, from: Data())
|
||||
}
|
||||
|
||||
func inTemporaryDirectory(_: (AbsolutePath) throws -> Void) throws {}
|
||||
|
@ -373,6 +373,10 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
func fileSize(path _: AbsolutePath) throws -> UInt64 {
|
||||
0
|
||||
}
|
||||
|
||||
func changeExtension(path: AbsolutePath, to newExtension: String) throws -> AbsolutePath {
|
||||
path.removingLastComponent().appending(component: "\(path.basenameWithoutExt).\(newExtension)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,24 @@ final class MockProjectGenerator: ProjectGenerating {
|
|||
return try generateWithGraphStub(path, projectOnly)
|
||||
}
|
||||
|
||||
var invokedGenerateProjectWorkspace = false
|
||||
var invokedGenerateProjectWorkspaceCount = 0
|
||||
var invokedGenerateProjectWorkspaceParameters: (path: AbsolutePath, Void)?
|
||||
var invokedGenerateProjectWorkspaceParametersList = [(path: AbsolutePath, Void)]()
|
||||
var stubbedGenerateProjectWorkspaceError: Error?
|
||||
var stubbedGenerateProjectWorkspaceResult: (AbsolutePath, Graph)!
|
||||
|
||||
func generateProjectWorkspace(path: AbsolutePath) throws -> (AbsolutePath, Graph) {
|
||||
invokedGenerateProjectWorkspace = true
|
||||
invokedGenerateProjectWorkspaceCount += 1
|
||||
invokedGenerateProjectWorkspaceParameters = (path, ())
|
||||
invokedGenerateProjectWorkspaceParametersList.append((path, ()))
|
||||
if let error = stubbedGenerateProjectWorkspaceError {
|
||||
throw error
|
||||
}
|
||||
return stubbedGenerateProjectWorkspaceResult
|
||||
}
|
||||
|
||||
var loadStub: ((AbsolutePath) throws -> Graph)?
|
||||
func load(path: AbsolutePath) throws -> Graph {
|
||||
if let loadStub = loadStub {
|
||||
|
|
|
@ -85,6 +85,20 @@ final class FileHandlerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(result, "NWY0YmVjMTkyZDBmMTg4NGZkY2Y0OTc1YjM3MDY3ZGM=")
|
||||
}
|
||||
|
||||
func test_changeExtension() throws {
|
||||
// Given
|
||||
let testZippedFrameworkPath = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||
|
||||
// When
|
||||
let result = try subject.changeExtension(path: testZippedFrameworkPath, to: "txt")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result.pathString.dropLast(4), testZippedFrameworkPath.pathString.dropLast(4))
|
||||
XCTAssertEqual(result.basenameWithoutExt, testZippedFrameworkPath.basenameWithoutExt)
|
||||
XCTAssertEqual(result.basename, "\(testZippedFrameworkPath.basenameWithoutExt).txt")
|
||||
_ = try subject.changeExtension(path: result, to: "zip")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func countItemsInRootTempDirectory(appropriateFor url: URL) throws -> Int {
|
||||
|
|
Loading…
Reference in New Issue