STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_run_config.py
Go to the documentation of this file.
1"""Unit tests for the per-run config snapshot → human names.
2
3The entity spellings asserted here are the ones DatabaseConfig.cpp actually writes, and
4the slot arithmetic is LoadActiveBoards' (`board_id % 10`, 0 → 10). If either moves, this
5file is where the viewer finds out.
6
7Run: cd webviewer && .venv/bin/python -m pytest backend/test_run_config.py -q
8"""
9
10import pytest
11
12from . import config, run_config
13
14# A config shaped like config_base.toml, trimmed to what naming reads.
15CONFIG_TOML = """
16[boards.pt_board]
17type = "PT"
18board_id = 21
19
20[boards.pt_board_2]
21type = "PT"
22board_id = 22
23
24[boards.actuator_board_2]
25type = "ACTUATOR"
26board_id = 12
27
28[boards.encoder_board]
29type = "ENCODER"
30board_id = 61
31
32[sensor_roles_pt_board]
33"Ox Upstream" = 5
34"GN2 Regulated" = 6
35
36[sensor_roles_pt_board_2]
37"GSE High" = 1
38
39[sensor_roles_encoder_board]
40"Encoder 1" = 1
41
42[actuator_roles]
43"LOX Main" = ["NC", 1, 12]
44"Fuel Vent" = ["NC", 2, 12]
45
46[[states]]
47id = 16
48name = "Burn"
49"""
50
51
52@pytest.fixture
53def elodin_dir(tmp_path, monkeypatch):
54 monkeypatch.setattr(config, "ELODIN_DIR", tmp_path)
55 return tmp_path
56
57
58def snapshot(elodin_dir, run_id: str, text: str) -> None:
59 (elodin_dir / f"{run_id}.toml").write_text(text)
60
61
62@pytest.fixture
63def labels(elodin_dir):
64 snapshot(elodin_dir, "daq_20260902_120000", CONFIG_TOML)
65 return run_config.entity_labels(run_config.load("daq_20260902_120000"))
66
67
68# ── entity naming ────────────────────────────────────────────────────────────
69
70
72 # Same physical channel, two VTables — both carry the role.
73 assert labels["PT1.CH5"] == "Ox Upstream"
74 assert labels["PT1_Cal.CH5"] == "Ox Upstream"
75
76
78 # board_id 21 → PT1, 22 → PT2, 12 → ACT2, 61 → ENC1. Not the ordinal, not the id.
79 assert labels["PT2.CH1"] == "GSE High"
80 assert labels["ENC1.CH1"] == "Encoder 1"
81 assert "PT21.CH5" not in labels
82
83
85 # A board_id ending in 0 is slot 10, not slot 0 — the same 0→10 rule the C++ uses.
86 assert run_config._slot(30) == 10
87 assert run_config._slot(21) == 1
88
89
91 # The valve is the raw channel, its current sense, and what the sequencer commanded.
92 assert labels["ACT2.CH1"] == "LOX Main"
93 assert labels["ACT2_Cal.CH1"] == "LOX Main"
94 assert labels["ACT_CMD.B2.CH1"] == "LOX Main"
95
96
98 # Board 71 is slot 1, as is actuator board 11; the ball valve lives under BV*, so its role
99 # must not land on ACT1.* (which is board 11's).
100 snapshot(
101 elodin_dir,
102 "bv",
103 """
104[boards.act_11]
105type = "ACTUATOR"
106board_id = 11
107
108[boards.ball_valve_board]
109type = "BALL_VALVE"
110board_id = 71
111
112[actuator_roles]
113"Main Ball Valve" = ["NC", 1, 71]
114""",
115 )
116 labels = run_config.entity_labels(run_config.load("bv"))
117 assert labels["BV1.CH1"] == "Main Ball Valve"
118 assert labels["BV1_Cal.CH1"] == "Main Ball Valve"
119 assert labels["BV_CMD.B1.CH1"] == "Main Ball Valve"
120 assert "ACT1.CH1" not in labels
121 assert "ACT_CMD.B1.CH1" not in labels
122 assert labels["BOARD.HB_71"] == "Ball Valve Board"
123
124
126 # Two-element form (no board_id) predates multi-board actuators; it means board 11.
127 snapshot(
128 elodin_dir,
129 "daq_20260902_120001",
130 '[boards.actuator_board]\ntype = "ACTUATOR"\nboard_id = 11\n'
131 '\n[actuator_roles]\n"Fuel Fill" = ["NC", 7]\n',
132 )
133 labels = run_config.entity_labels(run_config.load("daq_20260902_120001"))
134 assert labels["ACT1.CH7"] == "Fuel Fill"
135
136
138 # An actuator board also reports sensors; on that channel the valve is the answer.
139 snapshot(
140 elodin_dir,
141 "daq_20260902_120002",
142 '[boards.actuator_board_2]\ntype = "ACTUATOR"\nboard_id = 12\n'
143 '\n[sensor_roles_actuator_board_2]\n"Some Sense" = 1\n'
144 '\n[actuator_roles]\n"LOX Main" = ["NC", 1, 12]\n',
145 )
146 labels = run_config.entity_labels(run_config.load("daq_20260902_120002"))
147 assert labels["ACT2.CH1"] == "LOX Main"
148
149
151 # BOARD.HB_/SELF_TEST use the raw board_id — the slot rule is for channel entities.
152 assert labels["BOARD.HB_21"] == "PT Board #1"
153 assert labels["BOARD.HB_22"] == "PT Board #2"
154 # One board of its type drops the ordinal, matching the config editor.
155 assert labels["BOARD.HB_12"] == "Actuator Board"
156 assert labels["BOARD.HB_61"] == "Encoder Board"
157
158
160 # SELF_TEST entities carry a per-sensor suffix that config never names directly.
161 assert run_config.label_for("SELF_TEST.BOARD_21.s3", labels) == "PT Board #1 sensor 3"
162
163
165 # Already-human entities and channels config says nothing about stay as they are.
166 assert run_config.label_for("CONTROLLER.state", labels) == ""
167 assert run_config.label_for("PT1.CH9", labels) == ""
168
169
170# ── states ───────────────────────────────────────────────────────────────────
171
172
174 names = run_config.state_names(run_config.load("daq_20260902_120000"))
175 assert names[16] == "Burn" # the snapshot renamed it
176 assert names[1] == "Idle" # everything else survives the partial list
177 assert names[20] == "Press Standby"
178
179
181 # Ids are a stable key by contract, so a run predating [[states]] still decodes.
182 assert run_config.state_names({})[16] == "Fire"
183
184
186 # BOARD.HB_*.engine_state is the coarse daq::EngineState, a different enum.
187 assert "engine_state" not in run_config.STATE_FIELDS
188 assert run_config.STATE_FIELDS == {"current_state", "from_state", "to_state"}
189
190
191# ── degradation ──────────────────────────────────────────────────────────────
192
193
195 assert run_config.load("daq_20260101_000000") == {}
196 assert run_config.entity_labels({}) == {}
197
198
200 # A truncated copy must cost readability, never the ability to open the run.
201 snapshot(elodin_dir, "daq_20260902_120003", "[boards.pt_board\ntype = ")
202 assert run_config.load("daq_20260902_120003") == {}
203
204
206 snapshot(elodin_dir, "daq_20260902_120004", CONFIG_TOML)
207 index = {
208 "components": [
209 {"name": "PT1.CH5.pressure_psi", "entity": "PT1.CH5"},
210 {"name": "CONTROLLER.state.to_state", "entity": "CONTROLLER.state"},
211 ]
212 }
213 out = run_config.annotate(index, "daq_20260902_120004")
214 assert out["components"][0]["label"] == "Ox Upstream"
215 assert out["components"][1]["label"] == ""
216 assert out["has_config"] is True
217 assert out["states"]["16"] == "Burn" # JSON keys are strings
218 assert "to_state" in out["state_fields"]
219
220
222 out = run_config.annotate({"components": []}, "daq_20260101_000000")
223 assert out["has_config"] is False
224 assert out["states"]["16"] == "Fire" # built-in table still names states
225
226
227# ── LabJack T7 (t7_bridge + calibration_service) ─────────────────────────────
228
229T7_TOML = """
230[boards.pt_board]
231type = "PT"
232board_id = 21
233
234[sensor_roles_pt_board]
235"Ox Upstream" = 5
236
237[boards.labjack_1]
238type = "T7"
239board_id = 82
240
241[[boards.labjack_1.channels]]
242ain = 1
243kind = "PT"
244role = "LOX Tank"
245
246[[boards.labjack_1.channels]]
247ain = 2
248kind = "TC"
249role = "Nozzle TC"
250tc_type = "K"
251
252[[boards.labjack_1.channels]]
253ain = 6
254kind = "VOLTAGE"
255"""
256
257
259 # The run's sidecar config is all a past T7 run needs to read as sensors, not AINs.
260 snapshot(elodin_dir, "daq_20261104_120000", T7_TOML)
261 labels = run_config.entity_labels(run_config.load("daq_20261104_120000"))
262 # board_id 82 -> slot 2, the same rule as every other board.
263 assert labels["T72.AIN1"] == "LOX Tank"
264 assert labels["T72_Cal.AIN1"] == "LOX Tank"
265 assert labels["T72_Cal.AIN2"] == "Nozzle TC"
266 # A board with a thermocouple also records AIN14 and its cold junction.
267 assert labels["T72.AIN14"] == "LabJack T7 Board internal sensor"
268 assert labels["T72.CJC"] == "LabJack T7 Board cold junction"
269 # Unnamed VOLTAGE input keeps its numeric name; the ESP32 board is untouched.
270 assert "T72.AIN6" not in labels
271 assert labels["PT1.CH5"] == "Ox Upstream"
272
273
275 snapshot(elodin_dir, "daq_20261104_120001", T7_TOML.replace('kind = "TC"', 'kind = "PT"'))
276 labels = run_config.entity_labels(run_config.load("daq_20261104_120001"))
277 assert "T72.CJC" not in labels and "T72.AIN14" not in labels
test_t7_channels_take_their_role_raw_and_calibrated(elodin_dir)
elodin_dir(tmp_path, monkeypatch)
test_malformed_snapshot_does_not_raise(elodin_dir)
None snapshot(elodin_dir, str run_id, str text)
test_t7_without_a_thermocouple_names_no_cold_junction(elodin_dir)
test_states_fall_back_to_the_builtin_table_with_no_snapshot()
test_actuator_role_board_id_defaults_to_the_first_actuator_board(elodin_dir)
test_states_override_the_builtin_table_entry_by_entry(labels, elodin_dir)
test_self_test_entity_resolves_through_its_board(labels)
test_board_entities_are_keyed_by_board_id_not_slot(labels)
test_annotate_stamps_labels_and_state_metadata(elodin_dir)
test_ball_valve_role_names_its_own_entities(elodin_dir)
test_sensor_role_names_raw_and_calibrated(labels)
test_annotate_reports_a_run_with_no_snapshot(elodin_dir)
test_board_id_last_digit_is_the_elodin_slot(labels)
test_actuator_role_names_sense_and_command_entities(labels)
test_missing_snapshot_degrades_to_numbers(elodin_dir)
test_actuator_role_wins_over_a_sensor_role_on_the_same_channel(elodin_dir)