diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 22655866a0..258f74e567 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1854,3 +1854,25 @@ def test_render_command_report_minimal(): label='foo' )) == """## foo """ + + +def test_retry_strategy_github_rate_limits(monkeypatch): + """ Test GitHub rate limit handling in RetryStrategy """ + strategy = tmt.utils.RetryStrategy() + mock_time = unittest.mock.MagicMock() + monkeypatch.setattr('time.time', mock_time) + mock_time.return_value = 1704114000 # Example: 2024-01-01 13:00:00 UTC + mock_sleep = unittest.mock.MagicMock() + monkeypatch.setattr('time.sleep', mock_sleep) + + # Test primary rate limit + mock_response = unittest.mock.MagicMock() + mock_response.status_code = 403 + mock_response.headers = { + 'X-GitHub-Request-Id': 'ABC123', + 'X-RateLimit-Remaining': '0', + 'X-RateLimit-Reset': '1704114100' # 2024-01-01 13:01:40 UTC (100 seconds later) + } + + strategy.increment(error=None, response=mock_response) + mock_sleep.assert_called_once_with(100) # Should wait 100 seconds until reset time diff --git a/tmt/utils/__init__.py b/tmt/utils/__init__.py index c1b258ba8d..072db75b02 100644 --- a/tmt/utils/__init__.py +++ b/tmt/utils/__init__.py @@ -4358,6 +4358,30 @@ def increment( raise GeneralError(message) from error + # Handle GitHub-specific responses + # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit + response = cast(requests.Response, kwargs.get('response')) + if response is not None and 'X-GitHub-Request-Id' in response.headers: + headers = response.headers + + # Handle rate limiting + if response.status_code in (403, 429): + # Primary rate limit exceeded + if ('X-RateLimit-Remaining' in headers and + int(headers['X-RateLimit-Remaining']) == 0): + reset_time = int(headers['X-RateLimit-Reset']) + # Wait until the rate limit resets + wait_time = reset_time - int(time.time()) + if wait_time > 0: + time.sleep(wait_time) + return super().increment(*args, **kwargs) + + # Secondary rate limit - respect Retry-After if provided + if 'Retry-After' in headers: + retry_after = int(headers['Retry-After']) + time.sleep(retry_after) + return super().increment(*args, **kwargs) + return super().increment(*args, **kwargs)