Coverage for tests/test_schema_docs.py: 100%

144 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-26 09:32 +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. 

11 

12from __future__ import annotations 

13 

14import json 

15import re 

16from pathlib import Path 

17 

18import pytest 

19 

20from lsst.images.schema_docs import generate_schema_docs 

21from lsst.images.serialization import write_frozen_schemas 

22 

23 

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 

34 

35 

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 

49 

50 

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 

74 

75 

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 

96 

97 

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 

108 

109 

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 

119 

120 

121def test_sub_schema_links(generated: tuple[Path, Path]) -> None: 

122 """Verify composite schema pages hyperlink their sub-schema pages. 

123 

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 

132 

133 

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 

162 

163 

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 

174 

175 

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 

197 

198 

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")) 

203 

204 

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 

220 

221 

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() 

233 

234 

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() 

247 

248 

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