//! Fixtures are trimmed from real MegaMek files. They keep the parts that are //! actually awkward to read - the clan name, the weapons count block, a crit //! section, a value with a second colon in it - rather than a tidy sample. use helm_unitfile::{Format, parse}; const ATLAS: &str = "\ # MegaMek Data (C) by The MegaMek Team, CC BY-NC-SA 4.0. generator:MegaMek Suite 0.51.0 chassis:Atlas model:AS7-D mul id:140 Config:Biped techbase:Inner Sphere era:2755 source:TR:3039 rules level:1 role:Juggernaut quirk:battle_fists_la quirk:command_mech mass:100 engine:300 Fusion Engine structure:IS Standard heat sinks:20 Single walk mp:3 jump mp:0 armor:Standard(Inner Sphere) LA armor:34 CT armor:47 RTC armor:14 Weapons:3 Medium Laser, Left Arm AC/20, Right Torso Medium Laser (R), Center Torso Left Arm: Shoulder Medium Laser -Empty- systemmanufacturer:CHASSIS:Foundation manufacturer:Yori 'Mech Works|Defiance Industries "; // The Clan-named case. MegaMek renders these "chassis (clanname) model", and // getting it wrong silently breaks every join against MegaMek's own naming. const RYOKEN: &str = "\ chassis:Ryoken III-XP clanname:Skinwalker model:B Config:Biped techbase:Clan era:3135 mass:55 walk mp:6 jump mp:0 armor:Standard(Clan) Weapons:0 "; const CONDOR: &str = "\ #Saved from version 0.51.0 Tank Condor HoverBall 2564 IS Level 2 Striker Hover 9 42 42 42 42 40 IS Ammo AC/5 ISCASE Machine Gun Autocannon/5 Machine Gun 50.0 "; #[test] fn reads_an_mtf() { let u = parse("meks/Atlas AS7-D.mtf", ATLAS.as_bytes()).expect("parses"); assert_eq!(u.format_str, Format::Mtf.as_str()); assert_eq!(u.chassis, "Atlas"); assert_eq!(u.model, "AS7-D"); assert_eq!(u.name, "Atlas AS7-D"); assert_eq!(u.mul_id, Some(140)); assert_eq!(u.unit_type.as_deref(), Some("Mek")); assert_eq!(u.config.as_deref(), Some("Biped")); assert_eq!(u.tech_base.as_deref(), Some("Inner Sphere")); assert_eq!(u.year, Some(2755)); assert_eq!(u.mass, Some(100.0)); assert_eq!(u.walk_mp, Some(3)); assert_eq!(u.role.as_deref(), Some("Juggernaut")); // "source:TR:3039" - the value keeps its own colon. assert_eq!(u.source.as_deref(), Some("TR:3039")); } #[test] fn armor_type_is_not_an_armor_location() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.armor.as_deref(), Some("Standard(Inner Sphere)")); assert_eq!(u.armor_locations.get("LA"), Some(&34)); assert_eq!(u.armor_locations.get("CT"), Some(&47)); assert_eq!(u.armor_locations.get("RTC"), Some(&14)); assert!(!u.armor_locations.contains_key("ARMOR")); } #[test] fn reads_the_weapons_block() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.equipment.len(), 3); assert_eq!(u.equipment[0].name, "Medium Laser"); assert_eq!(u.equipment[0].location.as_deref(), Some("Left Arm")); assert!(!u.equipment[0].rear); // The (R) suffix marks a rear mount and is not part of the name. assert_eq!(u.equipment[2].name, "Medium Laser"); assert!(u.equipment[2].rear); } #[test] fn reads_crit_slots_including_empties() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); let la = u.criticals.get("Left Arm").expect("left arm section"); assert_eq!(la, &["Shoulder", "Medium Laser", "-Empty-"]); } #[test] fn collects_quirks_rather_than_overwriting_them() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.quirks, vec!["battle_fists_la", "command_mech"]); } #[test] fn keeps_unclaimed_fields() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!( u.fields.get("manufacturer").map(String::as_str), Some("Yori 'Mech Works|Defiance Industries") ); // A repeated key joins rather than letting the last one win. assert!(u.fields.contains_key("systemmanufacturer")); } #[test] fn clan_name_goes_in_the_display_name() { let u = parse("meks/Ryoken III-XP B.mtf", RYOKEN.as_bytes()).unwrap(); assert_eq!(u.clan_name.as_deref(), Some("Skinwalker")); assert_eq!(u.name, "Ryoken III-XP (Skinwalker) B"); } #[test] fn reads_a_blk() { let u = parse("vehicles/Condor HoverBall.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.format_str, Format::Blk.as_str()); assert_eq!(u.chassis, "Condor HoverBall"); // An empty block leaves the name as the chassis alone. assert_eq!(u.model, ""); assert_eq!(u.name, "Condor HoverBall"); assert_eq!(u.unit_type.as_deref(), Some("Tank")); assert_eq!(u.year, Some(2564)); assert_eq!(u.mass, Some(50.0)); assert_eq!(u.motion_type.as_deref(), Some("Hover")); assert_eq!(u.walk_mp, Some(9)); } #[test] fn blk_equipment_blocks_carry_their_location() { let u = parse("a.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.equipment.len(), 5); let turret: Vec<&str> = u .equipment .iter() .filter(|m| m.location.as_deref() == Some("Turret")) .map(|m| m.name.as_str()) .collect(); assert_eq!(turret, vec!["Machine Gun", "Autocannon/5", "Machine Gun"]); } #[test] fn blk_armor_array_gets_location_names() { let u = parse("a.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.armor_locations.get("FRONT"), Some(&42)); assert_eq!(u.armor_locations.get("TURRET"), Some(&40)); assert_eq!(u.armor_locations.values().sum::(), 208); } // A shipped file opens its block with a blank line. Joining the // block body verbatim made its unit type "\nTank", which then matched no // filter and quietly escaped every unit-type exclusion. #[test] fn blank_lines_around_a_block_body_are_not_part_of_the_value() { let blk = "\n\nTank\n\n\n\n\n\nScrapper\n\n"; let u = parse("a.blk", blk.as_bytes()).unwrap(); assert_eq!(u.unit_type.as_deref(), Some("Tank")); assert_eq!(u.chassis, "Scrapper"); } #[test] fn rejects_a_file_with_no_unit_in_it() { assert!(parse("empty.mtf", b"# just a comment\n").is_err()); assert!(parse("notes.txt", b"chassis:Atlas\n").is_err()); } /// A patchwork design names an armour type per location, in front of the /// points: `LT armor:Clan Standard(Clan):26`. /// /// Reading this wrongly is silent and total. Insisting on a bare integer drops /// every location of every patchwork design, so the unit parses fine and comes /// out with almost no armour - which was true of all five of them in MegaMek /// 0.51.0 until this was fixed, and moved total armour from 28 to 247 on a /// Zeus-X. const PATCHWORK: &str = "\ chassis:Zeus-X model:ZEU-X mass:80 armor:Patchwork LA armor:IS Reactive(Inner Sphere):26 RA armor:IS Reactive(Inner Sphere):26 CT armor:Clan Standard(Clan):38 HD armor:9 "; #[test] fn patchwork_armor_keeps_both_its_points_and_its_type() { let u = parse("x.mtf", PATCHWORK.as_bytes()).unwrap(); assert_eq!(u.armor.as_deref(), Some("Patchwork")); assert_eq!(u.armor_locations.get("LA"), Some(&26)); assert_eq!(u.armor_locations.get("CT"), Some(&38)); assert_eq!(u.total_armor(), 99); assert_eq!( u.armor_types.get("LA").map(String::as_str), Some("IS Reactive(Inner Sphere)") ); assert_eq!(u.armor_kind_at("LA"), helm_core::ArmorKind::Reactive); assert_eq!(u.armor_kind_at("CT"), helm_core::ArmorKind::Standard); // A location that names no type falls back to the design's, which for a // patchwork design is Patchwork itself rather than a real armour. assert_eq!(u.armor_kind_at("HD"), helm_core::ArmorKind::Patchwork); assert!(!u.armor_types.contains_key("HD")); } /// The ordinary case must not have grown a type map. #[test] fn a_design_with_one_armour_type_records_no_per_location_types() { let u = parse("x.mtf", ATLAS.as_bytes()).unwrap(); assert!(u.armor_types.is_empty()); assert_eq!(u.armor_kind_at("LA"), helm_core::ArmorKind::Standard); }