astrodynamics-gnss 0.9.0

GNSS domain layer (SP3, broadcast ephemeris, multi-GNSS single-point positioning, ionosphere/troposphere, DOP) built on the astrodynamics core
Documentation
//! CRINEX decoder tests: kernel edge cases on tiny inline strings, plus a
//! round-trip against a committed real `.crx` and its `crx2rnx`-decoded `.rnx`.

use super::*;

fn esbc_crx() -> String {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.crx"
    );
    std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read CRINEX fixture {path}: {e}"))
}

fn esbc_reference_rnx() -> String {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/fixtures/obs/ESBC00DNK_R_20201770000_01D_30S_MO_trim.rnx"
    );
    std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read RINEX fixture {path}: {e}"))
}

#[test]
fn numdiff_third_order_recovers_reference_sequence() {
    // The canonical Hatanaka NumDiff sequence (level 3, the RNX2CRX default).
    let mut diff = NumDiff::new(126_298_057_858, 3);
    assert_eq!(diff.decompress(-15_603_288), 126_282_454_570);
    assert_eq!(diff.decompress(521_089), 126_267_372_371);
    assert_eq!(diff.decompress(-752), 126_252_810_509);
    assert_eq!(diff.decompress(1_575_419_284), 127_814_188_268);
    assert_eq!(diff.decompress(-3_150_848_707), 127_800_656_941);

    // Arc reinitialization mid-stream.
    diff.force_init(111_982_965_979, 3);
    assert_eq!(diff.decompress(-16_266_911), 111_966_699_068);
    assert_eq!(diff.decompress(609_858), 111_951_042_015);
    assert_eq!(diff.decompress(-213), 111_935_994_607);
}

#[test]
fn textdiff_keeps_blanks_and_overwrites() {
    let mut diff = TextDiff::default();
    diff.force_init("ABCDEFG 12 000 33 XXACQmpLf");
    // Space keeps, non-space overwrites, '&' blanks.
    let out = diff.decompress("         3   1 44 xxACq   F");
    assert_eq!(out, "ABCDEFG 13 001 44 xxACqmpLF");
    // A '&' blanks the corresponding column.
    let out = diff.decompress("&");
    assert_eq!(out, " BCDEFG 13 001 44 xxACqmpLF");
}

#[test]
fn parse_reset_distinguishes_reset_from_delta() {
    assert_eq!(
        parse_reset("3&126298057858").unwrap(),
        Some((3, 126_298_057_858))
    );
    assert_eq!(parse_reset("  -15603288  ").unwrap(), None);
    assert!(parse_reset("9&1").is_err()); // order out of range
    assert!(parse_reset("x&1").is_err()); // bad order
}

#[test]
fn format_value_matches_rinex_f14_3() {
    assert_eq!(format_value(40_715_949_461), "  40715949.461");
    assert_eq!(format_value(-2_196), "        -2.196");
    // crx2rnx drops the leading zero only for a negative value in (-1, 0).
    assert_eq!(format_value(-920), "         -.920");
    assert_eq!(format_value(515), "         0.515");
    assert_eq!(format_value(0), "         0.000");
}

#[test]
fn decode_rejects_unknown_crinex_version() {
    let bad = "2.0                 COMPACT RINEX FORMAT                    CRINEX VERS   / TYPE\nRNX2CRX\n";
    let err = decode(bad).unwrap_err();
    assert!(matches!(err, Error::Parse(_)));
}

#[test]
fn decode_rejects_stream_without_crinex_header() {
    let err = decode("not a crinex file\n").unwrap_err();
    assert!(matches!(err, Error::Parse(_)));
}

#[test]
fn round_trip_matches_crx2rnx_reference_byte_for_byte() {
    let decoded = decode(&esbc_crx()).expect("decode CRINEX fixture");
    let reference = esbc_reference_rnx();

    // Compare line by line so a mismatch points at the offending record.
    let dec_lines: Vec<&str> = decoded.lines().collect();
    let ref_lines: Vec<&str> = reference.lines().collect();
    assert_eq!(
        dec_lines.len(),
        ref_lines.len(),
        "line count differs: decoded {} vs reference {}",
        dec_lines.len(),
        ref_lines.len()
    );
    for (i, (d, r)) in dec_lines.iter().zip(ref_lines.iter()).enumerate() {
        assert_eq!(
            d,
            r,
            "line {} differs\n  decoded:  {:?}\n  reference:{:?}",
            i + 1,
            d,
            r
        );
    }
}

fn algo_v1_crx() -> String {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/fixtures/obs/algo0010_2015001_v1_trim.crx"
    );
    std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read CRINEX v1 fixture {path}: {e}"))
}

fn algo_v1_reference_rnx() -> String {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/fixtures/obs/algo0010_2015001_v1_trim.rnx"
    );
    std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read RINEX v1 fixture {path}: {e}"))
}

#[test]
fn round_trip_v1_matches_crx2rnx_reference_byte_for_byte() {
    // CRINEX 1.0 (RINEX 2) path: a mixed GPS+GLONASS epoch carrying 20 satellites
    // (so the 12-satellite epoch-line wrap fires) with 8 observation types (the
    // five-observations-per-line wrap). Compared byte-for-byte against the
    // crx2rnx-decoded reference.
    let decoded = decode(&algo_v1_crx()).expect("decode CRINEX v1 fixture");
    let reference = algo_v1_reference_rnx();

    let dec_lines: Vec<&str> = decoded.lines().collect();
    let ref_lines: Vec<&str> = reference.lines().collect();
    assert_eq!(
        dec_lines.len(),
        ref_lines.len(),
        "line count differs: decoded {} vs reference {}",
        dec_lines.len(),
        ref_lines.len()
    );
    for (i, (d, r)) in dec_lines.iter().zip(ref_lines.iter()).enumerate() {
        assert_eq!(
            d,
            r,
            "line {} differs\n  decoded:  {:?}\n  reference:{:?}",
            i + 1,
            d,
            r
        );
    }
}