6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
- #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>
114 lines
3.5 KiB
Rust
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}");
|
|
}
|