Skip to content

Commit 3a37b42

Browse files
Add tests
1 parent ac5dddc commit 3a37b42

2 files changed

Lines changed: 583 additions & 0 deletions

File tree

tests/goats_tom/test_jdaviz_app.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""Tests for :mod:`goats_tom.jdaviz_app`.
2+
3+
Focuses on the data-resolution layer (query parsing, the DRAGONS ``SCI`` FITS
4+
reader and ``_resolve_spectra``) that decides what gets handed to jdaviz. The
5+
Solara ``Page`` component and the ``Specviz`` helpers are not constructed here:
6+
they need a live jdaviz kernel and a browser, so they are out of scope for unit
7+
tests.
8+
"""
9+
10+
import types
11+
12+
import numpy as np
13+
import pytest
14+
from astropy.io import fits
15+
16+
from goats_tom import jdaviz_app
17+
from goats_tom.jdaviz_app import (
18+
SCIENCE_EXTENSION,
19+
_dragons_hdu_to_spectrum,
20+
_query_param,
21+
_read_dragons_spectra,
22+
_resolve_spectra,
23+
_spectra_are_2d,
24+
)
25+
26+
27+
def _spectrum(ndim):
28+
"""A stand-in spectrum exposing just the ``flux.ndim`` the code inspects."""
29+
return types.SimpleNamespace(flux=np.zeros((2, 3) if ndim == 2 else (3,)))
30+
31+
32+
def _linear_wcs_header(data, ctype="WAVE", bunit="count"):
33+
"""Header with a minimal linear spectral WCS matching ``data``'s shape."""
34+
hdu = fits.ImageHDU(data=data)
35+
header = hdu.header
36+
for axis in range(1, data.ndim + 1):
37+
header[f"CTYPE{axis}"] = ctype if axis == 1 else "PIXEL"
38+
header[f"CRVAL{axis}"] = 1.0
39+
header[f"CRPIX{axis}"] = 1.0
40+
header[f"CDELT{axis}"] = 1.0
41+
if bunit is not None:
42+
header["BUNIT"] = bunit
43+
return header
44+
45+
46+
# --------------------------------------------------------------------------- #
47+
# _query_param
48+
# --------------------------------------------------------------------------- #
49+
class TestQueryParam:
50+
def test_none_search_returns_none(self):
51+
assert _query_param(None, "dataproduct") is None
52+
53+
def test_empty_search_returns_none(self):
54+
assert _query_param("", "dataproduct") is None
55+
56+
def test_returns_value(self):
57+
assert _query_param("dataproduct=42", "dataproduct") == "42"
58+
59+
def test_returns_value_among_several_params(self):
60+
assert _query_param("x=1&dataproduct=42&y=2", "dataproduct") == "42"
61+
62+
def test_missing_param_returns_none(self):
63+
assert _query_param("x=1", "dataproduct") is None
64+
65+
def test_returns_first_when_repeated(self):
66+
assert _query_param("dataproduct=1&dataproduct=2", "dataproduct") == "1"
67+
68+
69+
# --------------------------------------------------------------------------- #
70+
# _spectra_are_2d
71+
# --------------------------------------------------------------------------- #
72+
class TestSpectraAre2d:
73+
def test_none_is_not_2d(self):
74+
assert _spectra_are_2d(None) is False
75+
76+
def test_empty_is_not_2d(self):
77+
assert _spectra_are_2d([]) is False
78+
79+
def test_all_1d_is_not_2d(self):
80+
assert _spectra_are_2d([("a", _spectrum(1)), ("b", _spectrum(1))]) is False
81+
82+
def test_any_2d_is_2d(self):
83+
assert _spectra_are_2d([("a", _spectrum(1)), ("b", _spectrum(2))]) is True
84+
85+
86+
# --------------------------------------------------------------------------- #
87+
# _dragons_hdu_to_spectrum
88+
# --------------------------------------------------------------------------- #
89+
class TestDragonsHduToSpectrum:
90+
def test_reads_1d_flux_with_bunit(self):
91+
data = np.arange(5, dtype="float64")
92+
hdu = fits.ImageHDU(data=data, header=_linear_wcs_header(data, bunit="Jy"))
93+
spectrum = _dragons_hdu_to_spectrum(hdu)
94+
assert spectrum is not None
95+
assert spectrum.flux.ndim == 1
96+
assert spectrum.flux.unit.to_string() == "Jy"
97+
98+
def test_defaults_to_count_when_bunit_missing(self):
99+
data = np.arange(5, dtype="float64")
100+
hdu = fits.ImageHDU(data=data, header=_linear_wcs_header(data, bunit=None))
101+
spectrum = _dragons_hdu_to_spectrum(hdu)
102+
assert spectrum is not None
103+
assert spectrum.flux.unit == jdaviz_app.u.count
104+
105+
def test_invalid_bunit_falls_back_to_count(self):
106+
data = np.arange(5, dtype="float64")
107+
header = _linear_wcs_header(data, bunit=None)
108+
header["BUNIT"] = "not-a-real-unit"
109+
hdu = fits.ImageHDU(data=data, header=header)
110+
spectrum = _dragons_hdu_to_spectrum(hdu)
111+
assert spectrum is not None
112+
assert spectrum.flux.unit == jdaviz_app.u.count
113+
114+
def test_unreadable_hdu_returns_none(self):
115+
# ``data is None`` makes the flux conversion raise; the helper swallows it.
116+
hdu = fits.ImageHDU(data=None)
117+
assert _dragons_hdu_to_spectrum(hdu) is None
118+
119+
120+
# --------------------------------------------------------------------------- #
121+
# _read_dragons_spectra
122+
# --------------------------------------------------------------------------- #
123+
class TestReadDragonsSpectra:
124+
def test_non_fits_file_returns_none(self, tmp_path):
125+
path = tmp_path / "data.csv"
126+
path.write_text("wavelength,flux\n1,2\n")
127+
assert _read_dragons_spectra(path) is None
128+
129+
def test_fits_without_sci_extension_returns_none(self, tmp_path):
130+
path = tmp_path / "image.fits"
131+
fits.HDUList([fits.PrimaryHDU(data=np.zeros(5))]).writeto(path)
132+
assert _read_dragons_spectra(path) is None
133+
134+
def test_single_sci_extension_uses_stem_label(self, tmp_path):
135+
path = tmp_path / "spec.fits"
136+
data = np.arange(5, dtype="float64")
137+
sci = fits.ImageHDU(
138+
data=data, header=_linear_wcs_header(data), name=SCIENCE_EXTENSION
139+
)
140+
fits.HDUList([fits.PrimaryHDU(), sci]).writeto(path)
141+
142+
spectra = _read_dragons_spectra(path)
143+
assert spectra is not None
144+
assert len(spectra) == 1
145+
(label, spectrum) = spectra[0]
146+
assert label == "spec"
147+
assert spectrum.flux.ndim == 1
148+
149+
def test_multiple_sci_extensions_get_versioned_labels(self, tmp_path):
150+
path = tmp_path / "multi.fits"
151+
data = np.arange(5, dtype="float64")
152+
hdus = [fits.PrimaryHDU()]
153+
for ver in (1, 2):
154+
sci = fits.ImageHDU(
155+
data=data, header=_linear_wcs_header(data), name=SCIENCE_EXTENSION
156+
)
157+
sci.ver = ver
158+
hdus.append(sci)
159+
fits.HDUList(hdus).writeto(path)
160+
161+
spectra = _read_dragons_spectra(path)
162+
assert spectra is not None
163+
assert len(spectra) == 2
164+
labels = {label for label, _ in spectra}
165+
assert labels == {"multi [SCI,1]", "multi [SCI,2]"}
166+
167+
def test_2d_sci_extension_is_read(self, tmp_path):
168+
path = tmp_path / "spec2d.fits"
169+
data = np.zeros((4, 6), dtype="float64")
170+
sci = fits.ImageHDU(
171+
data=data, header=_linear_wcs_header(data), name=SCIENCE_EXTENSION
172+
)
173+
fits.HDUList([fits.PrimaryHDU(), sci]).writeto(path)
174+
175+
spectra = _read_dragons_spectra(path)
176+
assert spectra is not None
177+
assert spectra[0][1].flux.ndim == 2
178+
179+
180+
# --------------------------------------------------------------------------- #
181+
# _resolve_spectra
182+
# --------------------------------------------------------------------------- #
183+
class TestResolveSpectra:
184+
def test_no_pk_returns_all_none(self):
185+
assert _resolve_spectra(None) == (None, None, None)
186+
187+
@pytest.mark.django_db
188+
def test_unknown_pk_returns_error(self):
189+
path, spectra, error = _resolve_spectra("999999")
190+
assert path is None and spectra is None
191+
assert "not found" in error
192+
193+
@pytest.mark.django_db
194+
def test_non_numeric_pk_returns_error(self):
195+
path, spectra, error = _resolve_spectra("not-a-pk")
196+
assert path is None and spectra is None
197+
assert "not found" in error
198+
199+
@pytest.mark.django_db
200+
def test_missing_file_on_disk_returns_error(self, monkeypatch):
201+
from goats_tom.tests.factories import DataProductFactory
202+
203+
dp = DataProductFactory()
204+
# Remove the backing file so the on-disk existence check fails.
205+
from pathlib import Path
206+
207+
Path(dp.data.path).unlink()
208+
path, spectra, error = _resolve_spectra(str(dp.pk))
209+
assert path is None and spectra is None
210+
assert "missing on disk" in error
211+
212+
@pytest.mark.django_db
213+
def test_dragons_reader_result_is_returned(self, monkeypatch):
214+
from goats_tom.tests.factories import DataProductFactory
215+
216+
dp = DataProductFactory()
217+
labelled = [("lbl", _spectrum(1))]
218+
monkeypatch.setattr(jdaviz_app, "_read_dragons_spectra", lambda p: labelled)
219+
220+
path, spectra, error = _resolve_spectra(str(dp.pk))
221+
assert error is None
222+
assert spectra is labelled
223+
assert path is not None
224+
225+
@pytest.mark.django_db
226+
def test_falls_back_to_path_when_no_reader_handles_file(self, monkeypatch):
227+
from goats_tom.tests.factories import DataProductFactory
228+
229+
dp = DataProductFactory()
230+
monkeypatch.setattr(jdaviz_app, "_read_dragons_spectra", lambda p: None)
231+
monkeypatch.setattr(jdaviz_app, "_read_processor_spectra", lambda dp: [])
232+
233+
path, spectra, error = _resolve_spectra(str(dp.pk))
234+
# No reader handled it: caller is told to try jdaviz's own loaders on path.
235+
assert error is None
236+
assert spectra is None
237+
assert path is not None
238+
239+
@pytest.mark.django_db
240+
def test_processor_spectra_get_indexed_labels_when_multiple(self, monkeypatch):
241+
from goats_tom.tests.factories import DataProductFactory
242+
243+
dp = DataProductFactory()
244+
monkeypatch.setattr(jdaviz_app, "_read_dragons_spectra", lambda p: None)
245+
monkeypatch.setattr(
246+
jdaviz_app,
247+
"_read_processor_spectra",
248+
lambda dp: [_spectrum(1), _spectrum(1)],
249+
)
250+
251+
path, spectra, error = _resolve_spectra(str(dp.pk))
252+
assert error is None
253+
assert len(spectra) == 2
254+
labels = [label for label, _ in spectra]
255+
assert labels == [f"{path.stem} [0]", f"{path.stem} [1]"]
256+
257+
@pytest.mark.django_db
258+
def test_single_processor_spectrum_uses_stem_label(self, monkeypatch):
259+
from goats_tom.tests.factories import DataProductFactory
260+
261+
dp = DataProductFactory()
262+
monkeypatch.setattr(jdaviz_app, "_read_dragons_spectra", lambda p: None)
263+
monkeypatch.setattr(
264+
jdaviz_app, "_read_processor_spectra", lambda dp: [_spectrum(1)]
265+
)
266+
267+
path, spectra, error = _resolve_spectra(str(dp.pk))
268+
assert error is None
269+
assert len(spectra) == 1
270+
assert spectra[0][0] == path.stem

0 commit comments

Comments
 (0)