|
|
@@ -0,0 +1,217 @@
|
|
|
+"""
|
|
|
+Unit tests for pipeline state management.
|
|
|
+
|
|
|
+Tests cover state definitions, transition rules, and validation functions.
|
|
|
+"""
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from src.core.states import PipelineState
|
|
|
+from src.core.transitions import (
|
|
|
+ is_transition_allowed,
|
|
|
+ get_allowed_transitions,
|
|
|
+ can_pause_from,
|
|
|
+ can_resume_to,
|
|
|
+ ALLOWED_TRANSITIONS,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+class TestPipelineState:
|
|
|
+ """Test PipelineState enum functionality."""
|
|
|
+
|
|
|
+ def test_all_states_defined(self):
|
|
|
+ """Test that all required states are defined."""
|
|
|
+ required_states = {
|
|
|
+ "IDLE",
|
|
|
+ "FINGERPRINTING",
|
|
|
+ "CLEANING",
|
|
|
+ "TERM_EXTRACTION",
|
|
|
+ "TRANSLATING",
|
|
|
+ "UPLOADING",
|
|
|
+ "PAUSED",
|
|
|
+ "COMPLETED",
|
|
|
+ "FAILED",
|
|
|
+ }
|
|
|
+ actual_states = {state.name for state in PipelineState}
|
|
|
+ assert actual_states == required_states
|
|
|
+
|
|
|
+ def test_state_values(self):
|
|
|
+ """Test that state values are correct."""
|
|
|
+ assert PipelineState.IDLE.value == "idle"
|
|
|
+ assert PipelineState.FINGERPRINTING.value == "fingerprinting"
|
|
|
+ assert PipelineState.CLEANING.value == "cleaning"
|
|
|
+ assert PipelineState.TERM_EXTRACTION.value == "term_extraction"
|
|
|
+ assert PipelineState.TRANSLATING.value == "translating"
|
|
|
+ assert PipelineState.UPLOADING.value == "uploading"
|
|
|
+ assert PipelineState.PAUSED.value == "paused"
|
|
|
+ assert PipelineState.COMPLETED.value == "completed"
|
|
|
+ assert PipelineState.FAILED.value == "failed"
|
|
|
+
|
|
|
+ def test_is_terminal(self):
|
|
|
+ """Test terminal state detection."""
|
|
|
+ assert PipelineState.COMPLETED.is_terminal() is True
|
|
|
+ assert PipelineState.FAILED.is_terminal() is True
|
|
|
+ assert PipelineState.IDLE.is_terminal() is False
|
|
|
+ assert PipelineState.TRANSLATING.is_terminal() is False
|
|
|
+
|
|
|
+ def test_is_active(self):
|
|
|
+ """Test active state detection."""
|
|
|
+ active_states = {
|
|
|
+ PipelineState.FINGERPRINTING,
|
|
|
+ PipelineState.CLEANING,
|
|
|
+ PipelineState.TERM_EXTRACTION,
|
|
|
+ PipelineState.TRANSLATING,
|
|
|
+ PipelineState.UPLOADING,
|
|
|
+ }
|
|
|
+ for state in active_states:
|
|
|
+ assert state.is_active() is True
|
|
|
+
|
|
|
+ inactive_states = {
|
|
|
+ PipelineState.IDLE,
|
|
|
+ PipelineState.PAUSED,
|
|
|
+ PipelineState.COMPLETED,
|
|
|
+ PipelineState.FAILED,
|
|
|
+ }
|
|
|
+ for state in inactive_states:
|
|
|
+ assert state.is_active() is False
|
|
|
+
|
|
|
+ def test_can_pause(self):
|
|
|
+ """Test states from which pausing is allowed."""
|
|
|
+ assert PipelineState.TRANSLATING.can_pause() is True
|
|
|
+ assert PipelineState.CLEANING.can_pause() is True
|
|
|
+ assert PipelineState.IDLE.can_pause() is False
|
|
|
+ assert PipelineState.COMPLETED.can_pause() is False
|
|
|
+
|
|
|
+ def test_can_resume(self):
|
|
|
+ """Test states from which resuming is allowed."""
|
|
|
+ assert PipelineState.PAUSED.can_resume() is True
|
|
|
+ assert PipelineState.IDLE.can_resume() is False
|
|
|
+ assert PipelineState.TRANSLATING.can_resume() is False
|
|
|
+
|
|
|
+
|
|
|
+class TestStateTransitions:
|
|
|
+ """Test state transition rules."""
|
|
|
+
|
|
|
+ def test_idle_to_fingerprinting(self):
|
|
|
+ """Test IDLE -> FINGERPRINTING transition."""
|
|
|
+ assert is_transition_allowed(PipelineState.IDLE, PipelineState.FINGERPRINTING) is True
|
|
|
+
|
|
|
+ def test_idle_to_other_states(self):
|
|
|
+ """Test IDLE cannot transition to most states."""
|
|
|
+ assert is_transition_allowed(PipelineState.IDLE, PipelineState.COMPLETED) is False
|
|
|
+ assert is_transition_allowed(PipelineState.IDLE, PipelineState.TRANSLATING) is False
|
|
|
+ assert is_transition_allowed(PipelineState.IDLE, PipelineState.FAILED) is False
|
|
|
+
|
|
|
+ def test_normal_flow(self):
|
|
|
+ """Test normal forward flow through the pipeline."""
|
|
|
+ flow = [
|
|
|
+ (PipelineState.IDLE, PipelineState.FINGERPRINTING),
|
|
|
+ (PipelineState.FINGERPRINTING, PipelineState.CLEANING),
|
|
|
+ (PipelineState.CLEANING, PipelineState.TERM_EXTRACTION),
|
|
|
+ (PipelineState.TERM_EXTRACTION, PipelineState.TRANSLATING),
|
|
|
+ (PipelineState.TRANSLATING, PipelineState.UPLOADING),
|
|
|
+ (PipelineState.UPLOADING, PipelineState.COMPLETED),
|
|
|
+ ]
|
|
|
+ for from_state, to_state in flow:
|
|
|
+ assert is_transition_allowed(from_state, to_state) is True, \
|
|
|
+ f"Failed: {from_state} -> {to_state}"
|
|
|
+
|
|
|
+ def test_failure_transitions(self):
|
|
|
+ """Test transitions to FAILED state."""
|
|
|
+ failure_sources = {
|
|
|
+ PipelineState.FINGERPRINTING,
|
|
|
+ PipelineState.CLEANING,
|
|
|
+ PipelineState.TERM_EXTRACTION,
|
|
|
+ PipelineState.TRANSLATING,
|
|
|
+ PipelineState.UPLOADING,
|
|
|
+ }
|
|
|
+ for state in failure_sources:
|
|
|
+ assert is_transition_allowed(state, PipelineState.FAILED) is True
|
|
|
+
|
|
|
+ def test_pause_to_resume(self):
|
|
|
+ """Test PAUSED can resume to active states."""
|
|
|
+ assert is_transition_allowed(PipelineState.PAUSED, PipelineState.FINGERPRINTING) is True
|
|
|
+ assert is_transition_allowed(PipelineState.PAUSED, PipelineState.TRANSLATING) is True
|
|
|
+ assert is_transition_allowed(PipelineState.PAUSED, PipelineState.UPLOADING) is True
|
|
|
+
|
|
|
+ def test_failed_to_idle(self):
|
|
|
+ """Test FAILED can reset to IDLE."""
|
|
|
+ assert is_transition_allowed(PipelineState.FAILED, PipelineState.IDLE) is True
|
|
|
+
|
|
|
+ def test_completed_to_idle(self):
|
|
|
+ """Test COMPLETED can reset to IDLE."""
|
|
|
+ assert is_transition_allowed(PipelineState.COMPLETED, PipelineState.IDLE) is True
|
|
|
+
|
|
|
+ def test_invalid_transitions(self):
|
|
|
+ """Test some invalid transitions."""
|
|
|
+ # Cannot skip states
|
|
|
+ assert is_transition_allowed(PipelineState.IDLE, PipelineState.TRANSLATING) is False
|
|
|
+ # Cannot go backwards (except via FAILED/COMPLETED -> IDLE)
|
|
|
+ assert is_transition_allowed(PipelineState.TRANSLATING, PipelineState.CLEANING) is False
|
|
|
+ # Cannot go from COMPLETED to active states directly
|
|
|
+ assert is_transition_allowed(PipelineState.COMPLETED, PipelineState.TRANSLATING) is False
|
|
|
+
|
|
|
+
|
|
|
+class TestTransitionHelpers:
|
|
|
+ """Test helper functions for state transitions."""
|
|
|
+
|
|
|
+ def test_get_allowed_transitions(self):
|
|
|
+ """Test getting allowed transitions from a state."""
|
|
|
+ allowed = get_allowed_transitions(PipelineState.IDLE)
|
|
|
+ assert allowed == {PipelineState.FINGERPRINTING}
|
|
|
+
|
|
|
+ allowed = get_allowed_transitions(PipelineState.TRANSLATING)
|
|
|
+ assert PipelineState.UPLOADING in allowed
|
|
|
+ assert PipelineState.FAILED in allowed
|
|
|
+ assert PipelineState.CLEANING not in allowed
|
|
|
+
|
|
|
+ def test_can_pause_from(self):
|
|
|
+ """Test can_pause_from helper."""
|
|
|
+ assert can_pause_from(PipelineState.TRANSLATING) is True
|
|
|
+ assert can_pause_from(PipelineState.UPLOADING) is True
|
|
|
+ assert can_pause_from(PipelineState.IDLE) is False
|
|
|
+ assert can_pause_from(PipelineState.COMPLETED) is False
|
|
|
+
|
|
|
+ def test_can_resume_to(self):
|
|
|
+ """Test can_resume_to helper."""
|
|
|
+ assert can_resume_to(PipelineState.TRANSLATING) is True
|
|
|
+ assert can_resume_to(PipelineState.CLEANING) is True
|
|
|
+ assert can_resume_to(PipelineState.COMPLETED) is False
|
|
|
+ assert can_resume_to(PipelineState.FAILED) is False
|
|
|
+
|
|
|
+ def test_allowed_transitions_completeness(self):
|
|
|
+ """Test that all states have transition rules defined."""
|
|
|
+ all_states = set(PipelineState)
|
|
|
+ defined_states = set(ALLOWED_TRANSITIONS.keys())
|
|
|
+ assert all_states == defined_states, \
|
|
|
+ f"Missing transitions for: {all_states - defined_states}"
|
|
|
+
|
|
|
+
|
|
|
+class TestTransitionEdgeCases:
|
|
|
+ """Test edge cases in state transitions."""
|
|
|
+
|
|
|
+ def test_same_state_transition(self):
|
|
|
+ """Test that staying in same state is not a transition."""
|
|
|
+ # Transitions to same state should not be explicitly allowed
|
|
|
+ # (they're handled as "no transition needed")
|
|
|
+ result = is_transition_allowed(PipelineState.IDLE, PipelineState.IDLE)
|
|
|
+ # This should be False since we didn't define it
|
|
|
+ assert result is False
|
|
|
+
|
|
|
+ def test_all_states_can_reach_terminal(self):
|
|
|
+ """Test that all active states can reach a terminal state."""
|
|
|
+ active_states = {
|
|
|
+ PipelineState.FINGERPRINTING,
|
|
|
+ PipelineState.CLEANING,
|
|
|
+ PipelineState.TERM_EXTRACTION,
|
|
|
+ PipelineState.TRANSLATING,
|
|
|
+ PipelineState.UPLOADING,
|
|
|
+ }
|
|
|
+ for state in active_states:
|
|
|
+ # Can reach FAILED
|
|
|
+ assert is_transition_allowed(state, PipelineState.FAILED), \
|
|
|
+ f"{state} cannot reach FAILED"
|
|
|
+ # Can reach COMPLETED via normal flow
|
|
|
+ assert state == PipelineState.UPLOADING or \
|
|
|
+ any(is_transition_allowed(state, s) for s in PipelineState), \
|
|
|
+ f"{state} cannot progress"
|