110 lines
4.7 KiB
Swift
110 lines
4.7 KiB
Swift
|
|
import XCTest
|
||
|
|
import Foundation
|
||
|
|
@testable import ForgeOffload
|
||
|
|
|
||
|
|
final class CopyEngineTests: XCTestCase {
|
||
|
|
|
||
|
|
var root: String!
|
||
|
|
var source: String!
|
||
|
|
var dest1: String!
|
||
|
|
var dest2: String!
|
||
|
|
|
||
|
|
override func setUpWithError() throws {
|
||
|
|
root = NSTemporaryDirectory() + "forge-copy-\(UUID().uuidString)"
|
||
|
|
source = root + "/CARD_A001"
|
||
|
|
dest1 = root + "/RAID1/A001"
|
||
|
|
dest2 = root + "/SHUTTLE/A001"
|
||
|
|
try FileManager.default.createDirectory(atPath: source + "/CLIPS", withIntermediateDirectories: true)
|
||
|
|
|
||
|
|
// Source tree: two clips + sidecar.
|
||
|
|
try makeFile(source + "/CLIPS/A001C001.mov", size: 200_000, seed: 1)
|
||
|
|
try makeFile(source + "/CLIPS/A001C002.mov", size: 350_000, seed: 2)
|
||
|
|
try makeFile(source + "/A001.xml", size: 512, seed: 3)
|
||
|
|
}
|
||
|
|
|
||
|
|
override func tearDown() {
|
||
|
|
try? FileManager.default.removeItem(atPath: root)
|
||
|
|
}
|
||
|
|
|
||
|
|
func makeFile(_ path: String, size: Int, seed: UInt64) throws {
|
||
|
|
var data = Data(capacity: size)
|
||
|
|
var s = seed
|
||
|
|
while data.count < size {
|
||
|
|
s = s &* 6364136223846793005 &+ 1442695040888963407
|
||
|
|
withUnsafeBytes(of: s) { data.append(contentsOf: $0.prefix(min(8, size - data.count))) }
|
||
|
|
}
|
||
|
|
try data.write(to: URL(fileURLWithPath: path))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Copy to two destinations: byte-identical, verified, report per file.
|
||
|
|
func testCopyTwoDestinationsVerified() throws {
|
||
|
|
let engine = CopyEngine()
|
||
|
|
let report = try engine.offload(source: source, destinations: [dest1, dest2])
|
||
|
|
|
||
|
|
XCTAssertEqual(report.files.count, 3)
|
||
|
|
XCTAssertTrue(report.files.allSatisfy { $0.verified })
|
||
|
|
|
||
|
|
for dest in [dest1!, dest2!] {
|
||
|
|
for rel in ["CLIPS/A001C001.mov", "CLIPS/A001C002.mov", "A001.xml"] {
|
||
|
|
let srcHash = try FileHasher.xxh64(path: source + "/" + rel)
|
||
|
|
let dstHash = try FileHasher.xxh64(path: dest + "/" + rel)
|
||
|
|
XCTAssertEqual(srcHash, dstHash, "hash mismatch for \(rel) in \(dest)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Report carries source hashes.
|
||
|
|
XCTAssertEqual(Set(report.files.map(\.relativePath)),
|
||
|
|
["CLIPS/A001C001.mov", "CLIPS/A001C002.mov", "A001.xml"])
|
||
|
|
XCTAssertTrue(report.files.allSatisfy { $0.xxh64.count == 16 })
|
||
|
|
}
|
||
|
|
|
||
|
|
// Existing identical file at destination: skipped, still verified.
|
||
|
|
func testExistingIdenticalSkipped() throws {
|
||
|
|
let engine = CopyEngine()
|
||
|
|
_ = try engine.offload(source: source, destinations: [dest1])
|
||
|
|
// Second run: all files exist + match.
|
||
|
|
let report2 = try engine.offload(source: source, destinations: [dest1])
|
||
|
|
XCTAssertTrue(report2.files.allSatisfy { $0.skipped })
|
||
|
|
XCTAssertTrue(report2.files.allSatisfy { $0.verified })
|
||
|
|
}
|
||
|
|
|
||
|
|
// Existing file with different content: hard error (never silently overwrite).
|
||
|
|
func testConflictErrors() throws {
|
||
|
|
let engine = CopyEngine()
|
||
|
|
_ = try engine.offload(source: source, destinations: [dest1])
|
||
|
|
// Tamper destination clip.
|
||
|
|
try makeFile(dest1 + "/CLIPS/A001C001.mov", size: 200_000, seed: 99)
|
||
|
|
XCTAssertThrowsError(try engine.offload(source: source, destinations: [dest1])) { error in
|
||
|
|
guard case OffloadError.destinationConflict = error else {
|
||
|
|
return XCTFail("wrong error: \(error)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Partial file (interrupted copy simulation): re-copied, ends verified.
|
||
|
|
func testPartialFileRecopied() throws {
|
||
|
|
let engine = CopyEngine()
|
||
|
|
// Simulate interrupted copy: partial temp content at final path with wrong size.
|
||
|
|
try FileManager.default.createDirectory(atPath: dest1 + "/CLIPS", withIntermediateDirectories: true)
|
||
|
|
try makeFile(dest1 + "/CLIPS/A001C001.mov", size: 50_000, seed: 1) // truncated
|
||
|
|
let report = try engine.offload(source: source, destinations: [dest1], overwritePartial: true)
|
||
|
|
let entry = report.files.first { $0.relativePath == "CLIPS/A001C001.mov" }!
|
||
|
|
XCTAssertFalse(entry.skipped)
|
||
|
|
XCTAssertTrue(entry.verified)
|
||
|
|
let srcHash = try FileHasher.xxh64(path: source + "/CLIPS/A001C001.mov")
|
||
|
|
XCTAssertEqual(try FileHasher.xxh64(path: dest1 + "/CLIPS/A001C001.mov"), srcHash)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Missing source dir throws.
|
||
|
|
func testMissingSourceThrows() {
|
||
|
|
XCTAssertThrowsError(try CopyEngine().offload(source: root + "/nope", destinations: [dest1]))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Report totals.
|
||
|
|
func testReportTotals() throws {
|
||
|
|
let report = try CopyEngine().offload(source: source, destinations: [dest1])
|
||
|
|
XCTAssertEqual(report.totalBytes, 200_000 + 350_000 + 512)
|
||
|
|
XCTAssertEqual(report.destinations, [dest1])
|
||
|
|
XCTAssertGreaterThan(report.duration, 0)
|
||
|
|
}
|
||
|
|
}
|