Option C Tools
Loading...
Searching...
No Matches
test_git_init.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_git_init.py
4
5"""
6Purpose
7-------
8Integration tests for ``oct git init`` (Phase 4C — G-C1/G-C3).
9
10Responsibilities
11----------------
12- Verify init bootstraps a git repo on a bare directory.
13- Verify .gitignore merge logic (append-only, no duplicates).
14- Verify .gitattributes creation and merge.
15- Verify --dry-run, --no-hooks, --github flags.
16- Verify idempotent re-runs.
17- Verify audit trail is written.
18
19Diagnostics
20-----------
21Domain: GIT-TESTS
22Levels:
23 L2 — test lifecycle
24 L3 — assertion details
25 L4 — deep tracing
26
27Contracts
28---------
29- All tests use tmp_path so they are automatically cleaned up.
30"""
31
32import json
33import shutil
34import subprocess
35import sys
36from pathlib import Path
37
38import pytest
39
40
41# =====================================================================
42# Helpers
43# =====================================================================
44
45
46def _git_available() -> bool:
47 return shutil.which("git") is not None
48
49
50def _git(repo: Path, *args: str, check: bool = True):
51 return subprocess.run(
52 ["git", *args],
53 cwd=repo,
54 check=check,
55 capture_output=True,
56 text=True,
57 )
58
59
60def _run_init(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
61 """Run ``oct git init`` in a subprocess with *root* as cwd."""
62 return subprocess.run(
63 [sys.executable, "-m", "oct.cli", "git", "init", *extra_args],
64 cwd=root,
65 capture_output=True,
66 text=True,
67 )
68
69
70def _make_option_c_dir(tmp_path: Path, name: str = "fresh") -> Path:
71 """Create a directory with minimal Option C project structure.
72
73 Includes docs/, tests/, oc_diagnostics/ so find_project_root works.
74 """
75 root = tmp_path / name
76 root.mkdir()
77 (root / "docs").mkdir()
78 (root / "tests").mkdir()
79 (root / "oc_diagnostics").mkdir()
80 (root / "logs").mkdir()
81 (root / "src").mkdir()
82 (root / "pyproject.toml").write_text(
83 f'[project]\nname = "{name}"\n', encoding="utf-8",
84 )
85 return root
86
87
88def _make_git_project(tmp_path: Path, name: str = "existing") -> Path:
89 """Create an Option C project inside a real git repo with one commit."""
90 root = _make_option_c_dir(tmp_path, name)
91 _git(root, "init", "--quiet", "-b", "main")
92 _git(root, "config", "user.email", "test@example.com")
93 _git(root, "config", "user.name", "Test")
94 _git(root, "config", "commit.gpgsign", "false")
95 _git(root, "add", "-A")
96 _git(root, "commit", "-m", "initial", "--quiet")
97 return root
98
99
100# =====================================================================
101# Init on a non-git directory
102# =====================================================================
103
104
106 """Init on a directory that is not yet a git repo."""
107
108 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
109 def test_creates_git_repo(self, tmp_path):
110 root = _make_option_c_dir(tmp_path)
111 result = _run_init(root)
112 assert result.returncode == 0, result.stderr
113 assert (root / ".git").is_dir()
114
115 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
116 def test_creates_gitignore(self, tmp_path):
117 root = _make_option_c_dir(tmp_path)
118 result = _run_init(root)
119 assert result.returncode == 0, result.stderr
120 gi = root / ".gitignore"
121 assert gi.is_file()
122 content = gi.read_text(encoding="utf-8")
123 assert "__pycache__/" in content
124 assert ".env" in content
125
126 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
127 def test_creates_gitattributes(self, tmp_path):
128 root = _make_option_c_dir(tmp_path)
129 result = _run_init(root)
130 assert result.returncode == 0, result.stderr
131 ga = root / ".gitattributes"
132 assert ga.is_file()
133 content = ga.read_text(encoding="utf-8")
134 assert "*.py text eol=lf" in content
135 assert "*.pem binary" in content
136 assert "*.key binary" in content
137
138 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
140 root = _make_option_c_dir(tmp_path)
141 _run_init(root)
142 content = (root / ".gitignore").read_text(encoding="utf-8")
143 assert "# --- Option C defaults ---" in content
144 assert "# --- end Option C ---" in content
145
146
147# =====================================================================
148# Idempotent re-run
149# =====================================================================
150
151
153 """Running init twice should not duplicate entries."""
154
155 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
156 def test_no_duplicates_on_rerun(self, tmp_path):
157 root = _make_git_project(tmp_path)
158 _run_init(root)
159 first_gi = (root / ".gitignore").read_text(encoding="utf-8")
160
161 # Second run.
162 result = _run_init(root)
163 assert result.returncode == 0
164 second_gi = (root / ".gitignore").read_text(encoding="utf-8")
165 assert first_gi == second_gi
166
167 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
168 def test_reports_up_to_date(self, tmp_path):
169 root = _make_git_project(tmp_path)
170 _run_init(root)
171 result = _run_init(root)
172 assert "already up-to-date" in result.stdout
173
174
175# =====================================================================
176# .gitignore merge logic
177# =====================================================================
178
179
181 """Merge preserves existing entries and only appends missing ones."""
182
183 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
185 root = _make_git_project(tmp_path)
186 existing = "# My custom ignores\nnode_modules/\n*.log\n"
187 (root / ".gitignore").write_text(existing, encoding="utf-8")
188
189 _run_init(root)
190 content = (root / ".gitignore").read_text(encoding="utf-8")
191 assert "node_modules/" in content
192 assert "*.log" in content
193 assert "__pycache__/" in content # newly added
194
195 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
197 root = _make_git_project(tmp_path)
198 # Pre-seed with some Option C entries.
199 (root / ".gitignore").write_text(
200 "__pycache__/\n.venv/\n", encoding="utf-8",
201 )
202 _run_init(root)
203 content = (root / ".gitignore").read_text(encoding="utf-8")
204 # Should only appear once.
205 assert content.count("__pycache__/") == 1
206
207
208# =====================================================================
209# .gitattributes merge logic
210# =====================================================================
211
212
214 """Merge creates or extends .gitattributes."""
215
216 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
217 def test_creates_from_scratch(self, tmp_path):
218 root = _make_git_project(tmp_path)
219 _run_init(root)
220 ga = root / ".gitattributes"
221 assert ga.is_file()
222 content = ga.read_text(encoding="utf-8")
223 assert "*.py text eol=lf" in content
224
225 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
226 def test_merges_existing(self, tmp_path):
227 root = _make_git_project(tmp_path)
228 (root / ".gitattributes").write_text(
229 "*.py text eol=lf\n", encoding="utf-8",
230 )
231 _run_init(root)
232 content = (root / ".gitattributes").read_text(encoding="utf-8")
233 assert "*.pem binary" in content
234 # Existing entry not duplicated.
235 assert content.count("*.py text eol=lf") == 1
236
237
238# =====================================================================
239# --dry-run
240# =====================================================================
241
242
244
245 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
246 def test_no_files_created(self, tmp_path):
247 root = _make_option_c_dir(tmp_path)
248 result = _run_init(root, "--dry-run")
249 assert result.returncode == 0
250 assert "[DRY RUN]" in result.stdout
251 assert not (root / ".gitignore").is_file()
252 assert not (root / ".gitattributes").is_file()
253
254 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
255 def test_dry_run_on_existing_repo(self, tmp_path):
256 root = _make_git_project(tmp_path)
257 result = _run_init(root, "--dry-run")
258 assert result.returncode == 0
259 assert "[DRY RUN]" in result.stdout
260
261
262# =====================================================================
263# --no-hooks
264# =====================================================================
265
266
268
269 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
270 def test_skips_hooks(self, tmp_path):
271 root = _make_git_project(tmp_path)
272 result = _run_init(root, "--no-hooks")
273 assert result.returncode == 0
274 assert not (root / ".pre-commit-config.yaml").is_file()
275 assert "Skipped hook installation" in result.stdout
276
277 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
279 root = _make_git_project(tmp_path)
280 result = _run_init(root)
281 assert result.returncode == 0
282 assert (root / ".pre-commit-config.yaml").is_file()
283
284
285# =====================================================================
286# --github
287# =====================================================================
288
289
291
292 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
293 def test_creates_workflow(self, tmp_path):
294 root = _make_git_project(tmp_path)
295 result = _run_init(root, "--github")
296 assert result.returncode == 0
297 wf = root / ".github" / "workflows" / "option-c.yml"
298 assert wf.is_file()
299 content = wf.read_text(encoding="utf-8")
300 assert "oct lint" in content
301 assert "oct format" in content
302
303 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
304 def test_no_workflow_without_flag(self, tmp_path):
305 root = _make_git_project(tmp_path)
306 _run_init(root)
307 assert not (root / ".github").exists()
308
309 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
310 def test_workflow_idempotent(self, tmp_path):
311 root = _make_git_project(tmp_path)
312 _run_init(root, "--github")
313 first = (root / ".github" / "workflows" / "option-c.yml").read_text(
314 encoding="utf-8",
315 )
316 result = _run_init(root, "--github")
317 assert "already exists" in result.stdout
318 second = (root / ".github" / "workflows" / "option-c.yml").read_text(
319 encoding="utf-8",
320 )
321 assert first == second
322
323
324# =====================================================================
325# Audit trail
326# =====================================================================
327
328
330
331 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
332 def test_audit_record_written(self, tmp_path):
333 root = _make_git_project(tmp_path)
334 result = _run_init(root)
335 assert result.returncode == 0
336 # Audit files are JSONL in logs/oct_git_audit*.jsonl.
337 audit_files = list((root / "logs").glob("oct_git_audit*.jsonl"))
338 if audit_files:
339 content = audit_files[0].read_text(encoding="utf-8")
340 assert "oct git init" in content
test_dry_run_on_existing_repo(self, tmp_path)
test_no_workflow_without_flag(self, tmp_path)
test_hooks_installed_by_default(self, tmp_path)
Path _make_git_project(Path tmp_path, str name="existing")
Path _make_option_c_dir(Path tmp_path, str name="fresh")
subprocess.CompletedProcess _run_init(Path root, *str extra_args)
_git(Path repo, *str args, bool check=True)