Option C Tools
Loading...
Searching...
No Matches
test_git_branch.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_git_branch.py
4
5"""
6Purpose
7-------
8Integration tests for ``oct git branch`` (Phase 4F — G-F1).
9
10Responsibilities
11----------------
12- Verify ``--create`` creates a branch with naming validation.
13- Verify ``--switch`` switches to an existing branch.
14- Verify mutual exclusivity of ``--create`` and ``--switch``.
15- Verify JSON output.
16- Verify audit trail.
17- Verify error cases (duplicate, non-existent, uncommitted changes).
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 subprocess invocation (same pattern as test_git_commit.py).
30- All tests use tmp_path fixtures so they are automatically cleaned up.
31"""
32
33import json
34import shutil
35import subprocess
36import sys
37from pathlib import Path
38
39import pytest
40
41from tests.tests_git.conftest import _git, commit_file
42
43
44# =====================================================================
45# Helpers
46# =====================================================================
47
48
49def _git_available() -> bool:
50 return shutil.which("git") is not None
51
52
53def _run_branch(root: Path, *extra_args: str) -> subprocess.CompletedProcess:
54 """Run ``oct git branch`` in a subprocess."""
55 return subprocess.run(
56 [sys.executable, "-m", "oct.cli", "git", "branch", *extra_args],
57 cwd=root,
58 capture_output=True,
59 text=True,
60 )
61
62
63def _run_branch_json(root: Path, *extra_args: str) -> dict:
64 """Run ``oct git branch --json`` and parse JSON output."""
65 result = _run_branch(root, "--json", *extra_args)
66 return json.loads(result.stdout)
67
68
69# =====================================================================
70# --create
71# =====================================================================
72
73
75
76 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
77 def test_create_valid_branch(self, temp_git_project: Path) -> None:
78 result = _run_branch(temp_git_project, "--create", "feature/new-thing")
79 assert result.returncode == 0
80 assert "Created branch" in result.stdout
81 # Verify branch exists in git.
82 proc = _git(temp_git_project, "branch", "--list", "feature/new-thing")
83 assert "feature/new-thing" in proc.stdout
84
85 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
86 def test_create_does_not_switch(self, temp_git_project: Path) -> None:
87 _run_branch(temp_git_project, "--create", "feature/stay-here")
88 proc = _git(temp_git_project, "rev-parse", "--abbrev-ref", "HEAD")
89 assert proc.stdout.strip() == "main"
90
91 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
92 def test_create_invalid_name_rejected(self, temp_git_project: Path) -> None:
93 result = _run_branch(temp_git_project, "--create", "bad-name")
94 assert result.returncode == 1
95 assert "does not match" in result.stderr.lower()
96
97 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
98 def test_create_duplicate_rejected(self, temp_git_project: Path) -> None:
99 _run_branch(temp_git_project, "--create", "feature/dup")
100 result = _run_branch(temp_git_project, "--create", "feature/dup")
101 assert result.returncode == 1
102 assert "already exists" in result.stderr.lower()
103
104 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
105 def test_create_json_output(self, temp_git_project: Path) -> None:
106 data = _run_branch_json(temp_git_project, "--create", "feature/json-test")
107 assert data["operation"] == "create"
108 assert data["branch"] == "feature/json-test"
109 assert data["exit_code"] == 0
110 assert data["errors"] == []
111
112
113# =====================================================================
114# --switch
115# =====================================================================
116
117
119
120 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
121 def test_switch_to_existing(self, temp_git_project: Path) -> None:
122 _git(temp_git_project, "branch", "feature/target")
123 result = _run_branch(temp_git_project, "--switch", "feature/target")
124 assert result.returncode == 0
125 assert "Switched to branch" in result.stdout
126 proc = _git(temp_git_project, "rev-parse", "--abbrev-ref", "HEAD")
127 assert proc.stdout.strip() == "feature/target"
128
129 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
130 def test_switch_nonexistent_rejected(self, temp_git_project: Path) -> None:
131 result = _run_branch(temp_git_project, "--switch", "feature/nope")
132 assert result.returncode == 1
133 assert "does not exist" in result.stderr.lower()
134
135 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
136 def test_switch_with_uncommitted_changes(self, temp_git_project: Path) -> None:
137 """Switching with dirty working tree is refused."""
138 _git(temp_git_project, "branch", "feature/clean-target")
139 # Create a tracked file and modify it without committing.
140 (temp_git_project / "src" / "dirty.py").write_text("x = 1\n", encoding="utf-8")
141 _git(temp_git_project, "add", "src/dirty.py")
142 result = _run_branch(temp_git_project, "--switch", "feature/clean-target")
143 assert result.returncode == 1
144 assert "uncommitted" in result.stderr.lower()
145
146 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
147 def test_switch_json_output(self, temp_git_project: Path) -> None:
148 _git(temp_git_project, "branch", "feature/json-switch")
149 data = _run_branch_json(temp_git_project, "--switch", "feature/json-switch")
150 assert data["operation"] == "switch"
151 assert data["branch"] == "feature/json-switch"
152 assert data["exit_code"] == 0
153
154
155# =====================================================================
156# Mutual exclusivity
157# =====================================================================
158
159
161
162 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
163 def test_both_flags_rejected(self, temp_git_project: Path) -> None:
164 result = _run_branch(
165 temp_git_project,
166 "--create", "feature/a",
167 "--switch", "feature/b",
168 )
169 assert result.returncode == 1
170 assert "mutually exclusive" in result.stderr.lower()
171
172 @pytest.mark.skipif(not _git_available(), reason="git not on PATH")
173 def test_no_flags_shows_usage(self, temp_git_project: Path) -> None:
174 result = _run_branch(temp_git_project)
175 assert result.returncode == 1
176 assert "--create" in result.stderr or "--switch" in result.stderr
None test_create_does_not_switch(self, Path temp_git_project)
None test_create_json_output(self, Path temp_git_project)
None test_create_duplicate_rejected(self, Path temp_git_project)
None test_create_invalid_name_rejected(self, Path temp_git_project)
None test_create_valid_branch(self, Path temp_git_project)
None test_switch_json_output(self, Path temp_git_project)
None test_switch_nonexistent_rejected(self, Path temp_git_project)
None test_switch_to_existing(self, Path temp_git_project)
None test_switch_with_uncommitted_changes(self, Path temp_git_project)
None test_both_flags_rejected(self, Path temp_git_project)
None test_no_flags_shows_usage(self, Path temp_git_project)
subprocess.CompletedProcess _run_branch(Path root, *str extra_args)
dict _run_branch_json(Path root, *str extra_args)