use super::*;
use crate::reader::FitsReader;
use bitvec::bitvec;
use std::fs::File;
fn table_header(naxis1: usize, naxis2: usize, tforms: &[&str]) -> Header {
let mut h = Header::new();
h.set("XTENSION", "BINTABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", naxis1 as i64)
.set("NAXIS2", naxis2 as i64)
.set("PCOUNT", 0)
.set("GCOUNT", 1)
.set("TFIELDS", tforms.len() as i64);
for (i, tform) in tforms.iter().enumerate() {
h.set(&format!("TFORM{}", i + 1), *tform);
}
h
}
fn tform(repeat: usize, kind: TformKind, vla_elem: Option<TformKind>) -> Tform {
Tform {
repeat,
kind,
vla_elem,
}
}
#[test]
fn parses_tform_repeat_and_kind() {
let cases = [
("8A", tform(8, TformKind::Char, None)),
("3D", tform(3, TformKind::F64, None)),
("0D", tform(0, TformKind::F64, None)),
("1J", tform(1, TformKind::I32, None)),
("E", tform(1, TformKind::F32, None)), ("16X", tform(16, TformKind::Bit, None)),
(
"1PE(5)",
tform(1, TformKind::ArrayDesc32, Some(TformKind::F32)),
),
(
"1QD",
tform(1, TformKind::ArrayDesc64, Some(TformKind::F64)),
),
];
for (s, expected) in cases {
assert_eq!(Tform::parse(s).unwrap(), expected, "{s}");
}
for bad in ["9Z", "", "1P", "2PE(5)", "3QD"] {
assert!(
matches!(Tform::parse(bad), Err(FitsError::InvalidTform { .. })),
"{bad}"
);
}
}
#[test]
fn theap_below_the_main_table_is_rejected() {
let mut header = table_header(4, 2, &["1J"]); header.set("PCOUNT", 4).set("THEAP", 4); assert!(matches!(
BinTable::from_data(&header, vec![0u8; 12]),
Err(FitsError::KeywordOutOfRange { name: "THEAP" })
));
}
#[test]
fn byte_width_handles_arrays_bits_and_descriptors() {
assert_eq!(Tform::parse("8A").unwrap().byte_width(), 8);
assert_eq!(Tform::parse("3D").unwrap().byte_width(), 24);
assert_eq!(Tform::parse("0D").unwrap().byte_width(), 0);
assert_eq!(Tform::parse("16X").unwrap().byte_width(), 2); assert_eq!(Tform::parse("9X").unwrap().byte_width(), 2); assert_eq!(Tform::parse("1PB").unwrap().byte_width(), 8); assert_eq!(Tform::parse("1QB").unwrap().byte_width(), 16); }
#[test]
fn decodes_fixed_width_columns_from_hand_built_data() {
let header = table_header(15, 2, &["1J", "2E", "3A"]);
let mut data = Vec::new();
for (j, e0, e1, text) in [(1i32, 1.0f32, 2.0f32, b"ABC"), (2, 3.0, 4.0, b"DE ")] {
data.extend_from_slice(&j.to_be_bytes());
data.extend_from_slice(&e0.to_be_bytes());
data.extend_from_slice(&e1.to_be_bytes());
data.extend_from_slice(text);
}
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(table.nrows, 2);
assert_eq!(
table
.columns
.iter()
.map(|c| c.byte_offset)
.collect::<Vec<_>>(),
vec![0, 4, 12]
);
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::I32(vec![1, 2])
);
assert_eq!(
table.column_by_idx(1).unwrap().raw().unwrap(),
ColumnData::F32(vec![1.0, 2.0, 3.0, 4.0])
);
assert_eq!(
table.column_by_idx(2).unwrap().raw().unwrap(),
ColumnData::Text(vec!["ABC".into(), "DE".into()]) );
}
#[test]
fn zero_repeat_column_decodes_to_empty() {
let header = table_header(4, 1, &["0D", "1J"]);
let data = 7i32.to_be_bytes().to_vec();
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::F64(vec![])
);
assert_eq!(
table.column_by_idx(1).unwrap().raw().unwrap(),
ColumnData::I32(vec![7])
);
}
#[test]
fn read_column_physical_applies_tscal_tzero_and_tnull() {
let mut header = table_header(2, 3, &["1I"]); header
.set("TSCAL1", 2.0)
.set("TZERO1", 10.0)
.set("TNULL1", 5);
let mut data = Vec::new();
for x in [3i16, 5, 7] {
data.extend_from_slice(&x.to_be_bytes());
}
let table = BinTable::from_data(&header, data).unwrap();
let phys = table.column_by_idx(0).unwrap().physical().unwrap();
assert_eq!(phys[0], 16.0);
assert!(phys[1].is_nan());
assert_eq!(phys[2], 24.0);
}
#[test]
fn read_column_physical_rejects_non_numeric_columns() {
let header = table_header(3, 1, &["3A"]);
let table = BinTable::from_data(&header, b"abc".to_vec()).unwrap();
assert!(matches!(
table.column_by_idx(0).unwrap().physical(),
Err(FitsError::NonNumericColumn { code: 'A' })
));
}
#[test]
fn read_column_on_a_vla_directs_to_read_vla_column() {
let header = table_header(8, 1, &["1PE(3)"]);
let table = BinTable::from_data(&header, vec![0u8; 8]).unwrap();
assert!(matches!(
table.column_by_idx(0).unwrap().raw(),
Err(FitsError::VariableLengthColumn { code: 'P' })
));
}
#[test]
fn decodes_variable_length_arrays_from_the_heap() {
let mut header = table_header(8, 2, &["1PE(3)"]);
header.set("PCOUNT", 12); let mut data = Vec::new();
for (nelem, offset) in [(2i32, 0i32), (1, 8)] {
data.extend_from_slice(&nelem.to_be_bytes());
data.extend_from_slice(&offset.to_be_bytes());
}
for x in [1.0f32, 2.0, 3.0] {
data.extend_from_slice(&x.to_be_bytes());
}
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().vla().unwrap(),
vec![ColumnData::F32(vec![1.0, 2.0]), ColumnData::F32(vec![3.0]),]
);
}
#[test]
fn parses_tdisp_display_formats() {
use TDispKind::*;
let d = |kind, width, decimals, exponent| TDisp {
kind,
width,
decimals,
exponent,
};
let cases = [
("I5", d(Integer, 5, None, None)),
("F8.2", d(Float, 8, Some(2), None)),
("E12.5E3", d(Exponential, 12, Some(5), Some(3))),
("ES15.6", d(Scientific, 15, Some(6), None)),
("EN10.3", d(Engineering, 10, Some(3), None)),
("A20", d(Char, 20, None, None)),
("Z8", d(Hex, 8, None, None)),
];
for (s, want) in cases {
assert_eq!(TDisp::parse(s), Some(want), "{s}");
}
assert_eq!(TDisp::parse("Q5"), None); assert_eq!(TDisp::parse("F"), None); let mut header = table_header(4, 1, &["1J"]);
header.set("TDISP1", "I5");
let table = BinTable::from_data(&header, vec![0u8; 4]).unwrap();
assert_eq!(table.columns[0].tdisp, Some(d(Integer, 5, None, None)));
}
#[test]
fn read_column_complex_widens_and_scales() {
let mut header = table_header(8, 1, &["1C"]);
header.set("TSCAL1", 2.0).set("TZERO1", 1.0);
let mut data = Vec::new();
data.extend_from_slice(&3.0f32.to_be_bytes());
data.extend_from_slice(&4.0f32.to_be_bytes());
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().complex().unwrap(),
vec![Complex { re: 7.0, im: 9.0 }] );
let h2 = table_header(4, 1, &["1J"]);
let t2 = BinTable::from_data(&h2, vec![0u8; 4]).unwrap();
assert!(matches!(
t2.column_by_idx(0).unwrap().complex(),
Err(FitsError::NotAComplexColumn { code: 'J' })
));
}
#[test]
fn read_column_unsigned_recovers_typed_values() {
let mut header = table_header(3, 1, &["1I", "1B"]);
header.set("TZERO1", 32768.0).set("TZERO2", -128.0);
let mut data = Vec::new();
data.extend_from_slice(&((50000u16 ^ 0x8000) as i16).to_be_bytes());
data.push(((-10i8) as u8) ^ 0x80);
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().unsigned().unwrap(),
Some(UnsignedView::U16(vec![50000]))
);
assert_eq!(
table.column_by_idx(1).unwrap().unsigned().unwrap(),
Some(UnsignedView::I8(vec![-10]))
);
}
#[test]
fn read_column_unsigned_is_exact_for_u64_and_none_otherwise() {
let mut header = table_header(12, 1, &["1K", "1J"]);
header.set("TZERO1", 9_223_372_036_854_775_808.0); let mut data = Vec::new();
data.extend_from_slice(&((u64::MAX ^ 0x8000_0000_0000_0000) as i64).to_be_bytes());
data.extend_from_slice(&7i32.to_be_bytes());
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_idx(0).unwrap().unsigned().unwrap(),
Some(UnsignedView::U64(vec![u64::MAX]))
);
assert_eq!(
table.column_by_idx(1).unwrap().unsigned().unwrap(),
None );
}
#[test]
fn read_vla_column_physical_scales_heap_arrays_and_nulls() {
let mut header = table_header(8, 2, &["1PJ(2)"]);
header
.set("PCOUNT", 12)
.set("TSCAL1", 2.0)
.set("TZERO1", 10.0)
.set("TNULL1", 99);
let mut data = Vec::new();
for (nelem, offset) in [(2i32, 0i32), (1, 8)] {
data.extend_from_slice(&nelem.to_be_bytes());
data.extend_from_slice(&offset.to_be_bytes());
}
for x in [5i32, 99, 3] {
data.extend_from_slice(&x.to_be_bytes());
}
let table = BinTable::from_data(&header, data).unwrap();
let phys = table.column_by_idx(0).unwrap().vla_physical().unwrap();
assert_eq!(phys[0][0], 20.0); assert!(phys[0][1].is_nan()); assert_eq!(phys[1], vec![16.0]); }
#[test]
fn vla_descriptor_overrunning_the_heap_is_rejected() {
let mut header = table_header(8, 1, &["1PE(3)"]);
header.set("PCOUNT", 8);
let mut data = Vec::new();
data.extend_from_slice(&3i32.to_be_bytes()); data.extend_from_slice(&0i32.to_be_bytes()); data.extend_from_slice(&[0u8; 8]); data.resize(2880, 0); let table = BinTable::from_data(&header, data).unwrap();
assert!(matches!(
table.column_by_idx(0).unwrap().vla(),
Err(FitsError::UnexpectedEof)
));
}
#[test]
fn x_bit_column_unpacks_msb_first() {
let header = table_header(2, 1, &["12X"]);
let table = BinTable::from_data(&header, vec![0xAB, 0xC0]).unwrap();
let bits = table.column_by_idx(0).unwrap().bits().unwrap();
assert_eq!(bits.nrows(), 1);
assert_eq!(
bits.row(0),
bitvec![u8, Msb0; 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0].as_bitslice()
);
assert_eq!(bits.get(0, 0), Some(true)); assert_eq!(bits.get(0, 1), Some(false));
assert_eq!(bits.get(0, 12), None); assert!(bits[0][0]);
assert!(!bits[0][1]);
assert!(bits[(0, 0)]);
assert!(!bits[(0, 1)]);
assert_eq!(
table.column_by_idx(0).unwrap().raw().unwrap(),
ColumnData::Bytes(vec![0xAB, 0xC0])
);
}
#[test]
fn read_column_by_name_and_one_step_physical() {
let mut header = table_header(2, 3, &["1I"]); header
.set("TTYPE1", "FLUX")
.set("TSCAL1", 2.0)
.set("TZERO1", 10.0);
let mut data = Vec::new();
for x in [1i16, 2, 3] {
data.extend_from_slice(&x.to_be_bytes());
}
let table = BinTable::from_data(&header, data).unwrap();
assert_eq!(
table.column_by_name("flux").unwrap().raw().unwrap(),
ColumnData::I16(vec![1, 2, 3])
);
assert_eq!(
table.column_by_idx(0).unwrap().physical().unwrap(),
vec![12.0, 14.0, 16.0]
);
assert_eq!(
table.column_by_name("FLUX").unwrap().physical().unwrap(),
vec![12.0, 14.0, 16.0]
);
assert!(matches!(
table.column_by_name("nope"),
Err(FitsError::ColumnNotFound { .. })
));
}
#[test]
fn read_bit_column_on_a_non_bit_column_errors() {
let header = table_header(4, 1, &["1J"]);
let table = BinTable::from_data(&header, vec![0u8; 4]).unwrap();
assert!(matches!(
table.column_by_idx(0).unwrap().bits(),
Err(FitsError::NotABitColumn { code: 'J' })
));
}
#[test]
fn column_index_is_case_insensitive() {
let mut header = table_header(4, 1, &["1J"]);
header.set("TTYPE1", "Flux");
let table = BinTable::from_data(&header, vec![0u8; 4]).unwrap();
assert_eq!(table.column_index("FLUX"), Some(0));
assert_eq!(table.column_index("flux"), Some(0));
assert_eq!(table.column_index("missing"), None);
}
#[test]
fn a_column_terminates_at_the_first_nul() {
assert_eq!(trim_text(b"AB\0CD\0\0"), "AB");
assert_eq!(trim_text(b"hello "), "hello"); assert_eq!(trim_text(b"\0junk"), ""); }
#[test]
fn read_vla_on_a_fixed_column_is_an_error() {
let header = table_header(4, 1, &["1J"]);
let table = BinTable::from_data(&header, vec![0u8; 4]).unwrap();
assert!(matches!(
table.column_by_idx(0).unwrap().vla(),
Err(FitsError::NotAVla { code: 'J' })
));
}
#[test]
fn row_width_mismatch_is_an_error() {
let header = table_header(99, 1, &["1J"]);
assert!(matches!(
BinTable::from_data(&header, vec![0u8; 4]),
Err(FitsError::RowWidthMismatch {
computed: 4,
declared: 99
})
));
}
#[test]
fn out_of_bounds_column_is_an_error() {
let header = table_header(4, 1, &["1J"]);
let table = BinTable::from_data(&header, vec![0u8; 4]).unwrap();
assert!(matches!(
table.column_by_idx(9),
Err(FitsError::ColumnIndexOutOfBounds { index: 9, len: 1 })
));
}
#[test]
fn reads_the_real_aips_antenna_table() {
let file = File::open("tests/data/fits/DDTSUVDATA.fits").unwrap();
let mut reader = FitsReader::open(file).unwrap();
let table = reader.read_table(1).unwrap();
assert_eq!(table.nrows, 28);
assert_eq!(table.columns.len(), 12);
assert_eq!(table.columns[0].name.as_deref(), Some("ANNAME"));
assert_eq!(table.columns[0].tform, tform(8, TformKind::Char, None));
assert_eq!(table.columns[1].tform, tform(3, TformKind::F64, None));
assert_eq!(table.columns[2].tform, tform(0, TformKind::F64, None));
assert_eq!(table.columns[2].byte_offset, 32);
assert_eq!(table.columns[3].byte_offset, 32);
assert_eq!(table.columns[1].unit.as_deref(), Some("METERS"));
match table.column_by_idx(0).unwrap().raw().unwrap() {
ColumnData::Text(v) => assert_eq!(v.len(), 28),
other => panic!("ANNAME should be Text, got {other:?}"),
}
match table.column_by_idx(1).unwrap().raw().unwrap() {
ColumnData::F64(v) => assert_eq!(v.len(), 28 * 3),
other => panic!("STABXYZ should be F64, got {other:?}"),
}
assert_eq!(
table.column_by_idx(2).unwrap().raw().unwrap(),
ColumnData::F64(vec![])
);
assert_eq!(table.column_index("NOSTA"), Some(3));
}
#[test]
fn read_table_rejects_non_bintable_hdus() {
let file = File::open("tests/data/fits/DDTSUVDATA.fits").unwrap();
let mut reader = FitsReader::open(file).unwrap();
assert!(matches!(reader.read_table(0), Err(FitsError::NotABinTable)));
}
#[test]
fn vla_bit_column_unpacks_msb_first() {
let mut header = Header::new();
header
.set("XTENSION", "BINTABLE")
.set("BITPIX", 8)
.set("NAXIS", 2)
.set("NAXIS1", 8) .set("NAXIS2", 2)
.set("PCOUNT", 3) .set("GCOUNT", 1)
.set("TFIELDS", 1)
.set("TFORM1", "1PX");
let mut data = Vec::new();
data.extend_from_slice(&12i32.to_be_bytes()); data.extend_from_slice(&0i32.to_be_bytes()); data.extend_from_slice(&4i32.to_be_bytes()); data.extend_from_slice(&2i32.to_be_bytes()); data.extend_from_slice(&[0xAB, 0xC0, 0xF0]); let table = BinTable::from_data(&header, data).unwrap();
let rows = table.column_by_idx(0).unwrap().vla_bits().unwrap();
assert_eq!(rows.nrows(), 2);
assert_eq!(
rows.row(0),
bitvec![u8, Msb0; 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0].as_bitslice()
);
assert_eq!(rows.row(1), bitvec![u8, Msb0; 1, 1, 1, 1].as_bitslice());
assert_eq!(rows.row(1).len(), 4);
}
#[test]
fn tfields_beyond_999_is_rejected() {
let mut header = table_header(0, 0, &[]);
header.set("TFIELDS", 1000);
assert!(matches!(
BinTable::from_data(&header, vec![]),
Err(FitsError::KeywordOutOfRange { name: "TFIELDS" })
));
}
#[test]
fn hostile_tform_repeat_saturates_to_a_width_mismatch() {
let header = table_header(8, 1, &["9999999999999999999J"]);
assert!(matches!(
BinTable::from_data(&header, vec![0u8; 8]),
Err(FitsError::RowWidthMismatch { .. })
));
}
#[test]
fn row_count_times_width_overflow_is_rejected_not_wrapped() {
let header = table_header(8, 3_000_000_000_000_000_000, &["1K"]);
assert!(matches!(
BinTable::from_data(&header, vec![0u8; 8]),
Err(FitsError::UnexpectedEof)
));
}