Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion BitMatch-iPadTests/TransferDetailsReportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ struct TransferDetailsReportTests {
#expect(object["operatorName"] as? String == record.reportSettings.operatorName)
#expect(object["notes"] as? String == record.reportSettings.notes)
#expect(object["state"] as? String == LocalTransferState.issues.rawValue)
let scope = try #require(object["verificationScope"] as? [String: Any])
#expect(scope["selectedMode"] as? String == "Standard")
#expect(scope["recordedVerifiedResults"] as? Int == 0)
#expect(scope["recordedSelectedResults"] as? Int == 1)
#expect(scope["configuredChecks"] == nil)
#expect(object["selectedFilePlanRecorded"] as? Bool == false)
let csv = String(decoding: try TransferHistoryDocument(record: record, asCSV: true).data, as: UTF8.self)
#expect(csv.contains("\"operatorName\",\"Alex, \"\"DIT\"\"\""))
#expect(csv.contains(record.reportSettings.notes))
#expect(csv.contains("Original selected-file plan was not saved; export describes recorded results only"))
let pdfData = try await ReportExporter.historyPDF(record: record, includeThumbnails: false)
let pdf = try #require(PDFDocument(data: pdfData))
if let path = ProcessInfo.processInfo.environment["BITMATCH_WORKFLOW_SNAPSHOT_OUTPUT_DIR"]
Expand All @@ -37,6 +44,11 @@ struct TransferDetailsReportTests {
#expect(text.contains("Operator: Alex"))
#expect(text.contains(record.reportSettings.notes))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(text.contains("1 recorded source file · 1 issue"))
#expect(!text.contains("Empty"))
#expect(!text.contains("Check plan:"))
#expect(!text.contains("no separate card reread"))
#expect(text.contains("Original selected-file plan was not saved"))
#expect(record.state == .issues)
}

Expand All @@ -48,17 +60,43 @@ struct TransferDetailsReportTests {
destinations: [try LocalTransferResource(url: root)], verificationMode: .standard,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .issues; record.summary = "Selected files verified"
record.plannedFileCounts = [1]
record.results = [ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "fixture", destination: "Backup"),
ResultRow(path: "/card/._clip.mov", status: ResultOutcome.excludedAppleDouble.statusText,
size: 42, checksum: nil, destination: "Backup")]
let document = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(document.string)
#expect(text.contains("0 file issues"))
#expect(text.contains("0 issues"))
#expect(!text.contains("Issues Detail"))
#expect(text.contains("1 intentionally excluded result"))
#expect(text.contains("Check plan:"))
#expect(text.contains("AppleDouble metadata was intentionally excluded"))
#expect(text.contains("Saved: 1/1 selected backup results marked verified"))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(!text.contains("Selected files verified"))
#expect(!text.contains("VERIFICATION SUCCESSFUL"))
}

@Test func completedPartialPlanStaysUnsafeInHistoryPDF() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent("ipad-completed-partial-report-\(UUID())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
var record = LocalTransferRecord(id: UUID(), createdAt: Date(), source: try LocalTransferResource(url: root),
destinations: [try LocalTransferResource(url: root)], verificationMode: .standard,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .completed
record.summary = "Transfer complete"
record.plannedFileCounts = [2]
record.results = [ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "fixture", destination: "Backup",
destinationPath: root.appendingPathComponent("clip.mov").path)]
#expect(TransferLibraryPresentation.safetyState(for: record) == .needsAttention)
let pdf = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(pdf.string)
#expect(text.contains("Saved results do not cover the selected-file plan"))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(!text.contains("Transfer complete"))
#expect(!text.contains("VERIFICATION SUCCESSFUL"))
}

Expand Down
53 changes: 53 additions & 0 deletions BitMatchTests/ReportExporterPDFTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Testing
import PDFKit
@testable import BitMatch
import BitMatchEngine

Expand Down Expand Up @@ -53,4 +54,56 @@ struct ReportExporterPDFTests {
#expect(files.contains { $0.pathExtension == "csv" })
#expect(files.contains { $0.pathExtension == "json" })
}

