Option C Tools
Loading...
Searching...
No Matches
test_status_scope.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_status_scope.py
4
5"""
6Purpose
7-------
8Integration tests for the ``--scope`` flag on ``oct git status``
9(Phase 4G). Validates the project / repo / auto modes that control
10whether files outside the OCT project root are surfaced.
11
12Responsibilities
13----------------
14- ``project`` (default) — only files inside the OCT project root.
15- ``repo`` — every file the git index/work-tree knows about.
16- ``auto`` — collapses to project when project_root == repo_root,
17 otherwise behaves like repo.
18
19Diagnostics
20-----------
21Domain: GIT-TESTS
22Levels:
23 L2 -- test lifecycle
24 L3 -- assertion details
25
26Contracts
27---------
28- All tests use subprocess invocation against ``oct.cli``.
29- All tests use tmp_path fixtures so they are automatically cleaned up.
30"""
31
32import json
33import shutil
34import subprocess
35import sys
36import textwrap
37from pathlib import Path
38
39import pytest
40
41
42def _git_available() -> bool:
43 return shutil.which("git") is not None
44
45
46def _git(
47 repo: Path, *args: str, check: bool = True,
48) -> subprocess.CompletedProcess:
49 return subprocess.run(
50 ["git", *args], cwd=repo, check=check,
51 capture_output=True, text=True,
52 )
53
54
56 project_dir: Path, *extra_args: str,
57) -> subprocess.CompletedProcess:
58 return subprocess.run(
59 [sys.executable, "-m", "oct.cli", "git", "status", *extra_args],
60 cwd=project_dir,
61 capture_output=True,
62 text=True,
63 )
64
65
66# =====================================================================
67# Fixtures
68# =====================================================================
69
70
71@pytest.fixture
72def monorepo_with_outside(tmp_path: Path) -> tuple[Path, Path]:
73 """Create a monorepo and place a file outside the OCT project."""
74 if not _git_available():
75 pytest.skip("git binary not available on PATH")
76
77 git_root = tmp_path / "monorepo"
78 git_root.mkdir()
79 _git(git_root, "init", "--quiet", "-b", "main")
80 _git(git_root, "config", "user.email", "test@example.com")
81 _git(git_root, "config", "user.name", "Test")
82 _git(git_root, "config", "commit.gpgsign", "false")
83
84 # Outside OCT project.
85 (git_root / "outside").mkdir()
86 (git_root / "outside" / "README.md").write_text(
87 "# Outside\n", encoding="utf-8",
88 )
89
90 # OCT subproject.
91 project = git_root / "subproject"
92 project.mkdir()
93 (project / ".option_c").mkdir()
94 (project / "src").mkdir()
95 (project / "tests").mkdir()
96 (project / "docs").mkdir()
97 (project / "logs").mkdir()
98 (project / "oc_diagnostics").mkdir()
99
100 (project / ".octrc.json").write_text(
101 json.dumps({
102 "linter": {"profile": "compact"},
103 "git": {"protected_branches": ["main", "master"]},
104 }, indent=2),
105 encoding="utf-8",
106 )
107 (project / "debug_config.json").write_text(
108 json.dumps({"domains": {"GIT": {"level": 1, "color": "cyan"}}}),
109 encoding="utf-8",
110 )
111 (project / "docs" / "ARCHITECTURE.md").write_text(
112 "# Architecture\n", encoding="utf-8",
113 )
114 (project / "pyproject.toml").write_text(
115 '[project]\nname = "test_project"\n', encoding="utf-8",
116 )
117
118 _git(git_root, "add", "-A")
119 _git(git_root, "commit", "-m", "initial", "--quiet")
120
121 # Now make modifications: one inside, one outside.
122 (project / "src" / "inside.py").write_text(
123 "x = 1\n", encoding="utf-8",
124 )
125 (git_root / "outside" / "outside_change.txt").write_text(
126 "y\n", encoding="utf-8",
127 )
128
129 return git_root.resolve(), project.resolve()
130
131
132# =====================================================================
133# scope=project (default)
134# =====================================================================
135
136
138
140 self, monorepo_with_outside,
141 ) -> None:
142 _git_root, project = monorepo_with_outside
143 result = _run_status(project, "--json")
144 assert result.returncode == 0, result.stderr
145 data = json.loads(result.stdout)
146 all_paths = (
147 [e["path"] for e in data["staged"]]
148 + [e["path"] for e in data["modified"]]
149 + [e["path"] for e in data["untracked"]]
150 )
151 assert all("outside" not in p for p in all_paths)
152 assert data["scope"] == "project"
153 # out_of_project is empty in project mode.
154 assert data["out_of_project"] == []
155
157 self, monorepo_with_outside,
158 ) -> None:
159 _git_root, project = monorepo_with_outside
160 result = _run_status(project, "--scope=project", "--json")
161 data = json.loads(result.stdout)
162 assert data["scope"] == "project"
163
164
165# =====================================================================
166# scope=repo
167# =====================================================================
168
169
171
172 def test_repo_includes_outside(self, monorepo_with_outside) -> None:
173 _git_root, project = monorepo_with_outside
174 result = _run_status(project, "--scope=repo", "--json")
175 assert result.returncode == 0, result.stderr
176 data = json.loads(result.stdout)
177 assert data["scope"] == "repo"
178 out_paths = [e["path"] for e in data["out_of_project"]]
179 assert any("outside" in p for p in out_paths), (
180 f"Expected an 'outside/...' path in out_of_project: {out_paths}"
181 )
182
184 self, monorepo_with_outside,
185 ) -> None:
186 _git_root, project = monorepo_with_outside
187 result = _run_status(project, "--scope=repo")
188 assert result.returncode == 0, result.stderr
189 assert "Other repo files" in result.stdout
190
191
192# =====================================================================
193# scope=auto
194# =====================================================================
195
196
198
200 self, monorepo_with_outside,
201 ) -> None:
202 _git_root, project = monorepo_with_outside
203 result = _run_status(project, "--scope=auto", "--json")
204 data = json.loads(result.stdout)
205 # auto resolves to repo when project_root != repo_root.
206 assert data["scope"] == "repo"
207
209 self, temp_git_project: Path,
210 ) -> None:
211 # In temp_git_project, project_root == repo_root.
212 result = subprocess.run(
213 [
214 sys.executable, "-m", "oct.cli",
215 "git", "status", "--scope=auto", "--json",
216 ],
217 cwd=temp_git_project, capture_output=True, text=True,
218 )
219 assert result.returncode == 0, result.stderr
220 data = json.loads(result.stdout)
221 assert data["scope"] == "project"
None test_auto_in_simple_repo_collapses_to_project(self, Path temp_git_project)
None test_auto_in_monorepo_collapses_to_repo(self, monorepo_with_outside)
None test_default_excludes_outside(self, monorepo_with_outside)
None test_explicit_project_scope(self, monorepo_with_outside)
None test_repo_includes_outside(self, monorepo_with_outside)
None test_terminal_section_rendered(self, monorepo_with_outside)
tuple[Path, Path] monorepo_with_outside(Path tmp_path)
subprocess.CompletedProcess _run_status(Path project_dir, *str extra_args)
subprocess.CompletedProcess _git(Path repo, *str args, bool check=True)