Option C Tools
Loading...
Searching...
No Matches
test_git_status_monorepo.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_git_status_monorepo.py
4
5"""
6Purpose
7-------
8Integration tests for ``oct git status`` in monorepo layouts where the
9git repo root differs from the OCT project root.
10
11Responsibilities
12----------------
13- Verify that ``oct git status`` resolves file paths correctly when
14 the OCT project lives in a subdirectory of the git repo.
15- Verify that no false "File too short" or "Missing module docstring"
16 warnings appear on well-formed files.
17- Verify that files outside the project scope are excluded.
18- Verify that duplicate entries do not appear across staged/modified
19 sections.
20
21Diagnostics
22-----------
23Domain: GIT-TESTS
24Levels:
25 L2 — test lifecycle
26 L3 — assertion details
27 L4 — deep tracing
28
29Contracts
30---------
31- Every test uses the ``monorepo_project`` fixture.
32- Tests never modify the system git config.
33"""
34
35import json
36import shutil
37import subprocess
38import sys
39import textwrap
40from pathlib import Path
41
42import pytest
43
44
45# =====================================================================
46# Helpers
47# =====================================================================
48
49
50def _git_available() -> bool:
51 return shutil.which("git") is not None
52
53
54def _git(
55 repo: Path, *args: str, check: bool = True,
56) -> 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 project_dir: Path, *extra_args: str,
69) -> subprocess.CompletedProcess:
70 """Run ``oct git status`` from inside the OCT project subdirectory."""
71 return subprocess.run(
72 [sys.executable, "-m", "oct.cli", "git", "status", *extra_args],
73 cwd=project_dir,
74 capture_output=True,
75 text=True,
76 )
77
78
79_COMPLIANT_TEMPLATE = textwrap.dedent("""\
80 #!/usr/bin/env python3
81 # -*- coding: utf-8 -*-
82 # {rel_path}
83
84 \"\"\"
85 Purpose
86 -------
87 Test module for monorepo layout validation.
88
89 Responsibilities
90 ----------------
91 - Placeholder for monorepo integration tests.
92
93 Diagnostics
94 -----------
95 Domain: TEST
96 Levels:
97 L1 — errors
98 L2 — lifecycle
99 L3 — details
100 L4 — deep trace
101
102 Contracts
103 ---------
104 - None.
105 \"\"\"
106
107 from oc_diagnostics import _dbg
108
109 func = "placeholder"
110 _dbg("TEST", func, "loaded", 2)
111""")
112
113
114# =====================================================================
115# Fixtures
116# =====================================================================
117
118
119@pytest.fixture
120def monorepo_project(tmp_path: Path) -> tuple[Path, Path]:
121 """Create a monorepo where the OCT project is a subdirectory.
122
123 Layout::
124
125 <git_root>/
126 ├── .git/
127 ├── other_project/
128 │ └── README.md
129 └── subproject/ ← OCT project root
130 ├── .option_c/
131 ├── .octrc.json
132 ├── oc_diagnostics/
133 ├── docs/
134 │ └── ARCHITECTURE.md
135 ├── logs/
136 ├── src/
137 ├── tests/
138 └── pyproject.toml
139
140 Returns ``(git_root, project_root)`` where ``project_root`` is
141 ``git_root / "subproject"``.
142
143 The project directory is named ``subproject`` (not ``oct``) so that
144 running ``python -m oct.cli`` in a subprocess does not shadow the
145 installed ``oct`` package.
146 """
147 if not _git_available():
148 pytest.skip("git binary not available on PATH")
149
150 git_root = tmp_path / "monorepo"
151 git_root.mkdir()
152
153 # Init git at the monorepo level.
154 _git(git_root, "init", "--quiet", "-b", "main")
155 _git(git_root, "config", "user.email", "test@example.com")
156 _git(git_root, "config", "user.name", "Test")
157 _git(git_root, "config", "commit.gpgsign", "false")
158
159 # Another project outside the OCT subproject.
160 (git_root / "other_project").mkdir()
161 (git_root / "other_project" / "README.md").write_text(
162 "# Other project\n", encoding="utf-8",
163 )
164
165 # OCT project subdirectory.
166 project = git_root / "subproject"
167 project.mkdir()
168
169 (project / ".option_c").mkdir()
170 (project / "src").mkdir()
171 (project / "tests").mkdir()
172 (project / "docs").mkdir()
173 (project / "logs").mkdir()
174 (project / "oc_diagnostics").mkdir()
175
176 octrc = {
177 "linter": {"profile": "compact"},
178 "git": {"protected_branches": ["main", "master"]},
179 }
180 (project / ".octrc.json").write_text(
181 json.dumps(octrc, indent=2), encoding="utf-8",
182 )
183
184 debug_cfg = {
185 "domains": {
186 "GIT": {"level": 1, "color": "cyan"},
187 "TEST": {"level": 1, "color": "white"},
188 },
189 }
190 (project / "debug_config.json").write_text(
191 json.dumps(debug_cfg, indent=2), encoding="utf-8",
192 )
193
194 (project / "docs" / "ARCHITECTURE.md").write_text(
195 "# Architecture\n\nStub for testing.\n", encoding="utf-8",
196 )
197
198 (project / "pyproject.toml").write_text(
199 '[project]\nname = "test_project"\n', encoding="utf-8",
200 )
201
202 # Initial commit at the monorepo root.
203 _git(git_root, "add", "-A")
204 _git(git_root, "commit", "-m", "initial: monorepo structure", "--quiet")
205
206 return git_root.resolve(), project.resolve()
207
208
209# =====================================================================
210# Tests: Path resolution
211# =====================================================================
212
213
215 """Verify that files resolve correctly when project root != git root."""
216
217 def test_staged_file_no_false_warnings(self, monorepo_project):
218 """A well-formed file should get [OK], not 'File too short'."""
219 git_root, project = monorepo_project
220 # Write a compliant file inside the OCT project.
221 path = project / "src" / "good.py"
222 path.parent.mkdir(parents=True, exist_ok=True)
223 path.write_text(
224 _COMPLIANT_TEMPLATE.format(rel_path="src/good.py"),
225 encoding="utf-8",
226 )
227
228 # Companion test file.
229 test_file = project / "tests" / "test_good.py"
230 test_file.write_text(
231 '"""Stub test for good."""\n\ndef test_good():\n pass\n',
232 encoding="utf-8",
233 )
234
235 # Stage from the git root (as git would see it).
236 _git(git_root, "add", "subproject/src/good.py")
237
238 result = _run_status(project, "--json")
239 assert result.returncode == 0, result.stderr
240
241 data = json.loads(result.stdout)
242 staged_paths = [e["path"] for e in data["staged"]]
243 staged_statuses = [e["status"] for e in data["staged"]]
244
245 # The file should appear in staged.
246 assert any("good.py" in p for p in staged_paths), (
247 f"good.py not in staged: {staged_paths}"
248 )
249
250 # It should be compliant, not flagged.
251 for entry in data["staged"]:
252 if "good.py" in entry["path"]:
253 assert entry["status"] == "ok", (
254 f"Expected ok but got {entry['status']}: "
255 f"{entry['issues']}"
256 )
257 assert "File too short" not in str(entry["issues"])
258 assert "Missing module docstring" not in str(entry["issues"])
259
260 def test_files_outside_project_excluded(self, monorepo_project):
261 """Files in other_project/ should not appear in oct git status."""
262 git_root, project = monorepo_project
263
264 # Modify a file outside the OCT project.
265 readme = git_root / "other_project" / "README.md"
266 readme.write_text("# Changed\n", encoding="utf-8")
267 _git(git_root, "add", "other_project/README.md")
268
269 result = _run_status(project, "--json")
270 assert result.returncode == 0, result.stderr
271
272 data = json.loads(result.stdout)
273 all_paths = (
274 [e["path"] for e in data["staged"]]
275 + [e["path"] for e in data["modified"]]
276 + [e["path"] for e in data["untracked"]]
277 )
278 for p in all_paths:
279 assert "other_project" not in p, (
280 f"File outside project scope appeared: {p}"
281 )
282
283
284# =====================================================================
285# Tests: Deduplication
286# =====================================================================
287
288
290 """Verify no spurious duplicates across sections."""
291
293 """A file that is only staged should not also appear as modified."""
294 git_root, project = monorepo_project
295
296 path = project / "src" / "only_staged.py"
297 path.parent.mkdir(parents=True, exist_ok=True)
298 path.write_text(
299 _COMPLIANT_TEMPLATE.format(rel_path="src/only_staged.py"),
300 encoding="utf-8",
301 )
302
303 _git(git_root, "add", "subproject/src/only_staged.py")
304
305 result = _run_status(project, "--json")
306 assert result.returncode == 0, result.stderr
307
308 data = json.loads(result.stdout)
309 staged_paths = {e["path"] for e in data["staged"]}
310 modified_paths = {e["path"] for e in data["modified"]}
311
312 overlap = staged_paths & modified_paths
313 assert not overlap, f"Duplicate entries in staged and modified: {overlap}"
subprocess.CompletedProcess _git(Path repo, *str args, bool check=True)
tuple[Path, Path] monorepo_project(Path tmp_path)
subprocess.CompletedProcess _run_status(Path project_dir, *str extra_args)