Option C Tools
Loading...
Searching...
No Matches
test_git_ignore.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_git_ignore.py
4
5"""
6Purpose
7-------
8Integration tests for ``oct git ignore`` (Phase 4G).
9
10Responsibilities
11----------------
12- Verify pattern addition into the OCT-managed block.
13- Verify pattern removal from the OCT-managed block.
14- Verify ``--list`` partitions OCT-managed and user patterns.
15- Verify pattern validation rejects unsafe inputs.
16- Verify the same fenced block is shared with ``oct git init``.
17- Verify JSON output schema.
18- Verify audit trail.
19
20Diagnostics
21-----------
22Domain: GIT-TESTS
23Levels:
24 L2 -- test lifecycle
25 L3 -- assertion details
26 L4 -- deep tracing
27
28Contracts
29---------
30- All tests use subprocess invocation against ``oct.cli``.
31- All tests use tmp_path fixtures so they are automatically cleaned up.
32"""
33
34import json
35import shutil
36import subprocess
37import sys
38from pathlib import Path
39
40import pytest
41
42
43def _git_available() -> bool:
44 return shutil.which("git") is not None
45
46
48 root: Path, *extra_args: str,
49) -> subprocess.CompletedProcess:
50 """Run ``oct git ignore`` in a subprocess."""
51 return subprocess.run(
52 [sys.executable, "-m", "oct.cli", "git", "ignore", *extra_args],
53 cwd=root,
54 capture_output=True,
55 text=True,
56 )
57
58
59# =====================================================================
60# Add patterns
61# =====================================================================
62
63
65
66 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
67 def test_appends_to_oct_block(self, temp_git_project: Path) -> None:
68 result = _run_ignore(temp_git_project, "*.tmp")
69 assert result.returncode == 0, result.stderr
70 text = (temp_git_project / ".gitignore").read_text(encoding="utf-8")
71 assert "*.tmp" in text
72 assert "# --- Option C defaults ---" in text
73 assert "# --- end Option C ---" in text
74
75 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
76 def test_duplicate_is_noop(self, temp_git_project: Path) -> None:
77 _run_ignore(temp_git_project, "*.tmp")
78 result = _run_ignore(temp_git_project, "*.tmp")
79 assert result.returncode == 0, result.stderr
80 # Pattern should not be duplicated.
81 text = (temp_git_project / ".gitignore").read_text(encoding="utf-8")
82 assert text.count("*.tmp") == 1
83
84 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
85 def test_multiple_patterns(self, temp_git_project: Path) -> None:
86 result = _run_ignore(
87 temp_git_project, "*.tmp", "*.log", "scratch/",
88 )
89 assert result.returncode == 0, result.stderr
90 text = (temp_git_project / ".gitignore").read_text(encoding="utf-8")
91 for pat in ("*.tmp", "*.log", "scratch/"):
92 assert pat in text
93
94
95# =====================================================================
96# Remove patterns
97# =====================================================================
98
99
101
102 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
103 def test_remove_from_oct_block(self, temp_git_project: Path) -> None:
104 _run_ignore(temp_git_project, "*.tmp")
105 result = _run_ignore(temp_git_project, "--remove", "*.tmp")
106 assert result.returncode == 0, result.stderr
107 text = (temp_git_project / ".gitignore").read_text(encoding="utf-8")
108 assert "*.tmp" not in text
109
110 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
112 self, temp_git_project: Path,
113 ) -> None:
114 # First create the OCT block by adding any pattern.
115 _run_ignore(temp_git_project, "*.tmp")
116 # Now append a user-managed pattern *outside* the block.
117 gitignore = temp_git_project / ".gitignore"
118 existing = gitignore.read_text(encoding="utf-8")
119 if not existing.endswith("\n"):
120 existing += "\n"
121 gitignore.write_text(
122 existing + "user_managed.txt\n", encoding="utf-8",
123 )
124 result = _run_ignore(
125 temp_git_project, "--remove", "user_managed.txt",
126 )
127 assert result.returncode == 1
128 assert "outside the oct-managed block" in result.stderr.lower()
129
130 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
132 self, temp_git_project: Path,
133 ) -> None:
134 _run_ignore(temp_git_project, "*.tmp") # ensure block exists
135 result = _run_ignore(
136 temp_git_project, "--remove", "does_not_exist.xyz",
137 )
138 assert result.returncode == 1
139 assert "not found" in result.stderr.lower()
140
141
142# =====================================================================
143# List
144# =====================================================================
145
146
148
149 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
151 self, temp_git_project: Path,
152 ) -> None:
153 # Pre-populate with a user pattern.
154 gitignore = temp_git_project / ".gitignore"
155 existing = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
156 gitignore.write_text(
157 existing + "\nuser_managed.txt\n", encoding="utf-8",
158 )
159 # Add an OCT-managed pattern.
160 _run_ignore(temp_git_project, "*.tmp")
161
162 result = _run_ignore(temp_git_project, "--list", "--json")
163 assert result.returncode == 0, result.stderr
164 data = json.loads(result.stdout)
165 assert "*.tmp" in data["oct_managed"]
166 assert "user_managed.txt" in data["other"]
167 assert "user_managed.txt" not in data["oct_managed"]
168
169
170# =====================================================================
171# Validation
172# =====================================================================
173
174
176
177 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
178 def test_negation_rejected(self, temp_git_project: Path) -> None:
179 result = _run_ignore(temp_git_project, "!important.txt")
180 assert result.returncode == 1
181 assert "negation" in result.stderr.lower()
182
183 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
185 self, temp_git_project: Path,
186 ) -> None:
187 result = _run_ignore(temp_git_project, "/absolute")
188 assert result.returncode == 1
189 assert "absolute" in result.stderr.lower()
190
191 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
192 def test_bad_chars_rejected(self, temp_git_project: Path) -> None:
193 result = _run_ignore(temp_git_project, "bad pattern with spaces")
194 assert result.returncode == 1
195
196
197# =====================================================================
198# Shared fenced block
199# =====================================================================
200
201
203
204 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
206 self, temp_git_project: Path,
207 ) -> None:
208 # Run oct git init to populate the OCT-managed block.
209 subprocess.run(
210 [sys.executable, "-m", "oct.cli", "git", "init"],
211 cwd=temp_git_project, capture_output=True, text=True,
212 )
213 # Now add a pattern via oct git ignore.
214 result = _run_ignore(temp_git_project, "*.scratch")
215 assert result.returncode == 0, result.stderr
216
217 text = (temp_git_project / ".gitignore").read_text(encoding="utf-8")
218 # There should be exactly one OCT block, not two.
219 assert text.count("# --- Option C defaults ---") == 1
220 assert text.count("# --- end Option C ---") == 1
221 # Both the init defaults and the new pattern should be present.
222 assert "*.scratch" in text
223
224
225# =====================================================================
226# JSON
227# =====================================================================
228
229
231
232 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
233 def test_add_json_schema(self, temp_git_project: Path) -> None:
234 result = _run_ignore(temp_git_project, "--json", "*.tmp")
235 assert result.returncode == 0, result.stderr
236 data = json.loads(result.stdout)
237 assert data["operation"] == "add"
238 assert data["patterns_added"] == ["*.tmp"]
239 assert data["exit_code"] == 0
240
241
242# =====================================================================
243# Audit
244# =====================================================================
245
246
248
249 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
250 def test_audit_record_written(self, temp_git_project: Path) -> None:
251 result = _run_ignore(temp_git_project, "*.tmp")
252 assert result.returncode == 0, result.stderr
253
254 audit_dir = temp_git_project / "logs"
255 audit_files = list(audit_dir.glob("git-audit-*.jsonl"))
256 assert audit_files, "no audit file written"
257
258 found = False
259 for af in audit_files:
260 for line in af.read_text(encoding="utf-8").splitlines():
261 try:
262 rec = json.loads(line)
263 except json.JSONDecodeError:
264 continue
265 if rec.get("command") == "oct git ignore":
266 found = True
267 break
268 if found:
269 break
270 assert found, "no 'oct git ignore' audit record found"
None test_appends_to_oct_block(self, Path temp_git_project)
None test_duplicate_is_noop(self, Path temp_git_project)
None test_multiple_patterns(self, Path temp_git_project)
None test_audit_record_written(self, Path temp_git_project)
None test_oct_init_and_oct_ignore_share_block(self, Path temp_git_project)
None test_add_json_schema(self, Path temp_git_project)
None test_list_partitions_correctly(self, Path temp_git_project)
None test_remove_nonexistent_pattern(self, Path temp_git_project)
None test_remove_outside_block_refused(self, Path temp_git_project)
None test_remove_from_oct_block(self, Path temp_git_project)
None test_absolute_pattern_rejected(self, Path temp_git_project)
None test_negation_rejected(self, Path temp_git_project)
None test_bad_chars_rejected(self, Path temp_git_project)
subprocess.CompletedProcess _run_ignore(Path root, *str extra_args)