kopia lustrzana https://github.com/RobertGawron/IonizationChamber
189 wiersze
7.5 KiB
Python
Executable File
189 wiersze
7.5 KiB
Python
Executable File
"""Unit tests for the refactored :pyclass:`measurement_acquisition.state_machine.StateMachine`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from measurement_acquisition.state_machine import StateMachine
|
|
|
|
|
|
class TestStateMachine:
|
|
"""Test suite covering state transitions, value persistence, and timing."""
|
|
|
|
# ------------------------------------------------------------------
|
|
# Fixtures
|
|
# ------------------------------------------------------------------
|
|
@pytest.fixture()
|
|
def mock_dependencies(self):
|
|
"""Return a dictionary with a state machine wired to mocks."""
|
|
mock_app_layer = MagicMock()
|
|
mock_storage = MagicMock()
|
|
|
|
# StateMachine now expects (application_layer, measurement_storage)
|
|
sm = StateMachine(mock_app_layer, mock_storage)
|
|
|
|
return {
|
|
"sm": sm,
|
|
"mock_app_layer": mock_app_layer,
|
|
"mock_storage": mock_storage,
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Constructor behaviour
|
|
# ------------------------------------------------------------------
|
|
def test_initialization(self, mock_dependencies):
|
|
"""Verify attributes are set and initial state is correct."""
|
|
sm = mock_dependencies["sm"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
storage = mock_dependencies["mock_storage"]
|
|
|
|
assert sm.application_layer == app_layer
|
|
assert sm.measurement_storage == storage
|
|
assert sm.device_measurement is None
|
|
assert sm.next_state == getattr(sm, "_init_ionization_chamber")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Full state-transition sequence
|
|
# ------------------------------------------------------------------
|
|
def test_state_transitions(self, mock_dependencies):
|
|
"""Ensure the machine walks through all states in the right order."""
|
|
sm = mock_dependencies["sm"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
storage = mock_dependencies["mock_storage"]
|
|
|
|
test_time = datetime(2023, 1, 1, 12, 0, 0)
|
|
sm.now_func = lambda: test_time # type: ignore[assignment]
|
|
time_iso = test_time.isoformat(sep=" ", timespec="milliseconds")
|
|
|
|
# 1) _init_ionization_chamber → _init_measurement_storage
|
|
sm.tick()
|
|
app_layer.connect.assert_called_once()
|
|
assert sm.next_state == getattr(sm, "_init_measurement_storage")
|
|
|
|
# 2) _init_measurement_storage → _get_measurement_from_ionization_chamber
|
|
sm.tick()
|
|
storage.connect.assert_called_once()
|
|
assert sm.next_state == getattr(sm, "_get_measurement_from_ionization_chamber")
|
|
|
|
# 3) _get_measurement_from_ionization_chamber → _save_measurement
|
|
app_layer.get_measurement.return_value = 42.5
|
|
sm.tick()
|
|
app_layer.get_measurement.assert_called_once()
|
|
assert sm.device_measurement == 42.5
|
|
assert sm.next_state == getattr(sm, "_save_measurement")
|
|
|
|
# 4) _save_measurement → _show_measurement_to_user
|
|
sm.tick()
|
|
storage.save_measurement.assert_called_once_with(time_iso, 42.5)
|
|
assert sm.next_state == getattr(sm, "_show_measurement_to_user")
|
|
|
|
# 5) _show_measurement_to_user prints and loops back
|
|
with patch("builtins.print") as mock_print:
|
|
sm.tick()
|
|
mock_print.assert_called_once_with(42.5)
|
|
assert sm.next_state == getattr(sm, "_get_measurement_from_ionization_chamber")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Multiple-cycle execution
|
|
# ------------------------------------------------------------------
|
|
def test_full_cycle_execution(self, mock_dependencies):
|
|
"""Run three consecutive measurement cycles and verify state updates."""
|
|
sm = mock_dependencies["sm"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
|
|
# Bring the machine to the measurement loop
|
|
sm.tick() # _init_ionization_chamber
|
|
sm.tick() # _init_measurement_storage
|
|
|
|
# First measurement
|
|
app_layer.get_measurement.return_value = 10.0
|
|
sm.tick() # _get_measurement
|
|
sm.tick() # _save_measurement
|
|
sm.tick() # _show_measurement
|
|
|
|
# Second measurement
|
|
app_layer.get_measurement.return_value = 20.0
|
|
sm.tick() # _get_measurement
|
|
assert sm.device_measurement == 20.0
|
|
sm.tick() # _save_measurement
|
|
sm.tick() # _show_measurement
|
|
|
|
# Third measurement
|
|
app_layer.get_measurement.return_value = 30.0
|
|
sm.tick() # _get_measurement
|
|
assert sm.device_measurement == 30.0
|
|
|
|
# ------------------------------------------------------------------
|
|
# Measurement value persistence across states
|
|
# ------------------------------------------------------------------
|
|
def test_measurement_value_persistence(self, mock_dependencies):
|
|
"""Check that self.device_measurement survives all interim states."""
|
|
sm = mock_dependencies["sm"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
|
|
sm.tick() # _init_ionization_chamber
|
|
sm.tick() # _init_measurement_storage
|
|
|
|
app_layer.get_measurement.return_value = 99.99
|
|
sm.tick() # _get_measurement
|
|
initial_value = sm.device_measurement
|
|
|
|
sm.tick() # _save_measurement
|
|
assert sm.device_measurement == initial_value
|
|
|
|
sm.tick() # _show_measurement
|
|
assert sm.device_measurement == initial_value
|
|
|
|
# ------------------------------------------------------------------
|
|
# Output formatting
|
|
# ------------------------------------------------------------------
|
|
def test_print_output_format(self, mock_dependencies):
|
|
"""Verify the value sent to print() is the measurement."""
|
|
sm = mock_dependencies["sm"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
|
|
sm.tick() # _init_ionization_chamber
|
|
sm.tick() # _init_measurement_storage
|
|
app_layer.get_measurement.return_value = 123.456
|
|
sm.tick() # _get_measurement
|
|
sm.tick() # _save_measurement
|
|
|
|
with patch("builtins.print") as mock_print:
|
|
sm.tick() # _show_measurement
|
|
mock_print.assert_called_once_with(123.456)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Timestamp accuracy across multiple cycles
|
|
# ------------------------------------------------------------------
|
|
@patch("measurement_acquisition.state_machine.datetime")
|
|
def test_timestamp_accuracy(self, mock_datetime, mock_dependencies):
|
|
"""Ensure each call to save_measurement() uses the correct timestamp."""
|
|
sm = mock_dependencies["sm"]
|
|
storage = mock_dependencies["mock_storage"]
|
|
app_layer = mock_dependencies["mock_app_layer"]
|
|
|
|
test_times = [
|
|
datetime(2023, 1, 1, 12, 0, 0),
|
|
datetime(2023, 1, 1, 12, 0, 5),
|
|
datetime(2023, 1, 1, 12, 0, 10),
|
|
]
|
|
mock_datetime.now.side_effect = test_times
|
|
sm.now_func = mock_datetime.now
|
|
|
|
app_layer.get_measurement.return_value = 0.0
|
|
|
|
sm.tick() # _init_ionization_chamber
|
|
sm.tick() # _init_measurement_storage
|
|
|
|
for expected_time in test_times:
|
|
sm.tick() # _get_measurement
|
|
sm.tick() # _save_measurement
|
|
sm.tick() # _show_measurement
|
|
storage.save_measurement.assert_any_call(
|
|
expected_time.isoformat(sep=" ", timespec="milliseconds"),
|
|
pytest.approx(0.0),
|
|
)
|