FossilRepo

fossilrepo / tests / test_cli.py
Source Blame History 1207 lines
254b467… ragelink 1 """Unit tests for fossil/cli.py -- FossilCLI subprocess wrapper.
254b467… ragelink 2
254b467… ragelink 3 Tests mock subprocess.run throughout since FossilCLI is a thin wrapper
254b467… ragelink 4 around the fossil binary. We verify that:
254b467… ragelink 5 - Correct commands are assembled for every method
254b467… ragelink 6 - Success/failure return values are propagated correctly
254b467… ragelink 7 - Environment variables are set properly (_env property)
254b467… ragelink 8 - Timeouts and exceptions are handled gracefully
254b467… ragelink 9 - Edge-case inputs (empty strings, special characters) work
254b467… ragelink 10 """
254b467… ragelink 11
254b467… ragelink 12 import os
254b467… ragelink 13 import subprocess
254b467… ragelink 14 from pathlib import Path
254b467… ragelink 15 from unittest.mock import MagicMock, patch
254b467… ragelink 16
254b467… ragelink 17 import pytest
254b467… ragelink 18
254b467… ragelink 19 from fossil.cli import FossilCLI
254b467… ragelink 20
254b467… ragelink 21 # ---------------------------------------------------------------------------
254b467… ragelink 22 # Helpers
254b467… ragelink 23 # ---------------------------------------------------------------------------
254b467… ragelink 24
254b467… ragelink 25
254b467… ragelink 26 def _ok(stdout="", stderr="", returncode=0):
254b467… ragelink 27 """Build a mock CompletedProcess for a successful command."""
254b467… ragelink 28 return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)
254b467… ragelink 29
254b467… ragelink 30
254b467… ragelink 31 def _fail(stdout="", stderr="error", returncode=1):
254b467… ragelink 32 """Build a mock CompletedProcess for a failed command."""
254b467… ragelink 33 return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)
254b467… ragelink 34
254b467… ragelink 35
254b467… ragelink 36 def _ok_bytes(stdout=b"", stderr=b"", returncode=0):
254b467… ragelink 37 """Build a mock CompletedProcess returning raw bytes (not text)."""
254b467… ragelink 38 return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)
254b467… ragelink 39
254b467… ragelink 40
254b467… ragelink 41 # ---------------------------------------------------------------------------
254b467… ragelink 42 # Constructor and _env
254b467… ragelink 43 # ---------------------------------------------------------------------------
254b467… ragelink 44
254b467… ragelink 45
254b467… ragelink 46 class TestFossilCLIInit:
254b467… ragelink 47 """Constructor: explicit binary path vs constance fallback."""
254b467… ragelink 48
254b467… ragelink 49 def test_explicit_binary(self):
254b467… ragelink 50 cli = FossilCLI(binary="/usr/local/bin/fossil")
254b467… ragelink 51 assert cli.binary == "/usr/local/bin/fossil"
254b467… ragelink 52
254b467… ragelink 53 def test_constance_fallback(self):
254b467… ragelink 54 mock_config = MagicMock()
254b467… ragelink 55 mock_config.FOSSIL_BINARY_PATH = "/opt/fossil/bin/fossil"
254b467… ragelink 56 with patch("constance.config", mock_config):
254b467… ragelink 57 cli = FossilCLI()
254b467… ragelink 58 assert cli.binary == "/opt/fossil/bin/fossil"
254b467… ragelink 59
254b467… ragelink 60
254b467… ragelink 61 class TestEnvProperty:
254b467… ragelink 62 """_env injects USER=fossilrepo into the inherited environment."""
254b467… ragelink 63
254b467… ragelink 64 def test_env_sets_user(self):
254b467… ragelink 65 cli = FossilCLI(binary="/bin/false")
254b467… ragelink 66 env = cli._env
254b467… ragelink 67 assert env["USER"] == "fossilrepo"
254b467… ragelink 68
254b467… ragelink 69 def test_env_inherits_system_env(self):
254b467… ragelink 70 cli = FossilCLI(binary="/bin/false")
254b467… ragelink 71 env = cli._env
254b467… ragelink 72 # PATH should come from os.environ
254b467… ragelink 73 assert "PATH" in env
254b467… ragelink 74
254b467… ragelink 75
254b467… ragelink 76 # ---------------------------------------------------------------------------
254b467… ragelink 77 # _run helper
254b467… ragelink 78 # ---------------------------------------------------------------------------
254b467… ragelink 79
254b467… ragelink 80
254b467… ragelink 81 class TestRunHelper:
254b467… ragelink 82 """_run assembles the command and delegates to subprocess.run."""
254b467… ragelink 83
254b467… ragelink 84 def test_run_builds_correct_command(self):
254b467… ragelink 85 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 86 with patch("subprocess.run", return_value=_ok("ok")) as mock_run:
254b467… ragelink 87 cli._run("version")
254b467… ragelink 88 mock_run.assert_called_once()
254b467… ragelink 89 cmd = mock_run.call_args[0][0]
254b467… ragelink 90 assert cmd == ["/usr/bin/fossil", "version"]
254b467… ragelink 91
254b467… ragelink 92 def test_run_passes_env(self):
254b467… ragelink 93 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 94 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 95 cli._run("version")
254b467… ragelink 96 env = mock_run.call_args[1]["env"]
254b467… ragelink 97 assert env["USER"] == "fossilrepo"
254b467… ragelink 98
254b467… ragelink 99 def test_run_uses_check_true(self):
254b467… ragelink 100 """_run uses check=True so CalledProcessError is raised on failure."""
254b467… ragelink 101 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 102 with (
254b467… ragelink 103 patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "fossil")),
254b467… ragelink 104 pytest.raises(subprocess.CalledProcessError),
254b467… ragelink 105 ):
254b467… ragelink 106 cli._run("bad-command")
254b467… ragelink 107
254b467… ragelink 108 def test_run_custom_timeout(self):
254b467… ragelink 109 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 110 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 111 cli._run("clone", "http://example.com", timeout=120)
254b467… ragelink 112 assert mock_run.call_args[1]["timeout"] == 120
254b467… ragelink 113
254b467… ragelink 114 def test_run_multiple_args(self):
254b467… ragelink 115 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 116 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 117 cli._run("push", "-R", "/tmp/repo.fossil")
254b467… ragelink 118 cmd = mock_run.call_args[0][0]
254b467… ragelink 119 assert cmd == ["/usr/bin/fossil", "push", "-R", "/tmp/repo.fossil"]
254b467… ragelink 120
254b467… ragelink 121
254b467… ragelink 122 # ---------------------------------------------------------------------------
254b467… ragelink 123 # init
254b467… ragelink 124 # ---------------------------------------------------------------------------
254b467… ragelink 125
254b467… ragelink 126
254b467… ragelink 127 class TestInit:
254b467… ragelink 128 def test_init_creates_parent_dirs_and_runs_fossil_init(self, tmp_path):
254b467… ragelink 129 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 130 target = tmp_path / "sub" / "dir" / "repo.fossil"
254b467… ragelink 131 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 132 result = cli.init(target)
254b467… ragelink 133 assert result == target
254b467… ragelink 134 # Parent dirs created
254b467… ragelink 135 assert target.parent.exists()
254b467… ragelink 136 cmd = mock_run.call_args[0][0]
254b467… ragelink 137 assert cmd == ["/usr/bin/fossil", "init", str(target)]
254b467… ragelink 138
254b467… ragelink 139 def test_init_returns_path(self, tmp_path):
254b467… ragelink 140 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 141 target = tmp_path / "repo.fossil"
254b467… ragelink 142 with patch("subprocess.run", return_value=_ok()):
254b467… ragelink 143 path = cli.init(target)
254b467… ragelink 144 assert isinstance(path, Path)
254b467… ragelink 145 assert path == target
254b467… ragelink 146
254b467… ragelink 147
254b467… ragelink 148 # ---------------------------------------------------------------------------
254b467… ragelink 149 # version
254b467… ragelink 150 # ---------------------------------------------------------------------------
254b467… ragelink 151
254b467… ragelink 152
254b467… ragelink 153 class TestVersion:
254b467… ragelink 154 def test_version_returns_stripped_stdout(self):
254b467… ragelink 155 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 156 with patch("subprocess.run", return_value=_ok(" This is fossil version 2.24\n")):
254b467… ragelink 157 result = cli.version()
254b467… ragelink 158 assert result == "This is fossil version 2.24"
254b467… ragelink 159
254b467… ragelink 160 def test_version_propagates_error(self):
254b467… ragelink 161 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 162 with (
254b467… ragelink 163 patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "fossil")),
254b467… ragelink 164 pytest.raises(subprocess.CalledProcessError),
254b467… ragelink 165 ):
254b467… ragelink 166 cli.version()
254b467… ragelink 167
254b467… ragelink 168
254b467… ragelink 169 # ---------------------------------------------------------------------------
254b467… ragelink 170 # is_available
254b467… ragelink 171 # ---------------------------------------------------------------------------
254b467… ragelink 172
254b467… ragelink 173
254b467… ragelink 174 class TestIsAvailable:
254b467… ragelink 175 def test_available_when_version_works(self):
254b467… ragelink 176 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 177 with patch("subprocess.run", return_value=_ok("2.24")):
254b467… ragelink 178 assert cli.is_available() is True
254b467… ragelink 179
254b467… ragelink 180 def test_not_available_on_file_not_found(self):
254b467… ragelink 181 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 182 with patch("subprocess.run", side_effect=FileNotFoundError):
254b467… ragelink 183 assert cli.is_available() is False
254b467… ragelink 184
254b467… ragelink 185 def test_not_available_on_called_process_error(self):
254b467… ragelink 186 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 187 with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "fossil")):
254b467… ragelink 188 assert cli.is_available() is False
254b467… ragelink 189
254b467… ragelink 190
254b467… ragelink 191 # ---------------------------------------------------------------------------
254b467… ragelink 192 # render_pikchr
254b467… ragelink 193 # ---------------------------------------------------------------------------
254b467… ragelink 194
254b467… ragelink 195
254b467… ragelink 196 class TestRenderPikchr:
254b467… ragelink 197 def test_renders_svg_on_success(self):
254b467… ragelink 198 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 199 svg = '<svg viewBox="0 0 100 100"></svg>'
254b467… ragelink 200 result_proc = subprocess.CompletedProcess(args=[], returncode=0, stdout=svg, stderr="")
254b467… ragelink 201 with patch("subprocess.run", return_value=result_proc) as mock_run:
254b467… ragelink 202 result = cli.render_pikchr("circle")
254b467… ragelink 203 assert result == svg
254b467… ragelink 204 cmd = mock_run.call_args[0][0]
254b467… ragelink 205 assert cmd == ["/usr/bin/fossil", "pikchr", "-"]
254b467… ragelink 206 assert mock_run.call_args[1]["input"] == "circle"
254b467… ragelink 207
254b467… ragelink 208 def test_returns_empty_on_failure(self):
254b467… ragelink 209 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 210 result_proc = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="error")
254b467… ragelink 211 with patch("subprocess.run", return_value=result_proc):
254b467… ragelink 212 assert cli.render_pikchr("bad") == ""
254b467… ragelink 213
254b467… ragelink 214 def test_returns_empty_on_file_not_found(self):
254b467… ragelink 215 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 216 with patch("subprocess.run", side_effect=FileNotFoundError):
254b467… ragelink 217 assert cli.render_pikchr("test") == ""
254b467… ragelink 218
254b467… ragelink 219 def test_returns_empty_on_timeout(self):
254b467… ragelink 220 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 221 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 10)):
254b467… ragelink 222 assert cli.render_pikchr("test") == ""
254b467… ragelink 223
254b467… ragelink 224
254b467… ragelink 225 # ---------------------------------------------------------------------------
254b467… ragelink 226 # ensure_default_user
254b467… ragelink 227 # ---------------------------------------------------------------------------
254b467… ragelink 228
254b467… ragelink 229
254b467… ragelink 230 class TestEnsureDefaultUser:
254b467… ragelink 231 def test_creates_user_when_missing(self, tmp_path):
254b467… ragelink 232 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 233 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 234 # First call: user list (user not present), second: create, third: default
254b467… ragelink 235 with patch("subprocess.run") as mock_run:
254b467… ragelink 236 mock_run.side_effect = [
254b467… ragelink 237 _ok(stdout="admin\n"), # user list -- "fossilrepo" not in output
254b467… ragelink 238 _ok(), # user new
254b467… ragelink 239 _ok(), # user default
254b467… ragelink 240 ]
254b467… ragelink 241 cli.ensure_default_user(repo_path)
254b467… ragelink 242 assert mock_run.call_count == 3
254b467… ragelink 243 # Verify the user new call
254b467… ragelink 244 new_cmd = mock_run.call_args_list[1][0][0]
254b467… ragelink 245 assert "user" in new_cmd
254b467… ragelink 246 assert "new" in new_cmd
254b467… ragelink 247 assert "fossilrepo" in new_cmd
254b467… ragelink 248
254b467… ragelink 249 def test_skips_create_when_user_exists(self, tmp_path):
254b467… ragelink 250 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 251 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 252 with patch("subprocess.run") as mock_run:
254b467… ragelink 253 mock_run.side_effect = [
254b467… ragelink 254 _ok(stdout="admin\nfossilrepo\n"), # user list -- fossilrepo IS present
254b467… ragelink 255 _ok(), # user default
254b467… ragelink 256 ]
254b467… ragelink 257 cli.ensure_default_user(repo_path)
254b467… ragelink 258 assert mock_run.call_count == 2 # no "new" call
254b467… ragelink 259
254b467… ragelink 260 def test_custom_username(self, tmp_path):
254b467… ragelink 261 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 262 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 263 with patch("subprocess.run") as mock_run:
254b467… ragelink 264 mock_run.side_effect = [
254b467… ragelink 265 _ok(stdout="admin\n"), # user list -- custom not present
254b467… ragelink 266 _ok(), # user new
254b467… ragelink 267 _ok(), # user default
254b467… ragelink 268 ]
254b467… ragelink 269 cli.ensure_default_user(repo_path, username="custom-bot")
254b467… ragelink 270 new_cmd = mock_run.call_args_list[1][0][0]
254b467… ragelink 271 assert "custom-bot" in new_cmd
254b467… ragelink 272
254b467… ragelink 273 def test_silently_swallows_exceptions(self, tmp_path):
254b467… ragelink 274 """ensure_default_user has a bare except -- should not raise."""
254b467… ragelink 275 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 276 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 277 with patch("subprocess.run", side_effect=Exception("kaboom")):
254b467… ragelink 278 cli.ensure_default_user(repo_path) # should not raise
254b467… ragelink 279
254b467… ragelink 280
254b467… ragelink 281 # ---------------------------------------------------------------------------
254b467… ragelink 282 # tarball
254b467… ragelink 283 # ---------------------------------------------------------------------------
254b467… ragelink 284
254b467… ragelink 285
254b467… ragelink 286 class TestTarball:
254b467… ragelink 287 def test_returns_bytes_on_success(self, tmp_path):
254b467… ragelink 288 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 289 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 290 fake_tar = b"\x1f\x8b\x08\x00" + b"\x00" * 100 # fake gzip header
254b467… ragelink 291 with patch("subprocess.run", return_value=_ok_bytes(stdout=fake_tar)) as mock_run:
254b467… ragelink 292 result = cli.tarball(repo_path, "trunk")
254b467… ragelink 293 assert result == fake_tar
254b467… ragelink 294 cmd = mock_run.call_args[0][0]
254b467… ragelink 295 assert cmd == ["/usr/bin/fossil", "tarball", "trunk", "-R", str(repo_path), "/dev/stdout"]
254b467… ragelink 296
254b467… ragelink 297 def test_returns_empty_bytes_on_failure(self, tmp_path):
254b467… ragelink 298 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 299 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 300 with patch("subprocess.run", return_value=_ok_bytes(returncode=1)):
254b467… ragelink 301 result = cli.tarball(repo_path, "trunk")
254b467… ragelink 302 assert result == b""
254b467… ragelink 303
254b467… ragelink 304
254b467… ragelink 305 # ---------------------------------------------------------------------------
254b467… ragelink 306 # zip_archive
254b467… ragelink 307 # ---------------------------------------------------------------------------
254b467… ragelink 308
254b467… ragelink 309
254b467… ragelink 310 class TestZipArchive:
254b467… ragelink 311 def test_returns_bytes_on_success(self, tmp_path):
254b467… ragelink 312 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 313 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 314 fake_zip = b"PK\x03\x04" + b"\x00" * 100
254b467… ragelink 315
254b467… ragelink 316 def side_effect(cmd, **kwargs):
254b467… ragelink 317 # Write content to the tempfile that fossil would create
254b467… ragelink 318 # The tempfile path is in the command args
254b467… ragelink 319 outfile = cmd[3] # zip <checkin> <outfile> -R <repo>
254b467… ragelink 320 Path(outfile).write_bytes(fake_zip)
254b467… ragelink 321 return _ok()
254b467… ragelink 322
254b467… ragelink 323 with patch("subprocess.run", side_effect=side_effect):
254b467… ragelink 324 result = cli.zip_archive(repo_path, "trunk")
254b467… ragelink 325 assert result == fake_zip
254b467… ragelink 326
254b467… ragelink 327 def test_returns_empty_bytes_on_failure(self, tmp_path):
254b467… ragelink 328 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 329 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 330 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 331 result = cli.zip_archive(repo_path, "trunk")
254b467… ragelink 332 assert result == b""
254b467… ragelink 333
254b467… ragelink 334
254b467… ragelink 335 # ---------------------------------------------------------------------------
254b467… ragelink 336 # blame
254b467… ragelink 337 # ---------------------------------------------------------------------------
254b467… ragelink 338
254b467… ragelink 339
254b467… ragelink 340 class TestBlame:
254b467… ragelink 341 def test_parses_blame_output(self, tmp_path):
254b467… ragelink 342 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 343 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 344 blame_output = (
254b467… ragelink 345 "abc12345 2026-01-15 ragelink: def hello():\n"
254b467… ragelink 346 "abc12345 2026-01-15 ragelink: return 'world'\n"
254b467… ragelink 347 "def67890 2026-01-20 contributor: pass\n"
254b467… ragelink 348 )
254b467… ragelink 349 with patch("subprocess.run") as mock_run:
254b467… ragelink 350 mock_run.side_effect = [
254b467… ragelink 351 _ok(), # fossil open
254b467… ragelink 352 _ok(stdout=blame_output), # fossil blame
254b467… ragelink 353 _ok(), # fossil close
254b467… ragelink 354 ]
254b467… ragelink 355 lines = cli.blame(repo_path, "main.py")
254b467… ragelink 356 assert len(lines) == 3
254b467… ragelink 357 assert lines[0]["uuid"] == "abc12345"
254b467… ragelink 358 assert lines[0]["date"] == "2026-01-15"
254b467… ragelink 359 assert lines[0]["user"] == "ragelink"
254b467… ragelink 360 assert lines[0]["text"] == "def hello():"
254b467… ragelink 361 assert lines[2]["user"] == "contributor"
254b467… ragelink 362
254b467… ragelink 363 def test_returns_empty_on_failure(self, tmp_path):
254b467… ragelink 364 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 365 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 366 with patch("subprocess.run") as mock_run:
254b467… ragelink 367 mock_run.side_effect = [
254b467… ragelink 368 _ok(), # fossil open
254b467… ragelink 369 _fail(), # fossil blame fails
254b467… ragelink 370 _ok(), # fossil close
254b467… ragelink 371 ]
254b467… ragelink 372 lines = cli.blame(repo_path, "nonexistent.py")
254b467… ragelink 373 assert lines == []
254b467… ragelink 374
254b467… ragelink 375 def test_returns_empty_on_exception(self, tmp_path):
254b467… ragelink 376 """blame has a broad except -- should not raise."""
254b467… ragelink 377 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 378 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 379 with patch("subprocess.run", side_effect=Exception("error")):
254b467… ragelink 380 lines = cli.blame(repo_path, "file.py")
254b467… ragelink 381 assert lines == []
254b467… ragelink 382
254b467… ragelink 383 def test_cleans_up_tmpdir(self, tmp_path):
254b467… ragelink 384 """Temp directory must be cleaned up even on error."""
254b467… ragelink 385 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 386 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 387
254b467… ragelink 388 created_dirs = []
254b467… ragelink 389 original_mkdtemp = __import__("tempfile").mkdtemp
254b467… ragelink 390
254b467… ragelink 391 def tracking_mkdtemp(**kwargs):
254b467… ragelink 392 d = original_mkdtemp(**kwargs)
254b467… ragelink 393 created_dirs.append(d)
254b467… ragelink 394 return d
254b467… ragelink 395
254b467… ragelink 396 with (
254b467… ragelink 397 patch("subprocess.run", side_effect=Exception("fail")),
254b467… ragelink 398 patch("tempfile.mkdtemp", side_effect=tracking_mkdtemp),
254b467… ragelink 399 ):
254b467… ragelink 400 cli.blame(repo_path, "file.py")
254b467… ragelink 401
254b467… ragelink 402 # The tmpdir should have been cleaned up by shutil.rmtree
254b467… ragelink 403 for d in created_dirs:
254b467… ragelink 404 assert not Path(d).exists()
254b467… ragelink 405
254b467… ragelink 406
254b467… ragelink 407 # ---------------------------------------------------------------------------
254b467… ragelink 408 # push
254b467… ragelink 409 # ---------------------------------------------------------------------------
254b467… ragelink 410
254b467… ragelink 411
254b467… ragelink 412 class TestPush:
254b467… ragelink 413 def test_push_success_with_artifacts(self, tmp_path):
254b467… ragelink 414 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 415 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 416 with patch("subprocess.run", return_value=_ok(stdout="Round-trips: 1 Artifacts sent: 5 sent: 5")):
254b467… ragelink 417 result = cli.push(repo_path)
254b467… ragelink 418 assert result["success"] is True
254b467… ragelink 419 assert result["artifacts_sent"] == 5
254b467… ragelink 420
254b467… ragelink 421 def test_push_with_remote_url(self, tmp_path):
254b467… ragelink 422 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 423 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 424 with patch("subprocess.run", return_value=_ok(stdout="sent: 3")) as mock_run:
254b467… ragelink 425 result = cli.push(repo_path, remote_url="https://fossil.example.com/repo")
254b467… ragelink 426 cmd = mock_run.call_args[0][0]
254b467… ragelink 427 assert "https://fossil.example.com/repo" in cmd
254b467… ragelink 428 assert result["artifacts_sent"] == 3
254b467… ragelink 429
254b467… ragelink 430 def test_push_no_artifacts_in_output(self, tmp_path):
254b467… ragelink 431 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 432 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 433 with patch("subprocess.run", return_value=_ok(stdout="nothing to push")):
254b467… ragelink 434 result = cli.push(repo_path)
254b467… ragelink 435 assert result["success"] is True
254b467… ragelink 436 assert result["artifacts_sent"] == 0
254b467… ragelink 437
254b467… ragelink 438 def test_push_failure(self, tmp_path):
254b467… ragelink 439 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 440 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 441 with patch("subprocess.run", return_value=_fail(stdout="connection refused")):
254b467… ragelink 442 result = cli.push(repo_path)
254b467… ragelink 443 assert result["success"] is False
254b467… ragelink 444
254b467… ragelink 445 def test_push_timeout(self, tmp_path):
254b467… ragelink 446 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 447 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 448 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 120)):
254b467… ragelink 449 result = cli.push(repo_path)
254b467… ragelink 450 assert result["success"] is False
254b467… ragelink 451 assert result["artifacts_sent"] == 0
254b467… ragelink 452 assert "timed out" in result["message"].lower()
254b467… ragelink 453
254b467… ragelink 454 def test_push_file_not_found(self, tmp_path):
254b467… ragelink 455 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 456 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 457 with patch("subprocess.run", side_effect=FileNotFoundError("No such file")):
254b467… ragelink 458 result = cli.push(repo_path)
254b467… ragelink 459 assert result["success"] is False
254b467… ragelink 460 assert result["artifacts_sent"] == 0
254b467… ragelink 461
254b467… ragelink 462
254b467… ragelink 463 # ---------------------------------------------------------------------------
254b467… ragelink 464 # sync
254b467… ragelink 465 # ---------------------------------------------------------------------------
254b467… ragelink 466
254b467… ragelink 467
254b467… ragelink 468 class TestSync:
254b467… ragelink 469 def test_sync_success(self, tmp_path):
254b467… ragelink 470 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 471 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 472 with patch("subprocess.run", return_value=_ok(stdout="sync complete")):
254b467… ragelink 473 result = cli.sync(repo_path)
254b467… ragelink 474 assert result["success"] is True
254b467… ragelink 475 assert result["message"] == "sync complete"
254b467… ragelink 476
254b467… ragelink 477 def test_sync_with_remote_url(self, tmp_path):
254b467… ragelink 478 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 479 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 480 with patch("subprocess.run", return_value=_ok(stdout="ok")) as mock_run:
254b467… ragelink 481 cli.sync(repo_path, remote_url="https://fossil.example.com/repo")
254b467… ragelink 482 cmd = mock_run.call_args[0][0]
254b467… ragelink 483 assert "https://fossil.example.com/repo" in cmd
254b467… ragelink 484
254b467… ragelink 485 def test_sync_failure(self, tmp_path):
254b467… ragelink 486 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 487 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 488 with patch("subprocess.run", return_value=_fail(stdout="error")):
254b467… ragelink 489 result = cli.sync(repo_path)
254b467… ragelink 490 assert result["success"] is False
254b467… ragelink 491
254b467… ragelink 492 def test_sync_timeout(self, tmp_path):
254b467… ragelink 493 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 494 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 495 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 120)):
254b467… ragelink 496 result = cli.sync(repo_path)
254b467… ragelink 497 assert result["success"] is False
254b467… ragelink 498 assert "timed out" in result["message"].lower()
254b467… ragelink 499
254b467… ragelink 500 def test_sync_file_not_found(self, tmp_path):
254b467… ragelink 501 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 502 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 503 with patch("subprocess.run", side_effect=FileNotFoundError("No such file")):
254b467… ragelink 504 result = cli.sync(repo_path)
254b467… ragelink 505 assert result["success"] is False
254b467… ragelink 506
254b467… ragelink 507
254b467… ragelink 508 # ---------------------------------------------------------------------------
254b467… ragelink 509 # pull
254b467… ragelink 510 # ---------------------------------------------------------------------------
254b467… ragelink 511
254b467… ragelink 512
254b467… ragelink 513 class TestPull:
254b467… ragelink 514 def test_pull_success_with_artifacts(self, tmp_path):
254b467… ragelink 515 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 516 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 517 with patch("subprocess.run", return_value=_ok(stdout="Round-trips: 1 received: 12")):
254b467… ragelink 518 result = cli.pull(repo_path)
254b467… ragelink 519 assert result["success"] is True
254b467… ragelink 520 assert result["artifacts_received"] == 12
254b467… ragelink 521
254b467… ragelink 522 def test_pull_no_artifacts(self, tmp_path):
254b467… ragelink 523 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 524 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 525 with patch("subprocess.run", return_value=_ok(stdout="nothing new")):
254b467… ragelink 526 result = cli.pull(repo_path)
254b467… ragelink 527 assert result["success"] is True
254b467… ragelink 528 assert result["artifacts_received"] == 0
254b467… ragelink 529
254b467… ragelink 530 def test_pull_failure(self, tmp_path):
254b467… ragelink 531 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 532 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 533 with patch("subprocess.run", return_value=_fail(stdout="connection refused")):
254b467… ragelink 534 result = cli.pull(repo_path)
254b467… ragelink 535 assert result["success"] is False
254b467… ragelink 536
254b467… ragelink 537 def test_pull_timeout(self, tmp_path):
254b467… ragelink 538 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 539 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 540 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 60)):
254b467… ragelink 541 result = cli.pull(repo_path)
254b467… ragelink 542 assert result["success"] is False
254b467… ragelink 543 assert result["artifacts_received"] == 0
254b467… ragelink 544
254b467… ragelink 545 def test_pull_file_not_found(self, tmp_path):
254b467… ragelink 546 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 547 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 548 with patch("subprocess.run", side_effect=FileNotFoundError("No such file")):
254b467… ragelink 549 result = cli.pull(repo_path)
254b467… ragelink 550 assert result["success"] is False
254b467… ragelink 551 assert result["artifacts_received"] == 0
254b467… ragelink 552
254b467… ragelink 553
254b467… ragelink 554 # ---------------------------------------------------------------------------
254b467… ragelink 555 # get_remote_url
254b467… ragelink 556 # ---------------------------------------------------------------------------
254b467… ragelink 557
254b467… ragelink 558
254b467… ragelink 559 class TestGetRemoteUrl:
254b467… ragelink 560 def test_returns_url_on_success(self, tmp_path):
254b467… ragelink 561 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 562 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 563 result_proc = subprocess.CompletedProcess(args=[], returncode=0, stdout="https://fossil.example.com/repo\n", stderr="")
254b467… ragelink 564 with patch("subprocess.run", return_value=result_proc):
254b467… ragelink 565 url = cli.get_remote_url(repo_path)
254b467… ragelink 566 assert url == "https://fossil.example.com/repo"
254b467… ragelink 567
254b467… ragelink 568 def test_returns_empty_on_failure(self, tmp_path):
254b467… ragelink 569 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 570 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 571 result_proc = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="not configured")
254b467… ragelink 572 with patch("subprocess.run", return_value=result_proc):
254b467… ragelink 573 url = cli.get_remote_url(repo_path)
254b467… ragelink 574 assert url == ""
254b467… ragelink 575
254b467… ragelink 576 def test_returns_empty_on_file_not_found(self, tmp_path):
254b467… ragelink 577 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 578 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 579 with patch("subprocess.run", side_effect=FileNotFoundError):
254b467… ragelink 580 url = cli.get_remote_url(repo_path)
254b467… ragelink 581 assert url == ""
254b467… ragelink 582
254b467… ragelink 583 def test_returns_empty_on_timeout(self, tmp_path):
254b467… ragelink 584 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 585 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 586 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 10)):
254b467… ragelink 587 url = cli.get_remote_url(repo_path)
254b467… ragelink 588 assert url == ""
254b467… ragelink 589
254b467… ragelink 590
254b467… ragelink 591 # ---------------------------------------------------------------------------
254b467… ragelink 592 # wiki_commit
254b467… ragelink 593 # ---------------------------------------------------------------------------
254b467… ragelink 594
254b467… ragelink 595
254b467… ragelink 596 class TestWikiCommit:
254b467… ragelink 597 def test_success(self, tmp_path):
254b467… ragelink 598 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 599 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 600 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 601 result = cli.wiki_commit(repo_path, "Home", "# Welcome")
254b467… ragelink 602 assert result is True
254b467… ragelink 603 assert mock_run.call_args[1]["input"] == "# Welcome"
254b467… ragelink 604 cmd = mock_run.call_args[0][0]
254b467… ragelink 605 assert cmd == ["/usr/bin/fossil", "wiki", "commit", "Home", "-R", str(repo_path)]
254b467… ragelink 606
254b467… ragelink 607 def test_with_user(self, tmp_path):
254b467… ragelink 608 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 609 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 610 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 611 cli.wiki_commit(repo_path, "Home", "content", user="admin")
254b467… ragelink 612 cmd = mock_run.call_args[0][0]
254b467… ragelink 613 assert "--technote-user" in cmd
254b467… ragelink 614 assert "admin" in cmd
254b467… ragelink 615
254b467… ragelink 616 def test_failure(self, tmp_path):
254b467… ragelink 617 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 618 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 619 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 620 result = cli.wiki_commit(repo_path, "Missing", "content")
254b467… ragelink 621 assert result is False
254b467… ragelink 622
254b467… ragelink 623
254b467… ragelink 624 # ---------------------------------------------------------------------------
254b467… ragelink 625 # wiki_create
254b467… ragelink 626 # ---------------------------------------------------------------------------
254b467… ragelink 627
254b467… ragelink 628
254b467… ragelink 629 class TestWikiCreate:
254b467… ragelink 630 def test_success(self, tmp_path):
254b467… ragelink 631 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 632 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 633 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 634 result = cli.wiki_create(repo_path, "NewPage", "# New content")
254b467… ragelink 635 assert result is True
254b467… ragelink 636 cmd = mock_run.call_args[0][0]
254b467… ragelink 637 assert cmd == ["/usr/bin/fossil", "wiki", "create", "NewPage", "-R", str(repo_path)]
254b467… ragelink 638 assert mock_run.call_args[1]["input"] == "# New content"
254b467… ragelink 639
254b467… ragelink 640 def test_failure(self, tmp_path):
254b467… ragelink 641 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 642 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 643 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 644 result = cli.wiki_create(repo_path, "Dup", "content")
254b467… ragelink 645 assert result is False
254b467… ragelink 646
254b467… ragelink 647
254b467… ragelink 648 # ---------------------------------------------------------------------------
254b467… ragelink 649 # ticket_add
254b467… ragelink 650 # ---------------------------------------------------------------------------
254b467… ragelink 651
254b467… ragelink 652
254b467… ragelink 653 class TestTicketAdd:
254b467… ragelink 654 def test_success_with_fields(self, tmp_path):
254b467… ragelink 655 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 656 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 657 fields = {"title": "Bug report", "status": "open", "type": "bug"}
254b467… ragelink 658 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 659 result = cli.ticket_add(repo_path, fields)
254b467… ragelink 660 assert result is True
254b467… ragelink 661 cmd = mock_run.call_args[0][0]
254b467… ragelink 662 # Should have: fossil ticket add -R <path> title "Bug report" status open type bug
254b467… ragelink 663 assert cmd[:4] == ["/usr/bin/fossil", "ticket", "add", "-R"]
254b467… ragelink 664 assert "title" in cmd
254b467… ragelink 665 assert "Bug report" in cmd
254b467… ragelink 666
254b467… ragelink 667 def test_empty_fields(self, tmp_path):
254b467… ragelink 668 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 669 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 670 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 671 result = cli.ticket_add(repo_path, {})
254b467… ragelink 672 assert result is True
254b467… ragelink 673 cmd = mock_run.call_args[0][0]
254b467… ragelink 674 assert cmd == ["/usr/bin/fossil", "ticket", "add", "-R", str(repo_path)]
254b467… ragelink 675
254b467… ragelink 676 def test_failure(self, tmp_path):
254b467… ragelink 677 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 678 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 679 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 680 result = cli.ticket_add(repo_path, {"title": "test"})
254b467… ragelink 681 assert result is False
254b467… ragelink 682
254b467… ragelink 683
254b467… ragelink 684 # ---------------------------------------------------------------------------
254b467… ragelink 685 # ticket_change
254b467… ragelink 686 # ---------------------------------------------------------------------------
254b467… ragelink 687
254b467… ragelink 688
254b467… ragelink 689 class TestTicketChange:
254b467… ragelink 690 def test_success(self, tmp_path):
254b467… ragelink 691 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 692 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 693 uuid = "abc123def456"
254b467… ragelink 694 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 695 result = cli.ticket_change(repo_path, uuid, {"status": "closed"})
254b467… ragelink 696 assert result is True
254b467… ragelink 697 cmd = mock_run.call_args[0][0]
254b467… ragelink 698 assert cmd[:5] == ["/usr/bin/fossil", "ticket", "change", uuid, "-R"]
254b467… ragelink 699 assert "status" in cmd
254b467… ragelink 700 assert "closed" in cmd
254b467… ragelink 701
254b467… ragelink 702 def test_failure(self, tmp_path):
254b467… ragelink 703 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 704 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 705 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 706 result = cli.ticket_change(repo_path, "badid", {"status": "open"})
254b467… ragelink 707 assert result is False
254b467… ragelink 708
254b467… ragelink 709
254b467… ragelink 710 # ---------------------------------------------------------------------------
254b467… ragelink 711 # technote_create
254b467… ragelink 712 # ---------------------------------------------------------------------------
254b467… ragelink 713
254b467… ragelink 714
254b467… ragelink 715 class TestTechnoteCreate:
254b467… ragelink 716 def test_with_explicit_timestamp(self, tmp_path):
254b467… ragelink 717 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 718 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 719 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 720 result = cli.technote_create(repo_path, "Release v1.0", "Details here", timestamp="2026-04-07T12:00:00")
254b467… ragelink 721 assert result is True
254b467… ragelink 722 cmd = mock_run.call_args[0][0]
254b467… ragelink 723 assert "--technote" in cmd
254b467… ragelink 724 assert "2026-04-07T12:00:00" in cmd
254b467… ragelink 725 assert mock_run.call_args[1]["input"] == "Details here"
254b467… ragelink 726
254b467… ragelink 727 def test_auto_generates_timestamp(self, tmp_path):
254b467… ragelink 728 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 729 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 730 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 731 cli.technote_create(repo_path, "Note", "body")
254b467… ragelink 732 cmd = mock_run.call_args[0][0]
254b467… ragelink 733 # Should have generated a timestamp in ISO format
254b467… ragelink 734 ts_idx = cmd.index("--technote") + 1
254b467… ragelink 735 assert "T" in cmd[ts_idx] # ISO datetime has T separator
254b467… ragelink 736
254b467… ragelink 737 def test_with_user(self, tmp_path):
254b467… ragelink 738 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 739 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 740 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 741 cli.technote_create(repo_path, "Note", "body", timestamp="2026-01-01T00:00:00", user="author")
254b467… ragelink 742 cmd = mock_run.call_args[0][0]
254b467… ragelink 743 assert "--technote-user" in cmd
254b467… ragelink 744 assert "author" in cmd
254b467… ragelink 745
254b467… ragelink 746 def test_failure(self, tmp_path):
254b467… ragelink 747 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 748 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 749 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 750 result = cli.technote_create(repo_path, "Fail", "body", timestamp="2026-01-01T00:00:00")
254b467… ragelink 751 assert result is False
254b467… ragelink 752
254b467… ragelink 753
254b467… ragelink 754 # ---------------------------------------------------------------------------
254b467… ragelink 755 # technote_edit
254b467… ragelink 756 # ---------------------------------------------------------------------------
254b467… ragelink 757
254b467… ragelink 758
254b467… ragelink 759 class TestTechnoteEdit:
254b467… ragelink 760 def test_success(self, tmp_path):
254b467… ragelink 761 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 762 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 763 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 764 result = cli.technote_edit(repo_path, "abc123", "Updated body")
254b467… ragelink 765 assert result is True
254b467… ragelink 766 cmd = mock_run.call_args[0][0]
254b467… ragelink 767 assert "--technote" in cmd
254b467… ragelink 768 assert "abc123" in cmd
254b467… ragelink 769 assert mock_run.call_args[1]["input"] == "Updated body"
254b467… ragelink 770
254b467… ragelink 771 def test_with_user(self, tmp_path):
254b467… ragelink 772 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 773 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 774 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 775 cli.technote_edit(repo_path, "abc123", "body", user="editor")
254b467… ragelink 776 cmd = mock_run.call_args[0][0]
254b467… ragelink 777 assert "--technote-user" in cmd
254b467… ragelink 778 assert "editor" in cmd
254b467… ragelink 779
254b467… ragelink 780 def test_failure(self, tmp_path):
254b467… ragelink 781 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 782 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 783 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 784 result = cli.technote_edit(repo_path, "badid", "body")
254b467… ragelink 785 assert result is False
254b467… ragelink 786
254b467… ragelink 787
254b467… ragelink 788 # ---------------------------------------------------------------------------
254b467… ragelink 789 # uv_add
254b467… ragelink 790 # ---------------------------------------------------------------------------
254b467… ragelink 791
254b467… ragelink 792
254b467… ragelink 793 class TestUvAdd:
254b467… ragelink 794 def test_success(self, tmp_path):
254b467… ragelink 795 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 796 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 797 filepath = tmp_path / "logo.png"
254b467… ragelink 798 with patch("subprocess.run", return_value=_ok()) as mock_run:
254b467… ragelink 799 result = cli.uv_add(repo_path, "logo.png", filepath)
254b467… ragelink 800 assert result is True
254b467… ragelink 801 cmd = mock_run.call_args[0][0]
254b467… ragelink 802 assert cmd == ["/usr/bin/fossil", "uv", "add", str(filepath), "--as", "logo.png", "-R", str(repo_path)]
254b467… ragelink 803
254b467… ragelink 804 def test_failure(self, tmp_path):
254b467… ragelink 805 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 806 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 807 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 808 result = cli.uv_add(repo_path, "file.txt", tmp_path / "file.txt")
254b467… ragelink 809 assert result is False
254b467… ragelink 810
254b467… ragelink 811
254b467… ragelink 812 # ---------------------------------------------------------------------------
254b467… ragelink 813 # uv_cat
254b467… ragelink 814 # ---------------------------------------------------------------------------
254b467… ragelink 815
254b467… ragelink 816
254b467… ragelink 817 class TestUvCat:
254b467… ragelink 818 def test_returns_bytes_on_success(self, tmp_path):
254b467… ragelink 819 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 820 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 821 content = b"\x89PNG\r\n\x1a\n" # PNG header bytes
254b467… ragelink 822 with patch("subprocess.run", return_value=_ok_bytes(stdout=content)) as mock_run:
254b467… ragelink 823 result = cli.uv_cat(repo_path, "logo.png")
254b467… ragelink 824 assert result == content
254b467… ragelink 825 cmd = mock_run.call_args[0][0]
254b467… ragelink 826 assert cmd == ["/usr/bin/fossil", "uv", "cat", "logo.png", "-R", str(repo_path)]
254b467… ragelink 827
254b467… ragelink 828 def test_raises_file_not_found_on_failure(self, tmp_path):
254b467… ragelink 829 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 830 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 831 with (
254b467… ragelink 832 patch("subprocess.run", return_value=_ok_bytes(returncode=1)),
254b467… ragelink 833 pytest.raises(FileNotFoundError, match="Unversioned file not found"),
254b467… ragelink 834 ):
254b467… ragelink 835 cli.uv_cat(repo_path, "missing.txt")
254b467… ragelink 836
254b467… ragelink 837
254b467… ragelink 838 # ---------------------------------------------------------------------------
254b467… ragelink 839 # git_export (supplements TestGitExportTokenHandling in test_security.py)
254b467… ragelink 840 # ---------------------------------------------------------------------------
254b467… ragelink 841
254b467… ragelink 842
254b467… ragelink 843 class TestGitExport:
254b467… ragelink 844 def test_basic_export_no_autopush(self, tmp_path):
254b467… ragelink 845 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 846 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 847 mirror_dir = tmp_path / "mirror"
254b467… ragelink 848 with patch("subprocess.run", return_value=_ok(stdout="exported 5 commits")) as mock_run:
254b467… ragelink 849 result = cli.git_export(repo_path, mirror_dir)
254b467… ragelink 850 assert result["success"] is True
254b467… ragelink 851 assert result["message"] == "exported 5 commits"
254b467… ragelink 852 cmd = mock_run.call_args[0][0]
254b467… ragelink 853 assert cmd == ["/usr/bin/fossil", "git", "export", str(mirror_dir), "-R", str(repo_path)]
254b467… ragelink 854 # mirror_dir should be created
254b467… ragelink 855 assert mirror_dir.exists()
254b467… ragelink 856
254b467… ragelink 857 def test_with_autopush_url(self, tmp_path):
254b467… ragelink 858 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 859 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 860 mirror_dir = tmp_path / "mirror"
254b467… ragelink 861 with patch("subprocess.run", return_value=_ok(stdout="pushed")) as mock_run:
254b467… ragelink 862 cli.git_export(repo_path, mirror_dir, autopush_url="https://github.com/user/repo.git")
254b467… ragelink 863 cmd = mock_run.call_args[0][0]
254b467… ragelink 864 assert "--autopush" in cmd
254b467… ragelink 865 assert "https://github.com/user/repo.git" in cmd
254b467… ragelink 866
254b467… ragelink 867 def test_timeout_returns_failure(self, tmp_path):
254b467… ragelink 868 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 869 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 870 mirror_dir = tmp_path / "mirror"
254b467… ragelink 871 with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 300)):
254b467… ragelink 872 result = cli.git_export(repo_path, mirror_dir)
254b467… ragelink 873 assert result["success"] is False
254b467… ragelink 874 assert "timed out" in result["message"].lower()
254b467… ragelink 875
254b467… ragelink 876 def test_failure_returncode(self, tmp_path):
254b467… ragelink 877 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 878 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 879 mirror_dir = tmp_path / "mirror"
254b467… ragelink 880 with patch("subprocess.run", return_value=_ok(stdout="fatal error", returncode=1)):
254b467… ragelink 881 result = cli.git_export(repo_path, mirror_dir)
254b467… ragelink 882 assert result["success"] is False
254b467… ragelink 883
254b467… ragelink 884 def test_temp_files_cleaned_on_success(self, tmp_path):
254b467… ragelink 885 """Askpass and token temp files are removed after successful export."""
254b467… ragelink 886 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 887 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 888 mirror_dir = tmp_path / "mirror"
254b467… ragelink 889
254b467… ragelink 890 created_files = []
254b467… ragelink 891
254b467… ragelink 892 original_mkstemp = __import__("tempfile").mkstemp
254b467… ragelink 893
254b467… ragelink 894 def tracking_mkstemp(**kwargs):
254b467… ragelink 895 fd, path = original_mkstemp(**kwargs)
254b467… ragelink 896 created_files.append(path)
254b467… ragelink 897 return fd, path
254b467… ragelink 898
254b467… ragelink 899 with (
254b467… ragelink 900 patch("subprocess.run", return_value=_ok(stdout="ok")),
254b467… ragelink 901 patch("tempfile.mkstemp", side_effect=tracking_mkstemp),
254b467… ragelink 902 ):
254b467… ragelink 903 cli.git_export(repo_path, mirror_dir, autopush_url="https://github.com/u/r.git", auth_token="tok123")
254b467… ragelink 904
254b467… ragelink 905 # Both temp files should be cleaned up
254b467… ragelink 906 assert len(created_files) == 2
254b467… ragelink 907 for f in created_files:
254b467… ragelink 908 assert not os.path.exists(f)
254b467… ragelink 909
254b467… ragelink 910 def test_temp_files_cleaned_on_timeout(self, tmp_path):
254b467… ragelink 911 """Askpass and token temp files are removed even when subprocess times out."""
254b467… ragelink 912 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 913 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 914 mirror_dir = tmp_path / "mirror"
254b467… ragelink 915
254b467… ragelink 916 created_files = []
254b467… ragelink 917 original_mkstemp = __import__("tempfile").mkstemp
254b467… ragelink 918
254b467… ragelink 919 def tracking_mkstemp(**kwargs):
254b467… ragelink 920 fd, path = original_mkstemp(**kwargs)
254b467… ragelink 921 created_files.append(path)
254b467… ragelink 922 return fd, path
254b467… ragelink 923
254b467… ragelink 924 with (
254b467… ragelink 925 patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 300)),
254b467… ragelink 926 patch("tempfile.mkstemp", side_effect=tracking_mkstemp),
254b467… ragelink 927 ):
254b467… ragelink 928 cli.git_export(repo_path, mirror_dir, autopush_url="https://github.com/u/r.git", auth_token="tok123")
254b467… ragelink 929
254b467… ragelink 930 for f in created_files:
254b467… ragelink 931 assert not os.path.exists(f)
254b467… ragelink 932
254b467… ragelink 933 def test_no_redaction_when_no_token(self, tmp_path):
254b467… ragelink 934 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 935 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 936 mirror_dir = tmp_path / "mirror"
254b467… ragelink 937 with patch("subprocess.run", return_value=_ok(stdout="push ok")):
254b467… ragelink 938 result = cli.git_export(repo_path, mirror_dir, autopush_url="https://github.com/u/r.git")
254b467… ragelink 939 assert result["message"] == "push ok"
254b467… ragelink 940 assert "[REDACTED]" not in result["message"]
254b467… ragelink 941
254b467… ragelink 942 def test_combines_stdout_and_stderr(self, tmp_path):
254b467… ragelink 943 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 944 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 945 mirror_dir = tmp_path / "mirror"
254b467… ragelink 946 with patch("subprocess.run", return_value=_ok(stdout="out\n", stderr="err")):
254b467… ragelink 947 result = cli.git_export(repo_path, mirror_dir)
254b467… ragelink 948 assert "out" in result["message"]
254b467… ragelink 949 assert "err" in result["message"]
254b467… ragelink 950
254b467… ragelink 951
254b467… ragelink 952 # ---------------------------------------------------------------------------
254b467… ragelink 953 # generate_ssh_key
254b467… ragelink 954 # ---------------------------------------------------------------------------
254b467… ragelink 955
254b467… ragelink 956
254b467… ragelink 957 class TestGenerateSSHKey:
254b467… ragelink 958 def test_success(self, tmp_path):
254b467… ragelink 959 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 960 key_path = tmp_path / "keys" / "id_ed25519"
254b467… ragelink 961 pub_key_content = "ssh-ed25519 AAAAC3Nza...== fossilrepo"
254b467… ragelink 962 fingerprint_output = "256 SHA256:abcdef123456 fossilrepo (ED25519)"
254b467… ragelink 963
254b467… ragelink 964 # Create the public key file that generate_ssh_key will try to read
254b467… ragelink 965 key_path.parent.mkdir(parents=True, exist_ok=True)
254b467… ragelink 966
254b467… ragelink 967 with patch("subprocess.run") as mock_run:
254b467… ragelink 968 # ssh-keygen creates the key, then we read pubkey, then fingerprint
254b467… ragelink 969 def side_effect(cmd, **kwargs):
254b467… ragelink 970 if "-t" in cmd:
254b467… ragelink 971 # Write fake pub key file on "creation"
254b467… ragelink 972 key_path.with_suffix(".pub").write_text(pub_key_content)
254b467… ragelink 973 return _ok()
254b467… ragelink 974 elif "-lf" in cmd:
254b467… ragelink 975 return _ok(stdout=fingerprint_output)
254b467… ragelink 976 return _ok()
254b467… ragelink 977
254b467… ragelink 978 mock_run.side_effect = side_effect
254b467… ragelink 979 result = cli.generate_ssh_key(key_path, comment="fossilrepo")
254b467… ragelink 980
254b467… ragelink 981 assert result["success"] is True
254b467… ragelink 982 assert result["public_key"] == pub_key_content
254b467… ragelink 983 assert result["fingerprint"] == "SHA256:abcdef123456"
254b467… ragelink 984
254b467… ragelink 985 def test_creates_parent_dirs(self, tmp_path):
254b467… ragelink 986 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 987 key_path = tmp_path / "deep" / "nested" / "id_ed25519"
254b467… ragelink 988 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 989 cli.generate_ssh_key(key_path)
254b467… ragelink 990 # Parent dirs should exist even if ssh-keygen fails
254b467… ragelink 991 assert key_path.parent.exists()
254b467… ragelink 992
254b467… ragelink 993 def test_failure_returns_error_dict(self, tmp_path):
254b467… ragelink 994 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 995 key_path = tmp_path / "id_ed25519"
254b467… ragelink 996 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 997 result = cli.generate_ssh_key(key_path)
254b467… ragelink 998 assert result["success"] is False
254b467… ragelink 999 assert result["public_key"] == ""
254b467… ragelink 1000 assert result["fingerprint"] == ""
254b467… ragelink 1001
254b467… ragelink 1002 def test_exception_returns_error_dict(self, tmp_path):
254b467… ragelink 1003 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1004 key_path = tmp_path / "id_ed25519"
254b467… ragelink 1005 with patch("subprocess.run", side_effect=Exception("ssh-keygen not found")):
254b467… ragelink 1006 result = cli.generate_ssh_key(key_path)
254b467… ragelink 1007 assert result["success"] is False
254b467… ragelink 1008 assert "ssh-keygen not found" in result["error"]
254b467… ragelink 1009
254b467… ragelink 1010 def test_keygen_command_uses_ed25519(self, tmp_path):
254b467… ragelink 1011 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1012 key_path = tmp_path / "id_ed25519"
254b467… ragelink 1013 with patch("subprocess.run", return_value=_fail()) as mock_run:
254b467… ragelink 1014 cli.generate_ssh_key(key_path, comment="test-key")
254b467… ragelink 1015 cmd = mock_run.call_args[0][0]
254b467… ragelink 1016 assert cmd == ["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", "", "-C", "test-key"]
254b467… ragelink 1017
254b467… ragelink 1018 def test_fingerprint_empty_on_keygen_lf_failure(self, tmp_path):
254b467… ragelink 1019 """If ssh-keygen -lf fails, fingerprint should be empty but success still True."""
254b467… ragelink 1020 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1021 key_path = tmp_path / "id_ed25519"
254b467… ragelink 1022 pub_key_content = "ssh-ed25519 AAAAC3Nza...== test"
254b467… ragelink 1023
254b467… ragelink 1024 with patch("subprocess.run") as mock_run:
254b467… ragelink 1025
254b467… ragelink 1026 def side_effect(cmd, **kwargs):
254b467… ragelink 1027 if "-t" in cmd:
254b467… ragelink 1028 key_path.with_suffix(".pub").write_text(pub_key_content)
254b467… ragelink 1029 return _ok()
254b467… ragelink 1030 elif "-lf" in cmd:
254b467… ragelink 1031 return _fail()
254b467… ragelink 1032 return _ok()
254b467… ragelink 1033
254b467… ragelink 1034 mock_run.side_effect = side_effect
254b467… ragelink 1035 result = cli.generate_ssh_key(key_path)
254b467… ragelink 1036 assert result["success"] is True
254b467… ragelink 1037 assert result["public_key"] == pub_key_content
254b467… ragelink 1038 assert result["fingerprint"] == ""
254b467… ragelink 1039
254b467… ragelink 1040
254b467… ragelink 1041 # ---------------------------------------------------------------------------
254b467… ragelink 1042 # http_proxy
254b467… ragelink 1043 # ---------------------------------------------------------------------------
254b467… ragelink 1044
254b467… ragelink 1045
254b467… ragelink 1046 class TestHttpProxy:
254b467… ragelink 1047 def test_parses_crlf_response(self, tmp_path):
254b467… ragelink 1048 """Standard HTTP response with \\r\\n\\r\\n separator."""
254b467… ragelink 1049 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1050 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1051 raw_response = b"HTTP/1.1 200 OK\r\nContent-Type: application/x-fossil\r\n\r\n\x00\x01\x02\x03"
254b467… ragelink 1052 with patch("subprocess.run", return_value=_ok_bytes(stdout=raw_response)):
254b467… ragelink 1053 body, content_type = cli.http_proxy(repo_path, b"request_body")
254b467… ragelink 1054 assert body == b"\x00\x01\x02\x03"
254b467… ragelink 1055 assert content_type == "application/x-fossil"
254b467… ragelink 1056
254b467… ragelink 1057 def test_parses_lf_response(self, tmp_path):
254b467… ragelink 1058 """Fallback: \\n\\n separator (no \\r)."""
254b467… ragelink 1059 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1060 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1061 raw_response = b"Content-Type: text/html\n\n<html>body</html>"
254b467… ragelink 1062 with patch("subprocess.run", return_value=_ok_bytes(stdout=raw_response)):
254b467… ragelink 1063 body, content_type = cli.http_proxy(repo_path, b"req")
254b467… ragelink 1064 assert body == b"<html>body</html>"
254b467… ragelink 1065 assert content_type == "text/html"
254b467… ragelink 1066
254b467… ragelink 1067 def test_no_separator_returns_entire_body(self, tmp_path):
254b467… ragelink 1068 """If no header/body separator, treat entire output as body."""
254b467… ragelink 1069 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1070 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1071 raw_response = b"raw binary data with no headers"
254b467… ragelink 1072 with patch("subprocess.run", return_value=_ok_bytes(stdout=raw_response)):
254b467… ragelink 1073 body, content_type = cli.http_proxy(repo_path, b"req")
254b467… ragelink 1074 assert body == raw_response
254b467… ragelink 1075 assert content_type == "application/x-fossil"
254b467… ragelink 1076
254b467… ragelink 1077 def test_localauth_flag(self, tmp_path):
254b467… ragelink 1078 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1079 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1080 with patch("subprocess.run", return_value=_ok_bytes(stdout=b"\r\n\r\n")) as mock_run:
254b467… ragelink 1081 cli.http_proxy(repo_path, b"body", localauth=True)
254b467… ragelink 1082 cmd = mock_run.call_args[0][0]
254b467… ragelink 1083 assert "--localauth" in cmd
254b467… ragelink 1084
254b467… ragelink 1085 def test_no_localauth_flag(self, tmp_path):
254b467… ragelink 1086 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1087 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1088 with patch("subprocess.run", return_value=_ok_bytes(stdout=b"\r\n\r\n")) as mock_run:
254b467… ragelink 1089 cli.http_proxy(repo_path, b"body", localauth=False)
254b467… ragelink 1090 cmd = mock_run.call_args[0][0]
254b467… ragelink 1091 assert "--localauth" not in cmd
254b467… ragelink 1092
254b467… ragelink 1093 def test_builds_http_request_on_stdin(self, tmp_path):
254b467… ragelink 1094 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1095 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1096 request_body = b"\x00\x01binary-data"
254b467… ragelink 1097 with patch("subprocess.run", return_value=_ok_bytes(stdout=b"\r\n\r\n")) as mock_run:
254b467… ragelink 1098 cli.http_proxy(repo_path, request_body, content_type="application/x-fossil")
254b467… ragelink 1099 http_input = mock_run.call_args[1]["input"]
254b467… ragelink 1100 # Should contain POST, Host, Content-Type, Content-Length headers + body
254b467… ragelink 1101 assert b"POST /xfer HTTP/1.1\r\n" in http_input
254b467… ragelink 1102 assert b"Host: localhost\r\n" in http_input
254b467… ragelink 1103 assert b"Content-Type: application/x-fossil\r\n" in http_input
254b467… ragelink 1104 assert f"Content-Length: {len(request_body)}".encode() in http_input
254b467… ragelink 1105 assert http_input.endswith(request_body)
254b467… ragelink 1106
254b467… ragelink 1107 def test_default_content_type(self, tmp_path):
254b467… ragelink 1108 """When no content_type provided, defaults to application/x-fossil."""
254b467… ragelink 1109 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1110 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1111 with patch("subprocess.run", return_value=_ok_bytes(stdout=b"\r\n\r\n")) as mock_run:
254b467… ragelink 1112 cli.http_proxy(repo_path, b"body")
254b467… ragelink 1113 http_input = mock_run.call_args[1]["input"]
254b467… ragelink 1114 assert b"Content-Type: application/x-fossil\r\n" in http_input
254b467… ragelink 1115
254b467… ragelink 1116 def test_timeout_raises(self, tmp_path):
254b467… ragelink 1117 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1118 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1119 with (
254b467… ragelink 1120 patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 120)),
254b467… ragelink 1121 pytest.raises(subprocess.TimeoutExpired),
254b467… ragelink 1122 ):
254b467… ragelink 1123 cli.http_proxy(repo_path, b"body")
254b467… ragelink 1124
254b467… ragelink 1125 def test_file_not_found_raises(self, tmp_path):
254b467… ragelink 1126 cli = FossilCLI(binary="/nonexistent/fossil")
254b467… ragelink 1127 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1128 with (
254b467… ragelink 1129 patch("subprocess.run", side_effect=FileNotFoundError),
254b467… ragelink 1130 pytest.raises(FileNotFoundError),
254b467… ragelink 1131 ):
254b467… ragelink 1132 cli.http_proxy(repo_path, b"body")
254b467… ragelink 1133
254b467… ragelink 1134 def test_nonzero_returncode_does_not_raise(self, tmp_path):
254b467… ragelink 1135 """Non-zero exit code logs a warning but does not raise."""
254b467… ragelink 1136 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1137 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1138 raw_response = b"Content-Type: application/x-fossil\r\n\r\nbody"
254b467… ragelink 1139 with patch("subprocess.run", return_value=_ok_bytes(stdout=raw_response, returncode=1)):
254b467… ragelink 1140 body, ct = cli.http_proxy(repo_path, b"req")
254b467… ragelink 1141 assert body == b"body"
254b467… ragelink 1142
254b467… ragelink 1143 def test_gateway_interface_stripped(self, tmp_path):
254b467… ragelink 1144 """GATEWAY_INTERFACE must not be in the env passed to fossil http."""
254b467… ragelink 1145 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1146 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1147 with (
254b467… ragelink 1148 patch.dict(os.environ, {"GATEWAY_INTERFACE": "CGI/1.1"}),
254b467… ragelink 1149 patch("subprocess.run", return_value=_ok_bytes(stdout=b"\r\n\r\n")) as mock_run,
254b467… ragelink 1150 ):
254b467… ragelink 1151 cli.http_proxy(repo_path, b"body")
254b467… ragelink 1152 env = mock_run.call_args[1]["env"]
254b467… ragelink 1153 assert "GATEWAY_INTERFACE" not in env
254b467… ragelink 1154
254b467… ragelink 1155
254b467… ragelink 1156 # ---------------------------------------------------------------------------
254b467… ragelink 1157 # shun / shun_list
254b467… ragelink 1158 # ---------------------------------------------------------------------------
254b467… ragelink 1159
254b467… ragelink 1160
254b467… ragelink 1161 class TestShun:
254b467… ragelink 1162 def test_shun_success(self, tmp_path):
254b467… ragelink 1163 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1164 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1165 with patch("subprocess.run", return_value=_ok(stdout="Shunned")):
254b467… ragelink 1166 result = cli.shun(repo_path, "abc123def456")
254b467… ragelink 1167 assert result["success"] is True
254b467… ragelink 1168 assert "Shunned" in result["message"]
254b467… ragelink 1169
254b467… ragelink 1170 def test_shun_failure(self, tmp_path):
254b467… ragelink 1171 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1172 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1173 with patch("subprocess.run", return_value=_fail(stdout="", stderr="not found")):
254b467… ragelink 1174 result = cli.shun(repo_path, "badid")
254b467… ragelink 1175 assert result["success"] is False
254b467… ragelink 1176 assert "not found" in result["message"]
254b467… ragelink 1177
254b467… ragelink 1178 def test_shun_combines_stdout_stderr(self, tmp_path):
254b467… ragelink 1179 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1180 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1181 with patch("subprocess.run", return_value=_ok(stdout="out\n", stderr="warning")):
254b467… ragelink 1182 result = cli.shun(repo_path, "abc123")
254b467… ragelink 1183 assert "out" in result["message"]
254b467… ragelink 1184 assert "warning" in result["message"]
254b467… ragelink 1185
254b467… ragelink 1186
254b467… ragelink 1187 class TestShunList:
254b467… ragelink 1188 def test_returns_uuids(self, tmp_path):
254b467… ragelink 1189 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1190 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1191 with patch("subprocess.run", return_value=_ok(stdout="abc123\ndef456\nghi789\n")):
254b467… ragelink 1192 result = cli.shun_list(repo_path)
254b467… ragelink 1193 assert result == ["abc123", "def456", "ghi789"]
254b467… ragelink 1194
254b467… ragelink 1195 def test_returns_empty_on_failure(self, tmp_path):
254b467… ragelink 1196 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1197 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1198 with patch("subprocess.run", return_value=_fail()):
254b467… ragelink 1199 result = cli.shun_list(repo_path)
254b467… ragelink 1200 assert result == []
254b467… ragelink 1201
254b467… ragelink 1202 def test_strips_whitespace_and_empty_lines(self, tmp_path):
254b467… ragelink 1203 cli = FossilCLI(binary="/usr/bin/fossil")
254b467… ragelink 1204 repo_path = tmp_path / "repo.fossil"
254b467… ragelink 1205 with patch("subprocess.run", return_value=_ok(stdout="\n abc123 \n\n def456\n\n")):
254b467… ragelink 1206 result = cli.shun_list(repo_path)
254b467… ragelink 1207 assert result == ["abc123", "def456"]

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button