|
11 | 11 | from cycode.cli import consts |
12 | 12 | from cycode.cli.files_collector.commit_range_documents import ( |
13 | 13 | _get_default_branches_for_merge_base, |
| 14 | + calculate_pre_receive_commit_range, |
14 | 15 | calculate_pre_push_commit_range, |
15 | 16 | get_diff_file_path, |
16 | 17 | get_safe_head_reference_for_diff, |
17 | 18 | parse_commit_range, |
| 19 | + parse_pre_receive_input, |
18 | 20 | parse_pre_push_input, |
19 | 21 | ) |
20 | 22 | from cycode.cli.utils.path_utils import get_path_by_os |
@@ -871,3 +873,129 @@ def test_single_commit_spec(self) -> None: |
871 | 873 |
|
872 | 874 | parsed_from, parsed_to = parse_commit_range(a, temp_dir) |
873 | 875 | 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