//! 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);
}