158 lines
5.0 KiB
Rust
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(_))
|
|
));
|
|
}
|