100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
|
|
import pytest
|
|
|
|
from k1link.simulation.contracts import ControlProfile
|
|
from k1link.simulation.provider_contract import (
|
|
ProviderRole,
|
|
SimulationClockDescriptor,
|
|
SimulationProviderContractError,
|
|
SimulationProviderDescriptor,
|
|
SimulationProviderProfile,
|
|
)
|
|
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE
|
|
|
|
|
|
def _unreal_profile() -> SimulationProviderProfile:
|
|
return SimulationProviderProfile(
|
|
profile_id="unreal-native-rover-v1",
|
|
providers=(
|
|
SimulationProviderDescriptor(
|
|
provider_id="unreal-native",
|
|
roles=(
|
|
ProviderRole.WORLD,
|
|
ProviderRole.PHYSICS,
|
|
ProviderRole.STATE,
|
|
ProviderRole.SENSOR,
|
|
),
|
|
capabilities=(
|
|
"clock.simulation",
|
|
"state.vehicle-pose",
|
|
"truth.ground-truth",
|
|
"sensor.virtual",
|
|
),
|
|
),
|
|
SimulationProviderDescriptor(
|
|
provider_id="unreal-direct-control",
|
|
roles=(ProviderRole.CONTROLLER,),
|
|
capabilities=("command.rover-speed-steering/v1",),
|
|
),
|
|
),
|
|
clock=SimulationClockDescriptor(
|
|
provider_id="unreal-native",
|
|
domain="unreal:fixed-step",
|
|
),
|
|
control_profiles=(ControlProfile.ROVER_SPEED_STEERING_V1,),
|
|
)
|
|
|
|
|
|
def test_stock_rover_provider_profile_round_trips_exactly() -> None:
|
|
restored = SimulationProviderProfile.from_dict(STOCK_ROVER_PROVIDER_PROFILE.to_dict())
|
|
|
|
assert restored == STOCK_ROVER_PROVIDER_PROFILE
|
|
assert restored.clock.domain == "gazebo:/clock"
|
|
assert any(ProviderRole.CONTROLLER in provider.roles for provider in restored.providers)
|
|
|
|
|
|
def test_provider_contract_accepts_unreal_without_changing_canonical_frames() -> None:
|
|
profile = SimulationProviderProfile.from_dict(_unreal_profile().to_dict())
|
|
|
|
assert profile.clock.domain == "unreal:fixed-step"
|
|
assert profile.world_frame == "map_enu"
|
|
assert profile.body_frame == "base_link_flu"
|
|
|
|
|
|
def test_provider_contract_rejects_missing_clock_capability() -> None:
|
|
profile = _unreal_profile()
|
|
world = profile.providers[0]
|
|
|
|
with pytest.raises(SimulationProviderContractError, match="clock.simulation"):
|
|
replace(
|
|
profile,
|
|
providers=(
|
|
replace(
|
|
world,
|
|
capabilities=tuple(
|
|
capability
|
|
for capability in world.capabilities
|
|
if capability != "clock.simulation"
|
|
),
|
|
),
|
|
profile.providers[1],
|
|
),
|
|
)
|
|
|
|
|
|
def test_provider_contract_rejects_undeclared_command_capability() -> None:
|
|
profile = _unreal_profile()
|
|
controller = profile.providers[1]
|
|
|
|
with pytest.raises(SimulationProviderContractError, match="control profiles"):
|
|
replace(
|
|
profile,
|
|
providers=(
|
|
profile.providers[0],
|
|
replace(controller, capabilities=("transport.custom",)),
|
|
),
|
|
)
|