use std::path::{Path, PathBuf};
use whiteout::m3;
use whiteout::math::Vector3f;
#[test]
fn m3_model_scalar_and_string_fields() {
let mut m = m3::Model::new();
m.set_name("TestM3");
assert_eq!(m.name(), "TestM3");
}
#[test]
fn m3_bitflag_fields_compose() {
let mut m = m3::Model::new();
let flags = m.flags();
let combined = flags | m3::ModelFlag::default();
assert!(combined.contains(m3::ModelFlag::default()));
m.set_flags(combined);
}
#[test]
fn m3_vector_elements_are_borrowed() {
let mut m = m3::Model::new();
m.resize_sequences(2);
assert_eq!(m.sequences_len(), 2);
assert!(m.sequences(0).is_some());
assert!(m.sequences(2).is_none());
}
#[test]
fn m3_synthesised_model_round_trips() {
let mut m = m3::Model::new();
m.set_name("RoundTrip");
let bytes = m3::Writer::new().write(&m);
if bytes.is_empty() {
return;
}
let back = m3::Parser::new().parse(&bytes).expect("re-parse failed");
assert_eq!(back.name(), "RoundTrip");
}
fn corpus_dir(name: &str) -> Option<PathBuf> {
for prefix in ["", "../", "../../", "../../../"] {
let p = PathBuf::from(format!("{prefix}Corpus/{name}"));
if p.is_dir() {
return Some(p);
}
}
None
}
fn collect(dir: &Path, ext: &str, out: &mut Vec<PathBuf>, limit: usize) {
if out.len() >= limit {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
let mut entries: Vec<_> = entries.flatten().map(|e| e.path()).collect();
entries.sort(); for p in entries {
if out.len() >= limit {
return;
}
if p.is_dir() {
collect(&p, ext, out, limit);
} else if p.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext)) {
out.push(p);
}
}
}
fn corpus_limit(default: usize) -> usize {
std::env::var("WHITEOUT_CORPUS_LIMIT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn m3_round_trip_corpus(name: &str) {
let Some(dir) = corpus_dir(name) else {
eprintln!("skipping: Corpus/{name} not present");
return;
};
let mut files = Vec::new();
collect(&dir, "m3", &mut files, corpus_limit(200));
assert!(!files.is_empty(), "Corpus/{name} held no .m3 files");
let (mut checked, mut skipped) = (0usize, 0usize);
for path in &files {
let Ok(src) = std::fs::read(path) else {
continue;
};
let Some(model) = m3::Parser::new().parse(&src) else {
skipped += 1;
continue;
};
let first = m3::Writer::new().write(&model);
if first.is_empty() {
skipped += 1;
continue;
}
let reparsed = m3::Parser::new()
.parse(&first)
.unwrap_or_else(|| panic!("{}: re-parse of our own output failed", path.display()));
let second = m3::Writer::new().write(&reparsed);
assert_eq!(
first.as_ref(),
second.as_ref(),
"{}: write→parse→write was not byte-identical ({} vs {} bytes)",
path.display(),
first.len(),
second.len()
);
checked += 1;
}
eprintln!(
"{name}: {checked} round-tripped, {skipped} skipped, {} total",
files.len()
);
assert!(checked > 0, "no {name} file survived a round-trip");
}
#[test]
fn m3_sc2_corpus_round_trips() {
m3_round_trip_corpus("Sc2M3");
}
#[test]
fn m3_hots_corpus_round_trips() {
m3_round_trip_corpus("HotSM3");
}
#[test]
fn m3_corpus_fields_are_readable() {
let Some(dir) = corpus_dir("Sc2M3") else {
eprintln!("skipping: Corpus/Sc2M3 not present");
return;
};
let mut files = Vec::new();
collect(&dir, "m3", &mut files, 100);
let mut divisions = 0usize;
for path in &files {
let Ok(src) = std::fs::read(path) else {
continue;
};
let Some(m) = m3::Parser::new().parse(&src) else {
continue;
};
let _ = m.name();
let _ = m.flags();
for s in m.sequences_iter() {
let _ = s.name();
}
divisions += m.divisions_len();
}
eprintln!(
"Sc2M3: read {divisions} divisions across {} files",
files.len()
);
}
#[test]
fn m2_model_fields_are_reachable() {
use whiteout::m2;
let mut m = m2::Model::new();
m.set_model_name("TestM2");
assert_eq!(m.model_name(), "TestM2");
m.resize_global_loops(2);
assert_eq!(m.global_loops_len(), 2);
assert!(m.global_loops(0).is_some());
assert!(m.global_loops(2).is_none());
}
#[test]
fn m2_nested_vector_tracks_are_per_sequence_slices() {
use whiteout::m2;
let mut t = m2::AnimationTrackVector3f::new();
t.resize_timestamps(2);
assert_eq!(t.timestamps_len(), 2);
t.set_timestamps(0, &[0u32, 100, 200]);
assert_eq!(t.timestamps(0), &[0u32, 100, 200]);
t.resize_values(2);
t.set_values(
0,
&[Vector3f::new(1.0, 2.0, 3.0), Vector3f::new(4.0, 5.0, 6.0)],
);
assert_eq!(t.values(0).len(), 2);
assert_eq!(t.values(0)[1], Vector3f::new(4.0, 5.0, 6.0));
assert!(t.timestamps(99).is_empty());
}
#[test]
fn m2_nested_vector_mutation_is_in_place() {
use whiteout::m2;
let mut t = m2::AnimationTrackF32::new();
t.resize_values(1);
t.set_values(0, &[1.0f32, 2.0, 3.0]);
t.values_mut(0)[1] = 9.5;
assert_eq!(t.values(0), &[1.0f32, 9.5, 3.0]);
}
#[test]
fn m2_nested_vector_of_handles_borrows_each_element() {
use whiteout::m2;
let mut t = m2::AnimationTrackM2CameraSpline::new();
t.resize_values(2);
assert_eq!(t.values_len(), 2);
t.resize_values_inner(0, 3);
assert_eq!(t.values_inner_len(0), 3);
assert!(t.values(0, 0).is_some());
assert!(t.values(0, 3).is_none(), "out of range must be None");
assert!(t.values(9, 0).is_none(), "out-of-range outer must be None");
t.values_mut(0, 1)
.expect("elem")
.set_value(Vector3f::new(1.0, 2.0, 3.0));
assert_eq!(
t.values(0, 1).expect("elem").value(),
Vector3f::new(1.0, 2.0, 3.0)
);
}