[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:
Romain Boulay 2020-06-30 19:43:34 +02:00 committed by GitHub
parent c03381ebfa
commit 223395c469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 449 additions and 139 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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