From df7bd17c272bf7209f97b2ba268af1497242a173 Mon Sep 17 00:00:00 2001 From: zackees Date: Wed, 1 Jul 2026 13:32:06 -0700 Subject: [PATCH] test(usb): codify #740 vendor-VID resolution table as a unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes FastLED/fbuild#740. The `Vendor-resolution results` table in #740 has been the manual verification pass every `online-data` publish: 19 headline VIDs, each verified via `curl .../usb-vid.json | jq '.["VVVV"].vendor'`. Every prior run has been a `gh` + `jq` sweep by hand, and #740 was pinned open as the "current state of the world" only because there was no programmatic guard against drift. Freeze the entire 19-row table into `usb::embedded::tests:: embedded_resolves_every_issue_740_vendor` so CI catches any archive regression at build time. Rows match the issue body's table verbatim (same VIDs, same expected-vendor substrings). For each row the test looks up the embedded archive's vendor name and confirms a case-insensitive substring match against ANY of the accepted alternatives, so upstream-label variance (e.g. "Silicon Labs" vs "Cygnal") never causes spurious churn. ## Three drift rows called out in the test Three VIDs in the #740 table are `vendor-override` targets of `online-data-tools/vendor_names_inlined.py`, whose overlay is applied to `usb-vid.json` via `overlay_usb_vid.py --mode vendor-override`: * 0x045B — upstream `usb.ids` says "Hitachi, Ltd"; overlay renames to "Renesas Electronics". * 0x2A03 — upstream says "dog hunter AG" (the VID holder); overlay renames to "Arduino LLC" (the downstream licensee). * 0x2544 — missing from upstream entirely; overlay adds as "Silicon Labs". The test accepts BOTH the upstream and overlay label for 0x045B / 0x2A03 so it survives an archive rebuild in either direction. 0x2544 is treated as a soft row — a missing entry is reported (`eprintln!`) but not fatal, since the archive rebuild is what pulls it in. ## Verified locally - `soldr --no-cache cargo test -p fbuild-core --lib usb::embedded` — exit 0 (5 tests pass including the new `embedded_resolves_every_issue_740_vendor`). - `soldr --no-cache cargo clippy -p fbuild-core --lib -- -D warnings` — exit 0. - `soldr cargo fmt -p fbuild-core -- --check` — exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 4 + crates/fbuild-core/src/usb/embedded.rs | 129 +++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2199cb17..cb20ab60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,7 @@ name = "fbuild-config" version = "2.3.16" dependencies = [ "fbuild-core", + "fbuild-paths", "include_dir", "regex", "serde", @@ -1316,6 +1317,7 @@ name = "fbuild-header-scan" version = "2.3.16" dependencies = [ "criterion", + "fbuild-paths", "rayon", "tempfile", "tracing", @@ -1330,6 +1332,7 @@ dependencies = [ "criterion", "fbuild-header-scan", "fbuild-packages", + "fbuild-paths", "fbuild-test-support", "prost", "serde", @@ -1425,6 +1428,7 @@ name = "fbuild-test-support" version = "2.3.16" dependencies = [ "fbuild-config", + "fbuild-core", "fbuild-header-scan", "fbuild-library-select", "fbuild-packages", diff --git a/crates/fbuild-core/src/usb/embedded.rs b/crates/fbuild-core/src/usb/embedded.rs index 3263c0d7..ad2672ea 100644 --- a/crates/fbuild-core/src/usb/embedded.rs +++ b/crates/fbuild-core/src/usb/embedded.rs @@ -237,6 +237,135 @@ mod tests { } } + #[test] + fn embedded_resolves_every_issue_740_vendor() { + // FastLED/fbuild#740 hand-verified 19 vendor VIDs at each + // `online-data` publish. Every prior verification pass has been + // a manual `gh` + `jq` sweep against the published JSON. + // + // Codify the entire table here so CI catches any regression the + // moment the embedded vendor archive is rebuilt without one of + // the headline VIDs — the exact class of drift that previously + // required manually re-running the verification each cycle. + // + // Match is case-insensitive substring; ANY alternative matches. + // ALL failures are collected before asserting so regressions + // surface as a single message with every affected row. + // + // ## Overlay vs embedded drift (documented, not aspirational) + // + // The #740 issue body's "Vendor-resolution results" table was + // taken from the PUBLISHED `online-data/data/usb-vid.json`, + // which has `vendor_names_inlined.py` applied via + // `overlay_usb_vid.py --mode vendor-override`. Three of the 19 + // VIDs in that table were vendor-name-overridden by that + // overlay pass (so the "Actual" column reads the overlay + // value): + // + // - 0x045B — upstream `usb.ids` says "Hitachi, Ltd"; + // overlay renames to "Renesas Electronics". + // - 0x2A03 — upstream says "dog hunter AG" (the actual + // VID holder); overlay renames to "Arduino LLC" (the + // downstream licensee that ships boards under this VID). + // - 0x2544 — missing from upstream entirely; overlay adds + // as "Silicon Labs" (a.k.a. Energy Micro). + // + // The embedded archive shipped in `data/usb-vendors.tar.zst` + // is built from the OVERLAID JSON, so it SHOULD also carry + // these overrides. Whether it does today is a snapshot of the + // last archive rebuild — this test asserts the current + // effective label, and the substring list carries BOTH the + // upstream and overlay names so the test survives either + // resolution outcome without silently accepting drift. + let rows: &[(u16, &[&str])] = &[ + (0x303a, &["Espressif"]), + (0x2e8a, &["Raspberry Pi"]), + (0x0483, &["STMicroelectronics", "STMicro"]), + (0x1fc9, &["NXP"]), + (0x1915, &["Nordic"]), + (0x03eb, &["Atmel"]), + (0x04d8, &["Microchip"]), + (0x10c4, &["Silicon Lab", "Cygnal"]), + (0x1a86, &["QinHeng", "WCH"]), + (0x0403, &["Future Technology", "FTDI"]), + // 0x1cbe is Luminary Micro (Cortex-M / Apollo3 bootloader + // VID reused by Sparkfun Artemis products in the field). + (0x1cbe, &["Luminary", "Apollo3", "Sparkfun"]), + (0x2341, &["Arduino"]), + (0x239a, &["Adafruit"]), + (0x1b4f, &["SparkFun", "Spark Fun"]), + (0x16c0, &["Van Ooijen", "PJRC"]), + (0x2886, &["Seeed"]), + // Overlay-covered VIDs — either name is accepted so the + // test survives an archive rebuild + overlay pipeline + // change in either direction. See the module comment + // above for the source of each alternative. + (0x045b, &["Renesas", "Hitachi"]), + (0x2a03, &["Arduino", "dog hunter"]), + ]; + + // 0x2544 (Silicon Labs) is a supplement-only VID — the archive + // MAY not carry it, depending on whether the archive was + // rebuilt after the vendor_names_inlined.py addition. Assert + // that IF present it resolves to Silicon Labs; a missing entry + // is tolerated but reported so the drift is visible in test + // logs. + let overlay_only: &[(u16, &[&str])] = &[(0x2544, &["Silicon Lab", "Cygnal"])]; + + let mut failures = Vec::new(); + for (vid, expected_alts) in rows { + match vendor_name(*vid) { + None => failures.push(format!( + "VID 0x{vid:04X}: missing from embedded archive; expected any of {expected_alts:?}" + )), + Some(name) => { + let name_lc = name.to_lowercase(); + let matched = expected_alts + .iter() + .any(|alt| name_lc.contains(&alt.to_lowercase())); + if !matched { + failures.push(format!( + "VID 0x{vid:04X}: got {name:?}, expected any-of {expected_alts:?}" + )); + } + } + } + } + + // Overlay-only rows: report drift, but don't fail. Missing = + // "archive hasn't been rebuilt with the current overlay yet"; + // wrong name = "archive was rebuilt but overlay changed the + // canonical string" — either way a follow-up rebuild is the + // remediation, not a source-code fix. + for (vid, expected_alts) in overlay_only { + match vendor_name(*vid) { + None => eprintln!( + "note: VID 0x{vid:04X} not in embedded archive \ + (overlay-only; archive rebuild will pick it up); \ + expected any of {expected_alts:?}" + ), + Some(name) => { + let name_lc = name.to_lowercase(); + let matched = expected_alts + .iter() + .any(|alt| name_lc.contains(&alt.to_lowercase())); + if !matched { + failures.push(format!( + "VID 0x{vid:04X}: got {name:?}, expected any-of {expected_alts:?}" + )); + } + } + } + } + + assert!( + failures.is_empty(), + "FastLED/fbuild#740 vendor-VID table drift ({} row(s) failed):\n {}", + failures.len(), + failures.join("\n "), + ); + } + #[test] fn unknown_vid_returns_none() { // 0xBADD is in the unallocated portion of the USB-IF range as of