Files
Ferrous-Solitaire/solitaire_engine/tests/icon_svg_pin.rs
T
funman300 6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 13:07:22 -07:00

114 lines
3.5 KiB
Rust

//! Pinning test for the application-icon SVG builder.
//!
//! Hashes the raw RGBA8 pixel bytes produced by rasterising
//! `icon_svg()` at every size in `ICON_SIZES`, compares each hash
//! to an embedded constant, and fails on any drift. Catches
//! `usvg`/`resvg`/`tiny_skia` rendering changes and any
//! intentional builder edit that wasn't paired with a hash
//! refresh.
//!
//! When the icon SVG changes intentionally (or a dependency
//! upgrade legitimately changes rendering), update `EXPECTED` by
//! emptying it (`&[]`) and re-running this test once — the test
//! will panic with the new hashes formatted as Rust source ready
//! to paste back in. Same bootstrap pattern as
//! `card_face_svg_pin.rs`.
use bevy::math::UVec2;
use solitaire_engine::assets::icon_svg::{ICON_SIZES, icon_svg};
use solitaire_engine::assets::rasterize_svg;
const EXPECTED: &[(u32, u64)] = &[
(16, 0x07e641beea430d66),
(24, 0x24e66767f4756a60),
(32, 0xf22a3104623a3873),
(48, 0x2d7f978cf7b12763),
(64, 0x1b377e3e30202eba),
(128, 0xafdc80f901b45518),
(256, 0x82b5b46f73c5921d),
(512, 0xe14c018e1e285209),
(1024, 0xfcd0a6a3beb68bdb),
];
#[test]
fn rasterised_icon_bytes_match_pinned_hashes() {
let actual = compute_actual_hashes();
if EXPECTED.is_empty() {
panic_with_hashes_to_paste(&actual);
}
assert_eq!(
actual.len(),
EXPECTED.len(),
"icon-size count drifted (actual {} vs expected {})",
actual.len(),
EXPECTED.len(),
);
let mut mismatches: Vec<String> = Vec::new();
for ((actual_size, actual_hash), (expected_size, expected_hash)) in
actual.iter().zip(EXPECTED.iter())
{
assert_eq!(actual_size, expected_size, "icon-size order drifted",);
if actual_hash != expected_hash {
mismatches.push(format!(
" icon_{actual_size}: actual 0x{actual_hash:016x} expected 0x{expected_hash:016x}",
));
}
}
if !mismatches.is_empty() {
let mut msg = String::from(
"rasterised icon bytes drifted from EXPECTED — usvg/resvg/tiny_skia/font upgrade?\n",
);
for m in &mismatches {
msg.push_str(m);
msg.push('\n');
}
msg.push_str(
"\nIf this drift is intentional, replace EXPECTED with `&[]` and re-run\nthis test to print fresh hashes.\n",
);
panic!("{msg}");
}
}
fn compute_actual_hashes() -> Vec<(u32, u64)> {
let svg = icon_svg();
ICON_SIZES
.iter()
.map(|&size| (size, hash_rasterised(&svg, size)))
.collect()
}
fn hash_rasterised(svg: &str, size: u32) -> u64 {
let target = UVec2::new(size, size);
let image = rasterize_svg(svg.as_bytes(), target).expect("rasterise icon SVG");
let bytes = image
.data
.expect("rasterised image carries RGBA pixel data");
fnv1a(&bytes)
}
/// FNV-1a 64-bit, inline. Same shape as `card_face_svg_pin.rs` —
/// no cryptographic strength needed, just stable byte fingerprints.
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn panic_with_hashes_to_paste(actual: &[(u32, u64)]) -> ! {
let mut out = String::from(
"\nEXPECTED is empty — paste the following into the const literal:\n\nconst EXPECTED: &[(u32, u64)] = &[\n",
);
for (size, hash) in actual {
out.push_str(&format!(" ({size}, 0x{hash:016x}),\n"));
}
out.push_str("];\n");
panic!("{out}");
}