"""Проверки автоматов сеанса: protocol/session_state.py. Свойства безопасности, перенесённые из функциональных проверок Lab041. До этого они проверялись только при полном запуске лабораторной на 180 сочетаний, а пять из них в отчёте вообще не измерялись. """ from __future__ import annotations import pytest from protocol.control_messages import ( ControlStateMessage, MessageContext, ResetRequest, SequenceComparison, compare_sequence, ) from protocol.session_state import ( GroundSessionController, GroundSessionState, RejectionCause, RoverSessionController, RoverSessionState, deterministic_identifier, ) CONTROL_STREAM_ID = 20 def active_pair(seed: int) -> tuple[GroundSessionController, RoverSessionController]: """Довести обе стороны до состояния, в котором разрешено движение.""" ground = GroundSessionController(100 + seed) rover = RoverSessionController(200 + seed) status = rover.receive_hello(ground.start_synchronization()) assert ground.receive_status(status) acknowledgement = rover.authorize_movement(ground.request_movement_authorization(1)) assert ground.receive_movement_authorize_ack(acknowledgement) return ground, rover def control_from(ground, rover, sequence_number: int, speed: float = 1.0) -> ControlStateMessage: context = MessageContext(ground.ground_session_id, rover.rover_boot_id, rover.control_epoch, sequence_number) return ControlStateMessage(context, speed, 0.0, False, True) # ------------------------------------------------------------------ безопасный запуск def test_both_sides_boot_without_movement() -> None: ground = GroundSessionController(7) rover = RoverSessionController(8) assert ground.state is GroundSessionState.GROUND_BOOT assert not ground.movement_permitted assert rover.state is RoverSessionState.ROVER_BOOT_SAFE assert rover.safe def test_session_status_alone_does_not_permit_movement() -> None: ground = GroundSessionController(9) rover = RoverSessionController(10) assert ground.receive_status(rover.receive_hello(ground.start_synchronization())) assert ground.state is GroundSessionState.GROUND_SAFE_SYNCHRONIZED assert not ground.movement_permitted assert rover.safe def test_authorization_ack_does_not_restore_motion() -> None: ground, rover = active_pair(11) assert ground.current_speed_mps == 0.0 assert not ground.movement_permitted assert rover.safe def test_movement_requires_a_new_operator_command() -> None: ground, rover = active_pair(12) assert rover.safe assert rover.receive_control(ground.new_operator_control(1.0), CONTROL_STREAM_ID).accepted assert rover.speed_mps == 1.0 def test_restarted_rover_is_immediately_safe() -> None: ground, rover = active_pair(13) rover.receive_control(ground.new_operator_control(1.0), CONTROL_STREAM_ID) restarted = RoverSessionController(999) assert restarted.state is RoverSessionState.ROVER_BOOT_SAFE assert restarted.safe def test_new_ground_session_stops_the_old_command_stream() -> None: old_ground, rover = active_pair(14) old_command = old_ground.new_operator_control(1.0) new_ground = GroundSessionController(1000) rover.receive_hello(new_ground.start_synchronization()) decision = rover.receive_control(old_command, CONTROL_STREAM_ID) assert decision.reason is RejectionCause.GROUND_SESSION_ID def test_duplicate_hello_is_idempotent() -> None: ground = GroundSessionController(30) rover = RoverSessionController(31) hello = ground.start_synchronization() first = rover.receive_hello(hello) second = rover.receive_hello(hello) assert first == second assert rover.duplicate_hellos == 1 # ------------------------------------------------------------------ старые пакеты @pytest.mark.parametrize( "field,expected", [ ("ground_session_id", RejectionCause.GROUND_SESSION_ID), ("rover_boot_id", RejectionCause.ROVER_BOOT_ID), ("control_epoch", RejectionCause.CONTROL_EPOCH), ], ) def test_stale_identifier_is_rejected(field: str, expected: RejectionCause) -> None: """Пакет предыдущего сеанса, запуска или периода управления не принимается.""" ground, rover = active_pair(hash(field) % 500) values = { "ground_session_id": ground.ground_session_id, "rover_boot_id": rover.rover_boot_id, "control_epoch": rover.control_epoch, "sequence_number": 1, } values[field] -= 1 message = ControlStateMessage(MessageContext(**values), 1.0, 0.0, False, True) assert rover.receive_control(message, CONTROL_STREAM_ID).reason is expected def test_sequence_zero_is_accepted_in_a_new_session() -> None: ground, rover = active_pair(1) assert rover.receive_control(control_from(ground, rover, 0), CONTROL_STREAM_ID).accepted def test_sequence_wrap_is_treated_as_newer() -> None: ground, rover = active_pair(5) key = (ground.ground_session_id, rover.rover_boot_id, rover.control_epoch, CONTROL_STREAM_ID) rover.last_sequences[key] = 0xFFFFFFFF assert rover.receive_control(control_from(ground, rover, 0), CONTROL_STREAM_ID).accepted assert compare_sequence(0, 0xFFFFFFFF) is SequenceComparison.NEWER def test_ambiguous_half_range_is_rejected() -> None: ground, rover = active_pair(6) key = (ground.ground_session_id, rover.rover_boot_id, rover.control_epoch, CONTROL_STREAM_ID) rover.last_sequences[key] = 0 decision = rover.receive_control(control_from(ground, rover, 0x80000000), CONTROL_STREAM_ID) assert not decision.accepted assert decision.ambiguous_sequence def test_replayed_command_is_rejected() -> None: ground, rover = active_pair(21) assert rover.receive_control(control_from(ground, rover, 10), CONTROL_STREAM_ID).accepted replay = rover.receive_control(control_from(ground, rover, 10), CONTROL_STREAM_ID) assert not replay.accepted assert replay.reason is RejectionCause.SEQUENCE_NUMBER # ------------------------------------------------------------------ аварийное намерение def test_persisted_emergency_intent_survives_a_ground_restart() -> None: ground = GroundSessionController(15, persisted_emergency_intent=True, persisted_emergency_event_id=77) assert ground.emergency_intent assert not ground.movement_permitted hello = ground.start_synchronization() assert hello.emergency_intent assert hello.emergency_event_id == 77 def test_emergency_acknowledgement_does_not_clear_the_intent() -> None: ground, rover = active_pair(16) acknowledgement = rover.receive_emergency(ground.request_emergency(88)) assert acknowledgement is not None assert ground.receive_emergency_ack(acknowledgement) assert ground.emergency_intent assert not ground.movement_permitted def test_ordinary_command_does_not_release_the_latch() -> None: ground, rover = active_pair(20) rover.receive_emergency(ground.request_emergency(96)) decision = rover.receive_control(control_from(ground, rover, 100), CONTROL_STREAM_ID) assert not decision.accepted assert rover.state is RoverSessionState.EMERGENCY_LATCHED def test_movement_authorization_is_refused_while_intent_stands() -> None: ground = GroundSessionController(40, persisted_emergency_intent=True, persisted_emergency_event_id=5) rover = RoverSessionController(41) ground.receive_status(rover.receive_hello(ground.start_synchronization())) with pytest.raises(RuntimeError): ground.request_movement_authorization(1) # ------------------------------------------------------------------ сброс def test_stale_reset_request_is_refused() -> None: ground, rover = active_pair(17) assert rover.receive_emergency(ground.request_emergency(90)) is not None request = ground.request_reset(91) stale_context = MessageContext( request.context.ground_session_id - 1, request.context.rover_boot_id, request.context.control_epoch, request.context.sequence_number, ) stale = ResetRequest(stale_context, request.emergency_event_id, request.reset_request_id, 0.0, False) assert not rover.receive_reset(stale).acknowledgement.accepted def test_repeated_reset_is_idempotent() -> None: ground, rover = active_pair(18) rover.receive_emergency(ground.request_emergency(92)) request = ground.request_reset(93) first = rover.receive_reset(request) assert first.performed repeated = rover.receive_reset(request) assert repeated.duplicate assert not repeated.performed assert repeated.acknowledgement.accepted def test_reset_requires_an_emergency_intent() -> None: ground, _ = active_pair(50) with pytest.raises(RuntimeError): ground.request_reset(1) def test_reset_does_not_restore_the_previous_command() -> None: ground, rover = active_pair(51) rover.receive_control(ground.new_operator_control(2.0), CONTROL_STREAM_ID) rover.receive_emergency(ground.request_emergency(60)) assert rover.speed_mps == 0.0 decision = rover.receive_reset(ground.request_reset(61)) assert decision.performed assert ground.receive_reset_ack(decision.acknowledgement) assert not ground.movement_permitted assert ground.current_speed_mps == 0.0 assert rover.safe # ------------------------------------------------------------------ идентификаторы def test_identifiers_are_reproducible_and_nonzero() -> None: assert deterministic_identifier(42, "ground") == deterministic_identifier(42, "ground") assert deterministic_identifier(42, "ground") != deterministic_identifier(42, "rover") assert deterministic_identifier(42, "ground") != deterministic_identifier(43, "ground") for seed in range(50): assert 0 < deterministic_identifier(seed, "ground") <= 0xFFFFFFFFFFFFFFFF def test_identifier_rejects_a_non_integer_seed() -> None: with pytest.raises(TypeError): deterministic_identifier("не число", "ground") # ------------------------------------------------------------------ границы скорости def test_negative_operator_speed_is_refused() -> None: """Свойство, которое в отчёте Lab041 стояло литеральным нулём.""" ground, _ = active_pair(70) for speed in (-0.1, -5.0, 0.0): with pytest.raises(ValueError): ground.new_operator_control(speed) def test_speed_above_the_limit_is_refused_by_encoding() -> None: from protocol.control_messages import ControlMessageError, encode_message ground, rover = active_pair(71) with pytest.raises(ControlMessageError): encode_message(control_from(ground, rover, 1, speed=15.1))