Option C Tools
Loading...
Searching...
No Matches
conftest.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/conftest.py
4
5"""
6Purpose
7-------
8Shared fixtures for the ``oct git`` integration tests (Phase 4B).
9Provides pre-seeded git repositories with Option C project structure
10so that ``oct git status`` and ``oct git check`` can be tested against
11realistic scenarios.
12
13Responsibilities
14----------------
15- ``temp_git_project``: a real git repo with minimal Option C structure.
16- ``make_compliant_py``: factory writing a fully-compliant Python file.
17- ``make_noncompliant_py``: factory writing a file with a chosen violation.
18- ``commit_file``: helper to stage + commit a file.
19
20Diagnostics
21-----------
22Domain: GIT-TESTS
23Levels:
24 L2 — fixture lifecycle
25 L3 — fixture setup details
26 L4 — deep tracing
27
28Contracts
29---------
30- ``temp_git_project`` always returns a directory that is a real git
31 working tree with ``user.name`` / ``user.email`` pre-configured and
32 at least one commit (HEAD exists).
33- All fixtures create files under ``tmp_path`` so they are automatically
34 cleaned up by pytest.
35"""
36
37import json
38import os
39import shutil
40import subprocess
41import textwrap
42from pathlib import Path
43
44import pytest
45
46
47# =====================================================================
48# Low-level helpers
49# =====================================================================
50
51
52def _git_available() -> bool:
53 return shutil.which("git") is not None
54
55
56def _git(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess:
57 """Run a git command inside *repo*."""
58 return subprocess.run(
59 ["git", *args],
60 cwd=repo,
61 check=check,
62 capture_output=True,
63 text=True,
64 )
65
66
68 repo: Path, name: str, content: str, message: str = "add file",
69) -> None:
70 """Write a file, stage it, and commit it."""
71 path = repo / name
72 path.parent.mkdir(parents=True, exist_ok=True)
73 path.write_text(content, encoding="utf-8")
74 _git(repo, "add", name)
75 _git(repo, "commit", "-m", message, "--quiet")
76
77
78# =====================================================================
79# Compliant / noncompliant file templates
80# =====================================================================
81
82
83_COMPLIANT_TEMPLATE = textwrap.dedent("""\
84 #!/usr/bin/env python3
85 # -*- coding: utf-8 -*-
86 # {rel_path}
87
88 \"\"\"
89 Purpose
90 -------
91 Test module for quality gate validation.
92
93 Responsibilities
94 ----------------
95 - Placeholder for Phase 4B integration tests.
96
97 Diagnostics
98 -----------
99 Domain: TEST
100 Levels:
101 L1 — errors
102 L2 — lifecycle
103 L3 — details
104 L4 — deep trace
105
106 Contracts
107 ---------
108 - None.
109 \"\"\"
110
111 from oc_diagnostics import _dbg
112
113 func = "placeholder"
114 _dbg("TEST", func, "loaded", 2)
115""")
116
117
118_NONCOMPLIANT_MISSING_HEADER = textwrap.dedent("""\
119 \"\"\"A module with no Option C header.\"\"\"
120
121 x = 1
122""")
123
124
125_NONCOMPLIANT_MISSING_DOCSTRING = textwrap.dedent("""\
126 #!/usr/bin/env python3
127 # -*- coding: utf-8 -*-
128 # test_project/src/bad_doc.py
129
130 x = 1
131""")
132
133
134_NONCOMPLIANT_HARDCODED_SECRET = textwrap.dedent("""\
135 #!/usr/bin/env python3
136 # -*- coding: utf-8 -*-
137 # test_project/src/has_secret.py
138
139 \"\"\"
140 Purpose
141 -------
142 Module that contains a hardcoded secret for testing.
143
144 Responsibilities
145 ----------------
146 - None.
147
148 Diagnostics
149 -----------
150 Domain: TEST
151 Levels:
152 L1 — errors
153
154 Contracts
155 ---------
156 - None.
157 \"\"\"
158
159 password = "hunter2"
160""")
161
162
163_NONCOMPLIANT_FORMAT_ERROR = textwrap.dedent("""\
164 #!/usr/bin/env python3
165 # -*- coding: utf-8 -*-
166 # WRONG_PROJECT/WRONG_PATH.py
167
168 \"\"\"
169 Purpose
170 -------
171 Module with wrong header path (format violation).
172
173 Responsibilities
174 ----------------
175 - None.
176
177 Diagnostics
178 -----------
179 Domain: TEST
180 Levels:
181 L1 — errors
182
183 Contracts
184 ---------
185 - None.
186 \"\"\"
187
188 x = 1
189""")
190
191
192# =====================================================================
193# Fixtures
194# =====================================================================
195
196
197@pytest.fixture
198def temp_git_project(tmp_path: Path) -> Path:
199 """Create a minimal Option C project inside a real git repo.
200
201 Directory structure::
202
203 <root>/
204 ├── .octrc.json
205 ├── debug_config.json
206 ├── docs/
207 │ └── ARCHITECTURE.md
208 ├── logs/
209 ├── oc_diagnostics/
210 ├── src/
211 └── tests/
212
213 The repo has one initial commit so HEAD exists, and
214 ``user.name``/``user.email`` are configured.
215 """
216 if not _git_available():
217 pytest.skip("git binary not available on PATH")
218
219 root = tmp_path / "project"
220 root.mkdir()
221
222 # Init git repo.
223 _git(root, "init", "--quiet", "-b", "main")
224 _git(root, "config", "user.email", "test@example.com")
225 _git(root, "config", "user.name", "Test")
226 _git(root, "config", "commit.gpgsign", "false")
227
228 # Create project structure.
229 (root / "src").mkdir()
230 (root / "tests").mkdir()
231 (root / "docs").mkdir()
232 (root / "logs").mkdir()
233 (root / "oc_diagnostics").mkdir()
234
235 # .octrc.json with compact default.
236 octrc = {
237 "linter": {"profile": "compact"},
238 "git": {"protected_branches": ["main", "master"]},
239 }
240 (root / ".octrc.json").write_text(
241 json.dumps(octrc, indent=2), encoding="utf-8",
242 )
243
244 # debug_config.json stub.
245 debug_cfg = {
246 "domains": {
247 "GIT": {"level": 1, "color": "cyan"},
248 "TEST": {"level": 1, "color": "white"},
249 },
250 }
251 (root / "debug_config.json").write_text(
252 json.dumps(debug_cfg, indent=2), encoding="utf-8",
253 )
254
255 # docs/ARCHITECTURE.md stub.
256 (root / "docs" / "ARCHITECTURE.md").write_text(
257 "# Architecture\n\nStub for testing.\n", encoding="utf-8",
258 )
259
260 # pyproject.toml marker (needed for get_project_root).
261 (root / "pyproject.toml").write_text(
262 '[project]\nname = "test_project"\n', encoding="utf-8",
263 )
264
265 # Initial commit.
266 _git(root, "add", "-A")
267 _git(root, "commit", "-m", "initial: project structure", "--quiet")
268
269 return root.resolve()
270
271
272@pytest.fixture
273def make_compliant_py(temp_git_project: Path):
274 """Factory: write a compliant Python file and return its Path.
275
276 Usage::
277
278 path = make_compliant_py("src/good.py")
279 """
280 def _factory(rel_path: str) -> Path:
281 content = _COMPLIANT_TEMPLATE.format(
282 rel_path=rel_path,
283 )
284 full = temp_git_project / rel_path
285 full.parent.mkdir(parents=True, exist_ok=True)
286 full.write_text(content, encoding="utf-8")
287
288 # Create companion test file so `tests_exist` check passes.
289 # The linter looks for tests/test_<module>.py.
290 module_name = full.stem # e.g. "good" from "src/good.py"
291 test_file = temp_git_project / "tests" / f"test_{module_name}.py"
292 if not test_file.exists():
293 test_file.write_text(
294 f'"""Stub test for {module_name}."""\n\n'
295 f'def test_{module_name}():\n pass\n',
296 encoding="utf-8",
297 )
298 return full
299
300 return _factory
301
302
303@pytest.fixture
304def make_noncompliant_py(temp_git_project: Path):
305 """Factory: write a noncompliant Python file with a chosen violation.
306
307 Usage::
308
309 path = make_noncompliant_py("src/bad.py", "missing_header")
310
311 Supported violations:
312 - ``"missing_header"``
313 - ``"missing_docstring"``
314 - ``"hardcoded_secret"``
315 - ``"format_error"``
316 """
317 templates = {
318 "missing_header": _NONCOMPLIANT_MISSING_HEADER,
319 "missing_docstring": _NONCOMPLIANT_MISSING_DOCSTRING,
320 "hardcoded_secret": _NONCOMPLIANT_HARDCODED_SECRET,
321 "format_error": _NONCOMPLIANT_FORMAT_ERROR,
322 }
323
324 def _factory(rel_path: str, violation: str) -> Path:
325 if violation not in templates:
326 raise ValueError(
327 f"Unknown violation {violation!r}; "
328 f"choose from: {', '.join(templates)}"
329 )
330 content = templates[violation]
331 full = temp_git_project / rel_path
332 full.parent.mkdir(parents=True, exist_ok=True)
333 full.write_text(content, encoding="utf-8")
334 return full
335
336 return _factory
make_noncompliant_py(Path temp_git_project)
Definition conftest.py:304
None commit_file(Path repo, str name, str content, str message="add file")
Definition conftest.py:69
subprocess.CompletedProcess _git(Path repo, *str args, bool check=True)
Definition conftest.py:56
make_compliant_py(Path temp_git_project)
Definition conftest.py:273
Path temp_git_project(Path tmp_path)
Definition conftest.py:198
bool _git_available()
Definition conftest.py:52