#![allow(clippy::too_many_lines)]
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
#![allow(clippy::trivially_copy_pass_by_ref)]
use astrogram::chart::{CoordinateSystem, EventType, HouseSystem, Zodiac};
use astrogram::sfcht::parse_file;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sfcht")
}
fn walk_specimens(dir: &Path, map: &mut std::collections::HashMap<String, PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
walk_specimens(&path, map);
} else if path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("sfcht"))
{
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
map.insert(stem.to_owned(), path);
}
}
}
}
fn specimens_map() -> Option<std::collections::HashMap<String, PathBuf>> {
let root = PathBuf::from(std::env::var_os("ASTRO_SPECIMENS")?);
let mut map = std::collections::HashMap::new();
walk_specimens(&root, &mut map);
Some(map)
}
fn event_type_u8(et: &EventType) -> u8 {
match et {
EventType::Unspecified => 0,
EventType::Male => 1,
EventType::Female => 2,
EventType::Event => 3,
EventType::Horary => 4,
}
}
fn house_system_u8(hs: &HouseSystem) -> u8 {
match hs {
HouseSystem::Campanus => 1,
HouseSystem::Koch => 2,
HouseSystem::Meridian => 3,
HouseSystem::Morinus => 4,
HouseSystem::Placidus => 5,
HouseSystem::Porphyry => 6,
HouseSystem::Regiomontanus => 7,
HouseSystem::Topocentric => 8,
HouseSystem::Equal => 9,
HouseSystem::ZeroAries => 10,
HouseSystem::SolarSign => 11,
HouseSystem::WholeSign => 26,
HouseSystem::HinduBhava => 27,
HouseSystem::Alcabitius => 28,
HouseSystem::Other(n) => *n,
}
}
fn zodiac_u8(z: &Zodiac) -> u8 {
match z {
Zodiac::Tropical => 1,
Zodiac::FaganAllen => 2,
Zodiac::Lahiri => 3,
Zodiac::DeLuce => 4,
Zodiac::Raman => 5,
Zodiac::UshaShashi => 6,
Zodiac::Krishnamurti => 7,
Zodiac::DjwhalKhul => 8,
Zodiac::Draconic => 9,
Zodiac::Svp => 10,
Zodiac::SriYukteswar => 11,
Zodiac::JnBhasin => 12,
Zodiac::LarryEly => 13,
Zodiac::TakraI => 14,
Zodiac::TakraII => 15,
Zodiac::SundaraRajan => 16,
Zodiac::ShillPond => 17,
Zodiac::Other(n) => *n,
}
}
fn coord_system_u8(cs: &CoordinateSystem) -> u8 {
match cs {
CoordinateSystem::Geocentric => 1,
CoordinateSystem::Heliocentric => 2,
}
}
fn opt_str(v: &Value) -> Option<&str> {
if v.is_null() { None } else { v.as_str() }
}
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn golden_sfcht_records() {
let fix_dir = fixtures_dir();
let Some(spec_map) = specimens_map() else {
eprintln!("ASTRO_SPECIMENS not set — skipping integration test");
return;
};
if !fix_dir.exists() {
eprintln!("fixtures dir absent; skipping");
return;
}
let mut files_checked = 0usize;
let mut records_checked = 0usize;
let mut fixture_paths: Vec<_> = fs::read_dir(&fix_dir)
.expect("read fixtures dir")
.filter_map(std::result::Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "json"))
.collect();
fixture_paths.sort();
for fix_path in &fixture_paths {
let fix_text = fs::read_to_string(fix_path).expect("read fixture");
let fix: Value = serde_json::from_str(&fix_text).expect("parse fixture json");
let stem = fix_path.file_stem().unwrap().to_string_lossy();
let Some(sfcht_path) = spec_map.get(stem.as_ref()) else {
eprintln!(" specimen missing for {stem}; skipping");
continue;
};
let bytes = fs::read(sfcht_path).expect("read specimen");
let (header, charts) =
parse_file(&bytes).unwrap_or_else(|e| panic!("{stem}: parse_file failed: {e}"));
let expected_count = fix["record_count"].as_u64().unwrap() as usize;
assert_eq!(
charts.len(),
expected_count,
"{stem}: expected {expected_count} charts, got {}",
charts.len()
);
assert_eq!(header.record_count as usize, expected_count);
let fix_records = fix["records"].as_array().unwrap();
for (i, (chart, fix_rec)) in charts.iter().zip(fix_records.iter()).enumerate() {
let ctx = format!("{stem} record[{i}] {:?}", chart.name);
assert_eq!(
chart.name.as_str(),
fix_rec["name"].as_str().unwrap_or(""),
"{ctx} name"
);
assert_eq!(
chart.secondary_name.as_deref(),
opt_str(&fix_rec["secondary_name"]),
"{ctx} secondary_name"
);
assert_eq!(
chart.city.as_deref(),
opt_str(&fix_rec["city"]),
"{ctx} city"
);
assert_eq!(
chart.region.as_deref(),
opt_str(&fix_rec["region"]),
"{ctx} region"
);
assert_eq!(
chart.tz_abbreviation.as_deref(),
opt_str(&fix_rec["tz_abbrev"]),
"{ctx} tz_abbrev"
);
assert_eq!(
chart.source_rating.as_deref(),
opt_str(&fix_rec["source_rating"]),
"{ctx} source_rating"
);
assert!(
approx_eq(
chart.longitude.degrees(),
fix_rec["longitude"].as_f64().unwrap()
),
"{ctx} longitude: {} vs {}",
chart.longitude.degrees(),
fix_rec["longitude"]
);
assert!(
approx_eq(
chart.latitude.degrees(),
fix_rec["latitude"].as_f64().unwrap()
),
"{ctx} latitude: {} vs {}",
chart.latitude.degrees(),
fix_rec["latitude"]
);
assert!(
approx_eq(
chart.tz_offset_hours,
fix_rec["tz_offset"].as_f64().unwrap()
),
"{ctx} tz_offset: {} vs {}",
chart.tz_offset_hours,
fix_rec["tz_offset"]
);
assert_eq!(
chart.year,
fix_rec["year"].as_i64().unwrap() as i16,
"{ctx} year"
);
assert_eq!(
chart.month,
fix_rec["month"].as_u64().unwrap() as u8,
"{ctx} month"
);
assert_eq!(
chart.day,
fix_rec["day"].as_u64().unwrap() as u8,
"{ctx} day"
);
assert_eq!(
chart.hour,
fix_rec["hour"].as_u64().unwrap() as u8,
"{ctx} hour"
);
assert_eq!(
chart.minute,
fix_rec["minute"].as_u64().unwrap() as u8,
"{ctx} minute"
);
assert_eq!(
chart.second,
fix_rec["second"].as_u64().unwrap() as u8,
"{ctx} second"
);
assert_eq!(
chart.is_lmt,
fix_rec["is_lmt"].as_bool().unwrap(),
"{ctx} is_lmt"
);
assert_eq!(
event_type_u8(&chart.event_type),
fix_rec["event_type"].as_u64().unwrap() as u8,
"{ctx} event_type"
);
assert_eq!(
house_system_u8(&chart.house_system),
fix_rec["house_system"].as_u64().unwrap() as u8,
"{ctx} house_system"
);
assert_eq!(
zodiac_u8(&chart.zodiac),
fix_rec["zodiac"].as_u64().unwrap() as u8,
"{ctx} zodiac"
);
assert_eq!(
coord_system_u8(&chart.coordinate_system),
fix_rec["coordinate_system"].as_u64().unwrap() as u8,
"{ctx} coordinate_system"
);
let fix_scs = fix_rec["sub_charts"].as_array().unwrap();
assert_eq!(
chart.sub_charts.len(),
fix_scs.len(),
"{ctx} sub_chart count"
);
for (si, (sc, fix_sc)) in chart.sub_charts.iter().zip(fix_scs.iter()).enumerate() {
let sc_ctx = format!("{ctx} sub[{si}]");
assert_eq!(
sc.name.as_str(),
fix_sc["name"].as_str().unwrap_or(""),
"{sc_ctx} name"
);
assert!(
approx_eq(
sc.longitude.degrees(),
fix_sc["longitude"].as_f64().unwrap()
),
"{sc_ctx} longitude"
);
assert!(
approx_eq(sc.latitude.degrees(), fix_sc["latitude"].as_f64().unwrap()),
"{sc_ctx} latitude"
);
assert!(
approx_eq(sc.tz_offset_hours, fix_sc["tz_offset"].as_f64().unwrap()),
"{sc_ctx} tz_offset"
);
assert_eq!(
sc.year,
fix_sc["year"].as_i64().unwrap() as i16,
"{sc_ctx} year"
);
assert_eq!(
sc.month,
fix_sc["month"].as_u64().unwrap() as u8,
"{sc_ctx} month"
);
assert_eq!(
sc.day,
fix_sc["day"].as_u64().unwrap() as u8,
"{sc_ctx} day"
);
assert_eq!(
sc.hour,
fix_sc["hour"].as_u64().unwrap() as u8,
"{sc_ctx} hour"
);
assert_eq!(
sc.minute,
fix_sc["minute"].as_u64().unwrap() as u8,
"{sc_ctx} minute"
);
assert_eq!(
sc.second,
fix_sc["second"].as_u64().unwrap() as u8,
"{sc_ctx} second"
);
assert_eq!(
sc.is_lmt,
fix_sc["is_lmt"].as_bool().unwrap(),
"{sc_ctx} is_lmt"
);
}
records_checked += 1;
}
eprintln!(" {stem}: {expected_count} records ok");
files_checked += 1;
}
if files_checked == 0 {
eprintln!("no fixture files found in {}; skipping", fix_dir.display());
return;
}
eprintln!("golden: {files_checked} files, {records_checked} records verified");
}