[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 TuistCore
|
||||||
import TuistSupport
|
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
|
// TODO: Later, add a warmup function to check if it's correctly authenticated ONCE
|
||||||
final class CacheRemoteStorage: CacheStoring {
|
final class CacheRemoteStorage: CacheStoring {
|
||||||
// MARK: - Attributes
|
// MARK: - Attributes
|
||||||
|
|
||||||
private let cloudClient: CloudClienting
|
private let cloudClient: CloudClienting
|
||||||
private let fileUploader: FileUploading
|
private let fileClient: FileClienting
|
||||||
private let fileArchiverFactory: FileArchiverManufacturing
|
private let fileArchiverFactory: FileArchiverManufacturing
|
||||||
private var fileArchiverMap: [AbsolutePath: FileArchiving] = [:]
|
private var fileArchiverMap: [AbsolutePath: FileArchiving] = [:]
|
||||||
|
|
||||||
|
@ -17,10 +34,10 @@ final class CacheRemoteStorage: CacheStoring {
|
||||||
|
|
||||||
init(cloudClient: CloudClienting,
|
init(cloudClient: CloudClienting,
|
||||||
fileArchiverFactory: FileArchiverManufacturing = FileArchiverFactory(),
|
fileArchiverFactory: FileArchiverManufacturing = FileArchiverFactory(),
|
||||||
fileUploader: FileUploading = FileUploader()) {
|
fileClient: FileClienting = FileClient()) {
|
||||||
self.cloudClient = cloudClient
|
self.cloudClient = cloudClient
|
||||||
self.fileArchiverFactory = fileArchiverFactory
|
self.fileArchiverFactory = fileArchiverFactory
|
||||||
self.fileUploader = fileUploader
|
self.fileClient = fileClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - CacheStoring
|
// MARK: - CacheStoring
|
||||||
|
@ -48,9 +65,18 @@ final class CacheRemoteStorage: CacheStoring {
|
||||||
func fetch(hash: String, config: Config) -> Single<AbsolutePath> {
|
func fetch(hash: String, config: Config) -> Single<AbsolutePath> {
|
||||||
do {
|
do {
|
||||||
let resource = try CloudCacheResponse.fetchResource(hash: hash, config: config)
|
let resource = try CloudCacheResponse.fetchResource(hash: hash, config: config)
|
||||||
return cloudClient.request(resource).map { _ in
|
return cloudClient
|
||||||
AbsolutePath.root // TODO:
|
.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 {
|
} catch {
|
||||||
return Single.error(error)
|
return Single.error(error)
|
||||||
}
|
}
|
||||||
|
@ -71,7 +97,7 @@ final class CacheRemoteStorage: CacheStoring {
|
||||||
.map { (responseTuple) -> URL in responseTuple.object.data.url }
|
.map { (responseTuple) -> URL in responseTuple.object.data.url }
|
||||||
.flatMapCompletable { (url: URL) in
|
.flatMapCompletable { (url: URL) in
|
||||||
let deleteCompletable = self.deleteZipArchiveCompletable(archiver: archiver)
|
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)
|
.andThen(deleteCompletable)
|
||||||
.catchError { deleteCompletable.concat(.error($0)) }
|
.catchError { deleteCompletable.concat(.error($0)) }
|
||||||
}
|
}
|
||||||
|
@ -82,6 +108,22 @@ final class CacheRemoteStorage: CacheStoring {
|
||||||
|
|
||||||
// MARK: - Private
|
// 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 {
|
private func fileArchiver(for path: AbsolutePath) -> FileArchiving {
|
||||||
let fileArchiver = fileArchiverMap[path] ?? fileArchiverFactory.makeFileArchiver(for: path)
|
let fileArchiver = fileArchiverMap[path] ?? fileArchiverFactory.makeFileArchiver(for: path)
|
||||||
fileArchiverMap[path] = fileArchiver
|
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: -
|
// MARK: -
|
||||||
|
|
||||||
|
// swiftlint:disable:next large_tuple
|
||||||
private func loadProject(path: AbsolutePath) throws -> (Project, Graph, [SideEffectDescriptor]) {
|
private func loadProject(path: AbsolutePath) throws -> (Project, Graph, [SideEffectDescriptor]) {
|
||||||
// Load all manifests
|
// Load all manifests
|
||||||
let manifests = try recursiveManifestLoader.loadProject(at: path)
|
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 {
|
public protocol FileArchiving {
|
||||||
func zip() throws -> AbsolutePath
|
func zip() throws -> AbsolutePath
|
||||||
|
func unzip(to: AbsolutePath) throws
|
||||||
func delete() throws
|
func delete() throws
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileArchiver: FileArchiving {
|
public class FileArchiver: FileArchiving {
|
||||||
private let path: AbsolutePath
|
private let path: AbsolutePath
|
||||||
private var temporaryDirectory: TemporaryDirectory!
|
private var temporaryArtefact: AbsolutePath!
|
||||||
|
|
||||||
init(path: AbsolutePath) {
|
init(path: AbsolutePath) {
|
||||||
self.path = path
|
self.path = path
|
||||||
}
|
}
|
||||||
|
|
||||||
public func zip() throws -> AbsolutePath {
|
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")
|
let destinationZipPath = temporaryDirectory.path.appending(component: "\(path.basenameWithoutExt).zip")
|
||||||
try Zip.zipFiles(paths: [path.url], zipFilePath: destinationZipPath.url, password: nil, progress: nil)
|
try Zip.zipFiles(paths: [path.url], zipFilePath: destinationZipPath.url, password: nil, progress: nil)
|
||||||
return destinationZipPath
|
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 {
|
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 fileNotFound(AbsolutePath)
|
||||||
case unreachableFileSize(AbsolutePath)
|
case unreachableFileSize(AbsolutePath)
|
||||||
case unconvertibleToData(AbsolutePath)
|
case unconvertibleToData(AbsolutePath)
|
||||||
|
case expectedAFile(AbsolutePath)
|
||||||
|
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -20,6 +21,8 @@ public enum FileHandlerError: FatalError, Equatable {
|
||||||
return "Could not get the file size at path \(path.pathString)"
|
return "Could not get the file size at path \(path.pathString)"
|
||||||
case let .unconvertibleToData(path):
|
case let .unconvertibleToData(path):
|
||||||
return "Could not convert to Data the file content (at path \(path.pathString))"
|
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 {
|
switch self {
|
||||||
case .invalidTextEncoding:
|
case .invalidTextEncoding:
|
||||||
return .bug
|
return .bug
|
||||||
case .writingError, .fileNotFound, .unreachableFileSize, .unconvertibleToData:
|
case .writingError, .fileNotFound, .unreachableFileSize, .unconvertibleToData, .expectedAFile:
|
||||||
return .abort
|
return .abort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,6 +156,8 @@ public protocol FileHandling: AnyObject {
|
||||||
/// - Returns: The file’s size in bytes.
|
/// - Returns: The file’s size in bytes.
|
||||||
/// - Throws: An error if path's file size can't be retrieved.
|
/// - Throws: An error if path's file size can't be retrieved.
|
||||||
func fileSize(path: AbsolutePath) throws -> UInt64
|
func fileSize(path: AbsolutePath) throws -> UInt64
|
||||||
|
|
||||||
|
func changeExtension(path: AbsolutePath, to newExtension: String) throws -> AbsolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileHandler: FileHandling {
|
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) }
|
guard let size = attr[FileAttributeKey.size] as? UInt64 else { throw FileHandlerError.unreachableFileSize(path) }
|
||||||
return size
|
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 Foundation
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import TSCBasic
|
import TSCBasic
|
||||||
import TuistCache
|
import TuistSupport
|
||||||
|
|
||||||
// swiftlint:disable large_tuple
|
// swiftlint:disable large_tuple
|
||||||
|
|
||||||
public final class MockFileUploader: FileUploading {
|
public final class MockFileClient: FileClienting {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var invokedUpload = false
|
public var invokedUpload = false
|
||||||
|
@ -21,6 +21,20 @@ public final class MockFileUploader: FileUploading {
|
||||||
invokedUploadParametersList.append((file, hash, url))
|
invokedUploadParametersList.append((file, hash, url))
|
||||||
return stubbedUploadResult
|
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
|
// swiftlint:enable large_tuple
|
|
@ -15,6 +15,7 @@ public final class MockFileHandler: FileHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var homeDirectoryStub: AbsolutePath?
|
public var homeDirectoryStub: AbsolutePath?
|
||||||
|
public var cacheDirectoryStub: AbsolutePath?
|
||||||
|
|
||||||
// swiftlint:disable:next force_try
|
// swiftlint:disable:next force_try
|
||||||
override public var homeDirectory: AbsolutePath { homeDirectoryStub ?? (try! temporaryDirectory()) }
|
override public var homeDirectory: AbsolutePath { homeDirectoryStub ?? (try! temporaryDirectory()) }
|
||||||
|
|
|
@ -15,6 +15,8 @@ public class MockEnvironment: Environmenting {
|
||||||
attributes: nil)
|
attributes: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var cacheDirectoryStub: AbsolutePath?
|
||||||
|
|
||||||
public var shouldOutputBeColoured: Bool = false
|
public var shouldOutputBeColoured: Bool = false
|
||||||
public var isStandardOutputInteractive: Bool = false
|
public var isStandardOutputInteractive: Bool = false
|
||||||
public var tuistVariables: [String: String] = [:]
|
public var tuistVariables: [String: String] = [:]
|
||||||
|
@ -28,7 +30,7 @@ public class MockEnvironment: Environmenting {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var cacheDirectory: AbsolutePath {
|
public var cacheDirectory: AbsolutePath {
|
||||||
directory.path.appending(component: "Cache")
|
cacheDirectoryStub ?? directory.path.appending(component: "Cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
public var projectDescriptionHelpersCacheDirectory: AbsolutePath {
|
public var projectDescriptionHelpersCacheDirectory: AbsolutePath {
|
||||||
|
|
|
@ -49,6 +49,22 @@ public class MockFileArchiver: FileArchiving {
|
||||||
return stubbedZipResult
|
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 invokedDelete = false
|
||||||
public var invokedDeleteCount = 0
|
public var invokedDeleteCount = 0
|
||||||
public var stubbedDeleteError: Error?
|
public var stubbedDeleteError: Error?
|
||||||
|
|
|
@ -2,6 +2,8 @@ import Foundation
|
||||||
import TSCBasic
|
import TSCBasic
|
||||||
import TuistSupport
|
import TuistSupport
|
||||||
|
|
||||||
|
// swiftlint:disable large_tuple
|
||||||
|
|
||||||
public final class MockOpener: Opening {
|
public final class MockOpener: Opening {
|
||||||
var openStub: Error?
|
var openStub: Error?
|
||||||
var openArgs: [(String, Bool, AbsolutePath?)] = []
|
var openArgs: [(String, Bool, AbsolutePath?)] = []
|
||||||
|
@ -31,3 +33,5 @@ public final class MockOpener: Opening {
|
||||||
if let openStub = openStub { throw openStub }
|
if let openStub = openStub { throw openStub }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable large_tuple
|
||||||
|
|
|
@ -4,6 +4,7 @@ import TuistCacheTesting
|
||||||
import TuistCloud
|
import TuistCloud
|
||||||
import TuistCore
|
import TuistCore
|
||||||
import TuistCoreTesting
|
import TuistCoreTesting
|
||||||
|
import TuistSupport
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import TuistCache
|
@testable import TuistCache
|
||||||
|
@ -15,16 +16,24 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
var config: Config!
|
var config: Config!
|
||||||
var fileArchiverFactory: MockFileArchiverFactory!
|
var fileArchiverFactory: MockFileArchiverFactory!
|
||||||
var fileArchiver: MockFileArchiver!
|
var fileArchiver: MockFileArchiver!
|
||||||
var mockFileUploader: MockFileUploader!
|
var fileClient: MockFileClient!
|
||||||
|
var zipPath: AbsolutePath!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|
||||||
config = TuistCore.Config.test()
|
config = TuistCore.Config.test()
|
||||||
|
zipPath = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||||
|
|
||||||
fileArchiverFactory = MockFileArchiverFactory()
|
fileArchiverFactory = MockFileArchiverFactory()
|
||||||
fileArchiver = MockFileArchiver()
|
fileArchiver = MockFileArchiver()
|
||||||
fileArchiver.stubbedZipResult = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
fileArchiver.stubbedZipResult = zipPath
|
||||||
fileArchiverFactory.stubbedMakeFileArchiverResult = fileArchiver
|
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() {
|
override func tearDown() {
|
||||||
|
@ -33,6 +42,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
cloudClient = nil
|
cloudClient = nil
|
||||||
fileArchiver = nil
|
fileArchiver = nil
|
||||||
fileArchiverFactory = nil
|
fileArchiverFactory = nil
|
||||||
|
fileClient = nil
|
||||||
|
zipPath = nil
|
||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +54,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
typealias ResponseType = CloudResponse<CloudHEADResponse>
|
typealias ResponseType = CloudResponse<CloudHEADResponse>
|
||||||
typealias ErrorType = CloudHEADResponseError
|
typealias ErrorType = CloudHEADResponseError
|
||||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: 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
|
// When
|
||||||
let result = subject.exists(hash: "acho tio", config: config)
|
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 cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||||
let httpResponse: HTTPURLResponse = .test(statusCode: 500)
|
let httpResponse: HTTPURLResponse = .test(statusCode: 500)
|
||||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
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
|
// When
|
||||||
let result = try subject.exists(hash: "acho tio", config: config)
|
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 cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||||
let httpResponse: HTTPURLResponse = .test()
|
let httpResponse: HTTPURLResponse = .test()
|
||||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
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
|
// When
|
||||||
let result = try subject.exists(hash: "acho tio", config: config)
|
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 cloudResponse = ResponseType(status: "shaki", data: CloudHEADResponse())
|
||||||
let httpResponse: HTTPURLResponse = .test(statusCode: 202)
|
let httpResponse: HTTPURLResponse = .test(statusCode: 202)
|
||||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
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
|
// When
|
||||||
let result = try subject.exists(hash: "acho tio", config: config)
|
let result = try subject.exists(hash: "acho tio", config: config)
|
||||||
|
@ -124,7 +135,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
typealias ErrorType = CloudResponseError
|
typealias ErrorType = CloudResponseError
|
||||||
let expectedError: ErrorType = .test()
|
let expectedError: ErrorType = .test()
|
||||||
let cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: expectedError)
|
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
|
// When
|
||||||
let result = subject.fetch(hash: "acho tio", config: config)
|
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
|
// Given
|
||||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||||
typealias ErrorType = CloudResponseError
|
typealias ErrorType = CloudResponseError
|
||||||
|
@ -151,15 +162,125 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
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
|
// 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()
|
.toBlocking()
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
// Then
|
// 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
|
// - store
|
||||||
|
@ -171,7 +292,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
let expectedError = CloudResponseError.test()
|
let expectedError = CloudResponseError.test()
|
||||||
let cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForError(error: expectedError)
|
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
|
// When
|
||||||
let result = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
let result = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
||||||
|
@ -201,7 +322,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
object: cloudResponse,
|
object: cloudResponse,
|
||||||
response: .test()
|
response: .test()
|
||||||
)
|
)
|
||||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
_ = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
_ = subject.store(hash: "acho tio", config: config, xcframeworkPath: .root)
|
||||||
|
@ -209,7 +330,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
.materialize()
|
.materialize()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
if let tuple = fileClient.invokedUploadParameters {
|
||||||
XCTAssertEqual(tuple.url, url)
|
XCTAssertEqual(tuple.url, url)
|
||||||
} else {
|
} else {
|
||||||
XCTFail("Could not unwrap the file uploader input tuple")
|
XCTFail("Could not unwrap the file uploader input tuple")
|
||||||
|
@ -228,7 +349,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
object: cloudResponse,
|
object: cloudResponse,
|
||||||
response: .test()
|
response: .test()
|
||||||
)
|
)
|
||||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
||||||
|
@ -236,7 +357,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
.materialize()
|
.materialize()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
if let tuple = fileClient.invokedUploadParameters {
|
||||||
XCTAssertEqual(tuple.hash, hash)
|
XCTAssertEqual(tuple.hash, hash)
|
||||||
} else {
|
} else {
|
||||||
XCTFail("Could not unwrap the file uploader input tuple")
|
XCTFail("Could not unwrap the file uploader input tuple")
|
||||||
|
@ -256,9 +377,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
response: .test()
|
response: .test()
|
||||||
)
|
)
|
||||||
|
|
||||||
let path = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
fileArchiver.stubbedZipResult = zipPath
|
||||||
fileArchiver.stubbedZipResult = path
|
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||||
subject = CacheRemoteStorage(cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileUploader: mockFileUploader)
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
_ = subject.store(hash: hash, config: config, xcframeworkPath: .root)
|
||||||
|
@ -266,8 +386,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
||||||
.materialize()
|
.materialize()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
if let tuple = mockFileUploader.invokedUploadParameters {
|
if let tuple = fileClient.invokedUploadParameters {
|
||||||
XCTAssertEqual(tuple.file, path)
|
XCTAssertEqual(tuple.file, zipPath)
|
||||||
} else {
|
} else {
|
||||||
XCTFail("Could not unwrap the file uploader input tuple")
|
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 {
|
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 {}
|
func inTemporaryDirectory(_: (AbsolutePath) throws -> Void) throws {}
|
||||||
|
@ -373,6 +373,10 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
||||||
func fileSize(path _: AbsolutePath) throws -> UInt64 {
|
func fileSize(path _: AbsolutePath) throws -> UInt64 {
|
||||||
0
|
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)
|
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)?
|
var loadStub: ((AbsolutePath) throws -> Graph)?
|
||||||
func load(path: AbsolutePath) throws -> Graph {
|
func load(path: AbsolutePath) throws -> Graph {
|
||||||
if let loadStub = loadStub {
|
if let loadStub = loadStub {
|
||||||
|
|
|
@ -85,6 +85,20 @@ final class FileHandlerTests: TuistUnitTestCase {
|
||||||
XCTAssertEqual(result, "NWY0YmVjMTkyZDBmMTg4NGZkY2Y0OTc1YjM3MDY3ZGM=")
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func countItemsInRootTempDirectory(appropriateFor url: URL) throws -> Int {
|
private func countItemsInRootTempDirectory(appropriateFor url: URL) throws -> Int {
|
||||||
|
|
Loading…
Reference in New Issue