11"""Regression tests for issue #133.
22
3- When git is unusable the CLI must report a clean, one-line diagnosis and exit
4- non-zero -- never dump a Python traceback.
5-
6- The failure is import-time: GitPython probes for the git executable when it is
7- first imported and raises from module scope, before ``main()`` runs. A check
8- inside ``main()`` cannot intercept it, so these tests run the CLI in a
9- subprocess and assert on what a user would actually see. Two failure modes are
10- covered, because they surface through different GitPython paths:
11-
12- - git missing entirely -- nothing named ``git`` on PATH.
13- - git present but broken -- a ``git`` that resolves but exits non-zero, as on
14- macOS without the Command Line Tools. ``GIT_PYTHON_REFRESH=quiet`` does not
15- suppress this one, which is why a PATH lookup alone is not a sufficient check.
16-
17- ``sys.executable`` is launched by absolute path, so replacing PATH does not stop
18- Python from starting -- it only controls which git, if any, can be found.
3+ When git is unusable the CLI must report a clean message and exit 1, never dump
4+ a traceback. The failure is at import time (GitPython raises from module scope
5+ before main() runs), so the CLI is run in a subprocess and asserted on what a
6+ user would actually see. sys.executable is launched by absolute path, so
7+ replacing PATH only controls which git, if any, can be found.
198"""
209
2110import os
11+ import re
2212import subprocess
2313import sys
2414from pathlib import Path
2515
2616import pytest
2717
28- from git_preflight import (
29- GIT_BROKEN_MESSAGE ,
30- GIT_MISSING_MESSAGE ,
31- MACOS_BROKEN_GIT_HINT ,
32- broken_git_message ,
33- git_runs ,
34- )
18+ from git_preflight import GIT_BROKEN_MESSAGE , GIT_MISSING_MESSAGE
3519
3620REPO_ROOT = Path (__file__ ).resolve ().parents [1 ]
37-
3821TRACEBACK_MARKER = "Traceback (most recent call last)"
3922
40- # Every entry point, including the ones that do no git work themselves: the crash
41- # was in the import chain, so it hit them all equally.
42- CLI_INVOCATIONS = [
43- pytest .param (["--version" ], id = "version" ),
44- pytest .param (["--status" ], id = "status" ),
45- pytest .param (["does-not-exist.plain" ], id = "render" ),
46- ]
47-
23+ # The crash was in the import chain, so it hit every entry point equally.
24+ CLI_INVOCATIONS = [["--version" ], ["--status" ], ["does-not-exist.plain" ]]
4825
49- def _env (path_dir = None ):
50- """The current environment, with PATH optionally replaced.
5126
52- GitPython overrides are dropped so the PATH search is the only thing
53- deciding whether git is found.
54- """
27+ def run_cli ( args , path_dir = None ):
28+ """Run the CLI and return its exit code and output, stripped of styling and
29+ with whitespace collapsed, so assertions survive console line wrapping. """
5530 env = os .environ .copy ()
31+ # Drop GitPython overrides so the PATH search decides whether git is found.
5632 env .pop ("GIT_PYTHON_GIT_EXECUTABLE" , None )
5733 env .pop ("GIT_PYTHON_REFRESH" , None )
34+ env .setdefault ("CODEPLAIN_API_KEY" , "dummy" )
5835 if path_dir is not None :
5936 env ["PATH" ] = str (path_dir )
60- env .setdefault ("CODEPLAIN_API_KEY" , "dummy" )
61- return env
62-
6337
64- def _run_cli (args , env ):
6538 result = subprocess .run (
6639 [sys .executable , "plain2code.py" , * args ],
6740 cwd = REPO_ROOT ,
@@ -70,12 +43,8 @@ def _run_cli(args, env):
7043 text = True ,
7144 timeout = 120 ,
7245 )
73- return result .returncode , result .stdout + result .stderr
74-
75-
76- def _normalize (output ):
77- """Collapse whitespace so assertions survive console line wrapping."""
78- return " " .join (output .split ())
46+ output = re .sub (r"\x1b\[[0-9;]*m" , "" , result .stdout + result .stderr )
47+ return result .returncode , " " .join (output .split ())
7948
8049
8150@pytest .fixture
@@ -89,15 +58,11 @@ def no_git_dir(tmp_path):
8958@pytest .fixture
9059def broken_git_dir (tmp_path ):
9160 """A directory whose git resolves but always fails, used as the whole PATH.
92-
93- This is the shape of macOS without the Command Line Tools: the name is
94- there, running it is not possible.
95- """
61+ This is the shape of macOS without the Command Line Tools."""
9662 d = tmp_path / "broken-git"
9763 d .mkdir ()
9864 if sys .platform == "win32" :
99- git = d / "git.bat"
100- git .write_text ("@echo off\n exit /b 127\n " )
65+ (d / "git.bat" ).write_text ("@echo off\n exit /b 127\n " )
10166 else :
10267 git = d / "git"
10368 git .write_text ("#!/bin/sh\n exit 127\n " )
@@ -107,92 +72,27 @@ def broken_git_dir(tmp_path):
10772
10873@pytest .mark .parametrize ("args" , CLI_INVOCATIONS )
10974def test_missing_git_reports_cleanly (args , no_git_dir ):
110- rc , output = _run_cli (args , _env ( no_git_dir ) )
75+ rc , output = run_cli (args , no_git_dir )
11176
112- assert GIT_MISSING_MESSAGE in _normalize ( output ) , output
77+ assert GIT_MISSING_MESSAGE in output , output
11378 assert TRACEBACK_MARKER not in output , output
79+ # GitPython's own advice about GIT_PYTHON_REFRESH must not reach the user.
80+ assert "GIT_PYTHON_REFRESH" not in output , output
11481 assert rc == 1 , output
11582
11683
11784@pytest .mark .parametrize ("args" , CLI_INVOCATIONS )
11885def test_broken_git_reports_cleanly (args , broken_git_dir ):
119- # A git that resolves but cannot run must be diagnosed as broken rather than
120- # missing -- and must not reach GitPython's import-time traceback.
121- rc , output = _run_cli (args , _env (broken_git_dir ))
122- normalized = _normalize (output )
86+ rc , output = run_cli (args , broken_git_dir )
12387
124- assert GIT_BROKEN_MESSAGE in normalized , output
125- assert GIT_MISSING_MESSAGE not in normalized , output
88+ assert GIT_BROKEN_MESSAGE in output , output
89+ assert GIT_MISSING_MESSAGE not in output , output
12690 assert TRACEBACK_MARKER not in output , output
12791 assert rc == 1 , output
12892
12993
130- @pytest .mark .parametrize ("args" , CLI_INVOCATIONS )
131- def test_missing_git_does_not_leak_gitpython_advice (args , no_git_dir ):
132- # GitPython's own message coaches the user about $GIT_PYTHON_REFRESH and
133- # git.refresh(), which are irrelevant to someone who just needs to install
134- # git. None of it should reach the user.
135- _ , output = _run_cli (args , _env (no_git_dir ))
136-
137- assert "GIT_PYTHON_REFRESH" not in output , output
138- assert "Bad git executable" not in output , output
139-
140-
141- # plain_modules is deliberately not tested as a standalone import: it has a
142- # pre-existing circular import with file_utils that fails whether or not git is
143- # present, so the assertion would hold for the wrong reason.
144- @pytest .mark .parametrize ("module" , ["plain2code" , "file_utils" ])
145- @pytest .mark .parametrize ("path_fixture" , ["no_git_dir" , "broken_git_dir" ])
146- def test_importing_git_dependent_module_exits_cleanly (module , path_fixture , request ):
147- """Importing a module in the CLI's git-dependent import chain must diagnose
148- git rather than traceback."""
149- path_dir = request .getfixturevalue (path_fixture )
150- result = subprocess .run (
151- [sys .executable , "-c" , f"import { module } " ],
152- cwd = REPO_ROOT ,
153- env = _env (path_dir ),
154- capture_output = True ,
155- text = True ,
156- timeout = 120 ,
157- )
158- output = result .stdout + result .stderr
159-
160- assert TRACEBACK_MARKER not in output , output
161- assert result .returncode == 1 , output
162-
163-
16494def test_cli_works_when_git_is_present ():
165- # The check must not get in the way of normal use.
166- rc , output = _run_cli (["--version" ], _env ())
95+ rc , output = run_cli (["--version" ])
16796
16897 assert rc == 0 , output
16998 assert "codeplain version" in output , output
170-
171-
172- def test_git_runs_accepts_real_git ():
173- import shutil
174-
175- git_path = shutil .which ("git" )
176- assert git_path is not None , "test environment has no git"
177- assert git_runs (git_path ) is True
178-
179-
180- def test_git_runs_rejects_failing_git (broken_git_dir ):
181- broken = broken_git_dir / ("git.bat" if sys .platform == "win32" else "git" )
182-
183- assert git_runs (str (broken )) is False
184-
185-
186- def test_git_runs_rejects_nonexistent_path (tmp_path ):
187- # OSError from exec must be treated as unusable, not propagated.
188- assert git_runs (str (tmp_path / "definitely-not-here" )) is False
189-
190-
191- @pytest .mark .skipif (sys .platform != "darwin" , reason = "hint is macOS-specific" )
192- def test_broken_git_message_hints_at_command_line_tools_on_macos ():
193- assert MACOS_BROKEN_GIT_HINT in broken_git_message ()
194-
195-
196- @pytest .mark .skipif (sys .platform == "darwin" , reason = "hint is macOS-specific" )
197- def test_broken_git_message_has_no_macos_hint_elsewhere ():
198- assert broken_git_message () == GIT_BROKEN_MESSAGE
0 commit comments