|
8 | 8 | from celery.contrib.testing.mocks import TaskMessage |
9 | 9 | from celery.exceptions import MaxRetriesExceededError, Retry, SoftTimeLimitExceeded |
10 | 10 | from prometheus_client import REGISTRY |
| 11 | +from redis.exceptions import LockError |
| 12 | +from redis.lock import Lock |
11 | 13 | from sqlalchemy.exc import ( |
12 | 14 | DBAPIError, |
13 | 15 | IntegrityError, |
@@ -917,3 +919,166 @@ def test_real_example_override_from_upload( |
917 | 919 | time_limit=450, |
918 | 920 | user_plan="users-enterprisey", |
919 | 921 | ) |
| 922 | + |
| 923 | + |
| 924 | +@pytest.mark.django_db(databases={"default", "timeseries"}) |
| 925 | +class TestBaseCodecovTaskWithLoggedLock: |
| 926 | + def test_with_logged_lock_logs_acquiring_and_acquired(self, mocker): |
| 927 | + """Test that with_logged_lock logs 'Acquiring lock' and 'Acquired lock'.""" |
| 928 | + mock_log = mocker.patch("tasks.base.log") |
| 929 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 930 | + mock_lock.__enter__ = mocker.MagicMock(return_value=None) |
| 931 | + mock_lock.__exit__ = mocker.MagicMock(return_value=None) |
| 932 | + |
| 933 | + task = BaseCodecovTask() |
| 934 | + with task.with_logged_lock(mock_lock, lock_name="test_lock", repoid=123): |
| 935 | + pass |
| 936 | + |
| 937 | + acquiring_calls = [ |
| 938 | + call |
| 939 | + for call in mock_log.info.call_args_list |
| 940 | + if call[0][0] == "Acquiring lock" |
| 941 | + ] |
| 942 | + assert len(acquiring_calls) == 1 |
| 943 | + assert acquiring_calls[0][1]["extra"]["lock_name"] == "test_lock" |
| 944 | + assert acquiring_calls[0][1]["extra"]["repoid"] == 123 |
| 945 | + |
| 946 | + acquired_calls = [ |
| 947 | + call |
| 948 | + for call in mock_log.info.call_args_list |
| 949 | + if call[0][0] == "Acquired lock" |
| 950 | + ] |
| 951 | + assert len(acquired_calls) == 1 |
| 952 | + assert acquired_calls[0][1]["extra"]["lock_name"] == "test_lock" |
| 953 | + assert acquired_calls[0][1]["extra"]["repoid"] == 123 |
| 954 | + |
| 955 | + def test_with_logged_lock_logs_releasing_with_duration(self, mocker): |
| 956 | + """Test that with_logged_lock logs 'Releasing lock' with duration.""" |
| 957 | + mock_log = mocker.patch("tasks.base.log") |
| 958 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 959 | + mock_lock.__enter__ = mocker.MagicMock(return_value=None) |
| 960 | + mock_lock.__exit__ = mocker.MagicMock(return_value=None) |
| 961 | + |
| 962 | + # Mock time.time to control duration |
| 963 | + mock_time = mocker.patch("tasks.base.time.time") |
| 964 | + mock_time.side_effect = [1000.0, 1000.5] # 0.5 second duration |
| 965 | + |
| 966 | + task = BaseCodecovTask() |
| 967 | + with task.with_logged_lock(mock_lock, lock_name="test_lock", commitid="abc123"): |
| 968 | + pass |
| 969 | + |
| 970 | + releasing_calls = [ |
| 971 | + call |
| 972 | + for call in mock_log.info.call_args_list |
| 973 | + if call[0][0] == "Releasing lock" |
| 974 | + ] |
| 975 | + assert len(releasing_calls) == 1 |
| 976 | + assert releasing_calls[0][1]["extra"]["lock_name"] == "test_lock" |
| 977 | + assert releasing_calls[0][1]["extra"]["commitid"] == "abc123" |
| 978 | + # Use approximate comparison due to floating point precision |
| 979 | + assert ( |
| 980 | + abs(releasing_calls[0][1]["extra"]["lock_duration_seconds"] - 0.5) < 0.001 |
| 981 | + ) |
| 982 | + |
| 983 | + def test_with_logged_lock_includes_extra_context(self, mocker): |
| 984 | + """Test that with_logged_lock includes all extra context in logs.""" |
| 985 | + mock_log = mocker.patch("tasks.base.log") |
| 986 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 987 | + mock_lock.__enter__ = mocker.MagicMock(return_value=None) |
| 988 | + mock_lock.__exit__ = mocker.MagicMock(return_value=None) |
| 989 | + |
| 990 | + task = BaseCodecovTask() |
| 991 | + with task.with_logged_lock( |
| 992 | + mock_lock, |
| 993 | + lock_name="test_lock", |
| 994 | + repoid=123, |
| 995 | + commitid="abc123", |
| 996 | + report_type="coverage", |
| 997 | + custom_field="custom_value", |
| 998 | + ): |
| 999 | + pass |
| 1000 | + |
| 1001 | + # Check that all extra context is included in all log calls |
| 1002 | + for log_call in mock_log.info.call_args_list: |
| 1003 | + extra = log_call[1]["extra"] |
| 1004 | + assert extra["lock_name"] == "test_lock" |
| 1005 | + assert extra["repoid"] == 123 |
| 1006 | + assert extra["commitid"] == "abc123" |
| 1007 | + assert extra["report_type"] == "coverage" |
| 1008 | + assert extra["custom_field"] == "custom_value" |
| 1009 | + |
| 1010 | + def test_with_logged_lock_executes_code_within_lock(self, mocker): |
| 1011 | + """Test that code within with_logged_lock executes correctly.""" |
| 1012 | + mock_log = mocker.patch("tasks.base.log") |
| 1013 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 1014 | + mock_lock.__enter__ = mocker.MagicMock(return_value=None) |
| 1015 | + mock_lock.__exit__ = mocker.MagicMock(return_value=None) |
| 1016 | + |
| 1017 | + task = BaseCodecovTask() |
| 1018 | + result = None |
| 1019 | + with task.with_logged_lock(mock_lock, lock_name="test_lock"): |
| 1020 | + result = "executed" |
| 1021 | + |
| 1022 | + assert result == "executed" |
| 1023 | + mock_lock.__enter__.assert_called_once() |
| 1024 | + mock_lock.__exit__.assert_called_once() |
| 1025 | + |
| 1026 | + def test_with_logged_lock_propagates_lock_error(self, mocker): |
| 1027 | + """Test that LockError from lock acquisition is not caught by with_logged_lock.""" |
| 1028 | + mock_log = mocker.patch("tasks.base.log") |
| 1029 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 1030 | + mock_lock.__enter__ = mocker.MagicMock(side_effect=LockError("Lock failed")) |
| 1031 | + |
| 1032 | + task = BaseCodecovTask() |
| 1033 | + with pytest.raises(LockError, match="Lock failed"): |
| 1034 | + with task.with_logged_lock(mock_lock, lock_name="test_lock"): |
| 1035 | + pass |
| 1036 | + |
| 1037 | + # Should have logged "Acquiring lock" but not "Acquired lock" or "Releasing lock" |
| 1038 | + acquiring_calls = [ |
| 1039 | + call |
| 1040 | + for call in mock_log.info.call_args_list |
| 1041 | + if call[0][0] == "Acquiring lock" |
| 1042 | + ] |
| 1043 | + assert len(acquiring_calls) == 1 |
| 1044 | + |
| 1045 | + acquired_calls = [ |
| 1046 | + call |
| 1047 | + for call in mock_log.info.call_args_list |
| 1048 | + if call[0][0] == "Acquired lock" |
| 1049 | + ] |
| 1050 | + assert len(acquired_calls) == 0 |
| 1051 | + |
| 1052 | + releasing_calls = [ |
| 1053 | + call |
| 1054 | + for call in mock_log.info.call_args_list |
| 1055 | + if call[0][0] == "Releasing lock" |
| 1056 | + ] |
| 1057 | + assert len(releasing_calls) == 0 |
| 1058 | + |
| 1059 | + def test_with_logged_lock_logs_release_even_on_exception(self, mocker): |
| 1060 | + """Test that 'Releasing lock' is logged even if code within raises an exception.""" |
| 1061 | + mock_log = mocker.patch("tasks.base.log") |
| 1062 | + mock_lock = mocker.MagicMock(spec=Lock) |
| 1063 | + mock_lock.__enter__ = mocker.MagicMock(return_value=None) |
| 1064 | + mock_lock.__exit__ = mocker.MagicMock(return_value=None) |
| 1065 | + |
| 1066 | + # Mock time.time to control duration |
| 1067 | + mock_time = mocker.patch("tasks.base.time.time") |
| 1068 | + mock_time.side_effect = [1000.0, 1000.2] # 0.2 second duration |
| 1069 | + |
| 1070 | + task = BaseCodecovTask() |
| 1071 | + with pytest.raises(ValueError): |
| 1072 | + with task.with_logged_lock(mock_lock, lock_name="test_lock"): |
| 1073 | + raise ValueError("Test exception") |
| 1074 | + |
| 1075 | + releasing_calls = [ |
| 1076 | + call |
| 1077 | + for call in mock_log.info.call_args_list |
| 1078 | + if call[0][0] == "Releasing lock" |
| 1079 | + ] |
| 1080 | + assert len(releasing_calls) == 1 |
| 1081 | + # Use approximate comparison due to floating point precision |
| 1082 | + assert ( |
| 1083 | + abs(releasing_calls[0][1]["extra"]["lock_duration_seconds"] - 0.2) < 0.001 |
| 1084 | + ) |
0 commit comments