use std::fs;
use std::mem;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use sidereon_core::ephemeris::{
precise_interpolant_store_checksum64, MmapPreciseEphemerisInterpolant,
PreciseEphemerisInterpolant, PreciseInterpolantStoreError, Sp3,
};
use sidereon_core::{GnssSatelliteId, GnssSystem};
const COD_5M_FIXTURE: &str = "tests/fixtures/sp3/COD0MGXFIN_20201770000_01D_05M_ORB.SP3";
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(name)
}
fn temp_path(name: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time after epoch")
.as_nanos();
std::env::temp_dir().join(format!("{name}-{}-{nonce}", std::process::id()))
}
fn fixture_sp3() -> Sp3 {
let bytes = fs::read(fixture_path(COD_5M_FIXTURE)).expect("read SP3 fixture");
Sp3::parse(&bytes).expect("parse SP3 fixture")
}
fn gps(prn: u8) -> GnssSatelliteId {
GnssSatelliteId::new(GnssSystem::Gps, prn).expect("valid GPS satellite")
}
fn assert_state_bits_eq(
sat: GnssSatelliteId,
epoch_j2000_s: f64,
mapped: sidereon_core::ephemeris::Sp3State,
memory: sidereon_core::ephemeris::Sp3State,
) {
assert_eq!(
mapped.position.as_array().map(f64::to_bits),
memory.position.as_array().map(f64::to_bits),
"{sat} position bits differ at {epoch_j2000_s}"
);
assert_eq!(
mapped.clock_s.map(f64::to_bits),
memory.clock_s.map(f64::to_bits),
"{sat} clock bits differ at {epoch_j2000_s}"
);
}
#[test]
fn precise_interpolant_store_build_is_deterministic_for_same_product() {
let sp3 = fixture_sp3();
let first = PreciseEphemerisInterpolant::from_sp3(&sp3)
.to_mmap_store_bytes()
.expect("first artifact build");
let second = PreciseEphemerisInterpolant::from_sp3(&sp3)
.to_mmap_store_bytes()
.expect("second artifact build");
assert_eq!(first, second);
let mapped =
MmapPreciseEphemerisInterpolant::from_bytes(&first).expect("artifact opens from bytes");
assert_eq!(mapped.as_bytes().as_ptr(), first.as_ptr());
assert_eq!(
mapped.checksum64(),
precise_interpolant_store_checksum64(&first)
);
assert_eq!(mapped.as_bytes(), first.as_slice());
}
#[test]
fn mapped_precise_interpolant_matches_in_memory_bits_at_records_and_midpoints() {
let sp3 = fixture_sp3();
let memory = PreciseEphemerisInterpolant::from_sp3(&sp3);
let bytes = memory
.to_mmap_store_bytes()
.expect("build precise interpolant artifact");
let store_path = temp_path("precise-interpolant-store").with_extension("bin");
fs::write(&store_path, &bytes).expect("write artifact");
{
let mapped =
MmapPreciseEphemerisInterpolant::from_path(&store_path).expect("read artifact");
assert_eq!(mapped.time_scale(), memory.time_scale());
assert_eq!(&mapped.satellites()[..3], &[gps(1), gps(2), gps(3)]);
let epochs = sp3.epochs_j2000_seconds();
let mut queries = Vec::new();
queries.extend(epochs.iter().take(24).copied());
queries.extend(
epochs
.windows(2)
.take(24)
.map(|window| 0.5 * (window[0] + window[1])),
);
for sat in [gps(1), gps(2), gps(3)] {
for &epoch_j2000_s in &queries {
let got = mapped
.position_at_j2000_seconds(sat, epoch_j2000_s)
.expect("mapped evaluation");
let want = memory
.position_at_j2000_seconds(sat, epoch_j2000_s)
.expect("in-memory evaluation");
assert_state_bits_eq(sat, epoch_j2000_s, got, want);
}
}
}
fs::remove_file(store_path).expect("remove temp artifact");
}
#[test]
fn borrowed_precise_interpolant_store_rejects_unaligned_zero_copy_slice() {
let sp3 = fixture_sp3();
let bytes = sp3
.precise_interpolant_store_bytes()
.expect("build precise interpolant artifact");
let mut padded = vec![0u8; bytes.len() + mem::align_of::<f64>()];
for offset in 0..mem::align_of::<f64>() {
padded[offset..offset + bytes.len()].copy_from_slice(&bytes);
let candidate = &padded[offset..offset + bytes.len()];
if (candidate.as_ptr() as usize).is_multiple_of(mem::align_of::<f64>()) {
continue;
}
let err = MmapPreciseEphemerisInterpolant::from_bytes(candidate)
.expect_err("unaligned borrowed artifact must fail");
assert!(matches!(err, PreciseInterpolantStoreError::Parse { .. }));
return;
}
panic!("test allocator did not provide any unaligned candidate slice");
}
#[test]
fn precise_interpolant_store_rejects_corrupt_and_truncated_artifacts() {
let sp3 = fixture_sp3();
let bytes = sp3
.precise_interpolant_store_bytes()
.expect("build precise interpolant artifact");
let mut corrupt = bytes.clone();
let last = corrupt.len() - 1;
corrupt[last] ^= 0x80;
let err = MmapPreciseEphemerisInterpolant::from_bytes(&corrupt)
.expect_err("corrupt artifact must fail");
assert!(matches!(err, PreciseInterpolantStoreError::Checksum { .. }));
let truncated = &bytes[..bytes.len() - 1];
let err = MmapPreciseEphemerisInterpolant::from_bytes(truncated)
.expect_err("truncated artifact must fail");
assert!(matches!(
err,
PreciseInterpolantStoreError::Checksum { .. } | PreciseInterpolantStoreError::Parse { .. }
));
}