@Test
func automaticReportSeparatesSelectedChecksFromRecordedResults() async throws {
let fm = FileManager.default
let root = fm.temporaryDirectory.appendingPathComponent("bitmatch-assurance-report-\(UUID())", isDirectory: true)
let destination = root.appendingPathComponent("Backup", isDirectory: true)
try fm.createDirectory(at: destination, withIntermediateDirectories: true)
defer { try? fm.removeItem(at: root) }

let rows = [
ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "digest", destination: destination.path),
ResultRow(path: "/card/other.mov", status: ResultOutcome.failed.statusText,
size: 5, checksum: nil, destination: destination.path),
ResultRow(path: "/card/._clip.mov", status: ResultOutcome.excludedAppleDouble.statusText,
size: 1, checksum: nil, destination: destination.path)
]
try await ReportExporter.export(
mode: .copyAndVerify, jobID: UUID(), started: Date(timeIntervalSince1970: 1_800_000_000),
finished: Date(timeIntervalSince1970: 1_800_000_060), sourceURL: URL(fileURLWithPath: "/card"),
destinationURLs: [destination], results: rows, fileCount: rows.count, matchCount: 1,
prefs: ReportPrefs(verificationMode: .standard, makeReport: true), workers: 1,
totalBytesProcessed: 10, safetyState: .needsAttention
)
let reports = destination.appendingPathComponent("Reports", isDirectory: true)
let files = try fm.contentsOfDirectory(at: reports, includingPropertiesForKeys: nil)
let pdfURL = try #require(files.first { $0.pathExtension == "pdf" })
let pdf = try #require(PDFDocument(data: Data(contentsOf: pdfURL)))
let text = try #require(pdf.string)
#expect(text.contains("Check plan:"))
#expect(text.contains("no separate card reread"))
#expect(text.contains("Saved: 1/2 selected backup results marked verified"))
#expect(text.contains("NOT SAFE TO ERASE"))

let csvURL = try #require(files.first { $0.pathExtension == "csv" })
let csv = try String(contentsOf: csvURL, encoding: .utf8)
#expect(csv.contains("Recorded Verified Backup Results,1"))
#expect(csv.contains("Recorded Excluded Source Files,1"))
#expect(csv.contains("selected mode does not prove every check finished"))

let jsonURL = try #require(files.first { $0.pathExtension == "json" })
let json = try Data(contentsOf: jsonURL)
let object = try #require(JSONSerialization.jsonObject(with: json) as? [String: Any])
let verification = try #require(object["verification"] as? [String: Any])
let scope = try #require(verification["scope"] as? [String: Any])
#expect(scope["selectedMode"] as? String == "Standard")
#expect(scope["recordedSelectedResults"] as? Int == 2)
#expect(scope["recordedVerifiedResults"] as? Int == 1)
#expect(scope["recordedExcludedSourceFiles"] as? Int == 1)
#expect(object["safeToErase"] as? Bool == false)
#expect(EvidenceReader.snapshot(reportData: json, reportURL: jsonURL) != nil)
}
}
104 changes: 103 additions & 1 deletion BitMatchTests/TransferDetailsReportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ struct TransferDetailsReportTests {
#expect(object["operatorName"] as? String == record.reportSettings.operatorName)
#expect(object["notes"] as? String == record.reportSettings.notes)
#expect(object["state"] as? String == LocalTransferState.issues.rawValue)
let scope = try #require(object["verificationScope"] as? [String: Any])
#expect(scope["selectedMode"] as? String == "Standard")
#expect(scope["recordedVerifiedResults"] as? Int == 0)
#expect(scope["recordedSelectedResults"] as? Int == 1)
#expect(scope["configuredChecks"] == nil)
#expect(object["selectedFilePlanRecorded"] as? Bool == false)
let csv = String(decoding: try TransferHistoryDocument(record: record, asCSV: true).data, as: UTF8.self)
#expect(csv.contains("\"operatorName\",\"Alex, \"\"DIT\"\"\""))
#expect(csv.contains(record.reportSettings.notes))
#expect(csv.contains("Original selected-file plan was not saved; export describes recorded results only"))
let pdfData = try await ReportExporter.historyPDF(record: record, includeThumbnails: false)
let pdf = try #require(PDFDocument(data: pdfData))
if let path = ProcessInfo.processInfo.environment["BITMATCH_WORKFLOW_SNAPSHOT_OUTPUT_DIR"]
Expand All @@ -37,6 +44,11 @@ struct TransferDetailsReportTests {
#expect(text.contains("Operator: Alex"))
#expect(text.contains(record.reportSettings.notes))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(text.contains("1 recorded source file · 1 issue"))
#expect(!text.contains("Empty"))
#expect(!text.contains("Check plan:"))
#expect(!text.contains("no separate card reread"))
#expect(text.contains("Original selected-file plan was not saved"))
#expect(record.state == .issues)
}

Expand All @@ -48,17 +60,107 @@ struct TransferDetailsReportTests {
destinations: [try LocalTransferResource(url: root)], verificationMode: .standard,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .issues; record.summary = "Selected files verified"
record.plannedFileCounts = [1]
record.results = [ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "fixture", destination: "Backup"),
ResultRow(path: "/card/._clip.mov", status: ResultOutcome.excludedAppleDouble.statusText,
size: 42, checksum: nil, destination: "Backup")]
let document = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(document.string)
#expect(text.contains("0 file issues"))
#expect(text.contains("0 issues"))
#expect(!text.contains("Issues Detail"))
#expect(text.contains("1 intentionally excluded result"))
#expect(text.contains("Check plan:"))
#expect(text.contains("AppleDouble metadata was intentionally excluded"))
#expect(text.contains("Saved: 1/1 selected backup results marked verified"))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(!text.contains("Selected files verified"))
#expect(!text.contains("VERIFICATION SUCCESSFUL"))
}

