Coverage for tests/test_fits_date_header.py: 100%
39 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14from pathlib import Path
16import astropy.io.fits
17import astropy.time
19from lsst.images import Image
20from lsst.images.fits import ExtensionKey, FitsInputArchive
23def _simple_image() -> Image:
24 return Image(0.0, shape=(4, 4), dtype="float32")
27def _at_date_precision(time: astropy.time.Time) -> astropy.time.Time:
28 """Return ``time`` at the precision a FITS ``DATE`` card preserves.
30 Parameters
31 ----------
32 time : `astropy.time.Time`
33 Time to truncate.
35 Returns
36 -------
37 truncated : `astropy.time.Time`
38 ``time`` truncated to milliseconds.
40 Notes
41 -----
42 ``Time.fits`` truncates to milliseconds, so a DATE card records an instant
43 up to a millisecond earlier than the write that produced it. A lower bound
44 is only comparable against a stored DATE once it has been through the same
45 truncation; because truncation is monotonic, doing so keeps the bound
46 exact rather than trading it for a tolerance.
47 """
48 return astropy.time.Time(time.fits, format="fits")
51def test_every_hdu_has_a_fits_compliant_date(tmp_path: Path) -> None:
52 """Test that each HDU carries a DATE card recording approximately when
53 the file was written.
54 """
55 before = _at_date_precision(astropy.time.Time.now())
56 path = tmp_path / "x.fits"
57 _simple_image().write(path)
58 after = astropy.time.Time.now()
59 with astropy.io.fits.open(path) as hdul:
60 assert len(hdul) > 1
61 for index, hdu in enumerate(hdul):
62 extname = hdu.header.get("EXTNAME", "PRIMARY")
63 assert "DATE" in hdu.header, f"hdu={index} ({extname}): no DATE header"
64 # The value must parse in the FITS time format.
65 date = astropy.time.Time(hdu.header["DATE"], format="fits")
66 # And it must reflect this write, not a stale value.
67 assert date.jd >= before.jd, f"hdu={index} ({extname}): DATE before write started"
68 assert date.jd <= after.jd, f"hdu={index} ({extname}): DATE after write finished"
71def test_update_header_cannot_set_a_stale_primary_date(tmp_path: Path) -> None:
72 """Test that an ``update_header`` callback cannot leave a stale DATE in
73 the primary header.
74 """
75 before = _at_date_precision(astropy.time.Time.now())
76 path = tmp_path / "x.fits"
77 _simple_image().write(path, update_header=lambda h: h.set("DATE", "1999-01-01T00:00:00"))
78 after = astropy.time.Time.now()
79 with astropy.io.fits.open(path) as hdul:
80 date = astropy.time.Time(hdul[0].header["DATE"], format="fits")
81 assert date.jd >= before.jd
82 assert date.jd <= after.jd
85def test_date_is_not_captured_as_opaque_metadata(tmp_path: Path) -> None:
86 """Test that DATE is not stored in opaque metadata.
88 DATE is regenerated on every write, so the value read back must not
89 be carried in opaque metadata (which would propagate a stale date to
90 the next write).
91 """
92 path = tmp_path / "x.fits"
93 _simple_image().write(path)
94 with FitsInputArchive.open(path) as archive:
95 opaque = archive.get_opaque_metadata()
96 primary_header = opaque.headers[ExtensionKey()]
97 assert "DATE" not in primary_header