Files
astrokit/tests/reference_format.rs
T
2026-10-02 20:46:01 -04:00

158 lines
5.0 KiB
Rust

//! The reference-trajectory format: that it round trips, and that it refuses a fixture
//! which would otherwise produce a number looking like a result.
//!
//! No fixture files are committed yet — generating them needs a JVM, which this machine
//! does not have. What is tested here is the format itself, which a generator has to
//! conform to.
mod common;
use common::{FixtureError, ReferenceArc, ReferenceSetup, ReferenceState};
fn setup() -> ReferenceSetup {
ReferenceSetup {
tool: "orekit".to_string(),
version: "13.1.8".to_string(),
generated: "2026-10-02".to_string(),
forces: vec!["HolmesFeatherstoneAttractionModel(EGM96, 70, 70)".to_string()],
integrator: "DormandPrince853, abs 1e-12, rel 1e-12".to_string(),
notes: vec!["EOP: none applied".to_string()],
}
}
fn arc(states: Vec<ReferenceState>) -> ReferenceArc {
ReferenceArc {
name: "low-earth-one-day".to_string(),
setup: setup(),
epoch: "2026-01-01T00:00:00 UTC".to_string(),
frame: "EME2000".to_string(),
states,
}
}
fn state(elapsed: f64) -> ReferenceState {
ReferenceState {
elapsed,
position: [6.978e6, 0.0, 0.0],
velocity: [0.0, 7.56e3, 0.0],
}
}
fn write(arc: &ReferenceArc) -> tempfile_path::Written {
tempfile_path::write(&serde_json::to_string_pretty(arc).unwrap())
}
/// A minimal stand-in for a temporary file, so the test needs no extra dependency.
mod tempfile_path {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct Written(PathBuf);
impl Written {
pub fn path(&self) -> &Path {
&self.0
}
}
impl Drop for Written {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub fn write(text: &str) -> Written {
static COUNT: AtomicUsize = AtomicUsize::new(0);
let path = std::env::temp_dir().join(format!(
"astrokit-fixture-{}-{}.json",
std::process::id(),
COUNT.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, text).expect("the temporary directory is writable");
Written(path)
}
}
#[test]
fn test_an_arc_round_trips_through_the_format() {
let original = arc(vec![state(0.0), state(60.0), state(120.0)]);
let written = write(&original);
assert_eq!(ReferenceArc::load(written.path()).unwrap(), original);
}
#[test]
fn test_the_setup_is_carried_verbatim() {
// The point of the block: it records what the *other* tool was told, in that tool's
// own words, so a mismatch is visible rather than translated away.
let original = arc(vec![state(0.0), state(60.0)]);
let written = write(&original);
let loaded = ReferenceArc::load(written.path()).unwrap();
assert_eq!(loaded.setup.tool, "orekit");
assert_eq!(loaded.setup.version, "13.1.8");
assert!(loaded.setup.forces[0].contains("EGM96"));
assert!(loaded.setup.integrator.contains("1e-12"));
assert_eq!(loaded.duration(), 60.0);
}
#[test]
fn test_a_fixture_that_would_mislead_is_refused() {
let unusable = |arc: ReferenceArc, expected: &str| {
let written = write(&arc);
match ReferenceArc::load(written.path()) {
Err(FixtureError::Invalid(why)) => {
assert!(why.contains(expected), "expected {expected:?} in {why:?}")
}
other => panic!("should have been refused: {other:?}"),
}
};
// One state is not an arc.
unusable(arc(vec![state(0.0)]), "1 states");
// Not starting at its own epoch means every comparison is offset by an unknown.
unusable(
arc(vec![state(30.0), state(60.0)]),
"rather than at its own epoch",
);
// Times that do not advance mean the states cannot be matched to anything.
unusable(
arc(vec![state(0.0), state(60.0), state(60.0)]),
"not increasing",
);
// An arc with no recorded force model cannot be matched, so it cannot be compared.
let mut no_forces = arc(vec![state(0.0), state(60.0)]);
no_forces.setup.forces.clear();
unusable(no_forces, "records no force model");
}
/// A non-finite state cannot survive the format at all: JSON has no NaN, so it is written
/// as `null` and refused on the way back in.
#[test]
fn test_a_non_finite_state_cannot_round_trip() {
let mut broken = arc(vec![state(0.0), state(60.0)]);
broken.states[1].position[0] = f64::NAN;
let written = write(&broken);
assert!(matches!(
ReferenceArc::load(written.path()),
Err(FixtureError::Malformed(_))
));
}
#[test]
fn test_a_malformed_file_is_told_apart_from_an_unusable_one() {
let written = tempfile_path::write("{\"name\": \"half a fixture\"}");
assert!(matches!(
ReferenceArc::load(written.path()),
Err(FixtureError::Malformed(_))
));
let missing = std::path::Path::new("tests/fixtures/orekit/not-generated-yet.json");
assert!(matches!(
ReferenceArc::load(missing),
Err(FixtureError::Unreadable(_))
));
}