@Test func legacyQuickMatchCannotBecomeVerifiedInHistoryPDF() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent("quick-legacy-report-\(UUID())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
var record = LocalTransferRecord(id: UUID(), createdAt: Date(), source: try LocalTransferResource(url: root),
destinations: [try LocalTransferResource(url: root)], verificationMode: .quick,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .issues
record.summary = "Selected files verified"
record.plannedFileCounts = [1]
record.results = [ResultRow(path: "/card/clip.mov", status: "✅ Match", size: 5,
checksum: nil, destination: "Backup"),
ResultRow(path: "/card/._clip.mov", status: ResultOutcome.excludedAppleDouble.statusText,
size: 42, checksum: nil, destination: "Backup")]
let pdf = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(pdf.string)
#expect(text.contains("Selected Quick"))
#expect(text.contains("Size checks only; file contents are not verified"))
#expect(text.contains("Saved: 0/1 selected backup results marked verified"))
#expect(text.contains("Quick mode did not verify file contents"))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(!text.contains("Selected files verified"))
#expect(!text.contains("VERIFICATION SUCCESSFUL"))
let json = try #require(JSONSerialization.jsonObject(with: TransferHistoryDocument(record: record, asCSV: false).data) as? [String: Any])
#expect(json["summary"] as? String == "Selected files verified")
let csv = String(decoding: try TransferHistoryDocument(record: record, asCSV: true).data, as: UTF8.self)
#expect(csv.contains("Selected files verified"))
}

@Test func quickHistoryRowLabelDoesNotRewriteExportedEvidence() throws {
let legacy = ResultRow(path: "/card/clip.mov", status: "✅ Match", size: 5,
checksum: nil, destination: "Backup")
let failed = ResultRow(path: "/card/bad.mov", status: ResultOutcome.failed.statusText,
size: 5, checksum: nil, destination: "Backup")
#expect(TransferHistoryResultPresentation.statusText(for: legacy, mode: .quick) == "Copied · contents not verified")
#expect(TransferHistoryResultPresentation.statusText(for: legacy, mode: .standard) == "✅ Match")
#expect(TransferHistoryResultPresentation.statusText(for: failed, mode: .quick) == failed.status)
#expect(legacy.status == "✅ Match")
}

@Test func partialSelectedPlanDoesNotClaimAllFilesVerifiedInHistoryPDF() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent("partial-plan-report-\(UUID())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
var record = LocalTransferRecord(id: UUID(), createdAt: Date(), source: try LocalTransferResource(url: root),
destinations: [try LocalTransferResource(url: root)], verificationMode: .standard,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .issues
record.summary = "Incomplete attempt"
record.plannedFileCounts = [2]
record.results = [ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "fixture", destination: "Backup"),
ResultRow(path: "/card/._clip.mov", status: ResultOutcome.excludedAppleDouble.statusText,
size: 42, checksum: nil, destination: "Backup")]
let pdf = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(pdf.string)
#expect(text.contains("Saved: 1/1 selected backup results marked verified"))
#expect(text.contains("Recorded selected results are marked verified"))
#expect(!text.contains("Selected files verified"))
#expect(text.contains("NOT SAFE TO ERASE"))
}

@Test func completedPartialPlanStaysUnsafeInHistoryAndPDF() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent("completed-partial-report-\(UUID())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
var record = LocalTransferRecord(id: UUID(), createdAt: Date(), source: try LocalTransferResource(url: root),
destinations: [try LocalTransferResource(url: root)], verificationMode: .standard,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
record.state = .completed
record.summary = "Transfer complete"
record.plannedFileCounts = [2]
record.results = [ResultRow(path: "/card/clip.mov", status: ResultOutcome.verified.statusText,
size: 5, checksum: "fixture", destination: "Backup",
destinationPath: root.appendingPathComponent("clip.mov").path)]
#expect(TransferLibraryPresentation.savedResultsCoverSelectedPlan(record) == false)
#expect(TransferLibraryPresentation.safetyState(for: record) == .needsAttention)
#expect(TransferLibraryPresentation.stateLabel(for: record).tint != .green)
let pdf = try #require(PDFDocument(data: try await ReportExporter.historyPDF(record: record, includeThumbnails: false)))
let text = try #require(pdf.string)
#expect(text.contains("Saved results do not cover the selected-file plan"))
#expect(text.contains("NOT SAFE TO ERASE"))
#expect(!text.contains("Transfer complete"))
#expect(!text.contains("VERIFICATION SUCCESSFUL"))
}

Expand Down
Loading
Loading