|
| 1 | +import Foundation |
| 2 | +import Testing |
| 3 | +@testable import WhatCableCore |
| 4 | + |
| 5 | +/// Corpus-replay sweep for `CableDB.vendorName(vid:)`, `CableDB.isUSBIFRegistered(_:)`, |
| 6 | +/// and `CableDB.curatedCables(vid:pid:)` (`Sources/WhatCableCore/Cable/CableDB.swift`), |
| 7 | +/// the bundled-SQLite-backed lookup no existing test drives against real |
| 8 | +/// corpus identities. |
| 9 | +/// |
| 10 | +/// Reads `research/customer-probes/corpus.jsonl`, the committed distillation |
| 11 | +/// (not gitignored raw probes), so this sweep runs on a fresh clone with no |
| 12 | +/// re-fetch needed. Each record's `devices` and `cables` arrays carry |
| 13 | +/// `{"vid": "0x...", "pid": "0x..."}` pairs seen on real hardware: `devices` |
| 14 | +/// are connected-peripheral SOP identities, `cables` are cable e-marker |
| 15 | +/// (SOP'/SOP'') identities. |
| 16 | +@Suite("CableDB: corpus lookup sweep") |
| 17 | +struct CableDBCorpusLookupTests { |
| 18 | + |
| 19 | + private static let corpusRoot: URL = { |
| 20 | + URL(fileURLWithPath: #filePath) |
| 21 | + .deletingLastPathComponent() // WhatCableCoreTests |
| 22 | + .deletingLastPathComponent() // Tests |
| 23 | + .deletingLastPathComponent() // repo root |
| 24 | + .appendingPathComponent("research/customer-probes") |
| 25 | + }() |
| 26 | + |
| 27 | + private struct Identity: Hashable { |
| 28 | + let vid: Int |
| 29 | + let pid: Int |
| 30 | + } |
| 31 | + |
| 32 | + /// Every (VID, PID) pair seen anywhere in `corpus.jsonl`'s `devices` or |
| 33 | + /// `cables` arrays, across every folder. `vid`/`pid` are hex strings like |
| 34 | + /// `"0x05AC"`; entries missing either field, or equal to `"0x0000"`, are |
| 35 | + /// skipped (mirrors `CableDB.curatedCables`'s own zero-VID/PID guard -- |
| 36 | + /// there is nothing to look up for an unidentified accessory). |
| 37 | + private static let identities: Set<Identity> = { |
| 38 | + let url = corpusRoot.appendingPathComponent("corpus.jsonl") |
| 39 | + guard let text = try? String(contentsOf: url, encoding: .utf8) else { return [] } |
| 40 | + |
| 41 | + var result: Set<Identity> = [] |
| 42 | + for line in text.split(separator: "\n") { |
| 43 | + guard let data = line.data(using: .utf8), |
| 44 | + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] |
| 45 | + else { continue } |
| 46 | + let arrays = [obj["devices"] as? [[String: Any]] ?? [], obj["cables"] as? [[String: Any]] ?? []] |
| 47 | + for array in arrays { |
| 48 | + for entry in array { |
| 49 | + guard let vidStr = entry["vid"] as? String, vidStr.hasPrefix("0x"), |
| 50 | + let pidStr = entry["pid"] as? String, pidStr.hasPrefix("0x"), |
| 51 | + let vid = Int(vidStr.dropFirst(2), radix: 16), |
| 52 | + let pid = Int(pidStr.dropFirst(2), radix: 16), |
| 53 | + vid != 0, pid != 0 |
| 54 | + else { continue } |
| 55 | + result.insert(Identity(vid: vid, pid: pid)) |
| 56 | + } |
| 57 | + } |
| 58 | + } |
| 59 | + return result |
| 60 | + }() |
| 61 | + |
| 62 | + /// The 5 corpus-identified cables added to `data/known-cables.md` / |
| 63 | + /// `whatcable.db` from test-kit evidence (commit c45018418, "Add 5 |
| 64 | + /// corpus-identified cables to the known-cables database"). Each must |
| 65 | + /// resolve to a curated row: this is the same database the app bundles, |
| 66 | + /// so a lookup miss here means the running app can't identify these |
| 67 | + /// cables either. |
| 68 | + private static let recentlyAddedCables: [(vid: Int, pid: Int, brandContains: String)] = [ |
| 69 | + (0x05AC, 0x7209, "Studio Display"), |
| 70 | + (0x05AC, 0x7203, "Apple"), |
| 71 | + (0x20C2, 0x080F, "Sumitomo"), |
| 72 | + (0x20C2, 0x0714, "Sumitomo"), |
| 73 | + (0x0C62, 0xC8F1, "Chant Sincere"), |
| 74 | + ] |
| 75 | + |
| 76 | + // MARK: - Coverage floor |
| 77 | + // |
| 78 | + // Measured directly against the committed corpus.jsonl at the time this |
| 79 | + // sweep was written, by a Python script mirroring this file's own filter |
| 80 | + // exactly (vid/pid must be "0x"-prefixed strings that parse as hex and |
| 81 | + // are both non-zero): 231 distinct (VID, PID) pairs across every folder's |
| 82 | + // `devices` + `cables` arrays. Floor = 85% of 231, rounded down: |
| 83 | + // 231 * 0.85 = 196.35 -> 196. |
| 84 | + // |
| 85 | + // An earlier scoping pass (used only to gauge scale before this file was |
| 86 | + // written, not the filter that shipped) had counted 268 by checking the |
| 87 | + // vid/pid strings were merely non-empty rather than non-zero, so it |
| 88 | + // counted zero-VID/zero-PID entries (e.g. `"0x0000"`/`"0x0000"`) as a |
| 89 | + // valid pair. This file's actual filter excludes those (mirroring |
| 90 | + // `CableDB.curatedCables`'s own zero-VID/PID guard, see the doc comment |
| 91 | + // above), so 268 never matched what the shipped code counts; 231 is the |
| 92 | + // correct figure for the filter actually in this file. |
| 93 | + // |
| 94 | + // corpus.jsonl is committed (unlike the gitignored raw probes the other |
| 95 | + // sweeps depend on), so this floor never skips on a fresh clone. |
| 96 | + private static let coverageFloor = 196 |
| 97 | + |
| 98 | + // MARK: - Tests |
| 99 | + |
| 100 | + @Test("Coverage: the corpus has enough distinct (VID, PID) identities to exercise CableDB") |
| 101 | + func coverageFloorHolds() { |
| 102 | + #expect(Self.identities.count >= Self.coverageFloor, |
| 103 | + "Expected at least \(Self.coverageFloor) distinct (VID, PID) identities (85% of the 231 counted when this sweep was written); found \(Self.identities.count).") |
| 104 | + } |
| 105 | + |
| 106 | + @Test("No crash: every corpus (VID, PID) identity survives a CableDB round trip") |
| 107 | + func noCrashAcrossCorpus() { |
| 108 | + for id in Self.identities { |
| 109 | + _ = CableDB.vendorName(vid: id.vid) |
| 110 | + _ = CableDB.isUSBIFRegistered(id.vid) |
| 111 | + _ = CableDB.curatedCables(vid: id.vid, pid: id.pid) |
| 112 | + } |
| 113 | + #expect(Bool(true)) |
| 114 | + } |
| 115 | + |
| 116 | + @Test("Invariant: a vendor name is never empty when CableDB resolves one") |
| 117 | + func vendorNameNeverEmptyWhenResolved() { |
| 118 | + var examined = 0 |
| 119 | + var violations: [String] = [] |
| 120 | + for id in Self.identities { |
| 121 | + guard let name = CableDB.vendorName(vid: id.vid) else { continue } |
| 122 | + examined += 1 |
| 123 | + if name.isEmpty { |
| 124 | + violations.append("VID 0x\(String(format: "%04X", id.vid)) resolved to an empty vendor name") |
| 125 | + } |
| 126 | + } |
| 127 | + if examined == 0 { |
| 128 | + Issue.record("No corpus VID resolved a vendor name at all; this invariant is untested by this sweep") |
| 129 | + } |
| 130 | + #expect(violations.isEmpty, "\(violations.joined(separator: "\n"))") |
| 131 | + } |
| 132 | + |
| 133 | + @Test("Invariant: isUSBIFRegistered implies vendorName is also resolvable") |
| 134 | + func usbifRegisteredImpliesVendorNameResolves() { |
| 135 | + // Source: `isUSBIFRegistered` checks `store.vendors[vid]?.source == "usbif"`, |
| 136 | + // which can only be true if `store.vendors[vid]` exists at all -- |
| 137 | + // the same dictionary `vendorName(vid:)` reads. A VID that is |
| 138 | + // USB-IF-registered but resolves no name at all would mean the two |
| 139 | + // methods are reading inconsistent state. |
| 140 | + var examined = 0 |
| 141 | + for id in Self.identities { |
| 142 | + guard CableDB.isUSBIFRegistered(id.vid) else { continue } |
| 143 | + examined += 1 |
| 144 | + #expect(CableDB.vendorName(vid: id.vid) != nil, |
| 145 | + "VID 0x\(String(format: "%04X", id.vid)) is USB-IF registered but vendorName(vid:) returned nil") |
| 146 | + } |
| 147 | + if examined == 0 { |
| 148 | + Issue.record("No corpus VID was USB-IF registered; this invariant is untested by this sweep") |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + @Test("The 5 recently-added corpus-identified cables resolve to their curated rows", arguments: Self.recentlyAddedCables) |
| 153 | + func recentlyAddedCablesResolve(_ fixture: (vid: Int, pid: Int, brandContains: String)) { |
| 154 | + let matches = CableDB.curatedCables(vid: fixture.vid, pid: fixture.pid) |
| 155 | + #expect(!matches.isEmpty, |
| 156 | + "0x\(String(format: "%04X", fixture.vid)):0x\(String(format: "%04X", fixture.pid)) resolved no curated cable row") |
| 157 | + #expect(matches.contains { $0.brand.localizedCaseInsensitiveContains(fixture.brandContains) }, |
| 158 | + "0x\(String(format: "%04X", fixture.vid)):0x\(String(format: "%04X", fixture.pid)) resolved \(matches.map(\.brand)), none containing '\(fixture.brandContains)'") |
| 159 | + } |
| 160 | +} |
0 commit comments