Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
19e4f80
Check explicitly recorded metadata files against saved hashes
mikecerisano Oct 20, 2026
59282ce
Test mounted-drive recovery and filesystem faults
mikecerisano Oct 20, 2026
5396f6c
Exercise mounted drive pairs and saved-history handoffs
mikecerisano Oct 20, 2026
c09ced8
Keep report names and identity within page layouts
mikecerisano Oct 20, 2026
c6b9bfa
Preserve full filenames when wrapping Master summaries
mikecerisano Oct 20, 2026
38eb518
Exercise mounted workflow evidence and recovery
mikecerisano Oct 20, 2026
30888a5
Add an opt-in physical-drive validation runner
mikecerisano Oct 20, 2026
dbf4b68
Accept selected removable SD readers in drive validation
mikecerisano Oct 20, 2026
3e03d62
Generate mounted workflow clip fixtures off MainActor
mikecerisano Oct 20, 2026
ef7cd05
Add portable transfer settings with an import preview
mikecerisano Oct 20, 2026
1d72c53
Show reported USB 2 connections in Advanced
mikecerisano Oct 20, 2026
5360acd
Resolve selected backup folders before reading USB connection facts
mikecerisano Oct 20, 2026
0a18806
Add opt-in physical workflow evidence checks
mikecerisano Oct 20, 2026
df43d6f
Require requested matrix tests to execute
mikecerisano Oct 20, 2026
888ae45
Keep tall Mac Settings panes fully reachable
mikecerisano Oct 20, 2026
ae5eeb9
Retain workflow fixtures when assertions or settlement fail
mikecerisano Oct 20, 2026
e482faf
Resolve selected folders to their physical backup disk
mikecerisano Oct 20, 2026
e473944
Preserve destination progress through MHL and reports
mikecerisano Oct 20, 2026
3325968
Add attributable classic MHL interoperability fixtures
mikecerisano Oct 20, 2026
a4c2c07
Make coordinator workflow fixtures explicit about disk identity
mikecerisano Oct 20, 2026
6ed8a2b
Clarify where automatic transfer reports are saved
mikecerisano Oct 20, 2026
583dcbc
Validate filesystem matrix execution and safe cleanup
mikecerisano Oct 20, 2026
5343e4f
Keep Mac transfer settings reviews compact and scrollable
mikecerisano Oct 20, 2026
4f14888
Bound diagnostic export reads and report rejected inputs
mikecerisano Oct 20, 2026
bd43a81
Align the guide and hardware checks with saved evidence rules
mikecerisano Oct 20, 2026
9669609
Add read-only counters for marked test drives
mikecerisano Oct 20, 2026
763d8e8
Record transfer windows for physical device counter checks
mikecerisano Oct 20, 2026
29e0fa8
Add opt-in physical transfer and large-file stress coverage
mikecerisano Oct 20, 2026
4106a48
Bound buffer benchmark workers and trace their phases
mikecerisano Oct 20, 2026
97d773e
Record overnight validation coverage and remaining limits
mikecerisano Oct 20, 2026
bb2c991
test: bound memory during 4K report preview decoding
mikecerisano Oct 20, 2026
c28c7c2
Record translated x86 engine validation and limits
mikecerisano Oct 20, 2026
d3df29b
Update local validation evidence and remaining release gates
mikecerisano Oct 20, 2026
84445c2
fix: keep PDF identity in the page footer
mikecerisano Oct 20, 2026
77061e1
Record integrated PDF and mobile regression results
mikecerisano Oct 20, 2026
5fb59a6
Test transfer recovery after worker process death
mikecerisano Oct 20, 2026
3c28470
Record process interruption and signed build validation
mikecerisano Oct 20, 2026
ae94101
test: strengthen seeded soak evidence and worker memory guard
mikecerisano Oct 20, 2026
1beed21
Index partial transfer results without changing row matching
mikecerisano Oct 20, 2026
548a129
Paginate long Master Report fields without clipping
mikecerisano Oct 20, 2026
383918a
Exercise additional 4K codecs and repeated PDF reports
mikecerisano Oct 20, 2026
79f0748
Isolate diagnostic assertions from concurrent app runs
mikecerisano Oct 20, 2026
039e64e
Exercise live coordinator memory with large file inventories
mikecerisano Oct 20, 2026
058d6bd
Observe queue completion transitions without progress churn
mikecerisano Oct 20, 2026
89187c5
Keep memory-probe cancellation scoped to active work
mikecerisano Oct 20, 2026
2c3ac90
Track repeated completed-queue summary work
mikecerisano Oct 20, 2026
7c7c326
Record integrated app and memory validation
mikecerisano Oct 20, 2026
0d62cbc
test: cover exact FAT32 single-file boundaries
mikecerisano Oct 20, 2026
43cd675
Record large physical transfer and FAT32 boundary results
mikecerisano Oct 20, 2026
216f37a
Record stopped app benchmark setup and retry coverage limits
mikecerisano Oct 20, 2026
32d62c7
Clarify diagnostic limits and Mac drive-check scope
mikecerisano Oct 20, 2026
953c779
Track archive-based updater requirements follow-up
mikecerisano Oct 20, 2026
b34ea51
Record bounded History scaling checks
mikecerisano Oct 20, 2026
423aab9
Preserve legacy row identity in History optimization plans
mikecerisano Oct 20, 2026
e828463
Handle vanished metadata companions during test cleanup
mikecerisano Oct 20, 2026
6aef644
Record completed hardware validation and cleanup
mikecerisano Oct 20, 2026
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
4 changes: 3 additions & 1 deletion BitMatch-iPad/Views/ModularContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ struct SettingsSheetView: View {
.disabled(coordinator.isOperationInProgress || coordinator.isReplayingQueuedTransfer)
}

