1//! The allowlist held to the vendored pack. Reads the files, so it is a test, not library code. 2 3use std::collections::{BTreeMap, BTreeSet}; 4use std::path::PathBuf; 5 6use whiskers_icons::raster::Icon; 7use whiskers_icons::{IconError, IconId, Verdict, allowlist}; 8 9fn pack() -> PathBuf { 10 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../third-party/pixel-icons") 11} 12 13fn files() -> BTreeSet<String> { 14 std::fs::read_dir(pack().join("icons")) 15 .expect("the vendored pack is there") 16 .map(|e| e.unwrap().file_name().into_string().unwrap()) 17 .filter_map(|n| n.strip_suffix(".svg").map(str::to_owned)) 18 .collect() 19} 20 21/// The category the pack's own README files each icon under. 22fn upstream_categories() -> BTreeMap<String, String> { 23 let text = std::fs::read_to_string(pack().join("UPSTREAM-README.md")).expect("the upstream README is kept"); 24 let (mut cat, mut out) = (String::new(), BTreeMap::new()); 25 for line in text.lines() { 26 if let Some(c) = line.strip_prefix("## ") { 27 cat = c.trim().to_owned(); 28 } else if let Some(at) = line.find("icons/") { 29 if let Some(name) = line[at + 6..].split(".svg").next() { 30 out.insert(name.to_owned(), cat.clone()); 31 } 32 } 33 } 34 out 35} 36 37#[test] 38fn the_allowlist_file_parses() { 39 assert!(allowlist().entries().len() > 300); 40} 41 42#[test] 43fn every_file_of_the_pack_is_decided_exactly_once_and_nothing_else_is() { 44 let decided: BTreeSet<String> = allowlist().entries().iter().map(|e| e.name.to_owned()).collect(); 45 assert_eq!(decided.len(), allowlist().entries().len(), "no name twice"); 46 let on_disk = files(); 47 let undecided: Vec<_> = on_disk.difference(&decided).collect(); 48 let phantom: Vec<_> = decided.difference(&on_disk).collect(); 49 assert!(undecided.is_empty(), "icons nobody has decided: {undecided:?}"); 50 assert!(phantom.is_empty(), "allowlisted names that are not files: {phantom:?}"); 51} 52 53#[test] 54fn every_icon_sits_under_the_category_the_pack_files_it_under() { 55 let up = upstream_categories(); 56 for e in allowlist().entries() { 57 assert_eq!(up.get(e.name).map(String::as_str), Some(e.category), "{} is in the wrong section", e.name); 58 } 59} 60 61#[test] 62fn no_category_excluded_whole_leaks_an_admitted_icon() { 63 let excluded: Vec<&str> = allowlist().categories().iter().filter(|c| c.excluded_because.is_some()).map(|c| c.name).collect(); 64 let up = upstream_categories(); 65 for id in IconId::all() { 66 let cat = up[id.as_str()].as_str(); 67 assert!(!excluded.contains(&cat), "{id} is admitted but its category {cat} is excluded"); 68 } 69} 70 71#[test] 72fn what_does_not_suit_a_young_child_is_excluded() { 73 for name in ["beer", "wine", "credit-card", "dollar", "paypal", "take-my-money", "stonks", "sad-pepe", "this-is-fine", "alert-triangle"] { 74 assert_eq!(IconId::new(name), Err(IconError::Excluded), "{name}"); 75 } 76 // What she is likely to love is in. 77 for name in ["cat", "birthday-cake", "pikachu", "mario", "pacman", "charmander", "youtube", "spotify", "google", "rainbow", "unicorn"] { 78 assert!(IconId::new(name).is_ok(), "{name}"); 79 } 80 assert_eq!(IconId::new("not-an-icon"), Err(IconError::Unknown)); 81 assert_eq!(IconId::new(""), Err(IconError::Unknown)); 82 assert_eq!(IconId::new("../cat"), Err(IconError::Unknown)); 83} 84 85#[test] 86fn every_admitted_icon_rasterises_and_the_ids_round_trip_through_json() { 87 for id in IconId::all() { 88 let svg = std::fs::read_to_string(pack().join("icons").join(format!("{id}.svg"))).unwrap(); 89 let icon = Icon::parse(&svg).unwrap_or_else(|e| panic!("{id}: {e}")); 90 assert_eq!(icon.size, 9, "{id}"); 91 assert!(icon.colours().len() <= 36, "{id}"); 92 let json = serde_json::to_string(&id).unwrap(); 93 assert_eq!(serde_json::from_str::<IconId>(&json).unwrap(), id); 94 } 95 assert!(serde_json::from_str::<IconId>("\"beer\"").is_err(), "an excluded name does not parse"); 96 assert!(serde_json::from_str::<IconId>("\"nope\"").is_err()); 97} 98 99#[test] 100fn the_pack_is_vendored_unchanged() { 101 // The pack's own commit is recorded in its README; this holds the count so a stray edit or a lost file shows. 102 assert_eq!(files().len(), 357); 103 assert!(pack().join("LICENSE").exists(), "the pack's own file stays beside it"); 104} 105 106#[test] 107fn the_counts_the_reviewer_reads_are_what_the_file_says() { 108 let admitted = allowlist().entries().iter().filter(|e| e.verdict == Verdict::Admitted).count(); 109 assert_eq!(admitted, IconId::all().count()); 110} 111 112#[test] 113fn a_known_icon_rasterises_to_the_pixels_it_should() { 114 // The pack's pencil, upright: an eraser, a body with a shaded side, a band and a point. 115 let svg = std::fs::read_to_string(pack().join("icons/pencil.svg")).unwrap(); 116 let icon = Icon::parse(&svg).unwrap(); 117 assert_eq!( 118 icon.ascii(), 119 ".........\n...000...\n...121...\n...121...\n...121...\n...121...\n...333...\n....4....\n........." 120 ); 121} 122 123#[test] 124fn the_rasteriser_refuses_what_it_cannot_draw_faithfully() { 125 let wrap = |inner: &str| format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 9">{inner}</svg>"#); 126 for bad in [ 127 r##"<polygon fill="#000000" points="0,0 3,1 0,2"/>"##, // a diagonal 128 r##"<path fill="#000000" d="M0,0Q1,1,2,0z"/>"##, // a quadratic curve 129 r##"<path fill="#000000" d="M0,0C1,1,2,2,3,0z"/>"##, // a real curve 130 r##"<rect fill="red" x="0" y="0" width="1" height="1"/>"##, // a named colour 131 r##"<circle fill="#000000" cx="4" cy="4" r="2"/>"##, // another shape 132 r##"<rect x="0" y="0" width="1" height="1"/>"##, // no fill 133 ] { 134 assert!(Icon::parse(&wrap(bad)).is_err(), "{bad}"); 135 } 136 assert!(Icon::parse(&wrap("")).is_err(), "nothing drawn"); 137 assert!(Icon::parse(r##"<svg viewBox="0 0 9 8"><rect fill="#000000" width="1" height="1"/></svg>"##).is_err()); 138}