noaa-goes-visualization/tests/test_fitsio.py

230 lines
7.7 KiB
Python
Raw Permalink Normal View History

import numpy as np
import pytest
from astropy.io import fits
from conftest import solar_disc, write_fits
from suvi import fitsio
@pytest.fixture
def disc():
return solar_disc()
def corrupt(path, tmp_path, name, transform):
raw = open(path, "rb").read()
out = str(tmp_path / name)
with open(out, "wb") as handle:
handle.write(transform(raw))
return out
# ------------------------------------------------------------------ value parsing
@pytest.mark.parametrize(
"raw,expected",
[
(" T / comment", True),
(" F", False),
(" 171 / [angstrom]", 171),
(" 2.5 / arcsec", 2.5),
("7.40304005902818E-05 / [W m-2]", pytest.approx(7.40304005902818e-05)),
("1.5D3 / fortran exponent", 1500.0),
("'UTC ' / principal time system", "UTC"),
("'W m-2 sr-1'", "W m-2 sr-1"),
("'0 ' / data unit checksum", "0"),
("'it''s quoted' / embedded quote", "it's quoted"),
("'a / b' / slash inside string", "a / b"),
("", None),
(" -999999", -999999),
],
)
def test_parse_value(raw, expected):
assert fitsio._parse_value(raw) == expected
# ------------------------------------------------------------------ header reading
def test_reads_header_of_a_compressed_image(good_frame):
cards, error = fitsio.read_header(good_frame)
assert error is None
assert cards["WAVELNTH"] == 171
assert cards["ZIMAGE"] is True
assert cards["ZNAXIS1"] == 1280
assert cards["DEGRADED"] is False
assert cards["IMG_MEAN"] > 0
def test_reads_header_of_an_uncompressed_image(tmp_path, disc):
path = write_fits(str(tmp_path / "plain.fits"), disc, compress=False)
cards, error = fitsio.read_header(path)
assert error is None
assert cards["NAXIS"] == 2 and cards["NAXIS1"] == 1280
assert cards["WAVELNTH"] == 171
def test_header_metadata_projects_onto_db_columns(good_frame):
values, error = fitsio.scan_header(good_frame)
assert error is None
# Booleans become 0/1 so SQL can filter on them.
assert values["degraded"] == 0 and values["eclipse"] == 0 and values["empty"] == 0
assert values["wavelnth"] == 171
assert isinstance(values["img_mean"], float)
assert isinstance(values["datasum"], str)
assert set(values) <= set(fitsio.db.HEADER_FIELDS)
def test_header_metadata_drops_wrongly_typed_cards():
values = fitsio.header_metadata({"IMG_MEAN": "not a number", "WAVELNTH": 171})
assert "img_mean" not in values
assert values["wavelnth"] == 171
def test_flags_are_read_from_a_degraded_frame(tmp_path, disc):
"""The real eclipse failure mode: near-zero radiance with DEGRADED/ECLIPSE set."""
path = write_fits(
str(tmp_path / "eclipse.fits"),
disc * 1e-4,
headers={"DEGRADED": True, "ECLIPSE": 2},
)
values, error = fitsio.scan_header(path)
assert error is None
assert values["degraded"] == 1 and values["eclipse"] == 2
assert values["img_mean"] < 1e-3
# ----------------------------------------------------------------------- integrity
def test_empty_file(tmp_path):
path = str(tmp_path / "empty.fits")
open(path, "wb").close()
cards, error = fitsio.read_header(path)
assert cards == {} and error == "empty file"
def test_missing_file(tmp_path):
cards, error = fitsio.read_header(str(tmp_path / "absent.fits"))
assert cards == {} and "unreadable" in error
def test_not_a_fits_file(tmp_path):
path = str(tmp_path / "junk.fits")
open(path, "wb").write(b"X" * 5000)
cards, error = fitsio.read_header(path)
assert cards == {} and "missing SIMPLE" in error
def test_shorter_than_one_block(tmp_path, good_frame):
path = corrupt(good_frame, tmp_path, "tiny.fits", lambda raw: raw[:100])
_, error = fitsio.read_header(path)
assert "truncated" in error
def test_truncation_not_on_a_block_boundary_is_reported(tmp_path, good_frame):
path = corrupt(good_frame, tmp_path, "odd.fits", lambda raw: raw[:-100])
_, error = fitsio.read_header(path)
assert "not a multiple of 2880" in error
def test_header_without_end_card(tmp_path, good_frame):
"""A header whose END is gone must not be parsed as if it were complete."""
path = corrupt(
good_frame, tmp_path, "noend.fits", lambda raw: raw.replace(b"END ", b"XXX ")
)
cards, error = fitsio.read_header(path)
assert cards == {} and "END" in error
def test_primary_only_file_has_no_image_hdu(tmp_path):
path = str(tmp_path / "primary.fits")
fits.HDUList([fits.PrimaryHDU()]).writeto(path, overwrite=True)
cards, error = fitsio.read_header(path)
assert "no image HDU" in error
def test_max_header_bytes_is_respected(good_frame):
"""A malformed file must not pull unbounded data into memory."""
cards, error = fitsio.read_header(good_frame, max_bytes=fitsio.BLOCK)
assert cards == {} or error is not None
# ------------------------------------------------------------------------ datasums
def test_datasum_matches_the_stored_keyword(good_frame):
assert fitsio.verify_datasums(good_frame) is None
def test_datasum_detects_a_flipped_bit(tmp_path, good_frame):
def flip(raw):
offset = len(raw) - fitsio.BLOCK # inside the final data unit
return raw[:offset] + bytes([raw[offset] ^ 0xFF]) + raw[offset + 1 :]
path = corrupt(good_frame, tmp_path, "flipped.fits", flip)
assert "datasum mismatch" in fitsio.verify_datasums(path)
def test_datasum_detects_truncation(tmp_path, good_frame):
path = corrupt(good_frame, tmp_path, "half.fits", lambda raw: raw[: len(raw) // 2])
assert "truncated" in fitsio.verify_datasums(path)
def test_datasum_reports_when_no_keywords_are_present(tmp_path, disc):
path = write_fits(str(tmp_path / "nosum.fits"), disc, checksum=False)
assert fitsio.verify_datasums(path) == "no DATASUM keywords present"
def test_datasum_on_a_non_fits_file(tmp_path):
path = str(tmp_path / "junk.fits")
open(path, "wb").write(b"nope" * 1000)
assert fitsio.verify_datasums(path) == "not a FITS file"
def test_datasum_is_the_fits_ones_complement_sum():
# Two words that overflow 32 bits, forcing an end-around carry.
payload = (0xFFFFFFFF).to_bytes(4, "big") + (0x00000002).to_bytes(4, "big")
assert fitsio.datasum(payload) == 2
assert fitsio.datasum(b"") == 0
# A trailing partial word is zero-padded on the right: 0x00000001 + 0x01000000.
assert fitsio.datasum(b"\x00\x00\x00\x01\x01") == 0x01000001
# --------------------------------------------------------------------- image reads
def test_reads_pixel_data(good_frame, disc):
data, error = fitsio.read_image(good_frame)
assert error is None
assert data.shape == (1280, 1280) and data.dtype == np.float32
assert np.allclose(data, disc, atol=1e-3)
def test_zblank_becomes_nan(tmp_path, disc):
holed = disc.copy()
holed[10:20, 10:20] = fitsio.ZBLANK
path = write_fits(str(tmp_path / "blank.fits"), holed)
data, error = fitsio.read_image(path)
assert error is None
assert np.isnan(data[10:20, 10:20]).all()
assert np.isfinite(data[500:600, 500:600]).all()
def test_image_read_reports_a_missing_array(tmp_path):
path = str(tmp_path / "primary.fits")
fits.HDUList([fits.PrimaryHDU()]).writeto(path, overwrite=True)
data, error = fitsio.read_image(path)
assert data is None and "no 2-D image array" in error
def test_image_read_reports_corruption_instead_of_raising(tmp_path, good_frame):
path = corrupt(good_frame, tmp_path, "broken.fits", lambda raw: raw[: len(raw) // 2])
data, error = fitsio.read_image(path)
assert data is None and error
def test_quiet_astropy_is_callable():
fitsio.quiet_astropy()