Skip to content

Commit c13bab5

Browse files
committed
PICARD-3290: Add tests for _retry_on_permission_error
Tests mock time.sleep to avoid actual delays and mock IS_WIN to run the Windows-specific retry logic on any platform.
1 parent 3eab591 commit c13bab5

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

test/test_file.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from unittest.mock import (
2929
MagicMock,
3030
Mock,
31+
patch,
3132
)
3233

3334
from test.picardtestcase import PicardTestCase
@@ -847,3 +848,57 @@ class MyFormat(File):
847848

848849
score = MyFormat.score('/somepath/somefile.ogg', Mock(), b"abc")
849850
self.assertEqual(score, 0)
851+
852+
853+
class RetryOnPermissionErrorTest(PicardTestCase):
854+
def setUp(self):
855+
super().setUp()
856+
self.patch_tagger_instance('picard.item')
857+
self.file = File('somepath/somefile.mp3')
858+
859+
@patch('picard.file.IS_WIN', False)
860+
def test_no_retry_on_non_windows(self):
861+
"""On non-Windows, PermissionError propagates immediately."""
862+
func = Mock(side_effect=PermissionError("locked"))
863+
with self.assertRaises(PermissionError):
864+
self.file._retry_on_permission_error(func)
865+
func.assert_called_once()
866+
867+
@patch('picard.file.IS_WIN', True)
868+
@patch('picard.file.time.sleep')
869+
def test_succeeds_first_try(self, mock_sleep):
870+
func = Mock(return_value='result')
871+
result = self.file._retry_on_permission_error(func)
872+
self.assertEqual('result', result)
873+
func.assert_called_once()
874+
mock_sleep.assert_not_called()
875+
876+
@patch('picard.file.IS_WIN', True)
877+
@patch('picard.file.time.sleep')
878+
def test_succeeds_after_retry(self, mock_sleep):
879+
"""Succeeds on second attempt after one PermissionError."""
880+
func = Mock(side_effect=[PermissionError("locked"), 'result'])
881+
result = self.file._retry_on_permission_error(func)
882+
self.assertEqual('result', result)
883+
self.assertEqual(2, func.call_count)
884+
mock_sleep.assert_called_once_with(self.file._PERMISSION_ERROR_RETRY_DELAY)
885+
886+
@patch('picard.file.IS_WIN', True)
887+
@patch('picard.file.time.sleep')
888+
def test_raises_after_max_retries(self, mock_sleep):
889+
"""Raises PermissionError after exhausting all retries."""
890+
func = Mock(side_effect=PermissionError("locked"))
891+
with self.assertRaises(PermissionError):
892+
self.file._retry_on_permission_error(func)
893+
self.assertEqual(self.file._PERMISSION_ERROR_RETRIES, func.call_count)
894+
self.assertEqual(self.file._PERMISSION_ERROR_RETRIES - 1, mock_sleep.call_count)
895+
896+
@patch('picard.file.IS_WIN', True)
897+
@patch('picard.file.time.sleep')
898+
def test_non_permission_error_not_retried(self, mock_sleep):
899+
"""Other exceptions propagate immediately without retry."""
900+
func = Mock(side_effect=OSError("other error"))
901+
with self.assertRaises(OSError):
902+
self.file._retry_on_permission_error(func)
903+
func.assert_called_once()
904+
mock_sleep.assert_not_called()

0 commit comments

Comments
 (0)