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