TransferSettingsSection(coordinator: coordinator)

Section("Diagnostics") {
LocalCrashEvidenceSettingView(settings: generalSettings)
}
Expand Down Expand Up @@ -358,7 +360,7 @@ struct SettingsSheetView: View {
}

Section {
Text("A report is a record of what happened during a transfer, saved next to your destinations so you can hand it to anyone.")
Text("Transfer reports are saved in Reports on your first destination.")
.font(.footnote)
.foregroundColor(.secondary)
} header: {
Expand Down
108 changes: 108 additions & 0 deletions BitMatch-iPadTests/MasterReportPaginationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Foundation
import PDFKit
import Testing
@testable import BitMatch_iPad

@MainActor
struct MasterReportPaginationTests {
@Test(arguments: ["production", "client", "company"])
func oversizedMetadataPreservesAllTextAndSummary(field: String) async throws {
let value = String(repeating: "METADATAVALUE", count: 600) + " ENDMETADATA"
var configuration = SharedReportGenerationService.ReportConfiguration.default()
switch field {
case "production": configuration.production = value
case "client": configuration.client = value
default: configuration.company = value
}
let result = try await SharedReportGenerationService().generateMasterReport(
transfers: [transfer()], configuration: configuration)
let document = try #require(PDFDocument(data: result.pdfData))
#expect(document.pageCount > 1)
#expect(compact(document.string ?? "").contains(compact(value)))
#expect(document.string?.contains("Total Transfers:") == true)
#expect(document.string?.contains("Unknown Camera") == true)
let json = try #require(JSONSerialization.jsonObject(with: result.jsonData) as? [String: Any])
#expect(json[field] as? String == value)
try assertPrintable(document, configuration: configuration)
}

@Test
func multilineNotesContinueWithoutDroppingTheLastLine() async throws {
var configuration = SharedReportGenerationService.ReportConfiguration.default()
configuration.productionNotes = (0..<200).map { "NOTE\($0) preserved production note" }.joined(separator: "\n") + "\nENDNOTES"
let result = try await SharedReportGenerationService().generateMasterReport(
transfers: [transfer()], configuration: configuration)
let document = try #require(PDFDocument(data: result.pdfData))
#expect(document.pageCount > 2)
#expect(compact(document.string ?? "").contains(compact(configuration.productionNotes)))
#expect(document.string?.contains("Unknown Camera") == true)
let json = try #require(JSONSerialization.jsonObject(with: result.jsonData) as? [String: Any])
#expect(json["productionNotes"] as? String == configuration.productionNotes)
try assertPrintable(document, configuration: configuration)
}

@Test
func longUnicodeNotesPreserveComposedCharactersAcrossPages() async throws {
var configuration = SharedReportGenerationService.ReportConfiguration.default()
configuration.productionNotes = String(repeating: "cafe\u{301} カメラ ", count: 1_200) + "ENDNOTES"
let result = try await SharedReportGenerationService().generateMasterReport(
transfers: [transfer()], configuration: configuration)
let document = try #require(PDFDocument(data: result.pdfData))
#expect(document.pageCount > 2)
// PDF font mappings may compose the accent; compare canonically equal
// text while requiring the JSON to preserve the exact original string.
#expect(compact(document.string ?? "").contains(compact(configuration.productionNotes)))
let json = try #require(JSONSerialization.jsonObject(with: result.jsonData) as? [String: Any])
#expect(json["productionNotes"] as? String == configuration.productionNotes)
try assertPrintable(document, configuration: configuration)
}

@Test
func aSingleOversizedTransferRowContinuesInsidePrintablePages() async throws {
// Inert model values deliberately exceed filesystem name limits to
// exercise a single wrapped row taller than one printable page.
let sourceName = String(repeating: "SOURCE", count: 600) + " ENDSOURCE"
let backupName = String(repeating: "BACKUP", count: 600) + " ENDBACKUP"
let configuration = SharedReportGenerationService.ReportConfiguration.default()
let result = try await SharedReportGenerationService().generateMasterReport(
transfers: [transfer(sourceName: sourceName, backupName: backupName)], configuration: configuration)
let document = try #require(PDFDocument(data: result.pdfData))
#expect(document.pageCount > 2)
let text = compact(document.string ?? "")
// Once in the summary, then once per source / three backup row entries.
#expect(text.components(separatedBy: compact(sourceName)).count - 1 >= 2)
#expect(text.components(separatedBy: compact(backupName)).count - 1 >= 4)
try assertPrintable(document, configuration: configuration)
}

private func transfer(sourceName: String = "Card", backupName: String = "Backup") -> TransferCard {
let source = FolderInfo(url: URL(fileURLWithPath: "/synthetic/" + sourceName), fileCount: 7,
totalSize: 1_000_000, lastModified: Date(), isInternalDrive: false)
let backups = (0..<3).map { index in
FolderInfo(url: URL(fileURLWithPath: "/synthetic/backup\(index)/" + backupName), fileCount: 7,
totalSize: 1_000_000, lastModified: Date(), isInternalDrive: false)
}
return TransferCard(source: source, destinations: backups, cameraCard: nil, metadata: nil,
progress: 1, state: .completed(.init(success: true, message: "Verified")))
}

private func compact(_ text: String) -> String {
text.precomposedStringWithCanonicalMapping.filter { !$0.isWhitespace }
}

private func assertPrintable(_ document: PDFDocument,
configuration: SharedReportGenerationService.ReportConfiguration) throws {
for pageIndex in 0..<document.pageCount {
let page = try #require(document.page(at: pageIndex))
let media = page.bounds(for: .mediaBox)
for index in 0..<page.numberOfCharacters {
let bounds = page.characterBounds(at: index)
guard !bounds.isEmpty else { continue }
#expect(bounds.minX >= configuration.margins.left - 1)
#expect(bounds.maxX <= media.maxX - configuration.margins.right + 1)
#expect(bounds.minY >= configuration.margins.bottom - 1)
#expect(bounds.maxY <= media.maxY - configuration.margins.top + 1)
}
}
}
}
141 changes: 141 additions & 0 deletions BitMatch-iPadTests/PortableTransferSettingsIOSTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import Foundation
import Testing
import XCTest
import SwiftUI
import UIKit
import Vision
@testable import BitMatch_iPad
import BitMatchEngine

@MainActor
struct PortableTransferSettingsIOSTests {
@Test func mobileSettingsRoundTripPreviewCancelApplyAndPersistence() throws {
let name = "BitMatch-iPadTests.PortableSettings.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: name))
let root = FileManager.default.temporaryDirectory.appendingPathComponent("mobile-settings-\(UUID())")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { defaults.removePersistentDomain(forName: name); try? FileManager.default.removeItem(at: root) }
func coordinator() -> SharedAppCoordinator {
SharedAppCoordinator(platformManager: IOSPlatformManager.shared,
transferJournal: LocalTransferJournal(fileURL: root.appendingPathComponent("journal-\(UUID()).json")),
projectStore: UserDefaultsPhotographerJobStore(defaults: defaults), defaults: defaults)
}
let app = coordinator()
app.reportSettings.operatorName = "Private operator"
app.reportSettings.notes = "Private notes"
app.cameraLabels.settings.label = "Private camera"
let original = app.portableTransferSettings
let proposed = mobileSettingsProposal()
let model = TransferSettingsImportModel()
try model.preview(proposed.encoded(), coordinator: app)
#expect(app.portableTransferSettings == original)
model.cancel()
#expect(app.portableTransferSettings == original)
#expect(model.review == nil)
try model.preview(proposed.encoded(), coordinator: app)
app.isOperationInProgress = true
#expect(throws: TransferSettingsImportError.busy) { try model.apply(to: app) }
app.isOperationInProgress = false
try model.apply(to: app)
#expect(app.portableTransferSettings == proposed)
#expect(app.reportSettings.operatorName == "Private operator")
#expect(app.reportSettings.notes == "Private notes")
#expect(app.cameraLabels.settings.label == "Private camera")
#expect(coordinator().portableTransferSettings == proposed)
let text = String(decoding: try proposed.encoded(), as: UTF8.self)
#expect(!text.contains("Private"))
}

@Test func mobileDecoderRejectsUnsupportedAndOversizedFiles() throws {
let valid = try mobileSettingsProposal().encoded()
var object = try #require(JSONSerialization.jsonObject(with: valid) as? [String: Any])
object["schemaVersion"] = 2
#expect(throws: TransferSettingsImportError.unsupportedVersion) {
_ = try PortableTransferSettings.decode(JSONSerialization.data(withJSONObject: object))
}
#expect(throws: TransferSettingsImportError.tooLarge) {
_ = try PortableTransferSettings.decode(Data(repeating: 32, count: PortableTransferSettings.maximumBytes + 1))
}
#expect(throws: TransferSettingsImportError.invalidFile) { _ = try PortableTransferSettings.decode(Data("no".utf8)) }
}
}

