mod common;
use common::{append_extension, fits_file, fixture, write_temp_fits};
use fits_io::Fits;
use fits_io::fs::FsFits;
use fits_io::hdu::{BinTableHDU, ExtensionHDU};
use serde::Deserialize;
use std::error::Error;
type TestResult = Result<(), Box<dyn Error + Send + Sync>>;
fn row_bytes() -> Vec<u8> {
let mut row = Vec::new();
row.extend_from_slice(b"Vega "); row.push(b'T'); row.push(200); row.extend_from_slice(&(-1234_i16).to_be_bytes()); row.extend_from_slice(&70_000_i32.to_be_bytes()); row.extend_from_slice(&5_000_000_000_i64.to_be_bytes()); row.extend_from_slice(&1.5_f32.to_be_bytes()); row.extend_from_slice(&2.25_f64.to_be_bytes()); row
}
const ROW_LEN: usize = 10 + 1 + 1 + 2 + 4 + 8 + 4 + 8;
fn table_file(rows: usize) -> Vec<u8> {
let mut data = Vec::new();
for _ in 0..rows {
data.extend_from_slice(&row_bytes());
}
let mut file = fits_file(
&[
("SIMPLE", "T"),
("BITPIX", "8"),
("NAXIS", "0"),
("EXTEND", "T"),
],
&[],
);
let row_len = ROW_LEN.to_string();
let row_count = rows.to_string();
append_extension(
&mut file,
&[
("XTENSION", "'BINTABLE'"),
("BITPIX", "8"),
("NAXIS", "2"),
("NAXIS1", &row_len),
("NAXIS2", &row_count),
("PCOUNT", "0"),
("GCOUNT", "1"),
("TFIELDS", "8"),
("TFORM1", "'10A '"),
("TTYPE1", "'name '"),
("TFORM2", "'1L '"),
("TTYPE2", "'flag '"),
("TFORM3", "'1B '"),
("TTYPE3", "'tiny '"),
("TFORM4", "'1I '"),
("TTYPE4", "'small '"),
("TFORM5", "'1J '"),
("TTYPE5", "'medium '"),
("TFORM6", "'1K '"),
("TTYPE6", "'large '"),
("TFORM7", "'1E '"),
("TTYPE7", "'single '"),
("TFORM8", "'1D '"),
("TTYPE8", "'double '"),
],
&data,
);
file
}
fn rows<T: serde::de::DeserializeOwned + Send + Sync>(
name: &str,
count: usize,
) -> Result<Vec<T>, Box<dyn Error + Send + Sync>> {
let path = write_temp_fits(name, &table_file(count))?;
let fits = FsFits::open(&path)?;
let Some(ExtensionHDU::BinTable(table)) = fits.extension_hdu(0) else {
panic!("expected a binary table extension");
};
table.read_rows()
}
#[derive(Debug, Deserialize, PartialEq)]
struct EveryType {
name: String,
flag: bool,
tiny: u8,
small: i16,
medium: i32,
large: i64,
single: f32,
double: f64,
}
#[test]
fn deserializes_every_scalar_column_type() -> TestResult {
let rows: Vec<EveryType> = rows("every-type.fits", 3)?;
assert_eq!(rows.len(), 3);
for row in &rows {
assert_eq!(
row,
&EveryType {
name: "Vega".to_string(),
flag: true,
tiny: 200,
small: -1234,
medium: 70_000,
large: 5_000_000_000,
single: 1.5,
double: 2.25,
}
);
}
Ok(())
}
#[derive(Debug, Deserialize, PartialEq)]
struct JustTwo {
name: String,
double: f64,
}
#[test]
fn a_struct_may_name_a_subset_of_the_columns() -> TestResult {
let rows: Vec<JustTwo> = rows("subset.fits", 1)?;
assert_eq!(
rows,
vec![JustTwo {
name: "Vega".to_string(),
double: 2.25,
}]
);
Ok(())
}
#[derive(Debug, Deserialize, PartialEq)]
struct Optional {
name: Option<String>,
medium: Option<i32>,
}
#[test]
fn option_fields_read_the_column_value() -> TestResult {
let rows: Vec<Optional> = rows("optional.fits", 1)?;
assert_eq!(
rows,
vec![Optional {
name: Some("Vega".to_string()),
medium: Some(70_000),
}]
);
Ok(())
}
#[derive(Debug, Deserialize)]
struct Widened {
tiny: i64,
small: f64,
}
#[test]
fn narrow_columns_widen_into_larger_fields() -> TestResult {
let rows: Vec<Widened> = rows("widened.fits", 1)?;
assert_eq!(rows[0].tiny, 200);
assert_eq!(rows[0].small, -1234.0);
Ok(())
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct TooNarrow {
medium: u8,
}
#[test]
fn a_value_that_does_not_fit_the_field_is_an_error() -> TestResult {
let Err(error) = rows::<TooNarrow>("too-narrow.fits", 1) else {
panic!("70000 must not deserialize into a u8");
};
assert!(error.to_string().contains("does not fit"), "got: {error}");
Ok(())
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct WrongType {
name: i32,
}
#[test]
fn a_mismatched_column_type_is_an_error() -> TestResult {
let Err(error) = rows::<WrongType>("wrong-type.fits", 1) else {
panic!("a string column must not deserialize into an i32");
};
assert!(error.to_string().contains("invalid type"), "got: {error}");
Ok(())
}
#[test]
fn the_real_gaia_catalogue_still_deserializes() -> TestResult {
#[derive(Debug, Deserialize)]
struct GaiaCatalogEntry {
pub source_id: i64,
pub ra: f64,
pub dec: f64,
pub phot_g_mean_mag: f32,
}
let Some(path) = fixture("gaia-dr3-mag-gt-12.fits") else {
return Ok(());
};
let fits = FsFits::open(&path)?;
let Some(ExtensionHDU::BinTable(table)) = fits.extension_hdu(0) else {
panic!("expected a binary table extension");
};
let rows: Vec<GaiaCatalogEntry> = table.read_rows()?;
assert_eq!(rows.len(), 2_482_633);
let first = &rows[0];
assert!(first.source_id != 0);
assert!((-90.0..=90.0).contains(&first.dec), "dec {}", first.dec);
assert!((0.0..=360.0).contains(&first.ra), "ra {}", first.ra);
assert!(first.phot_g_mean_mag > 0.0);
Ok(())
}