Coverage for tests/test_project_scan.py: 100%
71 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
1import pathlib
2import typing
4import pytest
6from archdocs.main import ArchitectureParserAndRenderer, SettingsForArchdocs
7from tests.rendered_diagram import KUBERNETES_VARIANTS_ROOT, WITHOUT_MANIFESTS, render_diagram
10_OWN_SOURCE: typing.Final = """import redis
13cache_client = redis.Redis(host="localhost")
14"""
15_VENDORED_SOURCE: typing.Final = """import celery
16import uvicorn
19app = celery.Celery(__name__)
20"""
21_APPLICATION_SOURCE: typing.Final = "import fastapi\n\napp = fastapi.FastAPI()\n"
22_CHART_RELATIVE_PATH: typing.Final = "deploy/mychart"
23_SOURCES_RELATIVE_PATH: typing.Final = "src"
24_NEIGHBOUR_CHART_VALUES: typing.Final = """replicaCount: 4
26ingress:
27 enabled: true
28 hosts:
29 - host: neighbour.example.com
30"""
31_RAW_INGRESS_MANIFEST: typing.Final = """apiVersion: networking.k8s.io/v1
32kind: Ingress
33metadata:
34 name: mychart
35spec:
36 rules:
37 - host: from-templates.example.com
38"""
39_DECOY_CHART_VALUES: typing.Final = """replicaCount: 9
41ingress:
42 enabled: true
43 hosts:
44 - host: decoy.example.com
45"""
48def _build_charted_project(
49 project_path: pathlib.Path,
50 /,
51 *,
52 chart_path: pathlib.Path | None = None,
53 chart_values: str = _NEIGHBOUR_CHART_VALUES,
54) -> pathlib.Path:
55 source_dir: typing.Final = project_path / _SOURCES_RELATIVE_PATH
56 source_dir.mkdir(parents=True)
57 (source_dir / "main.py").write_text(_APPLICATION_SOURCE)
58 chart_dir: typing.Final = project_path / _CHART_RELATIVE_PATH if chart_path is None else chart_path
59 chart_dir.mkdir(parents=True)
60 (chart_dir / "Chart.yaml").write_text("apiVersion: v2\nname: mychart\n")
61 (chart_dir / "values.yaml").write_text(chart_values)
62 return source_dir
65# The last case is the mirror one: a service living in a `build` directory must not skip itself.
66@pytest.mark.parametrize(
67 ("project_subpath", "vendored_relative_path"),
68 [
69 (".", ".venv/lib/python3.12/site-packages/celery"),
70 (".", "venv/celery"),
71 (".", "node_modules/celery"),
72 (".", "build/lib/celery"),
73 ("build/dist/myservice", ".venv/celery"),
74 ],
75)
76def test_dependencies_stay_out_of_the_diagram(
77 tmp_path: pathlib.Path,
78 project_subpath: str,
79 vendored_relative_path: str,
80) -> None:
81 project_path: typing.Final = tmp_path / project_subpath
82 project_path.mkdir(parents=True, exist_ok=True)
83 (project_path / "service.py").write_text(_OWN_SOURCE)
84 vendored_dir: typing.Final = project_path / vendored_relative_path
85 vendored_dir.mkdir(parents=True)
86 (vendored_dir / "vendored.py").write_text(_VENDORED_SOURCE)
88 rendered_diagram: typing.Final = render_diagram(
89 SettingsForArchdocs(
90 root_dir=project_path,
91 service_name="vendor-svc",
92 kubernetes_dir=WITHOUT_MANIFESTS,
93 ),
94 )
96 assert 'redisdb["redis"]' in rendered_diagram
97 assert "celery" not in rendered_diagram
98 assert "uvicorn" not in rendered_diagram
101@pytest.mark.parametrize("sources_subpath", [".", "one"])
102def test_manifests_are_found_above_the_sources(tmp_path: pathlib.Path, sources_subpath: str) -> None:
103 rendered_diagram: typing.Final = render_diagram(
104 SettingsForArchdocs(
105 root_dir=_build_charted_project(tmp_path / sources_subpath, chart_path=tmp_path / _CHART_RELATIVE_PATH),
106 service_name="above-svc",
107 ),
108 )
110 assert 'above_svc{"above-svc (replicas 4)"}' in rendered_diagram
111 assert "HTTP neighbour.example.com" in rendered_diagram
114def test_chart_is_found_by_its_templates(tmp_path: pathlib.Path) -> None:
115 templates_dir: typing.Final = tmp_path / _CHART_RELATIVE_PATH / "templates"
116 templates_dir.mkdir(parents=True)
117 (templates_dir / "ingress.yaml").write_text(_RAW_INGRESS_MANIFEST)
119 rendered_diagram: typing.Final = render_diagram(
120 SettingsForArchdocs(root_dir=tmp_path, service_name="templates-svc"),
121 )
123 assert "HTTP from-templates.example.com" in rendered_diagram
126# The decoy chart is what the working directory would offer a relative path.
127@pytest.mark.parametrize(
128 ("kubernetes_dir", "expected_part", "forbidden_part"),
129 [
130 (KUBERNETES_VARIANTS_ROOT / "loadbalancer", "LoadBalancer", "neighbour.example.com"),
131 ("there-is-no-such-chart", 'config_svc{"config-svc"}', "neighbour.example.com"),
132 (_CHART_RELATIVE_PATH, "HTTP neighbour.example.com", "decoy.example.com"),
133 ],
134)
135def test_configured_dir_wins_over_the_search(
136 tmp_path: pathlib.Path,
137 monkeypatch: pytest.MonkeyPatch,
138 kubernetes_dir: str | pathlib.Path,
139 expected_part: str,
140 forbidden_part: str,
141) -> None:
142 decoy_project_path: typing.Final = tmp_path / "elsewhere"
143 _build_charted_project(decoy_project_path, chart_values=_DECOY_CHART_VALUES)
144 monkeypatch.chdir(decoy_project_path)
146 rendered_diagram: typing.Final = render_diagram(
147 SettingsForArchdocs(
148 root_dir=_build_charted_project(tmp_path / "project"),
149 service_name="config-svc",
150 kubernetes_dir=kubernetes_dir,
151 ),
152 )
154 assert expected_part in rendered_diagram
155 assert forbidden_part not in rendered_diagram
158# A mounted route keeps one engine alive, and a rescan on every request would walk the whole
159# tree again: sources edited under a running process wait for a restart, see the playground.
160def test_sources_are_scanned_once_per_engine(tmp_path: pathlib.Path) -> None:
161 (tmp_path / "service.py").write_text(_OWN_SOURCE)
162 architecture_engine: typing.Final = ArchitectureParserAndRenderer(
163 local_settings=SettingsForArchdocs(
164 root_dir=tmp_path,
165 service_name="cached-svc",
166 kubernetes_dir=WITHOUT_MANIFESTS,
167 ),
168 )
169 first_diagram: typing.Final = architecture_engine.render_architecture_diagram()
171 (tmp_path / "service.py").write_text(_APPLICATION_SOURCE)
172 second_diagram: typing.Final = architecture_engine.render_architecture_diagram()
174 assert second_diagram == first_diagram
175 assert 'redisdb["redis"]' in second_diagram
176 assert "REST" not in second_diagram
179@pytest.mark.parametrize(("project_subpath", "repository_marker"), [("project", ".git"), ("one/two/three", "")])
180def test_far_away_manifests_are_ignored(
181 tmp_path: pathlib.Path,
182 project_subpath: str,
183 repository_marker: str,
184) -> None:
185 project_path: typing.Final = tmp_path / project_subpath
186 source_dir: typing.Final = _build_charted_project(
187 project_path,
188 chart_path=tmp_path / _CHART_RELATIVE_PATH,
189 chart_values=_DECOY_CHART_VALUES,
190 )
191 if repository_marker:
192 (project_path / repository_marker).mkdir()
194 rendered_diagram: typing.Final = render_diagram(
195 SettingsForArchdocs(root_dir=source_dir, service_name="far-svc"),
196 )
198 assert 'far_svc{"far-svc"}' in rendered_diagram
199 assert "decoy.example.com" not in rendered_diagram