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