pg-orrery-catalog/tests/test_output.py
Ryan Malloy 1d36729bed Implement pg-orrery-catalog: TLE catalog builder for pg_orrery
Core modules:
- tle.py: NORAD decoding (Alpha-5 + Super-5, matching get_el.c),
  3LE/2LE parsing, TLERecord dataclass with epoch-based dedup
- config.py: TOML config + env var overlay (XDG-compliant paths)
- cache.py: File-based cache with staleness checking
- catalog.py: Multi-source merge with MergeStats tracking
- regime.py: LEO/MEO/GEO/HEO classification by mean motion

Source downloaders (httpx):
- celestrak.py: Active catalog + supplemental GP groups
- satnogs.py: JSON API with 3LE conversion
- spacetrack.py: POST auth flow, bulk GP download

Output formatters:
- sql.py: pg_orrery-compatible INSERT generation (E'' strings)
- tle_file.py: Standard 3LE text output
- json_out.py: JSON with orbital metadata and regime

CLI (Click + Rich):
- download: Cache TLEs from all sources
- build: Merge + output SQL/3LE/JSON (pipes to psql)
- load: Direct DB load via psycopg (optional [pg] extra)
- info: Cache stats and configuration display

58 tests covering NORAD decoding (all 4 encoding cases),
parsing, merge/dedup, SQL escaping, regime classification.
2026-02-18 00:31:46 -07:00

125 lines
4.2 KiB
Python

"""Tests for output formatters (SQL, 3LE, JSON)."""
import json
from pg_orrery_catalog.output.json_out import generate_json
from pg_orrery_catalog.output.sql import escape_sql_string, generate_sql
from pg_orrery_catalog.output.tle_file import generate_3le
from pg_orrery_catalog.tle import TLERecord
def _make_record(norad_id: int, name: str = "", epoch: float = 24001.0) -> TLERecord:
line1 = f"1 {norad_id:05d}U 98067A {epoch:014.8f} .00000000 00000-0 00000-0 0 9990"
line2 = f"2 {norad_id:05d} 51.6400 100.0000 0007417 30.0000 330.1234 15.49456789999990"
return TLERecord(
line1=line1, line2=line2, name=name or f"SAT-{norad_id}",
norad_id=norad_id, epoch=epoch,
)
class TestSQLEscaping:
def test_single_quote(self):
assert escape_sql_string("it's") == "it''s"
def test_backslash(self):
assert escape_sql_string("a\\b") == "a\\\\b"
def test_both(self):
assert escape_sql_string("it's a\\b") == "it''s a\\\\b"
def test_clean(self):
assert escape_sql_string("HELLO") == "HELLO"
class TestSQLGeneration:
def test_basic_output(self):
records = {25544: _make_record(25544, "ISS (ZARYA)")}
sql = generate_sql(records, table="test_table")
assert "DROP TABLE IF EXISTS test_table;" in sql
assert "CREATE TABLE IF NOT EXISTS test_table" in sql
assert "INSERT INTO test_table" in sql
assert "ISS (ZARYA)" in sql
assert "E'" in sql # escape string syntax
assert "\\n" in sql # newline between TLE lines
def test_no_drop(self):
records = {1: _make_record(1)}
sql = generate_sql(records, table="t", drop_existing=False)
assert "DROP TABLE" not in sql
def test_sorted_by_norad(self):
records = {
99999: _make_record(99999),
1: _make_record(1),
25544: _make_record(25544),
}
sql = generate_sql(records)
lines = [line for line in sql.split("\n") if line.startswith("INSERT")]
assert len(lines) == 3
# First INSERT should be NORAD 1 (lowest)
assert "SAT-1" in lines[0]
def test_name_escaping(self):
records = {1: _make_record(1, "O'BRIEN SAT")}
sql = generate_sql(records)
assert "O''BRIEN SAT" in sql
def test_default_name(self):
"""When TLERecord has no name, SQL output uses 'NORAD {id}'."""
rec = _make_record(1)
rec.name = "" # clear the name to test fallback
records = {1: rec}
sql = generate_sql(records)
assert "NORAD 1" in sql
class TestTLEFileGeneration:
def test_3le_format(self):
records = {25544: _make_record(25544, "ISS (ZARYA)")}
text = generate_3le(records)
lines = text.strip().split("\n")
assert len(lines) == 3
assert lines[0].startswith("0 ")
assert lines[1].startswith("1 ")
assert lines[2].startswith("2 ")
def test_sorted_output(self):
records = {
2: _make_record(2, "B"),
1: _make_record(1, "A"),
}
text = generate_3le(records)
lines = text.strip().split("\n")
assert "A" in lines[0]
def test_name_prefix(self):
records = {1: _make_record(1, "0 ALREADY PREFIXED")}
text = generate_3le(records)
# Should not double-prefix
assert "0 0 ALREADY PREFIXED" not in text
assert "0 ALREADY PREFIXED" in text
class TestJSONGeneration:
def test_valid_json(self):
records = {25544: _make_record(25544, "ISS")}
text = generate_json(records)
data = json.loads(text)
assert isinstance(data, list)
assert len(data) == 1
assert data[0]["norad_id"] == 25544
assert data[0]["name"] == "ISS"
def test_fields_present(self):
records = {1: _make_record(1)}
data = json.loads(generate_json(records))
entry = data[0]
for field in ("norad_id", "name", "line1", "line2", "epoch", "mean_motion",
"inclination", "eccentricity", "regime", "source"):
assert field in entry
def test_regime_classification(self):
records = {1: _make_record(1)} # mean_motion ~15.49 → LEO
data = json.loads(generate_json(records))
assert data[0]["regime"] == "LEO"