Coverage for tests/test_schema_docs.py: 100%
144 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 09:23 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 09:23 +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
14import json
15import re
16from pathlib import Path
18import pytest
20from lsst.images.schema_docs import generate_schema_docs
21from lsst.images.serialization import write_frozen_schemas
24@pytest.fixture(scope="module")
25def generated(tmp_path_factory: pytest.TempPathFactory) -> tuple[Path, Path]:
26 """Generate pages from freshly-written frozen schemas."""
27 root = tmp_path_factory.mktemp("schema_docs")
28 schema_dir = root / "schemas"
29 page_dir = root / "pages"
30 extra_dir = root / "extra"
31 write_frozen_schemas(schema_dir)
32 generate_schema_docs(schema_dir, page_dir, extra_dir)
33 return page_dir, extra_dir
36def test_page_per_schema(generated: tuple[Path, Path]) -> None:
37 """Verify each frozen schema gets a directory-style page, reachable from
38 the top index through its family page rather than listed directly.
39 """
40 page_dir, _ = generated
41 (image_dir,) = [p for p in page_dir.glob("image-*") if p.is_dir()]
42 assert (image_dir / "index.rst").exists()
43 index = (page_dir / "index.rst").read_text()
44 assert "image/index" in index
45 assert f"{image_dir.name}/index" not in index
46 family = (page_dir / "image" / "index.rst").read_text()
47 version = image_dir.name.removeprefix("image-")
48 assert f":doc:`{version} <../{image_dir.name}/index>`" in family
51def test_family_page_versions(tmp_path: Path) -> None:
52 """Verify a family page lists versions newest-first with the current
53 version marked, and hosts the nav toctree for its version pages.
54 """
55 schema_dir = tmp_path / "schemas"
56 write_frozen_schemas(schema_dir)
57 old = {
58 "$id": "https://images.lsst.io/schemas/image-0.9.0",
59 "title": "image",
60 "description": "Old image schema.",
61 "type": "object",
62 "properties": {},
63 }
64 (schema_dir / "image" / "image-0.9.0.json").write_text(json.dumps(old) + "\n")
65 page_dir = tmp_path / "pages"
66 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
67 family = (page_dir / "image" / "index.rst").read_text()
68 i_new = family.index(":doc:`1.0.0 <../image-1.0.0/index>`")
69 i_old = family.index(":doc:`0.9.0 <../image-0.9.0/index>`")
70 assert i_new < i_old
71 assert "current" in family
72 assert "superseded" in family
73 assert ".. toctree::" in family
76def test_family_version_sort_numeric(tmp_path: Path) -> None:
77 """Verify versions sort numerically, so 1.0.10 outranks 1.0.2."""
78 schema_dir = tmp_path / "schemas"
79 schema_dir.mkdir()
80 for version in ("1.0.2", "1.0.10"):
81 gadget = {
82 "$id": f"https://example.org/schemas/gadget-{version}",
83 "title": "gadget",
84 "description": "A gadget.",
85 "type": "object",
86 "properties": {},
87 }
88 (schema_dir / "gadget").mkdir(exist_ok=True)
89 (schema_dir / "gadget" / f"gadget-{version}.json").write_text(json.dumps(gadget) + "\n")
90 page_dir = tmp_path / "pages"
91 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
92 family = (page_dir / "gadget" / "index.rst").read_text()
93 i_ten = family.index(":doc:`1.0.10 <../gadget-1.0.10/index>`")
94 i_two = family.index(":doc:`1.0.2 <../gadget-1.0.2/index>`")
95 assert i_ten < i_two
98def test_family_page_shows_latest_content(generated: tuple[Path, Path]) -> None:
99 """Verify the family (versionless) page renders the latest version's
100 composition diagram and field table inline, not just a list of versions.
101 """
102 page_dir, _ = generated
103 family = (page_dir / "image" / "index.rst").read_text()
104 assert ".. mermaid::" in family
105 assert "\nFields\n======\n" in family
106 # Both the versions table and the inline field table are list-tables.
107 assert family.count(".. list-table::") >= 2
110def test_page_content(generated: tuple[Path, Path]) -> None:
111 """Verify the image page has URL, raw-JSON link, mermaid, and fields."""
112 page_dir, _ = generated
113 (image_dir,) = [p for p in page_dir.glob("image-*") if p.is_dir()]
114 text = (image_dir / "index.rst").read_text()
115 assert f"https://images.lsst.io/schemas/{image_dir.name}" in text
116 assert f"<../{image_dir.name}.json>" in text
117 assert ".. mermaid::" in text
118 assert ".. list-table::" in text
121def test_sub_schema_links(generated: tuple[Path, Path]) -> None:
122 """Verify composite schema pages hyperlink their sub-schema pages.
124 The cell_coadd field table must link the mask and sky_projection types
125 to those schemas' own generated pages.
126 """
127 page_dir, _ = generated
128 (cc_dir,) = [p for p in page_dir.glob("cell_coadd-*") if p.is_dir()]
129 text = (cc_dir / "index.rst").read_text()
130 assert ":doc:`mask <../mask-" in text
131 assert ":doc:`sky_projection <../sky_projection-" in text
134def test_external_sub_schema_links(tmp_path: Path) -> None:
135 """Verify a sub-schema not hosted on this site is linked by its absolute
136 $id URL, and the page's canonical URL comes from the schema's own $id,
137 so an external package can generate pages under its own URL base.
138 """
139 schema_dir = tmp_path / "schemas"
140 schema_dir.mkdir()
141 widget = {
142 "$id": "https://example.org/schemas/widget-1.0.0",
143 "title": "widget",
144 "description": "A widget.",
145 "type": "object",
146 "properties": {"mask": {"$ref": "#/$defs/MaskModel", "description": "The mask."}},
147 "$defs": {
148 "MaskModel": {
149 "x-lsst-schema-url": "https://images.lsst.io/schemas/mask-1.0.0",
150 "title": "mask",
151 "type": "object",
152 }
153 },
154 }
155 (schema_dir / "widget").mkdir()
156 (schema_dir / "widget" / "widget-1.0.0.json").write_text(json.dumps(widget) + "\n")
157 page_dir = tmp_path / "pages"
158 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
159 text = (page_dir / "widget-1.0.0" / "index.rst").read_text()
160 assert "`mask <https://images.lsst.io/schemas/mask-1.0.0>`__" in text
161 assert "https://example.org/schemas/widget-1.0.0" in text
164def test_recursive_schema_page_has_fields(generated: tuple[Path, Path]) -> None:
165 """Verify a recursive schema, whose document root is a $ref into $defs,
166 still gets its description and field table from the referenced
167 definition.
168 """
169 page_dir, _ = generated
170 (pf_dir,) = [p for p in page_dir.glob("product_field-*") if p.is_dir()]
171 text = (pf_dir / "index.rst").read_text()
172 assert ".. list-table::" in text
173 assert "``operands``" in text
176def test_python_references_become_literals(tmp_path: Path) -> None:
177 """Verify single-backtick Python references from model docstrings are
178 rendered as inline literals, since they cannot resolve as py:obj targets
179 on the generated pages.
180 """
181 schema_dir = tmp_path / "schemas"
182 schema_dir.mkdir()
183 gadget = {
184 "$id": "https://images.lsst.io/schemas/gadget-1.0.0",
185 "title": "gadget",
186 "description": "A gadget backed by a `Gadget` object.",
187 "type": "object",
188 "properties": {},
189 }
190 (schema_dir / "gadget").mkdir()
191 (schema_dir / "gadget" / "gadget-1.0.0.json").write_text(json.dumps(gadget) + "\n")
192 page_dir = tmp_path / "pages"
193 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
194 text = (page_dir / "gadget-1.0.0" / "index.rst").read_text()
195 assert "``Gadget``" in text
196 assert re.search(r"(?<!`)`Gadget`(?!`)", text) is None
199def test_raw_json_staged(generated: tuple[Path, Path]) -> None:
200 """Verify the raw JSON is staged for html_extra_path publication."""
201 _, extra_dir = generated
202 assert list((extra_dir / "schemas").glob("image-*.json"))
205def test_superseded_version_notes_current(tmp_path: Path) -> None:
206 """Verify a frozen file for an old version still gets a page, with a
207 pointer at the current version and no diagram.
208 """
209 schema_dir = tmp_path / "schemas"
210 (schema_dir / "image").mkdir(parents=True)
211 (schema_dir / "image" / "image-0.0.1.json").write_text(
212 '{"$id": "https://images.lsst.io/schemas/image-0.0.1", "title": "image",'
213 ' "description": "Old.", "type": "object", "properties": {}}\n'
214 )
215 page_dir = tmp_path / "pages"
216 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
217 text = (page_dir / "image-0.0.1" / "index.rst").read_text()
218 assert "superseded" in text
219 assert ".. mermaid::" not in text
222def test_page_dir_is_regenerated(tmp_path: Path) -> None:
223 """Verify stale generated pages are removed on regeneration."""
224 schema_dir = tmp_path / "schemas"
225 write_frozen_schemas(schema_dir)
226 page_dir = tmp_path / "pages"
227 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
228 stale = page_dir / "gone-1.0.0" / "index.rst"
229 stale.parent.mkdir()
230 stale.write_text("stale\n")
231 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
232 assert not stale.exists()
235def test_extra_dir_is_regenerated(tmp_path: Path) -> None:
236 """Verify raw JSON staged for a removed frozen schema is not republished
237 by later builds.
238 """
239 schema_dir = tmp_path / "schemas"
240 write_frozen_schemas(schema_dir)
241 extra_dir = tmp_path / "extra"
242 generate_schema_docs(schema_dir, tmp_path / "pages", extra_dir)
243 stale = extra_dir / "schemas" / "gone-1.0.0.json"
244 stale.write_text("{}\n")
245 generate_schema_docs(schema_dir, tmp_path / "pages", extra_dir)
246 assert not stale.exists()
249def test_development_sub_schema_not_linked(tmp_path: Path) -> None:
250 """Verify a reference to a development sub-schema renders as plain text
251 with no link, since development schemas have no page.
252 """
253 schema_dir = tmp_path / "schemas"
254 schema_dir.mkdir()
255 widget = {
256 "$id": "https://images.lsst.io/schemas/widget-1.0.0",
257 "title": "widget",
258 "description": "A widget.",
259 "type": "object",
260 "properties": {"field": {"$ref": "#/$defs/DevFieldModel", "description": "A field."}},
261 "$defs": {
262 "DevFieldModel": {
263 "x-lsst-schema-url": "https://images.lsst.io/schemas/dev_field-1.0.0.dev0",
264 "title": "dev_field",
265 "type": "object",
266 }
267 },
268 }
269 (schema_dir / "widget").mkdir()
270 (schema_dir / "widget" / "widget-1.0.0.json").write_text(json.dumps(widget) + "\n")
271 page_dir = tmp_path / "pages"
272 generate_schema_docs(schema_dir, page_dir, tmp_path / "extra")
273 text = (page_dir / "widget-1.0.0" / "index.rst").read_text()
274 assert "dev_field" in text
275 assert ":doc:`dev_field" not in text
276 assert "dev_field-1.0.0.dev0>" not in text