|
28 | 28 | from unittest.mock import ( |
29 | 29 | MagicMock, |
30 | 30 | Mock, |
| 31 | + patch, |
31 | 32 | ) |
32 | 33 |
|
33 | 34 | from test.picardtestcase import PicardTestCase |
@@ -847,3 +848,57 @@ class MyFormat(File): |
847 | 848 |
|
848 | 849 | score = MyFormat.score('/somepath/somefile.ogg', Mock(), b"abc") |
849 | 850 | 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