| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- """
- Unit tests for StateMachine implementation in src/core.
- Tests cover state transitions, callbacks, context management,
- and error handling.
- """
- import pytest
- from src.core.states import PipelineState
- from src.core.state_machine import (
- StateMachine,
- StateMachineError,
- InvalidTransitionError,
- TransitionEvent,
- )
- class TestStateMachineInitialization:
- """Test StateMachine initialization and basic properties."""
- def test_initial_state_is_idle(self):
- """Test that state machine starts in IDLE state."""
- sm = StateMachine()
- assert sm.state == PipelineState.IDLE
- def test_context_is_empty_on_init(self):
- """Test that context is empty on initialization."""
- sm = StateMachine()
- assert sm.context == {}
- assert len(sm.context) == 0
- def test_history_is_empty_on_init(self):
- """Test that history is empty on initialization."""
- sm = StateMachine()
- assert sm.history == []
- assert len(sm.history) == 0
- def test_get_state_info(self):
- """Test getting comprehensive state information."""
- sm = StateMachine()
- info = sm.get_state_info()
- assert info["current_state"] == "idle"
- assert info["is_terminal"] is False
- assert info["is_active"] is False
- assert info["transition_count"] == 0
- assert "fingerprinting" in info["allowed_transitions"]
- class TestValidTransitions:
- """Test valid state transitions."""
- def test_idle_to_fingerprinting(self):
- """Test IDLE -> FINGERPRINTING transition."""
- sm = StateMachine()
- result = sm.transition_to(PipelineState.FINGERPRINTING)
- assert result is True
- assert sm.state == PipelineState.FINGERPRINTING
- def test_normal_pipeline_flow(self):
- """Test complete normal flow through the pipeline."""
- sm = StateMachine()
- flow = [
- PipelineState.FINGERPRINTING,
- PipelineState.CLEANING,
- PipelineState.TERM_EXTRACTION,
- PipelineState.TRANSLATING,
- PipelineState.UPLOADING,
- PipelineState.COMPLETED,
- ]
- for target_state in flow:
- result = sm.transition_to(target_state)
- assert result is True, f"Failed to transition to {target_state}"
- assert sm.state == target_state
- def test_transition_to_failed(self):
- """Test transition from active state to FAILED."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING)
- result = sm.transition_to(PipelineState.FAILED)
- assert result is True
- assert sm.state == PipelineState.FAILED
- def test_failed_to_idle(self):
- """Test reset from FAILED to IDLE."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING)
- sm.transition_to(PipelineState.FAILED)
- result = sm.transition_to(PipelineState.IDLE)
- assert result is True
- assert sm.state == PipelineState.IDLE
- def test_pause_and_resume(self):
- """Test pausing from active state and resuming."""
- sm = StateMachine()
- sm.transition_to(PipelineState.TRANSLATING)
- # Pause
- assert sm.transition_to(PipelineState.PAUSED) is True
- assert sm.state == PipelineState.PAUSED
- # Resume
- assert sm.transition_to(PipelineState.TRANSLATING) is True
- assert sm.state == PipelineState.TRANSLATING
- class TestInvalidTransitions:
- """Test invalid state transitions."""
- def test_idle_direct_to_translating(self):
- """Test that IDLE cannot transition directly to TRANSLATING."""
- sm = StateMachine()
- result = sm.transition_to(PipelineState.TRANSLATING)
- assert result is False
- assert sm.state == PipelineState.IDLE
- def test_completed_to_active_state(self):
- """Test that COMPLETED cannot transition directly to active states."""
- sm = StateMachine()
- # Complete the pipeline
- for state in [
- PipelineState.FINGERPRINTING,
- PipelineState.CLEANING,
- PipelineState.TERM_EXTRACTION,
- PipelineState.TRANSLATING,
- PipelineState.UPLOADING,
- PipelineState.COMPLETED,
- ]:
- sm.transition_to(state)
- # Try to go directly to TRANSLATING
- result = sm.transition_to(PipelineState.TRANSLATING)
- assert result is False
- assert sm.state == PipelineState.COMPLETED
- def test_backward_transition(self):
- """Test that backward transitions are not allowed."""
- sm = StateMachine()
- sm.transition_to(PipelineState.TRANSLATING)
- # Try to go back to CLEANING
- result = sm.transition_to(PipelineState.CLEANING)
- assert result is False
- assert sm.state == PipelineState.TRANSLATING
- def test_can_transition_to(self):
- """Test can_transition_to method."""
- sm = StateMachine()
- assert sm.can_transition_to(PipelineState.FINGERPRINTING) is True
- assert sm.can_transition_to(PipelineState.TRANSLATING) is False
- sm.transition_to(PipelineState.TRANSLATING)
- assert sm.can_transition_to(PipelineState.UPLOADING) is True
- assert sm.can_transition_to(PipelineState.CLEANING) is False
- class TestTransitionOrRaise:
- """Test transition_or_raise method."""
- def test_valid_transition_with_or_raise(self):
- """Test valid transition with transition_or_raise."""
- sm = StateMachine()
- sm.transition_or_raise(PipelineState.FINGERPRINTING)
- assert sm.state == PipelineState.FINGERPRINTING
- def test_invalid_transition_raises_error(self):
- """Test that invalid transition raises InvalidTransitionError."""
- sm = StateMachine()
- with pytest.raises(InvalidTransitionError) as exc_info:
- sm.transition_or_raise(PipelineState.TRANSLATING)
- assert exc_info.value.from_state == PipelineState.IDLE
- assert exc_info.value.to_state == PipelineState.TRANSLATING
- class TestContextManagement:
- """Test context storage and retrieval."""
- def test_context_update_on_transition(self):
- """Test that context is updated during transition."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING, file_path="/test.txt")
- assert sm.get_context_value("file_path") == "/test.txt"
- def test_set_and_get_context_value(self):
- """Test setting and getting context values."""
- sm = StateMachine()
- sm.set_context_value("key1", "value1")
- sm.set_context_value("key2", 123)
- assert sm.get_context_value("key1") == "value1"
- assert sm.get_context_value("key2") == 123
- assert sm.get_context_value("key3", "default") == "default"
- def test_context_returns_copy(self):
- """Test that context property returns a copy."""
- sm = StateMachine()
- sm.set_context_value("key", "value")
- context1 = sm.context
- context2 = sm.context
- # Modifying returned dict should not affect internal state
- context1["key"] = "modified"
- assert sm.get_context_value("key") == "value"
- assert context2["key"] == "value"
- def test_clear_context(self):
- """Test clearing context."""
- sm = StateMachine()
- sm.set_context_value("key1", "value1")
- sm.set_context_value("key2", "value2")
- sm.clear_context()
- assert sm.context == {}
- assert sm.get_context_value("key1") is None
- assert sm.get_context_value("key2") is None
- class TestCallbacks:
- """Test callback system."""
- def test_on_transition_callback(self):
- """Test on_transition callback."""
- sm = StateMachine()
- events = []
- def callback(event):
- events.append(event)
- sm.register_callback("on_transition", callback)
- sm.transition_to(PipelineState.FINGERPRINTING)
- assert len(events) == 1
- assert events[0].from_state == PipelineState.IDLE
- assert events[0].to_state == PipelineState.FINGERPRINTING
- def test_on_enter_state_callback(self):
- """Test on_enter_<state> callbacks."""
- sm = StateMachine()
- states = []
- sm.register_callback("on_enter_translating", lambda e: states.append("translating"))
- sm.register_callback("on_enter_completed", lambda e: states.append("completed"))
- # Flow through to TRANSLATING
- sm.transition_to(PipelineState.FINGERPRINTING)
- sm.transition_to(PipelineState.CLEANING)
- sm.transition_to(PipelineState.TERM_EXTRACTION)
- sm.transition_to(PipelineState.TRANSLATING)
- assert "translating" in states
- def test_multiple_callbacks_same_event(self):
- """Test multiple callbacks for the same event."""
- sm = StateMachine()
- results = []
- sm.register_callback("on_transition", lambda e: results.append(1))
- sm.register_callback("on_transition", lambda e: results.append(2))
- sm.register_callback("on_transition", lambda e: results.append(3))
- sm.transition_to(PipelineState.FINGERPRINTING)
- assert results == [1, 2, 3]
- def test_unregister_callback(self):
- """Test unregistering a callback."""
- sm = StateMachine()
- results = []
- def callback1(e):
- results.append(1)
- def callback2(e):
- results.append(2)
- sm.register_callback("on_transition", callback1)
- sm.register_callback("on_transition", callback2)
- sm.unregister_callback("on_transition", callback1)
- sm.transition_to(PipelineState.FINGERPRINTING)
- assert results == [2]
- def test_unregister_nonexistent_callback(self):
- """Test unregistering a callback that wasn't registered."""
- sm = StateMachine()
- def callback(e):
- pass
- result = sm.unregister_callback("on_transition", callback)
- assert result is False
- class TestTransitionHistory:
- """Test transition history tracking."""
- def test_history_records_transitions(self):
- """Test that transitions are recorded in history."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING)
- sm.transition_to(PipelineState.CLEANING)
- sm.transition_to(PipelineState.TERM_EXTRACTION)
- assert len(sm.history) == 3
- def test_history_order(self):
- """Test that history maintains chronological order."""
- sm = StateMachine()
- states = [
- PipelineState.FINGERPRINTING,
- PipelineState.CLEANING,
- PipelineState.TERM_EXTRACTION,
- ]
- for state in states:
- sm.transition_to(state)
- for i, event in enumerate(sm.history):
- assert event.to_state == states[i]
- def test_history_context_preserved(self):
- """Test that context during transition is preserved in history."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING, file="test.txt")
- sm.transition_to(PipelineState.CLEANING, chapters=10)
- assert sm.history[0].context == {"file": "test.txt"}
- assert sm.history[1].context == {"chapters": 10}
- class TestReset:
- """Test state machine reset functionality."""
- def test_reset_to_idle(self):
- """Test that reset returns state to IDLE."""
- sm = StateMachine()
- sm.transition_to(PipelineState.TRANSLATING)
- sm.reset()
- assert sm.state == PipelineState.IDLE
- def test_reset_clears_context(self):
- """Test that reset clears context."""
- sm = StateMachine()
- sm.set_context_value("key", "value")
- sm.transition_to(PipelineState.TRANSLATING)
- sm.reset()
- assert sm.context == {}
- def test_reset_clears_history(self):
- """Test that reset clears history."""
- sm = StateMachine()
- sm.transition_to(PipelineState.FINGERPRINTING)
- sm.transition_to(PipelineState.CLEANING)
- sm.reset()
- assert sm.history == []
|