//! The browser's index, built from a real release. //! //! Needs an install and a bridge dump, so it does not run by default: //! //! HELM_MEGAMEK=/path/to/megamek HELM_BRIDGE=/path/to/dump \ //! cargo test -p helm-cli -- --ignored //! //! What it holds is the contract a screen is written against rather than the //! shape of the JSON: every unit can be *drawn* and *priced*, and the file says //! which MegaMek it describes. The first two are what a force-building screen //! cannot work without, and the third is what stops it pointing at art from a //! release nobody synced. use std::path::PathBuf; use std::process::Command; fn env(name: &str) -> Option { std::env::var_os(name).map(PathBuf::from) } fn build_index() -> serde_json::Value { let megamek = env("HELM_MEGAMEK").expect("HELM_MEGAMEK is not set"); let bridge = env("HELM_BRIDGE").expect("HELM_BRIDGE is not set"); let out = std::env::temp_dir().join("helm-index-test.json"); let status = Command::new(env!("CARGO_BIN_EXE_helm")) .arg("index") .arg("--megamek") .arg(&megamek) .arg("--bridge-dir") .arg(&bridge) .arg("--out") .arg(&out) .status() .expect("run helm index"); assert!(status.success(), "helm index failed"); let text = std::fs::read_to_string(&out).expect("read the index"); serde_json::from_str(&text).expect("the index is JSON") } #[test] #[ignore = "needs a MegaMek install and a bridge dump"] fn every_unit_is_priced_and_every_mek_is_drawn() { let doc = build_index(); let units = doc["units"].as_array().expect("units is an array"); assert!(units.len() > 5000, "only {} units", units.len()); let no_bv: Vec<&str> = units .iter() .filter(|u| u["bv"].is_null()) .filter_map(|u| u["name"].as_str()) .collect(); assert!(no_bv.is_empty(), "no battle value for {no_bv:?}"); // Every Mek is drawn. That was the whole file's promise while the index // held Meks alone, and widening it must not quietly cost any of them. let no_art: Vec<&str> = units .iter() .filter(|u| u["sprite"].is_null()) .filter_map(|u| u["name"].as_str()) .collect(); let meks_undrawn: Vec<&str> = units .iter() .filter(|u| u["sprite"].is_null() && u["unit_type"] == "Mek") .filter_map(|u| u["name"].as_str()) .collect(); assert!( meks_undrawn.is_empty(), "no sprite for Mek {meks_undrawn:?}" ); // The rest are designs MegaMek has no `mekset.txt` entry for either - it // draws them with a default silhouette, and so does anything reading // this. Asserted as a shape rather than a count, which moves with the // release: whatever is undrawn is a vehicle, never a Mek. let count = doc["counts"]["without_sprite"].as_u64().expect("a count"); assert_eq!( count as usize, no_art.len(), "the header's count disagrees with the rows" ); } #[test] #[ignore = "needs a MegaMek install and a bridge dump"] fn the_index_carries_everything_a_force_fields() { // A match fields tanks, helicopters, battle armour and platoons, and a // report draws all of them. Without these each whole class falls back to // one generic silhouette while its own art sits in the same bucket. let doc = build_index(); let units = doc["units"].as_array().unwrap(); let named = |name: &str| { units .iter() .find(|u| u["name"] == name) .unwrap_or_else(|| panic!("{name} is in the library")) }; let vedette = named("Vedette Medium Tank"); assert_eq!(vedette["unit_type"], "Tank"); assert_eq!(vedette["sprite"], "vehicles/Vedette.png"); assert!(vedette["bv"].as_i64().is_some_and(|bv| bv > 0)); // A suit's squad size is part of its name and part of its picture, so // this is also the check that the two are not being read apart. let elemental = named("Elemental Battle Armor [Laser](Sqd5)"); assert_eq!(elemental["unit_type"], "BattleArmor"); assert_eq!(elemental["sprite"], "battle armor/Elemental_5-CO-LA.png"); assert!(elemental["bv"].as_i64().is_some_and(|bv| bv > 0)); // And nothing else: a gun emplacement, a building and a handheld weapon // are all in the library and none of them is a design a force offers. // // The three support types joined the list when helm-bv learned to score // them; anything else appearing here means the index has widened past // what can be priced, and a browser would be handed a null. let kinds: std::collections::BTreeSet<&str> = units .iter() .filter_map(|u| u["unit_type"].as_str()) .collect(); assert_eq!( kinds, [ "BattleArmor", "Infantry", "LargeSupportTank", "Mek", "SupportTank", "SupportVTOL", "Tank", "VTOL", ] .into_iter() .collect(), "the index widened past what helm-bv can price" ); } #[test] #[ignore = "needs a MegaMek install and a bridge dump"] fn every_suit_is_drawn_as_itself() { // Battle armour is the one class MegaMek names a picture for every // design of - 1,184 of 1,184 - so a default silhouette appearing here // means the mekset resolution stopped working rather than that MegaMek // ran out of art. let doc = build_index(); let fallen_back: Vec<&str> = doc["units"] .as_array() .unwrap() .iter() .filter(|u| u["unit_type"] == "BattleArmor") .filter(|u| { u["sprite"].is_null() || u["sprite"] .as_str() .is_some_and(|s| s.starts_with("defaults/")) }) .filter_map(|u| u["name"].as_str()) .collect(); assert!( fallen_back.is_empty(), "no art of its own for {fallen_back:?}" ); } #[test] #[ignore = "needs a MegaMek install and a bridge dump"] fn the_index_says_which_release_it_describes() { let doc = build_index(); let version = doc["megamek"].as_str().expect("a megamek version"); assert!( version.starts_with(|c: char| c.is_ascii_digit()), "megamek is {version:?}" ); assert_eq!(doc["sprite_base"], "data/images/units"); assert!( doc["stats_producer"].as_str().is_some_and(|p| p != "none"), "no producer is named, so the battle values have no provenance" ); } #[test] #[ignore = "needs a MegaMek install and a bridge dump"] fn a_clan_design_keeps_both_of_its_names() { // Chassis and model are the pair a `.mul` records and the pair a force // record carries; the display name folds the Clan name in and is not a // key. Losing that split silently loses 434 designs to a lookup. let doc = build_index(); let units = doc["units"].as_array().unwrap(); let nova = units .iter() .find(|u| u["name"] == "Black Hawk (Nova) Prime") .expect("Black Hawk (Nova) Prime is in the library"); assert_eq!(nova["chassis"], "Black Hawk"); assert_eq!(nova["model"], "Prime"); assert_eq!(nova["sprite"], "meks/BlackHawk.png"); }