import builtins
import types
import sys
import subprocess
from pathlib import Path
import pytest
from domin8 import git_ops
class DummyFail:
def __init__(self, returncode=1, stdout='', stderr='patch failed'):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
def test_apply_diff_unidiff_missing(monkeypatch):
original = 'line1\nline2\n'
diff = 'broken'
# Force patch to 'fail'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
# Simulate ImportError when importing unidiff
real_import = builtins.__import__
def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == 'unidiff':
raise ImportError('no unidiff')
return real_import(name, globals, locals, fromlist, level)
monkeypatch.setattr(builtins, '__import__', fake_import)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'unidiff is unavailable' in str(exc.value)
def test_apply_diff_parse_failure(monkeypatch):
original = 'line1\nline2\n'
diff = 'broken'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
# Provide fake unidiff.PatchSet that raises
fake_unidiff = types.SimpleNamespace()
def raise_on_init(*a, **k):
raise RuntimeError('parse error')
fake_unidiff.PatchSet = raise_on_init
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'Failed to parse diff' in str(exc.value)
def test_apply_diff_no_file_hunks(monkeypatch):
original = 'line1\nline2\n'
diff = '--- a/file.txt\n+++ b/file.txt\n'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
class PatchSetEmpty(list):
def __init__(self, *a, **k):
super().__init__()
fake_unidiff = types.SimpleNamespace(PatchSet=PatchSetEmpty)
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'No file hunks found' in str(exc.value)
def test_apply_diff_hunk_out_of_bounds(monkeypatch):
original = 'line1\nline2\n'
diff = 'dummy'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
# Build fake hunk and file objects
class FakeLine:
def __init__(self, value, is_added=False, is_removed=False):
self.value = value
self.is_added = is_added
self.is_removed = is_removed
class FakeHunk(list):
def __init__(self):
super().__init__([FakeLine('line1\n', is_added=False)])
self.source_start = 999 # out of bounds
self.source_length = 1
class FakePatchedFile(list):
def __init__(self):
super().__init__([FakeHunk()])
class PatchSetOne(list):
def __init__(self, *a, **k):
super().__init__([FakePatchedFile()])
fake_unidiff = types.SimpleNamespace(PatchSet=PatchSetOne)
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'Hunk source start out of bounds' in str(exc.value)
def test_apply_diff_length_mismatch(monkeypatch):
# Hunk declares a source_length that doesn't match number of non-added lines
original = 'a\nb\n'
diff = 'dummy'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
class FakeLine:
def __init__(self, value, is_added=False, is_removed=False):
self.value = value
self.is_added = is_added
self.is_removed = is_removed
# Create hunk with source_length 2 but only one non-added line present
class FakeHunk(list):
def __init__(self):
super().__init__([FakeLine('a\n', is_added=False), FakeLine('+b\n', is_added=True)])
self.source_start = 1
self.source_length = 2
class FakePatchedFile(list):
def __init__(self):
super().__init__([FakeHunk()])
class PatchSetOne(list):
def __init__(self, *a, **k):
super().__init__([FakePatchedFile()])
fake_unidiff = types.SimpleNamespace(PatchSet=PatchSetOne)
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'Hunk length mismatch' in str(exc.value)
def test_apply_diff_context_mismatch(monkeypatch):
original = 'line1\nline2\n'
diff = 'dummy'
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
class FakeLine:
def __init__(self, value, is_added=False, is_removed=False):
self.value = value
self.is_added = is_added
self.is_removed = is_removed
class FakeHunk(list):
def __init__(self):
# Non-added lines don't match original region
super().__init__([FakeLine('different\n', is_added=False)])
self.source_start = 1
self.source_length = 1
class FakePatchedFile(list):
def __init__(self):
super().__init__([FakeHunk()])
class PatchSetOne(list):
def __init__(self, *a, **k):
super().__init__([FakePatchedFile()])
fake_unidiff = types.SimpleNamespace(PatchSet=PatchSetOne)
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'Hunk context does not match' in str(exc.value)
def test_apply_diff_empty_result_guard(monkeypatch):
original = 'hello\n'
diff = '--- a/hello.txt\n+++ b/hello.txt\n@@ -1,1 +0,0 @@\n-hello\n'
# Force patch to fail to exercise unidiff fallback parsing and safety guard
monkeypatch.setattr('subprocess.run', lambda *a, **k: DummyFail(returncode=2))
# Provide a PatchSet that describes a deletion of the only line
class FakeLine:
def __init__(self, value, is_added=False, is_removed=False):
self.value = value
self.is_added = is_added
self.is_removed = is_removed
class FakeHunk(list):
def __init__(self):
super().__init__([FakeLine('hello\n', is_added=False, is_removed=True)])
self.source_start = 1
self.source_length = 1
class FakePatchedFile(list):
def __init__(self):
super().__init__([FakeHunk()])
class PatchSetOne(list):
def __init__(self, *a, **k):
super().__init__([FakePatchedFile()])
fake_unidiff = types.SimpleNamespace(PatchSet=PatchSetOne)
monkeypatch.setitem(sys.modules, 'unidiff', fake_unidiff)
with pytest.raises(ValueError) as exc:
git_ops.apply_diff(original, diff)
assert 'Patch produced empty output' in str(exc.value)
def test_stage_and_commit_untracked_missing_raises(monkeypatch, tmp_path):
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
# Simulate `git ls-files --error-unmatch` returning non-zero (untracked)
if cmd[:3] == ['git', 'ls-files', '--error-unmatch']:
class R:
returncode = 1
stdout = ''
stderr = ''
return R()
return DummyFail(returncode=0)
monkeypatch.setattr('subprocess.run', fake_run)
p = tmp_path / 'missing.txt'
with pytest.raises(RuntimeError) as exc:
git_ops.stage_and_commit(p, 'msg')
assert 'not tracked' in str(exc.value)
def test_stage_and_commit_git_commit_fails(monkeypatch, tmp_path):
calls = []
def fake_run(cmd, **kwargs):
calls.append(cmd)
# Simulate normal add
if cmd[:2] == ['git', 'add']:
return DummyFail(returncode=0)
# Simulate git commit failing
if cmd[:2] == ['git', 'commit']:
raise subprocess.CalledProcessError(returncode=1, cmd=cmd, stderr=b'commit error')
return DummyFail(returncode=0)
monkeypatch.setattr('subprocess.run', fake_run)
p = tmp_path / 'file.txt'
p.write_text('x\n')
with pytest.raises(RuntimeError) as exc:
git_ops.stage_and_commit(p, 'msg')
assert 'commit error' in str(exc.value)