Skip to content

Commit a258705

Browse files
committed
CM-55207: improve testing and cover some edge cases in commit pre-receive
1 parent aac1446 commit a258705

File tree

2 files changed

+139
-3
lines changed

2 files changed

+139
-3
lines changed

cycode/cli/files_collector/commit_range_documents.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,20 @@ def calculate_pre_receive_commit_range(branch_update_details: str) -> Optional[s
111111
if end_commit == consts.EMPTY_COMMIT_SHA:
112112
return None
113113

114-
start_commit = _get_oldest_unupdated_commit_for_branch(end_commit)
114+
repo = git_proxy.get_repo(os.getcwd())
115+
start_commit = _get_oldest_unupdated_commit_for_branch(repo, end_commit)
115116

116117
# no new commit to update found
117118
if not start_commit:
118119
return None
119120

121+
# If the oldest not-yet-updated commit has no parent (root commit or orphaned history),
122+
# using '~1' will fail. In that case, scan from the end commit, which effectively
123+
# includes the entire history reachable from it (which is exactly what we need here).
124+
125+
if not bool(repo.commit(start_commit).parents):
126+
return f'{end_commit}'
127+
120128
return f'{start_commit}~1...{end_commit}'
121129

122130

@@ -126,10 +134,10 @@ def _get_end_commit_from_branch_update_details(update_details: str) -> str:
126134
return end_commit
127135

128136

129-
def _get_oldest_unupdated_commit_for_branch(commit: str) -> Optional[str]:
137+
def _get_oldest_unupdated_commit_for_branch(repo: 'Repo', commit: str) -> Optional[str]:
130138
# get a list of commits by chronological order that are not in the remote repository yet
131139
# more info about rev-list command: https://git-scm.com/docs/git-rev-list
132-
repo = git_proxy.get_repo(os.getcwd())
140+
133141
not_updated_commits = repo.git.rev_list(commit, '--topo-order', '--reverse', '--not', '--all')
134142

135143
commits = not_updated_commits.splitlines()

tests/cli/files_collector/test_commit_range_documents.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from cycode.cli import consts
1212
from cycode.cli.files_collector.commit_range_documents import (
1313
_get_default_branches_for_merge_base,
14+
calculate_pre_receive_commit_range,
1415
calculate_pre_push_commit_range,
1516
get_diff_file_path,
1617
get_safe_head_reference_for_diff,
1718
parse_commit_range,
19+
parse_pre_receive_input,
1820
parse_pre_push_input,
1921
)
2022
from cycode.cli.utils.path_utils import get_path_by_os
@@ -871,3 +873,129 @@ def test_single_commit_spec(self) -> None:
871873

872874
parsed_from, parsed_to = parse_commit_range(a, temp_dir)
873875
assert (parsed_from, parsed_to) == (a, c)
876+
877+
878+
class TestParsePreReceiveInput:
879+
"""Test the parse_pre_receive_input function with various pre-receive hook input scenarios."""
880+
881+
def test_parse_single_update_input(self) -> None:
882+
"""Test parsing a single branch update input."""
883+
pre_receive_input = '1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 refs/heads/main'
884+
885+
with patch('sys.stdin', StringIO(pre_receive_input)):
886+
result = parse_pre_receive_input()
887+
assert result == pre_receive_input
888+
889+
def test_parse_multiple_update_input_returns_first_line(self) -> None:
890+
"""Test parsing multiple branch updates returns only the first line."""
891+
pre_receive_input = """0000000000000000000000000000000000000000 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main
892+
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccc refs/heads/feature"""
893+
894+
with patch('sys.stdin', StringIO(pre_receive_input)):
895+
result = parse_pre_receive_input()
896+
assert result == '0000000000000000000000000000000000000000 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main'
897+
898+
def test_parse_empty_input_raises_error(self) -> None:
899+
"""Test that empty input raises ValueError."""
900+
with patch('sys.stdin', StringIO('')), pytest.raises(ValueError, match='Pre receive input was not found'):
901+
parse_pre_receive_input()
902+
903+
def test_parse_whitespace_only_input_raises_error(self) -> None:
904+
"""Test that whitespace-only input raises ValueError."""
905+
with patch('sys.stdin', StringIO(' \n\t ')), pytest.raises(ValueError, match='Pre receive input was not found'):
906+
parse_pre_receive_input()
907+
908+
909+
class TestCalculatePreReceiveCommitRange:
910+
"""Test the calculate_pre_receive_commit_range function with representative scenarios."""
911+
912+
def test_branch_deletion_returns_none(self) -> None:
913+
"""When end commit is all zeros (deletion), no scan is needed."""
914+
update_details = f'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {consts.EMPTY_COMMIT_SHA} refs/heads/feature'
915+
assert calculate_pre_receive_commit_range(update_details) is None
916+
917+
def test_no_new_commits_returns_none(self, monkeypatch: pytest.MonkeyPatch) -> None:
918+
"""When there are no commits not in remote, return None."""
919+
update_details = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb refs/heads/main'
920+
921+
class MockGit:
922+
def rev_list(self, *args: str) -> str:
923+
return '' # no commits not in remote
924+
925+
class MockRepo:
926+
git = MockGit()
927+
928+
monkeypatch.setenv('PWD', os.getcwd())
929+
monkeypatch.setattr('cycode.cli.files_collector.commit_range_documents.git_proxy.get_repo', lambda path: MockRepo())
930+
931+
assert calculate_pre_receive_commit_range(update_details) is None
932+
933+
def test_returns_triple_dot_range_from_oldest_unupdated(self, monkeypatch: pytest.MonkeyPatch) -> None:
934+
"""Returns '<oldest>~1...<end>' when there are new commits to scan."""
935+
end_sha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
936+
update_details = f'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {end_sha} refs/heads/main'
937+
938+
class MockGit:
939+
def rev_list(self, *args: str) -> str:
940+
# first line is the oldest unupdated commit; others follow
941+
return '1111111111111111111111111111111111111111\n2222222222222222222222222222222222222222'
942+
943+
class MockRepo:
944+
git = MockGit()
945+
def commit(self, _sha: str):
946+
# Pretend it has a parent
947+
return Mock(parents=['parent'])
948+
949+
monkeypatch.setattr('cycode.cli.files_collector.commit_range_documents.git_proxy.get_repo', lambda path: MockRepo())
950+
951+
result = calculate_pre_receive_commit_range(update_details)
952+
assert result == '1111111111111111111111111111111111111111~1...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
953+
954+
def test_initial_oldest_commit_without_parent_returns_single_commit_range(
955+
self, monkeypatch: pytest.MonkeyPatch
956+
) -> None:
957+
"""If oldest commit has no parent, avoid '~1' and scan from end commit only."""
958+
end_sha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
959+
update_details = f'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {end_sha} refs/heads/main'
960+
961+
class MockGit:
962+
def rev_list(self, *args: str) -> str:
963+
# Oldest unupdated commit is a root (no parent)
964+
return '1111111111111111111111111111111111111111'
965+
966+
class MockRepo:
967+
git = MockGit()
968+
def commit(self, _sha: str):
969+
return Mock(parents=[])
970+
971+
monkeypatch.setattr('cycode.cli.files_collector.commit_range_documents.git_proxy.get_repo', lambda path: MockRepo())
972+
973+
result = calculate_pre_receive_commit_range(update_details)
974+
assert result == end_sha
975+
976+
def test_initial_oldest_commit_without_parent_with_two_commits_returns_single_commit_range(
977+
self, monkeypatch: pytest.MonkeyPatch
978+
) -> None:
979+
"""If there are two new commits and the oldest has no parent, avoid '~1' and scan from end commit only."""
980+
end_sha = 'dddddddddddddddddddddddddddddddddddddddd'
981+
oldest_sha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
982+
second_sha = 'cccccccccccccccccccccccccccccccccccccccc'
983+
update_details = f'0000000000000000000000000000000000000000 {end_sha} refs/heads/main'
984+
985+
class MockGit:
986+
def rev_list(self, *args: str) -> str:
987+
# two commits not yet in remote: oldest first
988+
return f'{oldest_sha}\n{second_sha}'
989+
990+
class MockRepo:
991+
git = MockGit()
992+
def commit(self, sha: str):
993+
# the oldest is a root (no parent)
994+
if sha == oldest_sha:
995+
return Mock(parents=[])
996+
return Mock(parents=['parent'])
997+
998+
monkeypatch.setattr('cycode.cli.files_collector.commit_range_documents.git_proxy.get_repo', lambda path: MockRepo())
999+
1000+
result = calculate_pre_receive_commit_range(update_details)
1001+
assert result == end_sha

0 commit comments

Comments
 (0)