codeflash/tests/test_git_utils.py
claude[bot] 0a71905be8 test: update git_diff tests to match new all-extensions behavior
get_git_diff now uses all registered extensions instead of filtering
by current language, so update tests to reflect the new behavior.

Co-authored-by: Sarthak Agarwal <undefined@users.noreply.github.com>
2026-03-19 18:07:19 +00:00

346 lines
12 KiB
Python

import unittest
from unittest.mock import patch
import git
from codeflash.code_utils.git_utils import (
check_and_push_branch,
check_running_in_git_repo,
get_git_diff,
get_repo_owner_and_name,
)
class TestGitUtils(unittest.TestCase):
@patch("codeflash.code_utils.git_utils.get_remote_url")
def test_test_get_repo_owner_and_name(self, mock_get_remote_url):
# Test with a standard GitHub HTTPS URL
mock_get_remote_url.return_value = "https://github.com/owner/repo.git"
get_repo_owner_and_name.cache_clear()
owner, repo_name = get_repo_owner_and_name()
assert owner == "owner"
assert repo_name == "repo"
# Test with a GitHub SSH URL
mock_get_remote_url.return_value = "git@github.com:owner/repo.git"
get_repo_owner_and_name.cache_clear()
owner, repo_name = get_repo_owner_and_name()
assert owner == "owner"
assert repo_name == "repo"
# Test with another GitHub SSH URL
mock_get_remote_url.return_value = "git@github.com:codeflash-ai/posthog.git"
get_repo_owner_and_name.cache_clear()
owner, repo_name = get_repo_owner_and_name()
assert owner == "codeflash-ai"
assert repo_name == "posthog"
# Test with a URL without the .git suffix
mock_get_remote_url.return_value = "https://github.com/owner/repo"
get_repo_owner_and_name.cache_clear()
owner, repo_name = get_repo_owner_and_name()
assert owner == "owner"
assert repo_name == "repo"
# Test with another GitHub SSH URL
mock_get_remote_url.return_value = "git@github.com:codeflash-ai/posthog/"
get_repo_owner_and_name.cache_clear()
owner, repo_name = get_repo_owner_and_name()
assert owner == "codeflash-ai"
assert repo_name == "posthog"
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_check_running_in_git_repo_in_git_repo(self, mock_repo):
mock_repo.return_value.git_dir = "/path/to/repo/.git"
assert check_running_in_git_repo("/path/to/repo")
@patch("codeflash.code_utils.git_utils.git.Repo")
@patch("codeflash.code_utils.git_utils.sys.__stdin__.isatty", return_value=True)
@patch("codeflash.code_utils.git_utils.confirm_proceeding_with_no_git_repo", return_value=True)
def test_check_running_in_git_repo_not_in_git_repo_interactive(self, mock_confirm, mock_isatty, mock_repo):
mock_repo.side_effect = git.InvalidGitRepositoryError # type: ignore
assert check_running_in_git_repo("/path/to/non-repo") == False
@patch("codeflash.code_utils.git_utils.git.Repo")
@patch("codeflash.code_utils.git_utils.sys.__stdin__.isatty", return_value=False)
def test_check_running_in_git_repo_not_in_git_repo_non_interactive(self, mock_isatty, mock_repo):
mock_repo.side_effect = git.exc.InvalidGitRepositoryError # type: ignore
assert check_running_in_git_repo("/path/to/non-repo") is False
@patch("codeflash.code_utils.git_utils.git.Repo")
@patch("codeflash.code_utils.git_utils.sys.__stdin__.isatty", return_value=True)
@patch("codeflash.code_utils.git_utils.Confirm.ask", return_value=True)
def test_check_and_push_branch(self, mock_confirm, mock_isatty, mock_repo):
mock_repo_instance = mock_repo.return_value
# Mock HEAD not being detached
mock_repo_instance.head.is_detached = False
mock_repo_instance.active_branch.name = "test-branch"
mock_repo_instance.refs = []
mock_origin = mock_repo_instance.remote.return_value
mock_origin.push.return_value = None
assert check_and_push_branch(mock_repo_instance)
mock_origin.push.assert_called_once_with(mock_repo_instance.active_branch)
mock_origin.push.reset_mock()
# Test when branch is already pushed
mock_repo_instance.refs = [f"origin/{mock_repo_instance.active_branch.name}"]
assert check_and_push_branch(mock_repo_instance)
mock_origin.push.assert_not_called()
mock_origin.push.reset_mock()
@patch("codeflash.code_utils.git_utils.git.Repo")
@patch("codeflash.code_utils.git_utils.sys.__stdin__.isatty", return_value=False)
def test_check_and_push_branch_non_tty(self, mock_isatty, mock_repo):
mock_repo_instance = mock_repo.return_value
# Mock HEAD not being detached
mock_repo_instance.head.is_detached = False
mock_repo_instance.active_branch.name = "test-branch"
mock_repo_instance.refs = []
mock_origin = mock_repo_instance.remote.return_value
mock_origin.push.return_value = None
assert not check_and_push_branch(mock_repo_instance)
mock_origin.push.assert_not_called()
mock_origin.push.reset_mock()
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_check_and_push_branch_detached_head(self, mock_repo):
mock_repo_instance = mock_repo.return_value
# Mock HEAD being detached
mock_repo_instance.head.is_detached = True
mock_origin = mock_repo_instance.remote.return_value
mock_origin.push.return_value = None
# Should return False when HEAD is detached
assert not check_and_push_branch(mock_repo_instance)
mock_origin.push.assert_not_called()
DELETION_ONLY_DIFF = """\
--- a/example.py
+++ b/example.py
@@ -5,7 +5,5 @@ def foo():
a = 1
b = 2
- c = 3
- d = 4
e = 5
return a + b + e
"""
ADDITION_ONLY_DIFF = """\
--- a/example.py
+++ b/example.py
@@ -5,5 +5,7 @@ def foo():
a = 1
b = 2
+ c = 3
+ d = 4
e = 5
return a + b + e
"""
MIXED_DIFF = """\
--- a/example.py
+++ b/example.py
@@ -5,6 +5,6 @@ def foo():
a = 1
b = 2
- c = 3
+ c = 30
e = 5
return a + b + e
"""
MULTI_HUNK_DELETION_ONLY_DIFF = """\
--- a/example.py
+++ b/example.py
@@ -5,7 +5,5 @@ def foo():
a = 1
b = 2
- c = 3
- d = 4
e = 5
return a + b + e
@@ -20,6 +18,4 @@ def bar():
x = 1
y = 2
- z = 3
- w = 4
return x + y
"""
class TestGetGitDiffDeletionOnly(unittest.TestCase):
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_deletion_only_diff_returns_hunk_target_starts(self, mock_repo_cls):
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = DELETION_ONLY_DIFF
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
assert len(result) == 1
key = list(result.keys())[0]
assert str(key).endswith("example.py")
# The hunk target_start is 5 — this is the fix: deletion-only diffs
# should still report line numbers so the surrounding function is found.
assert result[key] == [5]
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_addition_only_diff_returns_added_lines(self, mock_repo_cls):
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = ADDITION_ONLY_DIFF
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
key = list(result.keys())[0]
# Added lines are at target line numbers 7 and 8
assert result[key] == [7, 8]
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_mixed_diff_returns_only_added_lines(self, mock_repo_cls):
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = MIXED_DIFF
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
key = list(result.keys())[0]
# Only the added line (c = 30) at target line 7
assert result[key] == [7]
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_multi_hunk_deletion_only_returns_all_hunk_starts(self, mock_repo_cls):
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = MULTI_HUNK_DELETION_ONLY_DIFF
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
key = list(result.keys())[0]
# Two hunks with target_start 5 and 18
assert result[key] == [5, 18]
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_deletion_only_diff_does_not_return_empty_list(self, mock_repo_cls):
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = DELETION_ONLY_DIFF
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
key = list(result.keys())[0]
# Without the fix, this would be an empty list, causing the function
# to be missed during discovery.
assert len(result[key]) > 0
JAVA_ADDITION_DIFF = """\
--- a/src/main/java/com/example/Fibonacci.java
+++ b/src/main/java/com/example/Fibonacci.java
@@ -5,5 +5,7 @@ public class Fibonacci {
public static int fibonacci(int n) {
if (n <= 1) return n;
+ int[] memo = new int[n + 1];
+ memo[0] = 0; memo[1] = 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
"""
MIXED_LANG_DIFF = """\
--- a/src/utils.py
+++ b/src/utils.py
@@ -1,3 +1,4 @@
def helper():
+ x = 1
return True
--- a/src/main/java/com/example/App.java
+++ b/src/main/java/com/example/App.java
@@ -1,3 +1,4 @@
public class App {
+ int x = 1;
public static void main(String[] args) {}
"""
class TestGetGitDiffMultiLanguage(unittest.TestCase):
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_java_diff_found_when_language_is_java(self, mock_repo_cls):
from codeflash.languages.current import reset_current_language, set_current_language
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = JAVA_ADDITION_DIFF
set_current_language("java")
try:
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
assert len(result) == 1
key = list(result.keys())[0]
assert str(key).endswith("Fibonacci.java")
assert result[key] == [7, 8]
finally:
reset_current_language()
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_java_diff_found_regardless_of_current_language(self, mock_repo_cls):
from codeflash.languages.current import reset_current_language, set_current_language
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = JAVA_ADDITION_DIFF
# get_git_diff uses all registered extensions, not just the current language's
set_current_language("python")
try:
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
assert len(result) == 1
key = list(result.keys())[0]
assert str(key).endswith("Fibonacci.java")
finally:
reset_current_language()
@patch("codeflash.code_utils.git_utils.git.Repo")
def test_mixed_lang_diff_returns_all_supported_extensions(self, mock_repo_cls):
from codeflash.languages.current import reset_current_language, set_current_language
repo = mock_repo_cls.return_value
repo.head.commit.hexsha = "abc123"
repo.working_dir = "/repo"
repo.git.diff.return_value = MIXED_LANG_DIFF
# All supported extensions are returned regardless of current language
set_current_language("python")
try:
result = get_git_diff(repo_directory=None, uncommitted_changes=True)
assert len(result) == 2
paths = [str(k) for k in result.keys()]
assert any(p.endswith("utils.py") for p in paths)
assert any(p.endswith("App.java") for p in paths)
finally:
reset_current_language()
if __name__ == "__main__":
unittest.main()