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