mirror of https://github.com/cirruslabs/tart.git
172 lines
6.1 KiB
Swift
172 lines
6.1 KiB
Swift
import Foundation
|
|
import XCTest
|
|
@testable import tart
|
|
|
|
final class ContentStoreTests: XCTestCase {
|
|
func testCreatesDigestDirectoryDuringInitialization() throws {
|
|
let store = try temporaryStore()
|
|
let contentURL = try store.contentURL(for: Digest.hash(Data()))
|
|
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: contentURL.deletingLastPathComponent().path))
|
|
}
|
|
|
|
func testInstallAndValidatedLookup() throws {
|
|
let store = try temporaryStore()
|
|
let data = Data("base disk".utf8)
|
|
let digest = Digest.hash(data)
|
|
let temporaryURL = try store.temporaryContentURL(for: digest)
|
|
try data.write(to: temporaryURL)
|
|
|
|
let installedURL = try store.install(temporaryURL, contentDigest: digest)
|
|
|
|
XCTAssertEqual(installedURL, try store.contentURL(for: digest))
|
|
XCTAssertEqual(try store.existingContentURL(for: digest), installedURL)
|
|
}
|
|
|
|
func testCorruptCacheEntryIsMiss() throws {
|
|
let store = try temporaryStore()
|
|
let expectedDigest = Digest.hash(Data("expected".utf8))
|
|
let contentURL = try store.contentURL(for: expectedDigest)
|
|
try FileManager.default.createDirectory(at: contentURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("corrupt".utf8).write(to: contentURL)
|
|
|
|
XCTAssertNil(try store.existingContentURL(for: expectedDigest))
|
|
}
|
|
|
|
func testResumableAndLockURLsAreStablePerDigest() throws {
|
|
let store = try temporaryStore()
|
|
let firstDigest = Digest.hash(Data("first".utf8))
|
|
let secondDigest = Digest.hash(Data("second".utf8))
|
|
|
|
XCTAssertEqual(
|
|
try store.resumableContentURL(for: firstDigest),
|
|
try store.resumableContentURL(for: firstDigest)
|
|
)
|
|
XCTAssertNotEqual(
|
|
try store.resumableContentURL(for: firstDigest),
|
|
try store.resumableContentURL(for: secondDigest)
|
|
)
|
|
XCTAssertEqual(
|
|
try store.lockURL(for: firstDigest),
|
|
try store.lockURL(for: firstDigest)
|
|
)
|
|
XCTAssertTrue(FileManager.default.fileExists(atPath: try store.lockURL(for: firstDigest).path))
|
|
}
|
|
|
|
func testInstallReplacesCorruptEntry() throws {
|
|
let store = try temporaryStore()
|
|
let data = Data("expected".utf8)
|
|
let digest = Digest.hash(data)
|
|
let contentURL = try store.contentURL(for: digest)
|
|
try FileManager.default.createDirectory(at: contentURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("corrupt".utf8).write(to: contentURL)
|
|
let temporaryURL = try store.temporaryContentURL(for: digest)
|
|
try data.write(to: temporaryURL)
|
|
|
|
XCTAssertEqual(try store.install(temporaryURL, contentDigest: digest), contentURL)
|
|
XCTAssertEqual(try Digest.hash(contentURL), digest)
|
|
}
|
|
|
|
func testInstallPreservesExistingValidEntry() throws {
|
|
let store = try temporaryStore()
|
|
let data = Data("expected".utf8)
|
|
let digest = Digest.hash(data)
|
|
let firstTemporaryURL = try store.temporaryContentURL(for: digest)
|
|
try data.write(to: firstTemporaryURL)
|
|
let installedURL = try store.install(firstTemporaryURL, contentDigest: digest)
|
|
let secondTemporaryURL = try store.temporaryContentURL(for: digest)
|
|
try data.write(to: secondTemporaryURL)
|
|
|
|
XCTAssertEqual(try store.install(secondTemporaryURL, contentDigest: digest), installedURL)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: secondTemporaryURL.path))
|
|
XCTAssertEqual(try Digest.hash(installedURL), digest)
|
|
}
|
|
|
|
func testConcurrentInstallsAcceptDigestValidWinner() throws {
|
|
try assertConcurrentInstalls(seedCorruptEntry: false)
|
|
}
|
|
|
|
func testConcurrentInstallsRepairCorruptEntry() throws {
|
|
try assertConcurrentInstalls(seedCorruptEntry: true)
|
|
}
|
|
|
|
func testInstallRejectsWrongContentDigest() throws {
|
|
let store = try temporaryStore()
|
|
let expectedDigest = Digest.hash(Data("expected".utf8))
|
|
let temporaryURL = try store.temporaryContentURL(for: expectedDigest)
|
|
try Data("actual".utf8).write(to: temporaryURL)
|
|
|
|
XCTAssertThrowsError(try store.install(temporaryURL, contentDigest: expectedDigest)) { error in
|
|
guard case ContentStoreError.contentDigestMismatch(let expected, _) = error else {
|
|
return XCTFail("unexpected error: \(error)")
|
|
}
|
|
|
|
XCTAssertEqual(expected, expectedDigest)
|
|
}
|
|
}
|
|
|
|
func testRejectsNonCanonicalDigest() throws {
|
|
let store = try temporaryStore()
|
|
|
|
XCTAssertThrowsError(try store.contentURL(for: "sha256:ABC")) { error in
|
|
XCTAssertEqual(error as? ContentStoreError, .invalidContentDigest("sha256:ABC"))
|
|
}
|
|
}
|
|
|
|
private func temporaryStore() throws -> ContentStore {
|
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
addTeardownBlock {
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
return try ContentStore(baseURL: url)
|
|
}
|
|
|
|
private func assertConcurrentInstalls(seedCorruptEntry: Bool) throws {
|
|
let store = try temporaryStore()
|
|
let data = Data("expected".utf8)
|
|
let digest = Digest.hash(data)
|
|
let contentURL = try store.contentURL(for: digest)
|
|
try FileManager.default.createDirectory(at: contentURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
if seedCorruptEntry {
|
|
try Data("corrupt".utf8).write(to: contentURL)
|
|
}
|
|
|
|
let temporaryURLs = try (0..<16).map { _ in
|
|
let url = try store.temporaryContentURL(for: digest)
|
|
try data.write(to: url)
|
|
return url
|
|
}
|
|
let errors = ErrorCollector()
|
|
|
|
DispatchQueue.concurrentPerform(iterations: temporaryURLs.count) { index in
|
|
do {
|
|
_ = try store.install(temporaryURLs[index], contentDigest: digest)
|
|
} catch {
|
|
errors.append(error)
|
|
}
|
|
}
|
|
|
|
XCTAssertTrue(errors.values.isEmpty, "unexpected install errors: \(errors.values)")
|
|
XCTAssertEqual(try Digest.hash(contentURL), digest)
|
|
XCTAssertTrue(temporaryURLs.allSatisfy { !FileManager.default.fileExists(atPath: $0.path) })
|
|
}
|
|
|
|
private final class ErrorCollector: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var errors: [Error] = []
|
|
|
|
var values: [Error] {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return errors
|
|
}
|
|
|
|
func append(_ error: Error) {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
errors.append(error)
|
|
}
|
|
}
|
|
}
|