tests/logo.rs 11.9 KiB 334 lines Raw
1 //! Logo suite (m6, f0rge issues #15 + #26 + #27 + #28):2 //! `static/logo.svg` (lockup with hu13 as <path>) + `static/favicon.svg`3 //! (simplified mark-only 16px variant) exist + valid XML + <2KB total,4 //! wordmark variant, owner block-art banner (`static/ascii.txt` +5 //! `static/logo-ansi.txt`, 12 lines) + purity/strip tests + `<pre>`6 //! rendering invariant.7 //!8 //! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests9 //! cover the `ascii_pre()` helper itself; here we assert on disk).10 11 use std::path::PathBuf;12 13 fn manifest() -> PathBuf {14     PathBuf::from(env!("CARGO_MANIFEST_DIR"))15 }16 17 fn read_static(name: &str) -> String {18     let path = manifest().join("static").join(name);19     std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("static/{name} exists"))20 }21 22 /// Minimal well-formedness check: balanced tags, quoted attributes, single23 /// root; skips comments, PIs and `<!...>` declarations. Enough to call the24 /// hand-written mark "valid XML" without a new dependency.25 fn check_well_formed(xml: &str) {26     let bytes = xml.as_bytes();27     let mut i = 0;28     let mut stack: Vec<String> = Vec::new();29     let mut root_done = false;30     while i < bytes.len() {31         if bytes[i] != b'<' {32             i += 1;33             continue;34         }35         let rest = &xml[i..];36         if rest.starts_with("<!--") {37             let end = rest.find("-->").expect("unterminated comment");38             i += end + 4;39             continue;40         }41         if rest.starts_with("<?") {42             let end = rest.find("?>").expect("unterminated PI");43             i += end + 2;44             continue;45         }46         if rest.starts_with("<!") {47             let end = rest.find('>').expect("unterminated declaration");48             i += end + 1;49             continue;50         }51         // Tag end, respecting quoted `>` inside attributes.52         let mut j = i + 1;53         let mut quote: Option<u8> = None;54         while j < bytes.len() {55             match (quote, bytes[j]) {56                 (Some(q), c) if c == q => quote = None,57                 (None, b'"' | b'\'') => quote = Some(bytes[j]),58                 (None, b'>') => break,59                 _ => {}60             }61             j += 1;62         }63         assert!(j < bytes.len(), "unterminated tag near {rest:?}");64         let tag = &xml[i..=j];65         if let Some(close) = tag.strip_prefix("</") {66             let name = close.trim_end_matches('>').trim().to_string();67             let open = stack.pop().expect("closing tag without opener");68             assert_eq!(open, name, "mismatched tag: <{open}> vs </{name}>");69             if stack.is_empty() {70                 root_done = true;71             }72         } else {73             let self_close = tag.ends_with("/>");74             let mut inner = &tag[1..tag.len() - 1];75             inner = inner.strip_suffix('/').unwrap_or(inner);76             let name: &str = inner77                 .split(|c: char| c.is_whitespace() || c == '/')78                 .next()79                 .unwrap_or("");80             assert!(!name.is_empty(), "empty tag name in {tag:?}");81             assert!(82                 name.bytes()83                     .next()84                     .is_some_and(|c| c.is_ascii_alphabetic() || c == b'_' || c == b':'),85                 "bad tag name {name:?}"86             );87             if self_close {88                 if stack.is_empty() {89                     root_done = true;90                 }91             } else {92                 assert!(!root_done, "second root element <{name}>");93                 stack.push(name.to_string());94             }95         }96         i = j + 1;97     }98     assert!(stack.is_empty(), "unclosed tags: {stack:?}");99     assert!(root_done, "no root element");100 }101 102 fn assert_sharp_no_gradient(name: &str, svg: &str) {103     let lower = svg.to_lowercase();104     for bad in [105         "rx=",106         "ry=",107         "<circle",108         "<ellipse",109         "border-radius",110         "linecap=\"round\"",111         "linejoin=\"round\"",112     ] {113         assert!(114             !lower.contains(bad),115             "{name}: no rounded corners ({bad}), sharp geometry only"116         );117     }118     assert!(119         !lower.contains("gradient"),120         "{name}: no gradients (must stay legible at 16px)"121     );122 }123 124 #[test]125 fn logo_svg_exists_valid_tiny() {126     let path = manifest().join("static").join("logo.svg");127     let len = std::fs::metadata(&path)128         .expect("static/logo.svg exists")129         .len();130     assert!(len < 2048, "logo.svg is {len} bytes, budget is 2KB");131     assert!(len > 100, "logo.svg is {len} bytes, suspiciously small");132     let svg = read_static("logo.svg");133     check_well_formed(&svg);134     assert!(svg.contains("<svg"), "has an <svg> root");135     assert!(136         svg.contains("http://www.w3.org/2000/svg"),137         "svg namespace present"138     );139     assert!(svg.contains("viewBox"), "scales via viewBox (16px + 32px)");140     for color in ["#7aa2f7", "#7dcfff", "#c0caf5"] {141         assert!(svg.contains(color), "Tokyo Night palette keeps {color}");142     }143     assert!(144         !svg.contains("fill=\"#1a1b26\""),145         "logo has no baked-in background (light theme must not show a dark slab)"146     );147     assert!(148         svg.contains("prefers-color-scheme:light"),149         "lettering flips ink on the light theme"150     );151     assert_sharp_no_gradient("logo.svg", &svg);152     // Logo-text (#27): hu13 lettering is hand-written <path>, never153     // <text>/fonts, so the header lockup has no font dependency.154     assert!(svg.contains("<path"), "logo letters are <path>");155     assert!(!svg.contains("<text"), "logo has no <text>");156     assert!(157         !svg.to_lowercase().contains("font"),158         "logo has no font refs"159     );160     assert!(161         svg.matches("<path").count() >= 2,162         "logo carries mark + letter paths"163     );164 }165 166 #[test]167 fn favicon_variant_is_simplified_mark_only() {168     // Logo-text (#27): the 16px favicon is the simplified mark-only169     // variant (no letters) so it stays legible; full lettering lives in170     // logo.svg. Both stay valid XML, sharp, tiny, with <2KB combined.171     let fav_path = manifest().join("static").join("favicon.svg");172     let fav_len = std::fs::metadata(&fav_path)173         .expect("static/favicon.svg exists")174         .len();175     assert!(176         fav_len < 2048,177         "favicon.svg is {fav_len} bytes, budget is 2KB"178     );179     let fav = read_static("favicon.svg");180     check_well_formed(&fav);181     assert!(fav.contains("<svg"), "favicon has an <svg> root");182     assert!(fav.contains("viewBox"), "favicon scales via viewBox");183     assert!(!fav.contains("<text"), "favicon has no <text>");184     assert!(185         !fav.to_lowercase().contains("font"),186         "favicon has no font refs"187     );188     assert!(189         !fav.contains("<path"),190         "favicon is mark-only (no letter <path>s)"191     );192     assert_sharp_no_gradient("favicon.svg", &fav);193     for color in ["#7aa2f7", "#7dcfff"] {194         assert!(fav.contains(color), "favicon keeps {color}");195     }196     assert!(197         !fav.contains("fill=\"#1a1b26\""),198         "favicon has no baked-in background"199     );200     let logo_len = std::fs::metadata(manifest().join("static").join("logo.svg"))201         .expect("static/logo.svg exists")202         .len();203     assert!(204         logo_len + fav_len < 2048,205         "logo ({logo_len}B) + favicon ({fav_len}B) stay under 2KB total"206     );207     let logo = read_static("logo.svg");208     assert_ne!(logo, fav, "favicon is a simplified variant, not a copy");209 }210 211 #[test]212 fn wordmark_variant_exists_tiny() {213     let path = manifest().join("static").join("wordmark.svg");214     let len = std::fs::metadata(&path)215         .expect("static/wordmark.svg exists")216         .len();217     assert!(len < 2048, "wordmark.svg is {len} bytes, budget is 2KB");218     let svg = read_static("wordmark.svg");219     check_well_formed(&svg);220     assert!(svg.contains("hu13"), "wordmark names hu13");221     assert_sharp_no_gradient("wordmark.svg", &svg);222 }223 224 #[test]225 fn ascii_banner_limits_and_aesthetic() {226     // Owner block-art (#26): 12-line block glyphs reading "hu13".227     // Charset is space + block set (█ ▓ ▒ ░ ▄ ▀ ▌ ▐) plus the legacy228     // line-art chars (| - / _ +) for forward compat — still no letters,229     // no `<>&"` so `<pre>` stays verbatim via esc().230     let art = read_static("ascii.txt");231     assert!(!art.trim().is_empty(), "banner is non-empty");232     let lines: Vec<&str> = art.lines().collect();233     assert_eq!(lines.len(), 12, "owner banner is 12 lines");234     let mut saw_block = false;235     for line in &lines {236         assert!(line.chars().count() <= 60, "banner line too wide: {line:?}");237         for ch in line.chars() {238             assert!(!ch.is_ascii_alphabetic(), "no letters, got {ch:?}");239             assert!(240                 !matches!(ch, '<' | '>' | '&' | '"'),241                 "nothing to escape, got {ch:?}"242             );243             saw_block |= matches!(ch, '█' | '▓' | '▒' | '░' | '▄' | '▀' | '▌' | '▐');244             assert!(245                 matches!(246                     ch,247                     ' ' | '█'248                         | '▓'249                         | '▒'250                         | '░'251                         | '▄'252                         | '▀'253                         | '▌'254                         | '▐'255                         | '|'256                         | '-'257                         | '/'258                         | '_'259                         | '+'260                 ),261                 "block set + legacy (| - / _ + space), got {ch:?}"262             );263         }264     }265     assert!(saw_block, "uses block glyphs");266     assert!(art.contains('█'), "block fill present");267 }268 269 #[test]270 fn ascii_renders_inside_pre_unescaped_except_esc() {271     // The banner has no `<>&"` so `esc()` must leave it byte-identical;272     // wrapping it in `<pre>` is therefore exact (unit tests in `src/logo.rs`273     // assert the same on the `ascii_pre()` helper itself).274     let art = read_static("ascii.txt");275     assert!(!art.contains(['<', '>', '&', '"']), "nothing to escape");276     let pre = format!("<div class=\"pre-scroll\"><pre>\n{art}</pre></div>");277     assert!(pre.contains("<pre>"), "inside <pre>");278     assert!(pre.contains(&art), "verbatim, unescaped-except-esc()");279 }280 281 /// Strip literal SGR codes (`[...m`, no ESC byte) the way the rubric282 /// prescribes: `sed 's/\[[0-9;]*m//g'`.283 fn strip_literal_sgr(s: &str) -> String {284     let bytes = s.as_bytes();285     let mut out = String::with_capacity(s.len());286     let mut i = 0;287     while i < bytes.len() {288         if bytes[i] == b'[' {289             let mut k = i + 1;290             while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {291                 k += 1;292             }293             if k < bytes.len() && bytes[k] == b'm' && k > i + 1 {294                 i = k + 1;295                 continue;296             }297         }298         let ch = s[i..].chars().next().expect("char boundary");299         out.push(ch);300         i += ch.len_utf8();301     }302     out303 }304 305 #[test]306 fn ansi_strips_to_ascii_byte_for_byte() {307     // Owner ANSI original (#26) strips to the plain banner byte-for-byte.308     let ansi = read_static("logo-ansi.txt");309     let ascii = read_static("ascii.txt");310     assert_eq!(strip_literal_sgr(&ansi), ascii, "strip(ansi) == ascii");311     assert_eq!(ansi.lines().count(), 12, "ansi is 12 lines");312     assert_eq!(ascii.lines().count(), 12, "ascii is 12 lines");313 }314 315 #[test]316 fn logo_ansi_is_esc_free_with_codes() {317     // Stored with literal `[...m` codes, block glyphs, NO raw ESC bytes.318     let ansi = read_static("logo-ansi.txt");319     assert!(!ansi.contains('\u{1b}'), "no raw ESC bytes stored");320     assert!(ansi.contains("[0;"), "holds SGR codes");321     assert!(ansi.contains("[0m"), "holds reset");322     assert!(ansi.contains('█'), "holds block glyphs");323 }324 325 #[test]326 fn static_holds_no_esc_bytes() {327     // Success rubric (#26): `grep -r $'\\x1b' static/` is empty.328     let dir = manifest().join("static");329     for name in ["logo-ansi.txt", "ascii.txt", "logo.svg", "favicon.svg"] {330         let bytes =331             std::fs::read(dir.join(name)).unwrap_or_else(|_| panic!("static/{name} exists"));332         assert!(!bytes.contains(&0x1b), "static/{name} holds no ESC bytes");333     }334 }