test_network_retry.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Network Retry-Mechanismen.
  4. """
  5. import asyncio
  6. import pytest
  7. from datetime import datetime
  8. from trixy_core.network.retry import (
  9. NetworkRetry,
  10. RetryConfig,
  11. RetryStrategy,
  12. RetryStats,
  13. RetryContext,
  14. RetryPolicy,
  15. RetryableError,
  16. NonRetryableError,
  17. with_retry,
  18. AGGRESSIVE_RETRY,
  19. CONSERVATIVE_RETRY,
  20. FAST_FAIL_RETRY,
  21. )
  22. class TestRetryConfig:
  23. """Tests für RetryConfig."""
  24. def test_default_config(self):
  25. """Testet Standard-Konfiguration."""
  26. config = RetryConfig()
  27. assert config.max_attempts == 3
  28. assert config.strategy == RetryStrategy.EXPONENTIAL
  29. assert config.base_delay == 1.0
  30. assert config.max_delay == 60.0
  31. def test_custom_config(self):
  32. """Testet benutzerdefinierte Konfiguration."""
  33. config = RetryConfig(
  34. max_attempts=5,
  35. strategy=RetryStrategy.LINEAR,
  36. base_delay=2.0,
  37. jitter=0.2,
  38. )
  39. assert config.max_attempts == 5
  40. assert config.strategy == RetryStrategy.LINEAR
  41. assert config.base_delay == 2.0
  42. assert config.jitter == 0.2
  43. class TestRetryContext:
  44. """Tests für RetryContext."""
  45. def test_context_creation(self):
  46. """Testet Context-Erstellung."""
  47. ctx = RetryContext(attempt=1, max_attempts=5)
  48. assert ctx.attempt == 1
  49. assert ctx.max_attempts == 5
  50. assert ctx.should_retry is True
  51. assert ctx.remaining_attempts == 4
  52. def test_is_last_attempt(self):
  53. """Testet letzter Versuch."""
  54. ctx = RetryContext(attempt=5, max_attempts=5)
  55. assert ctx.is_last_attempt is True
  56. ctx2 = RetryContext(attempt=3, max_attempts=5)
  57. assert ctx2.is_last_attempt is False
  58. def test_abort(self):
  59. """Testet Abbruch."""
  60. ctx = RetryContext(attempt=1, max_attempts=5)
  61. ctx.abort()
  62. assert ctx.should_retry is False
  63. def test_set_next_delay(self):
  64. """Testet benutzerdefinierte Verzögerung."""
  65. ctx = RetryContext(attempt=1, max_attempts=5)
  66. ctx.set_next_delay(5.0)
  67. assert ctx.next_delay == 5.0
  68. class TestNetworkRetry:
  69. """Tests für NetworkRetry."""
  70. @pytest.fixture
  71. def retry(self):
  72. """Erstellt NetworkRetry-Instanz."""
  73. config = RetryConfig(
  74. max_attempts=3,
  75. base_delay=0.01, # Schnelle Tests
  76. jitter=0.0,
  77. )
  78. return NetworkRetry(config=config)
  79. @pytest.mark.asyncio
  80. async def test_successful_operation(self, retry):
  81. """Testet erfolgreiche Operation."""
  82. call_count = 0
  83. async def operation():
  84. nonlocal call_count
  85. call_count += 1
  86. return "success"
  87. result = await retry.execute(operation)
  88. assert result == "success"
  89. assert call_count == 1
  90. assert retry.stats.successful_attempts == 1
  91. @pytest.mark.asyncio
  92. async def test_retry_on_failure(self, retry):
  93. """Testet Retry bei Fehler."""
  94. call_count = 0
  95. async def operation():
  96. nonlocal call_count
  97. call_count += 1
  98. if call_count < 3:
  99. raise ValueError("Fehler")
  100. return "success"
  101. result = await retry.execute(operation)
  102. assert result == "success"
  103. assert call_count == 3
  104. assert retry.stats.total_attempts == 3
  105. @pytest.mark.asyncio
  106. async def test_max_attempts_exceeded(self, retry):
  107. """Testet Überschreitung der maximalen Versuche."""
  108. async def operation():
  109. raise ValueError("Dauerhafter Fehler")
  110. with pytest.raises(ValueError):
  111. await retry.execute(operation)
  112. assert retry.stats.total_attempts == 3
  113. assert retry.stats.failed_attempts == 1
  114. @pytest.mark.asyncio
  115. async def test_non_retryable_error(self, retry):
  116. """Testet NonRetryableError."""
  117. call_count = 0
  118. async def operation():
  119. nonlocal call_count
  120. call_count += 1
  121. raise NonRetryableError("Kein Retry")
  122. with pytest.raises(NonRetryableError):
  123. await retry.execute(operation)
  124. assert call_count == 1 # Kein Retry
  125. @pytest.mark.asyncio
  126. async def test_retryable_error(self):
  127. """Testet RetryableError."""
  128. config = RetryConfig(
  129. max_attempts=3,
  130. base_delay=0.01,
  131. non_retryable_exceptions=(ValueError,),
  132. )
  133. retry = NetworkRetry(config=config)
  134. call_count = 0
  135. async def operation():
  136. nonlocal call_count
  137. call_count += 1
  138. if call_count < 3:
  139. raise RetryableError("Retry bitte")
  140. return "success"
  141. result = await retry.execute(operation)
  142. assert result == "success"
  143. assert call_count == 3
  144. @pytest.mark.asyncio
  145. async def test_exponential_backoff(self):
  146. """Testet exponentielles Backoff."""
  147. config = RetryConfig(
  148. max_attempts=4,
  149. strategy=RetryStrategy.EXPONENTIAL,
  150. base_delay=0.01,
  151. multiplier=2.0,
  152. jitter=0.0,
  153. )
  154. retry = NetworkRetry(config=config)
  155. delays = []
  156. async def operation():
  157. if retry.stats.total_attempts < 3:
  158. raise ValueError("Fehler")
  159. return "success"
  160. # Backoff: 0.01, 0.02, 0.04
  161. await retry.execute(operation)
  162. assert retry.stats.total_attempts == 3
  163. @pytest.mark.asyncio
  164. async def test_linear_backoff(self):
  165. """Testet lineares Backoff."""
  166. config = RetryConfig(
  167. max_attempts=4,
  168. strategy=RetryStrategy.LINEAR,
  169. base_delay=0.01,
  170. jitter=0.0,
  171. )
  172. retry = NetworkRetry(config=config)
  173. async def operation():
  174. if retry.stats.total_attempts < 3:
  175. raise ValueError("Fehler")
  176. return "success"
  177. # Linear: 0.01, 0.02, 0.03
  178. await retry.execute(operation)
  179. assert retry.stats.total_attempts == 3
  180. @pytest.mark.asyncio
  181. async def test_constant_backoff(self):
  182. """Testet konstantes Backoff."""
  183. config = RetryConfig(
  184. max_attempts=3,
  185. strategy=RetryStrategy.CONSTANT,
  186. base_delay=0.01,
  187. jitter=0.0,
  188. )
  189. retry = NetworkRetry(config=config)
  190. async def operation():
  191. if retry.stats.total_attempts < 2:
  192. raise ValueError("Fehler")
  193. return "success"
  194. await retry.execute(operation)
  195. assert retry.stats.total_attempts == 2
  196. @pytest.mark.asyncio
  197. async def test_max_delay_limit(self):
  198. """Testet maximale Verzögerung."""
  199. config = RetryConfig(
  200. max_attempts=5,
  201. strategy=RetryStrategy.EXPONENTIAL,
  202. base_delay=1.0,
  203. max_delay=0.05,
  204. multiplier=10.0,
  205. jitter=0.0,
  206. )
  207. retry = NetworkRetry(config=config)
  208. # Verzögerung sollte auf max_delay begrenzt sein
  209. ctx = RetryContext(attempt=3, max_attempts=5)
  210. delay = retry._calculate_delay(3, ctx)
  211. assert delay <= config.max_delay
  212. def test_reset_stats(self, retry):
  213. """Testet Stats-Reset."""
  214. retry.stats.total_attempts = 10
  215. retry.stats.successful_attempts = 5
  216. retry.reset_stats()
  217. assert retry.stats.total_attempts == 0
  218. assert retry.stats.successful_attempts == 0
  219. class TestWithRetryDecorator:
  220. """Tests für @with_retry Decorator."""
  221. @pytest.mark.asyncio
  222. async def test_decorator_success(self):
  223. """Testet erfolgreichen Decorator."""
  224. call_count = 0
  225. @with_retry(max_attempts=3, base_delay=0.01)
  226. async def operation():
  227. nonlocal call_count
  228. call_count += 1
  229. return "success"
  230. result = await operation()
  231. assert result == "success"
  232. assert call_count == 1
  233. @pytest.mark.asyncio
  234. async def test_decorator_retry(self):
  235. """Testet Decorator mit Retry."""
  236. call_count = 0
  237. @with_retry(max_attempts=3, base_delay=0.01)
  238. async def operation():
  239. nonlocal call_count
  240. call_count += 1
  241. if call_count < 3:
  242. raise ValueError("Fehler")
  243. return "success"
  244. result = await operation()
  245. assert result == "success"
  246. assert call_count == 3
  247. @pytest.mark.asyncio
  248. async def test_decorator_failure(self):
  249. """Testet Decorator mit Fehlschlag."""
  250. @with_retry(max_attempts=2, base_delay=0.01)
  251. async def operation():
  252. raise ValueError("Dauerhafter Fehler")
  253. with pytest.raises(ValueError):
  254. await operation()
  255. class TestRetryPolicy:
  256. """Tests für RetryPolicy."""
  257. @pytest.fixture
  258. def policy(self):
  259. """Erstellt RetryPolicy."""
  260. config = RetryConfig(
  261. max_attempts=3,
  262. base_delay=0.01,
  263. )
  264. return RetryPolicy("test_policy", config)
  265. @pytest.mark.asyncio
  266. async def test_policy_execute(self, policy):
  267. """Testet Policy-Ausführung."""
  268. async def operation():
  269. return "success"
  270. result = await policy.execute(operation, "test_op")
  271. assert result == "success"
  272. @pytest.mark.asyncio
  273. async def test_policy_stats(self, policy):
  274. """Testet Policy-Statistiken."""
  275. async def operation():
  276. return "success"
  277. await policy.execute(operation, "op1")
  278. await policy.execute(operation, "op2")
  279. stats = policy.get_stats()
  280. assert "op1" in stats
  281. assert "op2" in stats
  282. @pytest.mark.asyncio
  283. async def test_policy_single_stats(self, policy):
  284. """Testet einzelne Policy-Statistik."""
  285. async def operation():
  286. return "success"
  287. await policy.execute(operation, "test_op")
  288. stats = policy.get_stats("test_op")
  289. assert stats.successful_attempts == 1
  290. class TestPredefinedPolicies:
  291. """Tests für vordefinierte Policies."""
  292. def test_aggressive_retry(self):
  293. """Testet aggressive Retry-Config."""
  294. assert AGGRESSIVE_RETRY.max_attempts == 10
  295. assert AGGRESSIVE_RETRY.strategy == RetryStrategy.EXPONENTIAL
  296. def test_conservative_retry(self):
  297. """Testet konservative Retry-Config."""
  298. assert CONSERVATIVE_RETRY.max_attempts == 3
  299. assert CONSERVATIVE_RETRY.strategy == RetryStrategy.LINEAR
  300. def test_fast_fail_retry(self):
  301. """Testet Fast-Fail Retry-Config."""
  302. assert FAST_FAIL_RETRY.max_attempts == 2
  303. assert FAST_FAIL_RETRY.strategy == RetryStrategy.CONSTANT