Option C Tools
Loading...
Searching...
No Matches
test_workspace_ignore.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_workspace_ignore.py
4
5"""
6Purpose
7-------
8Integration tests for the F1.4 workspace-aware ``oct git ignore``
9``--scope`` flag and the matching ``oct git init`` workspace-mode
10behaviour (which augments the workspace-root ``.gitignore`` with the
11manifest's ``ignore_at_workspace_scope[]`` patterns).
12
13Responsibilities
14----------------
15- ``oct git ignore --scope=workspace`` writes to the workspace
16 root's ``.gitignore``, even when invoked from inside a subproject.
17- ``oct git ignore --scope=project`` writes to the
18 ``project_root``'s ``.gitignore`` (subproject if invoked there;
19 workspace root if invoked there).
20- ``oct git ignore --scope=nearest`` (the default) targets the
21 workspace root when CWD is the workspace root, otherwise the
22 subproject's ``.gitignore``.
23- ``oct git init`` at workspace scope merges the manifest's
24 ``ignore_at_workspace_scope[]`` patterns into the workspace
25 ``.gitignore``.
26
27Diagnostics
28-----------
29Domain: GIT-TESTS
30Levels:
31 L2 -- test lifecycle
32 L3 -- assertion details
33
34Contracts
35---------
36- All tests use subprocess invocation against ``oct.cli``.
37- All tests use tmp_path fixtures so they are automatically cleaned up.
38"""
39
40import json
41import shutil
42import subprocess
43import sys
44from pathlib import Path
45
46import pytest
47
48
49def _git_available() -> bool:
50 return shutil.which("git") is not None
51
52
53def _git(repo: Path, *args: str, check: bool = True):
54 return subprocess.run(
55 ["git", *args], cwd=repo, check=check,
56 capture_output=True, text=True,
57 )
58
59
61 cwd: Path, *extra_args: str,
62) -> subprocess.CompletedProcess:
63 return subprocess.run(
64 [sys.executable, "-m", "oct.cli", "git", "ignore", *extra_args],
65 cwd=cwd, capture_output=True, text=True,
66 )
67
68
70 cwd: Path, *extra_args: str,
71) -> subprocess.CompletedProcess:
72 return subprocess.run(
73 [sys.executable, "-m", "oct.cli", "git", "init", *extra_args],
74 cwd=cwd, capture_output=True, text=True,
75 )
76
77
78def _make_subproject(parent: Path, name: str) -> Path:
79 sub = parent / name
80 sub.mkdir(parents=True, exist_ok=True)
81 (sub / ".option_c").mkdir()
82 (sub / "src").mkdir()
83 (sub / "tests").mkdir()
84 (sub / "docs").mkdir()
85 (sub / "logs").mkdir()
86 (sub / "oc_diagnostics").mkdir()
87 (sub / ".octrc.json").write_text(
88 json.dumps({"linter": {"profile": "compact"}}),
89 encoding="utf-8",
90 )
91 (sub / "pyproject.toml").write_text(
92 f'[project]\nname = "{name}"\n', encoding="utf-8",
93 )
94 (sub / "debug_config.json").write_text(
95 json.dumps({"domains": {"GIT": {"level": 1, "color": "cyan"}}}),
96 encoding="utf-8",
97 )
98 (sub / "docs" / "ARCHITECTURE.md").write_text(
99 "# Architecture\n", encoding="utf-8",
100 )
101 return sub
102
103
104@pytest.fixture
105def workspace_with_subproject(tmp_path: Path):
106 """Build a workspace with one subproject for scope tests."""
107 if not _git_available():
108 pytest.skip("git binary not available on PATH")
109
110 ws_root = tmp_path / "ws"
111 ws_root.mkdir()
112 _git(ws_root, "init", "--quiet", "-b", "feature/test")
113 _git(ws_root, "config", "user.email", "test@example.com")
114 _git(ws_root, "config", "user.name", "Test")
115 _git(ws_root, "config", "commit.gpgsign", "false")
116
117 manifest = {
118 "version": "1.0",
119 "name": "Scope Test Workspace",
120 "ignore_at_workspace_scope": [
121 ".claude/settings.local.json",
122 ".claude/worktrees/**",
123 ],
124 "subprojects": [
125 {"path": "alpha", "name": "alpha", "profile": "compact"},
126 ],
127 }
128 (ws_root / ".option_c_workspace.json").write_text(
129 json.dumps(manifest, indent=2), encoding="utf-8",
130 )
131
132 alpha = _make_subproject(ws_root, "alpha")
133 _git(ws_root, "add", "-A")
134 _git(ws_root, "commit", "-m", "initial", "--quiet")
135
136 return ws_root.resolve(), alpha.resolve()
137
138
139# =====================================================================
140# --scope=workspace
141# =====================================================================
142
143
145
147 self, workspace_with_subproject,
148 ) -> None:
149 ws_root, _alpha = workspace_with_subproject
150 result = _run_ignore(
151 ws_root, "*.tmp", "--scope=workspace",
152 )
153 assert result.returncode == 0, result.stderr
154 ws_gitignore = (ws_root / ".gitignore").read_text(encoding="utf-8")
155 assert "*.tmp" in ws_gitignore
156
158 self, workspace_with_subproject,
159 ) -> None:
160 ws_root, alpha = workspace_with_subproject
161 result = _run_ignore(
162 alpha, "*.scratch", "--scope=workspace",
163 )
164 assert result.returncode == 0, result.stderr
165 ws_gitignore = (ws_root / ".gitignore").read_text(encoding="utf-8")
166 assert "*.scratch" in ws_gitignore
167 # Subproject .gitignore not touched (still doesn't exist).
168 assert not (alpha / ".gitignore").is_file()
169
171 self, tmp_path: Path,
172 ) -> None:
173 if not _git_available():
174 pytest.skip("git not on PATH")
175 # Create a single-project setup (no workspace manifest).
176 proj = tmp_path / "proj"
177 proj.mkdir()
178 _git(proj, "init", "--quiet", "-b", "main")
179 _git(proj, "config", "user.email", "t@e.com")
180 _git(proj, "config", "user.name", "T")
181 _git(proj, "config", "commit.gpgsign", "false")
182 _make_subproject(proj.parent, proj.name)
183 _git(proj, "add", "-A")
184 _git(proj, "commit", "-m", "init", "--quiet")
185
186 result = _run_ignore(proj, "*.tmp", "--scope=workspace")
187 assert result.returncode == 1
188 assert "workspace manifest" in result.stderr.lower()
189
190
191# =====================================================================
192# --scope=project
193# =====================================================================
194
195
197
199 self, workspace_with_subproject,
200 ) -> None:
201 ws_root, alpha = workspace_with_subproject
202 result = _run_ignore(
203 alpha, "build/", "--scope=project",
204 )
205 assert result.returncode == 0, result.stderr
206 # Subproject .gitignore created with the new pattern.
207 assert (alpha / ".gitignore").is_file()
208 content = (alpha / ".gitignore").read_text(encoding="utf-8")
209 assert "build/" in content
210 # Workspace .gitignore not touched by this scope.
211 ws_content = (ws_root / ".gitignore").read_text(
212 encoding="utf-8",
213 ) if (ws_root / ".gitignore").is_file() else ""
214 assert "build/" not in ws_content
215
216
217# =====================================================================
218# --scope=nearest (default)
219# =====================================================================
220
221
223
225 self, workspace_with_subproject,
226 ) -> None:
227 ws_root, _alpha = workspace_with_subproject
228 # No --scope flag -> default to "nearest". From the workspace
229 # root, that is the workspace gitignore.
230 result = _run_ignore(ws_root, "*.bak")
231 assert result.returncode == 0, result.stderr
232 ws_content = (ws_root / ".gitignore").read_text(encoding="utf-8")
233 assert "*.bak" in ws_content
234
236 self, workspace_with_subproject,
237 ) -> None:
238 ws_root, alpha = workspace_with_subproject
239 result = _run_ignore(alpha, "*.cache")
240 assert result.returncode == 0, result.stderr
241 # Subproject .gitignore created.
242 assert (alpha / ".gitignore").is_file()
243 sp_content = (alpha / ".gitignore").read_text(encoding="utf-8")
244 assert "*.cache" in sp_content
245 # Workspace .gitignore not touched.
246 ws_path = ws_root / ".gitignore"
247 ws_content = (
248 ws_path.read_text(encoding="utf-8")
249 if ws_path.is_file() else ""
250 )
251 assert "*.cache" not in ws_content
252
253
254# =====================================================================
255# oct git init workspace-aware (ignore_at_workspace_scope merged)
256# =====================================================================
257
258
260
262 self, workspace_with_subproject,
263 ) -> None:
264 ws_root, _alpha = workspace_with_subproject
265 # Remove the .gitignore the fixture may have committed so we
266 # measure init's contribution from a clean state.
267 gi = ws_root / ".gitignore"
268 if gi.is_file():
269 gi.unlink()
270
271 result = _run_init(ws_root)
272 assert result.returncode == 0, result.stderr
273 assert gi.is_file(), "init should have created .gitignore"
274 content = gi.read_text(encoding="utf-8")
275 # Standard OCT defaults are present.
276 assert "__pycache__/" in content
277 # Manifest's workspace-scope additions are present.
278 assert ".claude/settings.local.json" in content
279 assert ".claude/worktrees/**" in content
280
282 self, workspace_with_subproject,
283 ) -> None:
284 _ws_root, alpha = workspace_with_subproject
285 # Remove subproject .gitignore if it exists.
286 gi = alpha / ".gitignore"
287 if gi.is_file():
288 gi.unlink()
289
290 result = _run_init(alpha)
291 assert result.returncode == 0, result.stderr
292 if gi.is_file():
293 content = gi.read_text(encoding="utf-8")
294 # Standard defaults only — workspace patterns must NOT
295 # leak into subproject .gitignore via init.
296 assert ".claude/settings.local.json" not in content
297 assert ".claude/worktrees/**" not in content
298
299
300# =====================================================================
301# Backwards compat: outside any workspace, behaviour unchanged
302# =====================================================================
303
304
306
308 self, tmp_path: Path,
309 ) -> None:
310 if not _git_available():
311 pytest.skip("git not on PATH")
312 proj = tmp_path / "single"
313 _make_subproject(tmp_path, "single")
314 _git(proj, "init", "--quiet", "-b", "main")
315 _git(proj, "config", "user.email", "t@e.com")
316 _git(proj, "config", "user.name", "T")
317 _git(proj, "config", "commit.gpgsign", "false")
318 _git(proj, "add", "-A")
319 _git(proj, "commit", "-m", "init", "--quiet")
320
321 # Default scope (nearest) on a single-project setup writes to
322 # the project .gitignore.
323 result = _run_ignore(proj, "*.tmp")
324 assert result.returncode == 0, result.stderr
325 assert (proj / ".gitignore").is_file()
326 content = (proj / ".gitignore").read_text(encoding="utf-8")
327 assert "*.tmp" in content
None test_init_at_workspace_root_merges_ignore_patterns(self, workspace_with_subproject)
None test_init_at_subproject_does_not_use_workspace_patterns(self, workspace_with_subproject)
None test_default_at_workspace_root_targets_workspace(self, workspace_with_subproject)
None test_default_at_subproject_targets_subproject(self, workspace_with_subproject)
None test_writes_to_subproject_gitignore(self, workspace_with_subproject)
None test_writes_to_workspace_gitignore_from_workspace_root(self, workspace_with_subproject)
None test_writes_to_workspace_gitignore_from_subproject(self, workspace_with_subproject)
subprocess.CompletedProcess _run_init(Path cwd, *str extra_args)
Path _make_subproject(Path parent, str name)
_git(Path repo, *str args, bool check=True)
subprocess.CompletedProcess _run_ignore(Path cwd, *str extra_args)