C1: ExceptionStateMachine FSM + ExceptionStore
Per-EXID exception lifecycle for E5 §9. States mirror the wire flow: Posted equipment sent S5F9, awaiting host or autonomous clear Recovering host's S5F13 accepted; equipment running recovery RecoverFailed S5F15 reported a failed result; host may retry Cleared terminal — store removes the row Events: Created synthetic NoState->Posted observer signal Recover host's S5F13 (Posted/RecoverFailed -> Recovering) RecoveryComplete equipment internal (Recovering -> Cleared) RecoveryFailed equipment internal (Recovering -> RecoverFailed) RecoveryAbort host's S5F17 (Recovering -> Posted) Clear equipment internal (Posted/RecoverFailed -> Cleared) ExceptionStore mirrors ProcessJobStore: per-EXID FSMs heap-allocated via unique_ptr, non-movable to keep `this`-captures safe, synthetic Created fires after the row lands so observers can decide whether to emit S5F9 out of band. on_recover validates EXRECVRA against the candidates the post advertised. The store joins EquipmentDataModel alongside process_jobs / control_jobs. S5F9-F18 server-side dispatch lands in C2. Tests (12 cases) cover FSM transitions including retry, abort, and autonomous clear, plus store-level duplicate-rejection, EXRECVRA validation, and Cleared-removes-the-row semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ add_library(secsgem
|
||||
src/gem/communication_state.cpp
|
||||
src/gem/process_job_state.cpp
|
||||
src/gem/control_job_state.cpp
|
||||
src/gem/exception_state.cpp
|
||||
src/gem/host_handler.cpp
|
||||
src/config/loader.cpp
|
||||
src/endpoint.cpp
|
||||
@@ -96,6 +97,7 @@ add_executable(secsgem_tests
|
||||
tests/test_loader.cpp
|
||||
tests/test_process_jobs.cpp
|
||||
tests/test_control_jobs.cpp
|
||||
tests/test_exceptions.cpp
|
||||
)
|
||||
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
||||
target_compile_definitions(secsgem_tests PRIVATE
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "secsgem/gem/store/control_jobs.hpp"
|
||||
#include "secsgem/gem/store/equipment_constants.hpp"
|
||||
#include "secsgem/gem/store/event_reports.hpp"
|
||||
#include "secsgem/gem/store/exceptions.hpp"
|
||||
#include "secsgem/gem/store/host_commands.hpp"
|
||||
#include "secsgem/gem/store/limits.hpp"
|
||||
#include "secsgem/gem/store/process_jobs.hpp"
|
||||
@@ -34,6 +35,7 @@ struct EquipmentDataModel {
|
||||
TraceStore traces;
|
||||
ProcessJobStore process_jobs;
|
||||
ControlJobStore control_jobs;
|
||||
ExceptionStore exceptions;
|
||||
|
||||
// Convenience: VID -> value lookup spanning SVIDs and DVIDs.
|
||||
std::optional<s2::Item> vid_value(uint32_t vid) const {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/store/alarms.hpp" // AlarmAck (ACKC5)
|
||||
|
||||
// E5 §9 Exception lifecycle.
|
||||
//
|
||||
// An *exception* (distinct from a plain *alarm*) is a recoverable
|
||||
// equipment fault whose handling involves a host/equipment dialogue:
|
||||
// the equipment Posts an S5F9 with one or more candidate EXRECVRA
|
||||
// recovery actions; the host picks one via S5F13 to drive equipment
|
||||
// recovery; equipment Notifies completion via S5F15 (with EXRESULT
|
||||
// signalling success or failure); the host may abort an in-progress
|
||||
// recovery via S5F17.
|
||||
//
|
||||
// This state machine lives per-EXID inside an `ExceptionStore`; the
|
||||
// transitions mirror the request/response cadence on the wire so the
|
||||
// store can be wired into the server's S5F13 / S5F17 / internal
|
||||
// recovery hooks with a single dispatch through `on_host_command`
|
||||
// (host-initiated) or `on_internal` (equipment-initiated).
|
||||
//
|
||||
// The Posted/Cleared distinction matches E5 §9.3 figure 9 — when an
|
||||
// exception's underlying condition clears autonomously without ever
|
||||
// being recovered (rare but legal), the FSM transitions Posted →
|
||||
// Cleared directly via the `Clear` event.
|
||||
namespace secsgem::gem {
|
||||
|
||||
enum class ExceptionState : uint8_t {
|
||||
Posted = 0, // S5F9 sent, awaiting host action or autonomous clear
|
||||
Recovering = 1, // S5F13 accepted; equipment running the recovery
|
||||
RecoverFailed = 2, // S5F15 reported a failed recovery; retry possible
|
||||
Cleared = 3, // exception resolved (terminal; store removes the row)
|
||||
NoState = 255,
|
||||
};
|
||||
|
||||
const char* exception_state_name(ExceptionState s);
|
||||
std::optional<ExceptionState> parse_exception_state(const std::string& s);
|
||||
|
||||
enum class ExceptionEvent {
|
||||
Created, // synthetic NoState -> Posted (observer signal)
|
||||
Recover, // host's S5F13 — Posted/RecoverFailed -> Recovering
|
||||
RecoveryComplete, // internal — Recovering -> Cleared
|
||||
RecoveryFailed, // internal — Recovering -> RecoverFailed
|
||||
RecoveryAbort, // host's S5F17 — Recovering -> Posted
|
||||
Clear, // internal — Posted/RecoverFailed -> Cleared
|
||||
};
|
||||
|
||||
const char* exception_event_name(ExceptionEvent e);
|
||||
std::optional<ExceptionEvent> parse_exception_event(const std::string& s);
|
||||
|
||||
struct ExceptionTransition {
|
||||
ExceptionState from;
|
||||
ExceptionEvent on;
|
||||
std::optional<ExceptionState> to;
|
||||
std::optional<uint8_t> ack_code; // ACKC5 (AlarmAck) when applicable
|
||||
};
|
||||
|
||||
class ExceptionTransitionTable {
|
||||
public:
|
||||
void add(ExceptionTransition row);
|
||||
const ExceptionTransition* find(ExceptionState from, ExceptionEvent on) const;
|
||||
std::size_t size() const { return rows_.size(); }
|
||||
const std::vector<ExceptionTransition>& rows() const { return rows_; }
|
||||
|
||||
// The default table matches data/exception_state.yaml (when present);
|
||||
// kept in code so tests don't depend on the YAML being readable.
|
||||
static ExceptionTransitionTable default_table();
|
||||
|
||||
private:
|
||||
std::vector<ExceptionTransition> rows_;
|
||||
};
|
||||
|
||||
class ExceptionStateMachine {
|
||||
public:
|
||||
using StateChangeHandler =
|
||||
std::function<void(ExceptionState from, ExceptionState to,
|
||||
ExceptionEvent trigger)>;
|
||||
|
||||
ExceptionStateMachine();
|
||||
explicit ExceptionStateMachine(ExceptionTransitionTable table,
|
||||
ExceptionState initial = ExceptionState::Posted);
|
||||
|
||||
ExceptionState state() const { return state_; }
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
// Host-initiated event (S5F13 / S5F17). Returns AlarmAck::Accept on a
|
||||
// valid transition, AlarmAck::Error when no row matches. The default
|
||||
// table is permissive — every host event either advances state or is
|
||||
// silently accepted.
|
||||
AlarmAck on_host_command(ExceptionEvent event);
|
||||
|
||||
// Equipment-internal event (RecoveryComplete / RecoveryFailed / Clear).
|
||||
// Returns true if a row fired.
|
||||
bool on_internal(ExceptionEvent event);
|
||||
|
||||
private:
|
||||
const ExceptionTransition* fire(ExceptionEvent on);
|
||||
void transition(ExceptionState next, ExceptionEvent trigger);
|
||||
|
||||
ExceptionTransitionTable table_;
|
||||
ExceptionState state_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/exception_state.hpp"
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
// One Exception record. EXID keys the row; EXTYPE / EXMESSAGE are E5
|
||||
// §9 metadata; EXRECVRA lists the recovery-action names the host may
|
||||
// pick from on S5F13. The FSM is heap-allocated so the per-exception
|
||||
// state-change handler can capture a stable pointer.
|
||||
struct Exception {
|
||||
uint32_t exid;
|
||||
std::string extype;
|
||||
std::string exmessage;
|
||||
std::vector<std::string> exrecvra;
|
||||
std::unique_ptr<ExceptionStateMachine> fsm;
|
||||
};
|
||||
|
||||
class ExceptionStore {
|
||||
public:
|
||||
using TransitionTableFactory =
|
||||
std::function<ExceptionTransitionTable()>;
|
||||
using StateChangeHandler =
|
||||
std::function<void(uint32_t exid, ExceptionState from, ExceptionState to,
|
||||
ExceptionEvent trigger)>;
|
||||
|
||||
ExceptionStore()
|
||||
: factory_([] { return ExceptionTransitionTable::default_table(); }) {}
|
||||
|
||||
// Same `this`-capture caveat as ProcessJobStore: keep a stable address.
|
||||
ExceptionStore(const ExceptionStore&) = delete;
|
||||
ExceptionStore& operator=(const ExceptionStore&) = delete;
|
||||
ExceptionStore(ExceptionStore&&) = delete;
|
||||
ExceptionStore& operator=(ExceptionStore&&) = delete;
|
||||
|
||||
void set_table_factory(TransitionTableFactory f) { factory_ = std::move(f); }
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
enum class PostResult {
|
||||
Posted,
|
||||
Denied_AlreadyExists,
|
||||
};
|
||||
|
||||
// Equipment posts a new exception (S5F9). Returns Denied_AlreadyExists
|
||||
// if an entry for `exid` is already live. Fires a synthetic NoState ->
|
||||
// Posted change so the dispatcher can decide whether to actually emit
|
||||
// the S5F9 (skip if alerts disabled, etc.).
|
||||
PostResult post(uint32_t exid, std::string extype, std::string exmessage,
|
||||
std::vector<std::string> exrecvra) {
|
||||
if (by_id_.count(exid)) return PostResult::Denied_AlreadyExists;
|
||||
auto fsm = std::make_unique<ExceptionStateMachine>(factory_(),
|
||||
ExceptionState::Posted);
|
||||
fsm->set_state_change_handler(
|
||||
[this, exid](ExceptionState from, ExceptionState to, ExceptionEvent trig) {
|
||||
if (on_change_) on_change_(exid, from, to, trig);
|
||||
});
|
||||
by_id_.emplace(exid, Exception{exid, std::move(extype), std::move(exmessage),
|
||||
std::move(exrecvra), std::move(fsm)});
|
||||
if (on_change_) {
|
||||
on_change_(exid, ExceptionState::NoState, ExceptionState::Posted,
|
||||
ExceptionEvent::Created);
|
||||
}
|
||||
return PostResult::Posted;
|
||||
}
|
||||
|
||||
bool has(uint32_t exid) const { return by_id_.count(exid) > 0; }
|
||||
|
||||
const Exception* get(uint32_t exid) const {
|
||||
auto it = by_id_.find(exid);
|
||||
return it == by_id_.end() ? nullptr : &it->second;
|
||||
}
|
||||
Exception* get(uint32_t exid) {
|
||||
auto it = by_id_.find(exid);
|
||||
return it == by_id_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
ExceptionState state(uint32_t exid) const {
|
||||
auto it = by_id_.find(exid);
|
||||
return it == by_id_.end() ? ExceptionState::NoState : it->second.fsm->state();
|
||||
}
|
||||
|
||||
// S5F13 recovery dispatch. Validates EXRECVRA matches one the post
|
||||
// advertised; returns AlarmAck::Error on unknown EXID or unknown action.
|
||||
AlarmAck on_recover(uint32_t exid, const std::string& exrecvra) {
|
||||
auto* ex = get(exid);
|
||||
if (!ex) return AlarmAck::Error;
|
||||
bool known = false;
|
||||
for (const auto& a : ex->exrecvra) if (a == exrecvra) { known = true; break; }
|
||||
if (!known) return AlarmAck::Error;
|
||||
return ex->fsm->on_host_command(ExceptionEvent::Recover);
|
||||
}
|
||||
|
||||
// S5F17 recovery-abort dispatch.
|
||||
AlarmAck on_recover_abort(uint32_t exid) {
|
||||
auto* ex = get(exid);
|
||||
if (!ex) return AlarmAck::Error;
|
||||
return ex->fsm->on_host_command(ExceptionEvent::RecoveryAbort);
|
||||
}
|
||||
|
||||
// Equipment-internal lifecycle events. Cleared transitions remove the
|
||||
// entry — exceptions are not retained past clearance, matching the
|
||||
// E5 §9.3 model where Cleared is the lifecycle's terminus.
|
||||
bool fire_internal(uint32_t exid, ExceptionEvent event) {
|
||||
auto* ex = get(exid);
|
||||
if (!ex) return false;
|
||||
const bool fired = ex->fsm->on_internal(event);
|
||||
if (fired && ex->fsm->state() == ExceptionState::Cleared) {
|
||||
by_id_.erase(exid);
|
||||
}
|
||||
return fired;
|
||||
}
|
||||
|
||||
std::size_t size() const { return by_id_.size(); }
|
||||
std::vector<uint32_t> ids() const {
|
||||
std::vector<uint32_t> out;
|
||||
out.reserve(by_id_.size());
|
||||
for (const auto& kv : by_id_) out.push_back(kv.first);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<uint32_t, Exception> by_id_;
|
||||
TransitionTableFactory factory_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "secsgem/gem/exception_state.hpp"
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
const char* exception_state_name(ExceptionState s) {
|
||||
switch (s) {
|
||||
case ExceptionState::Posted: return "Posted";
|
||||
case ExceptionState::Recovering: return "Recovering";
|
||||
case ExceptionState::RecoverFailed: return "RecoverFailed";
|
||||
case ExceptionState::Cleared: return "Cleared";
|
||||
case ExceptionState::NoState: return "NoState";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
std::optional<ExceptionState> parse_exception_state(const std::string& s) {
|
||||
if (s == "Posted") return ExceptionState::Posted;
|
||||
if (s == "Recovering") return ExceptionState::Recovering;
|
||||
if (s == "RecoverFailed") return ExceptionState::RecoverFailed;
|
||||
if (s == "Cleared") return ExceptionState::Cleared;
|
||||
if (s == "NoState") return ExceptionState::NoState;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* exception_event_name(ExceptionEvent e) {
|
||||
switch (e) {
|
||||
case ExceptionEvent::Created: return "Created";
|
||||
case ExceptionEvent::Recover: return "Recover";
|
||||
case ExceptionEvent::RecoveryComplete: return "RecoveryComplete";
|
||||
case ExceptionEvent::RecoveryFailed: return "RecoveryFailed";
|
||||
case ExceptionEvent::RecoveryAbort: return "RecoveryAbort";
|
||||
case ExceptionEvent::Clear: return "Clear";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
std::optional<ExceptionEvent> parse_exception_event(const std::string& s) {
|
||||
// `created` is synthetic — never legal in a table row.
|
||||
if (s == "recover") return ExceptionEvent::Recover;
|
||||
if (s == "recovery_complete") return ExceptionEvent::RecoveryComplete;
|
||||
if (s == "recovery_failed") return ExceptionEvent::RecoveryFailed;
|
||||
if (s == "recovery_abort") return ExceptionEvent::RecoveryAbort;
|
||||
if (s == "clear") return ExceptionEvent::Clear;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void ExceptionTransitionTable::add(ExceptionTransition row) {
|
||||
rows_.push_back(row);
|
||||
}
|
||||
|
||||
const ExceptionTransition* ExceptionTransitionTable::find(
|
||||
ExceptionState from, ExceptionEvent on) const {
|
||||
for (const auto& r : rows_) {
|
||||
if (r.from == from && r.on == on) return &r;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ExceptionTransitionTable ExceptionTransitionTable::default_table() {
|
||||
using S = ExceptionState;
|
||||
using E = ExceptionEvent;
|
||||
ExceptionTransitionTable t;
|
||||
|
||||
// POSTED ----------------------------------------------------------
|
||||
t.add({S::Posted, E::Recover, S::Recovering, std::nullopt});
|
||||
t.add({S::Posted, E::Clear, S::Cleared, std::nullopt});
|
||||
|
||||
// RECOVERING ------------------------------------------------------
|
||||
t.add({S::Recovering, E::RecoveryComplete, S::Cleared, std::nullopt});
|
||||
t.add({S::Recovering, E::RecoveryFailed, S::RecoverFailed, std::nullopt});
|
||||
t.add({S::Recovering, E::RecoveryAbort, S::Posted, std::nullopt});
|
||||
|
||||
// RECOVER-FAILED --------------------------------------------------
|
||||
t.add({S::RecoverFailed, E::Recover, S::Recovering, std::nullopt});
|
||||
t.add({S::RecoverFailed, E::Clear, S::Cleared, std::nullopt});
|
||||
|
||||
// CLEARED is terminal — the store removes the exception entry.
|
||||
return t;
|
||||
}
|
||||
|
||||
ExceptionStateMachine::ExceptionStateMachine()
|
||||
: ExceptionStateMachine(ExceptionTransitionTable::default_table(),
|
||||
ExceptionState::Posted) {}
|
||||
|
||||
ExceptionStateMachine::ExceptionStateMachine(ExceptionTransitionTable table,
|
||||
ExceptionState initial)
|
||||
: table_(std::move(table)), state_(initial) {}
|
||||
|
||||
void ExceptionStateMachine::transition(ExceptionState next, ExceptionEvent trig) {
|
||||
if (state_ == next) return;
|
||||
const ExceptionState prev = state_;
|
||||
state_ = next;
|
||||
if (on_change_) on_change_(prev, next, trig);
|
||||
}
|
||||
|
||||
const ExceptionTransition* ExceptionStateMachine::fire(ExceptionEvent on) {
|
||||
const ExceptionTransition* row = table_.find(state_, on);
|
||||
if (!row) return nullptr;
|
||||
if (row->to) transition(*row->to, on);
|
||||
return row;
|
||||
}
|
||||
|
||||
AlarmAck ExceptionStateMachine::on_host_command(ExceptionEvent event) {
|
||||
const auto* row = fire(event);
|
||||
if (!row) return AlarmAck::Error;
|
||||
if (row->ack_code) return static_cast<AlarmAck>(*row->ack_code);
|
||||
return AlarmAck::Accept;
|
||||
}
|
||||
|
||||
bool ExceptionStateMachine::on_internal(ExceptionEvent event) {
|
||||
return fire(event) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/exception_state.hpp"
|
||||
#include "secsgem/gem/store/exceptions.hpp"
|
||||
|
||||
using namespace secsgem::gem;
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: default starts Posted") {
|
||||
ExceptionStateMachine fsm;
|
||||
CHECK(fsm.state() == ExceptionState::Posted);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: Posted -> Recovering via host Recover") {
|
||||
ExceptionStateMachine fsm;
|
||||
CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Accept);
|
||||
CHECK(fsm.state() == ExceptionState::Recovering);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: Recovering -> Cleared on RecoveryComplete") {
|
||||
ExceptionStateMachine fsm;
|
||||
fsm.on_host_command(ExceptionEvent::Recover);
|
||||
REQUIRE(fsm.state() == ExceptionState::Recovering);
|
||||
CHECK(fsm.on_internal(ExceptionEvent::RecoveryComplete));
|
||||
CHECK(fsm.state() == ExceptionState::Cleared);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: Recovering -> RecoverFailed -> Recovering retry") {
|
||||
ExceptionStateMachine fsm;
|
||||
fsm.on_host_command(ExceptionEvent::Recover);
|
||||
CHECK(fsm.on_internal(ExceptionEvent::RecoveryFailed));
|
||||
CHECK(fsm.state() == ExceptionState::RecoverFailed);
|
||||
CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Accept);
|
||||
CHECK(fsm.state() == ExceptionState::Recovering);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: RecoveryAbort returns to Posted") {
|
||||
ExceptionStateMachine fsm;
|
||||
fsm.on_host_command(ExceptionEvent::Recover);
|
||||
REQUIRE(fsm.state() == ExceptionState::Recovering);
|
||||
CHECK(fsm.on_host_command(ExceptionEvent::RecoveryAbort) == AlarmAck::Accept);
|
||||
CHECK(fsm.state() == ExceptionState::Posted);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: autonomous Clear from Posted -> Cleared") {
|
||||
ExceptionStateMachine fsm;
|
||||
CHECK(fsm.on_internal(ExceptionEvent::Clear));
|
||||
CHECK(fsm.state() == ExceptionState::Cleared);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStateMachine: illegal host event from Cleared returns Error") {
|
||||
ExceptionStateMachine fsm(ExceptionTransitionTable::default_table(),
|
||||
ExceptionState::Cleared);
|
||||
CHECK(fsm.on_host_command(ExceptionEvent::Recover) == AlarmAck::Error);
|
||||
}
|
||||
|
||||
// ---- ExceptionStore -----------------------------------------------------
|
||||
|
||||
TEST_CASE("ExceptionStore: post rejects duplicate EXID") {
|
||||
ExceptionStore store;
|
||||
CHECK(store.post(42, "FATAL", "vacuum lost", {"RETRY"}) ==
|
||||
ExceptionStore::PostResult::Posted);
|
||||
CHECK(store.post(42, "FATAL", "vacuum lost", {"RETRY"}) ==
|
||||
ExceptionStore::PostResult::Denied_AlreadyExists);
|
||||
CHECK(store.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStore: on_recover validates EXRECVRA against post list") {
|
||||
ExceptionStore store;
|
||||
store.post(1, "WARN", "fan stalled", {"RETRY", "RESTART"});
|
||||
CHECK(store.on_recover(1, "RETRY") == AlarmAck::Accept);
|
||||
// Same EXID, different action — must come from the original list.
|
||||
store.post(2, "WARN", "x", {"ONLY"});
|
||||
CHECK(store.on_recover(2, "BOGUS") == AlarmAck::Error);
|
||||
CHECK(store.on_recover(99, "RETRY") == AlarmAck::Error); // unknown EXID
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStore: Cleared removes the entry") {
|
||||
ExceptionStore store;
|
||||
store.post(7, "INFO", "x", {"OK"});
|
||||
REQUIRE(store.has(7));
|
||||
store.on_recover(7, "OK");
|
||||
store.fire_internal(7, ExceptionEvent::RecoveryComplete);
|
||||
CHECK_FALSE(store.has(7));
|
||||
CHECK(store.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("ExceptionStore: state-change handler observes Created + transitions") {
|
||||
ExceptionStore store;
|
||||
std::vector<std::tuple<uint32_t, ExceptionState, ExceptionState>> log;
|
||||
store.set_state_change_handler(
|
||||
[&](uint32_t exid, ExceptionState f, ExceptionState t, ExceptionEvent) {
|
||||
log.emplace_back(exid, f, t);
|
||||
});
|
||||
store.post(42, "FATAL", "x", {"R"});
|
||||
store.on_recover(42, "R");
|
||||
store.fire_internal(42, ExceptionEvent::RecoveryComplete);
|
||||
REQUIRE(log.size() == 3);
|
||||
CHECK(std::get<1>(log[0]) == ExceptionState::NoState);
|
||||
CHECK(std::get<2>(log[0]) == ExceptionState::Posted);
|
||||
CHECK(std::get<2>(log[1]) == ExceptionState::Recovering);
|
||||
CHECK(std::get<2>(log[2]) == ExceptionState::Cleared);
|
||||
}
|
||||
Reference in New Issue
Block a user