[docs] 1# @Test suite for Sphinx extension source tracing functionality, TEST_EXT_1, test, [IMPL_LNK_1, IMPL_ONE_1, IMPL_MRST_1]
2from collections.abc import Callable
3from pathlib import Path
4import shutil
5
6import pytest
7from sphinx.environment import CONFIG_OK
8from sphinx.testing.util import SphinxTestApp
9
10from sphinx_codelinks.analyse.projects import AnalyseProjects
11from sphinx_codelinks.config import (
12 SRC_TRACE_CACHE,
13 CodeLinksConfig,
14 check_configuration,
15)
16from sphinx_codelinks.sphinx_extension.source_tracing import set_config_to_sphinx
17
18
19@pytest.mark.parametrize(
20 ("codelinks_config", "result"),
21 [
22 (
23 {
24 "remote_url_field": 555,
25 "local_url_field": 789,
26 "set_local_url": "fdd",
27 "set_remote_url": "TrueString",
28 "projects": {
29 "dcdc": {
30 "remote_url_pattern": 44332,
31 "source_discover": {
32 "comment_type": "java",
33 "src_dir": ["../dcdc"],
34 "exclude": [123],
35 "include": [345],
36 "gitignore": "_true",
37 },
38 "analyse": {
39 "oneline_comment_style": {
40 "start_sequence": "[[",
41 "end_sequence": "]]",
42 "field_split_char": ",",
43 "needs_fields": [
44 {
45 "name": "title",
46 "type": "list[]",
47 },
48 {
49 "name": "type",
50 "default": "impl",
51 "type": "str",
52 },
53 ],
54 },
55 },
56 }
57 },
58 },
59 [
60 "Project 'dcdc' has the following errors:",
61 "Schema validation error in field 'exclude': 123 is not of type 'string'",
62 "Schema validation error in field 'comment_type': 'java' is not one of ['cpp', 'cs', 'go', 'jsonc', 'python', 'rust', 'yaml']",
63 "Schema validation error in field 'gitignore': '_true' is not of type 'boolean'",
64 "Schema validation error in field 'include': 345 is not of type 'string'",
65 "Schema validation error in field 'src_dir': ['../dcdc'] is not of type 'string'",
66 "Schema validation error in filed 'local_url_field': 789 is not of type 'string'",
67 "Schema validation error in filed 'remote_url_field': 555 is not of type 'string'",
68 "Schema validation error in filed 'set_local_url': 'fdd' is not of type 'boolean'",
69 "Schema validation error in filed 'set_remote_url': 'TrueString' is not of type 'boolean'",
70 "OneLineCommentStyle configuration errors:",
71 "Schema validation error in need_fields 'title': 'list[]' is not one of ['str', 'list[str]']",
72 "remote_url_pattern must be a string",
73 ],
74 ),
75 (
76 {
77 "remote_url_field": "remote-url",
78 "local_url_field": "local-url",
79 "set_local_url": True,
80 "set_remote_url": True,
81 "projects": {
82 "dcdc": {
83 # intentionally not given "remote_url_pattern": "https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}",
84 "source_discover": {
85 "comment_type": "cpp",
86 "src_dir": "../dcdc",
87 "exclude": [],
88 "include": [],
89 "gitignore": True,
90 },
91 "analyse": {
92 "oneline_comment_style": {
93 "start_sequence": "[[",
94 "end_sequence": "]]",
95 "field_split_char": ",",
96 "needs_fields": [
97 {
98 "name": "title",
99 "type": "str",
100 },
101 {
102 "name": "type",
103 "default": "impl",
104 "type": "str",
105 },
106 ],
107 },
108 },
109 }
110 },
111 },
112 [
113 "Project 'dcdc' has the following errors:",
114 "remote_url_pattern must be given, as set_remote_url is enabled",
115 ],
116 ),
117 ],
118)
119def test_src_tracing_config_negative(
120 make_app: Callable[..., SphinxTestApp],
121 codelinks_config,
122 result,
123):
124 this_file_dir = Path(__file__).parent
125 sphinx_project = Path("data") / "sphinx"
126 app = make_app(srcdir=(this_file_dir / sphinx_project))
127 set_config_to_sphinx(codelinks_config, app.env.config)
128 codelinks_sphinx_config = CodeLinksConfig.from_sphinx(app.env.config)
129 errors = check_configuration(codelinks_sphinx_config)
130 assert sorted(errors) == sorted(result)
131
132
133def test_src_tracing_config_positive(make_app: Callable[..., SphinxTestApp], tmp_path):
134 codelinks_config = {
135 "remote_url_field": "remote-url",
136 "local_url_field": "local-url",
137 "set_local_url": True,
138 "set_remote_url": True,
139 "outdir": tmp_path,
140 "projects": {
141 "dcdc": {
142 "source_discover": {
143 "comment_type": "cpp",
144 "src_dir": "../dcdc",
145 "exclude": ["**/*.hpp"],
146 "include": ["**/*.cpp"],
147 "gitignore": True,
148 },
149 "remote_url_pattern": "https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}",
150 "analyse": {
151 "oneline_comment_style": {
152 "start_sequence": "[[",
153 "end_sequence": "]]",
154 "field_split_char": ",",
155 "needs_fields": [
156 {
157 "name": "title",
158 "type": "str",
159 },
160 {
161 "name": "type",
162 "default": "impl",
163 "type": "str",
164 },
165 ],
166 },
167 },
168 }
169 },
170 }
171 this_file_dir = Path(__file__).parent
172 sphinx_project = Path("data") / "sphinx"
173 app = make_app(srcdir=(this_file_dir / sphinx_project))
174 set_config_to_sphinx(codelinks_config, app.env.config)
175 codelinks_sphinx_config = CodeLinksConfig.from_sphinx(app.env.config)
176 errors = check_configuration(codelinks_sphinx_config)
177 assert not errors
178
179
180@pytest.mark.parametrize(
181 ("sphinx_project", "source_code"),
182 [
183 (Path("data") / "sphinx", Path("data") / "dcdc"),
184 (
185 Path("doc_test") / "recursive_dirs",
186 Path("doc_test") / "recursive_dirs" / "dummy_src_lv1",
187 ),
188 (
189 Path("doc_test") / "minimum_config",
190 Path("doc_test") / "minimum_config",
191 ),
192 (
193 Path("doc_test") / "id_required",
194 Path("doc_test") / "id_required",
195 ),
196 (
197 Path("doc_test") / "cs_basic",
198 Path("doc_test") / "cs_basic",
199 ),
200 (
201 Path("doc_test") / "go_basic",
202 Path("doc_test") / "go_basic",
203 ),
204 ],
205)
206def test_build_html(
207 tmpdir: Path,
208 make_app: Callable[..., SphinxTestApp],
209 sphinx_project,
210 source_code,
211 snapshot_doctree,
212):
213 this_file_dir = Path(__file__).parent
214
215 sphinx_src_dir = tmpdir / sphinx_project
216 shutil.copytree(
217 this_file_dir / sphinx_project,
218 sphinx_src_dir,
219 dirs_exist_ok=True,
220 )
221 shutil.copytree(
222 this_file_dir / source_code,
223 tmpdir / source_code,
224 dirs_exist_ok=True,
225 )
226
227 app: SphinxTestApp = make_app(
228 srcdir=Path(sphinx_src_dir),
229 freshenv=True,
230 )
231 app.build()
232
233 html = Path(app.outdir, "index.html").read_text()
234 assert html
235
236 warnings = AnalyseProjects.load_warnings(Path(app.outdir) / SRC_TRACE_CACHE)
237 assert not warnings
238
239 assert app.env.get_doctree("index") == snapshot_doctree
240
241
242def test_incremental_build_keeps_src_trace_projects_unchanged(
243 tmpdir: Path,
244 make_app: Callable[..., SphinxTestApp],
245) -> None:
246 """An incremental rebuild with no source changes must not invalidate the env.
247
248 Regression test for the ``src-trace`` directive mutating the ``analyse_config``
249 object stored inside the ``rebuild="env"`` ``src_trace_projects`` config value.
250 The mutated object (populated ``src_dir``/``src_files``) was persisted into
251 ``environment.pickle``, so every incremental build compared it against the
252 freshly generated (empty) config and reported
253 ``[config changed ('src_trace_projects')]``, forcing a full re-read.
254 """
255 this_file_dir = Path(__file__).parent
256 sphinx_project = Path("data") / "sphinx"
257 source_code = Path("data") / "dcdc"
258
259 sphinx_src_dir = Path(tmpdir) / sphinx_project
260 shutil.copytree(this_file_dir / sphinx_project, sphinx_src_dir, dirs_exist_ok=True)
261 shutil.copytree(
262 this_file_dir / source_code, Path(tmpdir) / source_code, dirs_exist_ok=True
263 )
264
265 # First build populates environment.pickle in the shared build dir.
266 make_app(srcdir=sphinx_src_dir, freshenv=True).build()
267
268 # Second build reuses the same build dir and loads the pickled environment.
269 app = make_app(srcdir=sphinx_src_dir, freshenv=False)
270
271 captured: dict[str, object] = {}
272
273 def capture_config_status(_app, env, _added, _changed, _removed): # type: ignore[no-untyped-def]
274 # ``env-get-outdated`` fires during read() after the config comparison
275 # but before config_status is reset to CONFIG_OK at the end of read().
276 captured["status"] = env.config_status
277 captured["extra"] = env.config_status_extra
278 return []
279
280 app.connect("env-get-outdated", capture_config_status)
281 app.build()
282
283 assert captured["status"] == CONFIG_OK, (
284 f"incremental build wrongly invalidated the environment: "
285 f"config changed{captured.get('extra')}"
286 )