private func mobileSettingsProposal() -> PortableTransferSettings {
.init(verificationMode: .quick, generateASCMHL: false, ignoreAppleDouble: false,
reports: .init(makeReport: false, includeThumbnails: true),
naming: .init(position: .suffix, separator: .dash, autoNumber: false, groupByCamera: true))
}

@MainActor
final class PortableTransferSettingsSnapshotTests: XCTestCase {
func testRenderNarrowPhoneAndPadImportPreview() throws {
let environment = ProcessInfo.processInfo.environment
guard environment["BITMATCH_CAPTURE_WORKFLOW_SNAPSHOTS"] == "1"
|| environment["TEST_RUNNER_BITMATCH_CAPTURE_WORKFLOW_SNAPSHOTS"] == "1" else {
throw XCTSkip("Enable workflow snapshots for settings preview captures")
}
let current = PortableTransferSettings(verificationMode: .standard, generateASCMHL: true, ignoreAppleDouble: true,
reports: .init(makeReport: true, includeThumbnails: false),
naming: .init(position: .prefix, separator: .underscore, autoNumber: true, groupByCamera: false))
let review = TransferSettingsReview(current: current, proposed: mobileSettingsProposal())
XCTAssertEqual(review.changes.count, 9)
for (name, size) in [("smallphone-transfer-settings-preview", CGSize(width: 320, height: 568)),
("phone-transfer-settings-preview", CGSize(width: 390, height: 844)),
("narrowpad-transfer-settings-preview", CGSize(width: 500, height: 700))] {
let host = UIHostingController(rootView: TransferSettingsReviewView(review: review, apply: {}, cancel: {})
.preferredColorScheme(.dark)
.frame(width: size.width, height: size.height)
.transaction { $0.disablesAnimations = true })
let window = UIWindow(frame: CGRect(origin: .zero, size: size))
window.rootViewController = host
window.makeKeyAndVisible()
defer {
window.isHidden = true
// Let UIKit finish the window's disappearance before removing
// the hosting controller; the fixture must not strand a
// NavigationStack appearance transition between captures.
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
window.rootViewController = nil
}
host.view.frame = CGRect(origin: .zero, size: size)
host.view.setNeedsLayout()
host.view.layoutIfNeeded()
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3))
host.view.layoutIfNeeded()
try capture(host.view, size: size, name: name, expected: ["verification", "apply", "cancel"])
// A short screen must keep the disclosure and actions reachable;
// a tall render alone would conceal missing scrolling behavior.
let scroll = try XCTUnwrap(findScrollView(in: host.view))
// Form measures its later rows lazily. Re-settle after scrolling
// rather than mistaking its initial estimated height for the end.
for _ in 0..<4 {
scroll.setContentOffset(CGPoint(x: 0,
y: max(-scroll.adjustedContentInset.top,
scroll.contentSize.height - scroll.bounds.height + scroll.adjustedContentInset.bottom)), animated: false)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
host.view.layoutIfNeeded()
}
try capture(host.view, size: size, name: name + "-scrolled", expected: ["quick checks file sizes", "future transfers", "apply", "cancel"])
}
}

private func findScrollView(in view: UIView) -> UIScrollView? {
if let scroll = view as? UIScrollView, scroll.contentSize.height > 0 { return scroll }
return view.subviews.lazy.compactMap { self.findScrollView(in: $0) }.first
}

private func capture(_ view: UIView, size: CGSize, name: String, expected: [String]) throws {
let image = UIGraphicsImageRenderer(size: size).image { _ in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
try VNImageRequestHandler(cgImage: try XCTUnwrap(image.cgImage)).perform([request])
let text = (request.results ?? []).compactMap { $0.topCandidates(1).first?.string }.joined(separator: " ").lowercased()
for value in expected { XCTAssertTrue(text.contains(value), "\(name) missing \(value): \(text)") }
let attachment = XCTAttachment(data: try XCTUnwrap(image.pngData()), uniformTypeIdentifier: "public.png")
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}
Loading
Loading