128 lines
3.8 KiB
Python
128 lines
3.8 KiB
Python
"""
|
|
Tests for Worker — processing, retry with backoff, error handling.
|
|
|
|
Demonstrates: TDD (Interview Topic 8) — mocking processors, testing retry logic.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from core.chunker.models import Chunk, ChunkResult
|
|
from core.chunker.processor import Processor
|
|
from core.chunker.queue import ChunkQueue
|
|
from core.chunker.worker import Worker
|
|
|
|
|
|
class FailNTimesProcessor(Processor):
|
|
"""Test processor that fails N times then succeeds."""
|
|
|
|
def __init__(self, fail_count: int):
|
|
self.fail_count = fail_count
|
|
self.call_count = 0
|
|
|
|
def process(self, chunk: Chunk) -> ChunkResult:
|
|
self.call_count += 1
|
|
if self.call_count <= self.fail_count:
|
|
raise RuntimeError(f"Simulated failure #{self.call_count}")
|
|
return ChunkResult(
|
|
sequence=chunk.sequence,
|
|
success=True,
|
|
processing_time=0.001,
|
|
)
|
|
|
|
|
|
class AlwaysFailProcessor(Processor):
|
|
"""Test processor that always fails."""
|
|
|
|
def process(self, chunk: Chunk) -> ChunkResult:
|
|
raise RuntimeError("Always fails")
|
|
|
|
|
|
class TestWorker:
|
|
def test_processes_chunks(self, make_chunk):
|
|
"""Worker processes all chunks from queue."""
|
|
q = ChunkQueue(maxsize=5)
|
|
for i in range(3):
|
|
q.put(make_chunk(i))
|
|
q.close()
|
|
|
|
from core.chunker.processor import ChecksumProcessor
|
|
worker = Worker("w-0", q, ChecksumProcessor(), max_retries=0)
|
|
results = worker.run()
|
|
|
|
assert len(results) == 3
|
|
assert all(r.success for r in results)
|
|
|
|
def test_retry_on_failure(self, make_chunk):
|
|
"""Worker retries on processor failure."""
|
|
q = ChunkQueue(maxsize=5)
|
|
q.put(make_chunk(0))
|
|
q.close()
|
|
|
|
proc = FailNTimesProcessor(fail_count=2)
|
|
worker = Worker("w-0", q, proc, max_retries=3)
|
|
results = worker.run()
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].retries == 2
|
|
assert proc.call_count == 3 # 2 failures + 1 success
|
|
|
|
def test_max_retries_exceeded(self, make_chunk):
|
|
"""Worker gives up after max retries."""
|
|
q = ChunkQueue(maxsize=5)
|
|
q.put(make_chunk(0))
|
|
q.close()
|
|
|
|
worker = Worker("w-0", q, AlwaysFailProcessor(), max_retries=2)
|
|
results = worker.run()
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is False
|
|
assert results[0].error is not None
|
|
assert worker.error_count == 1
|
|
|
|
def test_worker_id_on_results(self, make_chunk):
|
|
"""Worker stamps its ID on results."""
|
|
q = ChunkQueue(maxsize=5)
|
|
q.put(make_chunk(0))
|
|
q.close()
|
|
|
|
from core.chunker.processor import ChecksumProcessor
|
|
worker = Worker("worker-7", q, ChecksumProcessor())
|
|
results = worker.run()
|
|
|
|
assert results[0].worker_id == "worker-7"
|
|
|
|
def test_event_callback(self, make_chunk):
|
|
"""Worker emits events via callback."""
|
|
q = ChunkQueue(maxsize=5)
|
|
q.put(make_chunk(0))
|
|
q.close()
|
|
|
|
events = []
|
|
callback = MagicMock(side_effect=lambda t, d: events.append((t, d)))
|
|
|
|
from core.chunker.processor import ChecksumProcessor
|
|
worker = Worker("w-0", q, ChecksumProcessor(), event_callback=callback)
|
|
worker.run()
|
|
|
|
event_types = [e[0] for e in events]
|
|
assert "worker_status" in event_types
|
|
assert "chunk_processing" in event_types
|
|
assert "chunk_done" in event_types
|
|
|
|
def test_processed_count(self, make_chunk):
|
|
"""Worker tracks processed count."""
|
|
q = ChunkQueue(maxsize=10)
|
|
for i in range(5):
|
|
q.put(make_chunk(i))
|
|
q.close()
|
|
|
|
from core.chunker.processor import ChecksumProcessor
|
|
worker = Worker("w-0", q, ChecksumProcessor())
|
|
worker.run()
|
|
|
|
assert worker.processed_count == 5
|