This commit is contained in:
Jonas Bengtsson
2026-10-04 20:59:22 +02:00
parent f82e8d4be9
commit 68d3c42d79
12 changed files with 394 additions and 73 deletions
+56
View File
@@ -346,6 +346,62 @@ class TestPlantEnv:
assert "DI0.0" in codes and "DO0.0" in codes
assert state.sim_time_ms == 0.0
def test_observe_carries_display_ranges(self, node):
"""FR-8.2.14 observation includes analog display ranges
(FR-10.2.14 dense store — defaults materialized)."""
env = PlantEnv(node=node)
state = env.observe()
ai = state.channel("AI0.0")
assert ai.range == (0, 4095) # default materialized
di = state.channel("DI0.0")
assert di.range is None # digital: no range
# a configured range is visible in the very same observation
node.set_channel_ranges({"AI0.0": (-32000, 32000)})
state = env.observe()
assert state.channel("AI0.0").range == (-32000, 32000)
def test_channel_lookup_by_name(self, node):
"""PlantState.channel() resolves the configured application
name too (FR-10.2.11 names, FR-8.2.16 accessor)."""
node.channel_names["AI0.0"] = "tank_level"
env = PlantEnv(node=node)
state = env.observe()
ch = state.channel("tank_level")
assert ch.io_code == "AI0.0"
assert ch.range == (0, 4095)
assert state.channel("AI0.0") is ch # code lookup matches
# code takes precedence over name; unknown -> KeyError
with pytest.raises(KeyError):
state.channel("no_such_channel")
# a name colliding with an I/O code resolves to the code
node.channel_names["AO0.1"] = "AI0.0"
state = env.observe()
assert state.channel("AI0.0").io_code == "AI0.0"
def test_channel_range_accessor(self, node):
"""state.channel_range(): analog (min, max) by code or name;
digital channels carry no range."""
node.channel_names["AI0.0"] = "tank_level"
node.set_channel_ranges({"AI0.0": (-32000, 32000)})
env = PlantEnv(node=node)
state = env.observe()
assert state.channel_range("AI0.0") == (-32000, 32000)
assert state.channel_range("tank_level") == (-32000, 32000)
assert state.channel_range("AO0.0") == (0, 4095) # dense default
with pytest.raises(KeyError):
state.channel_range("DI0.0") # digital
def test_node_range_map_is_dense(self, node):
"""FR-10.2.14: every analog channel always has an entry."""
analog = set(node.channel_ranges)
assert {"AI0.0", "AI0.1", "AO0.0", "AO0.1"} <= analog
assert all(node.channel_ranges[c] == (0, 4095) for c in analog)
# digital codes are pruned; defaults materialized on assign
node.set_channel_ranges({"AI0.0": (0, 4095), "DI0.0": (0, 100)})
assert "DI0.0" not in node.channel_ranges
assert node.channel_ranges["AI0.0"] == (0, 4095)
assert "AO0.1" in node.channel_ranges # re-materialized
def test_apply_writes_input_side(self, node):
env = PlantEnv(node=node)
env.apply({"DI0.0": True, "AI0.0": 1234})