Skip to content

Commit 0c11ac9

Browse files
committed
test(opcua,#386): cover the missing AlarmStateMachine transition cells
Verification round on the post-#386 review found 9 untested cells in the AlarmStateMachine transition matrix (issue #389 follow-up). Adding focused tests so a future refactor cannot silently break a corner case. New cells covered: - ``DisabledClearsHealedAlarm`` - Healed exit via Rule 2 (was_active=true for Healed too, so disabling a latched alarm must ClearFault, not just flip to Suppressed). - ``DisabledNoOpWhenAlreadyCleared`` - confirms Cleared+disabled lands at Suppressed/NoOp (no spurious second clear). - ``ShelvedClearsHealedAlarm`` / ``ShelvedNoOpWhenAlreadyCleared`` - same pair via Rule 3. - ``ActiveAlarmReportsConfirmedFromSuppressed`` - operator unshelves/re-enables an alarm whose source is still active; Rule 4 must promote Suppressed -> Confirmed with ReportConfirmed (NOT NoOp). This is exactly the unshelve+re-fire path scenario 2 of run_alarm_tests.sh exercises end-to-end. - ``BranchEventFromHealedNoOp`` / ``BranchEventFromSuppressedNoOp`` - Rule 1 precedence holds across all four prev_status values. - ``AckedAndConfirmedNoOpFromSuppressed`` - ``was_active=false`` branch of Rule 5: a fully cleared event delivered while suppressed advances next_status to Cleared but issues no ClearFault (no fault to clear). - ``InactiveUnackedFromSuppressedReportsHealed`` - ditto but with the ack/confirm bits unset, must surface as Healed/ReportHealed so the unfinished ack/confirm workflow item stays visible. State machine code itself is unchanged. 27 gtest cases, all PASSED. Pure-function tests, deterministic, no I/O.
1 parent f0e1eca commit 0c11ac9

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_alarm_state_machine.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,107 @@ TEST(AlarmStateMachineTest, ShelvedTakesPrecedenceOverActive) {
208208
EXPECT_EQ(out.action, AlarmAction::NoOp);
209209
}
210210

211+
// -- Coverage of remaining transition matrix cells (issue #389 follow-up).
212+
// The above tests exercise the obvious paths; these cover the corners where
213+
// prev_status is Healed or Suppressed and an exit / re-entry rule fires.
214+
215+
TEST(AlarmStateMachineTest, DisabledClearsHealedAlarm) {
216+
// Operator disables an already-latched (Healed) alarm: must transition to
217+
// Cleared with a ClearFault action so the latched fault disappears from
218+
// /faults instead of orphaning between HEALED and Cleared forever.
219+
AlarmEventInput in;
220+
in.enabled_state = false;
221+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Healed, in);
222+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Cleared);
223+
EXPECT_EQ(out.action, AlarmAction::ClearFault);
224+
}
225+
226+
TEST(AlarmStateMachineTest, DisabledNoOpWhenAlreadyCleared) {
227+
AlarmEventInput in;
228+
in.enabled_state = false;
229+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Cleared, in);
230+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Suppressed);
231+
EXPECT_EQ(out.action, AlarmAction::NoOp);
232+
}
233+
234+
TEST(AlarmStateMachineTest, ShelvedClearsHealedAlarm) {
235+
// Same exit shape as Disabled, via the shelving rule.
236+
AlarmEventInput in;
237+
in.enabled_state = true;
238+
in.active_state = false;
239+
in.shelved = true;
240+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Healed, in);
241+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Cleared);
242+
EXPECT_EQ(out.action, AlarmAction::ClearFault);
243+
}
244+
245+
TEST(AlarmStateMachineTest, ShelvedNoOpWhenAlreadyCleared) {
246+
AlarmEventInput in;
247+
in.enabled_state = true;
248+
in.active_state = false;
249+
in.shelved = true;
250+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Cleared, in);
251+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Suppressed);
252+
EXPECT_EQ(out.action, AlarmAction::NoOp);
253+
}
254+
255+
TEST(AlarmStateMachineTest, ActiveAlarmReportsConfirmedFromSuppressed) {
256+
// Operator unshelves / re-enables an alarm whose underlying source is
257+
// still active: the next event has active=true and the state machine
258+
// must promote Suppressed -> Confirmed with a ReportConfirmed action.
259+
AlarmEventInput in = live_event(true);
260+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Suppressed, in);
261+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Confirmed);
262+
EXPECT_EQ(out.action, AlarmAction::ReportConfirmed);
263+
}
264+
265+
TEST(AlarmStateMachineTest, BranchEventFromHealedNoOp) {
266+
AlarmEventInput in;
267+
in.enabled_state = true;
268+
in.branch_id_present = true;
269+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Healed, in);
270+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Healed);
271+
EXPECT_EQ(out.action, AlarmAction::NoOp);
272+
}
273+
274+
TEST(AlarmStateMachineTest, BranchEventFromSuppressedNoOp) {
275+
AlarmEventInput in;
276+
in.enabled_state = true;
277+
in.branch_id_present = true;
278+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Suppressed, in);
279+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Suppressed);
280+
EXPECT_EQ(out.action, AlarmAction::NoOp);
281+
}
282+
283+
TEST(AlarmStateMachineTest, AckedAndConfirmedNoOpFromSuppressed) {
284+
// Suppressed alarm receives a fully-cleared event (active=false,
285+
// acked=true, confirmed=true). Was already not-active per the
286+
// suppression; no ClearFault to issue, but next_status should track
287+
// to Cleared so a later re-fire re-promotes correctly. This is the
288+
// ``was_active=false`` branch of rule 5.
289+
AlarmEventInput in;
290+
in.enabled_state = true;
291+
in.active_state = false;
292+
in.acked_state = true;
293+
in.confirmed_state = true;
294+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Suppressed, in);
295+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Cleared);
296+
EXPECT_EQ(out.action, AlarmAction::NoOp);
297+
}
298+
299+
TEST(AlarmStateMachineTest, InactiveUnackedFromSuppressedReportsHealed) {
300+
// Suppressed alarm sees an inactive+unacked event (operator unshelved
301+
// while the source had already self-cleared but wasn't acked). The
302+
// state machine must surface this as Healed so the operator sees the
303+
// pending ack/confirm workflow item rather than silently forgetting it.
304+
AlarmEventInput in;
305+
in.enabled_state = true;
306+
in.active_state = false;
307+
in.acked_state = false;
308+
in.confirmed_state = false;
309+
auto out = AlarmStateMachine::compute(SovdAlarmStatus::Suppressed, in);
310+
EXPECT_EQ(out.next_status, SovdAlarmStatus::Healed);
311+
EXPECT_EQ(out.action, AlarmAction::ReportHealed);
312+
}
313+
211314
} // namespace ros2_medkit_gateway

0 commit comments

Comments
 (0)