06f664dfab
EptStateMachine now retains per-state cumulative dwell time so the host can read it as SVIDs. The implementation is zero-overhead while the FSM is idle (no timers, no background work) — on every transition we add the prior state's dwell to its bucket and reset the entered_ timestamp. Live dwell in the current state is included in accumulated() via a now-vs-entered_ delta at read time. New public API: accumulated(EptState) per-state cumulative ms (incl. live dwell) total_elapsed() denominator for utilization ratios reset_history() S2F43-style history clear This closes the gap I called out: previously we emitted CEIDs on transition but didn't accumulate the bucket the host actually queries for utilization metrics. Wiring these into specific SVIDs is the application's job (equipment.yaml declares SVIDs against any read callable); the runtime data is now there. 4 new test cases cover accumulation, live-dwell inclusion, and reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
103 lines
3.8 KiB
C++
103 lines
3.8 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/ept_state.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
TEST_CASE("EPT: initial state is NonScheduledTime") {
|
|
EptStateMachine ept;
|
|
CHECK(ept.state() == EptState::NonScheduledTime);
|
|
}
|
|
|
|
TEST_CASE("EPT: every event transitions to exactly its target state") {
|
|
EptStateMachine ept;
|
|
CHECK(ept.on_event(EptEvent::EnterStandby));
|
|
CHECK(ept.state() == EptState::Standby);
|
|
CHECK(ept.on_event(EptEvent::EnterProductive));
|
|
CHECK(ept.state() == EptState::Productive);
|
|
CHECK(ept.on_event(EptEvent::EnterUnscheduledDown));
|
|
CHECK(ept.state() == EptState::UnscheduledDowntime);
|
|
CHECK(ept.on_event(EptEvent::EnterEngineering));
|
|
CHECK(ept.state() == EptState::Engineering);
|
|
CHECK(ept.on_event(EptEvent::EnterScheduledDown));
|
|
CHECK(ept.state() == EptState::ScheduledDowntime);
|
|
CHECK(ept.on_event(EptEvent::EnterNonScheduled));
|
|
CHECK(ept.state() == EptState::NonScheduledTime);
|
|
}
|
|
|
|
TEST_CASE("EPT: change handler reports dwell time") {
|
|
EptStateMachine ept;
|
|
std::vector<std::pair<EptState, std::chrono::milliseconds>> transitions;
|
|
ept.set_state_change_handler(
|
|
[&](EptState, EptState to, EptEvent, std::chrono::milliseconds dwell) {
|
|
transitions.emplace_back(to, dwell);
|
|
});
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
ept.on_event(EptEvent::EnterStandby);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
ept.on_event(EptEvent::EnterProductive);
|
|
|
|
REQUIRE(transitions.size() == 2);
|
|
CHECK(transitions[0].first == EptState::Standby);
|
|
CHECK(transitions[0].second.count() >= 1); // dwelled in NonScheduledTime
|
|
CHECK(transitions[1].first == EptState::Productive);
|
|
CHECK(transitions[1].second.count() >= 1); // dwelled in Standby
|
|
}
|
|
|
|
TEST_CASE("EPT: same-state event is a no-op (no handler call)") {
|
|
EptStateMachine ept;
|
|
ept.on_event(EptEvent::EnterStandby);
|
|
int calls = 0;
|
|
ept.set_state_change_handler(
|
|
[&](EptState, EptState, EptEvent, std::chrono::milliseconds) { ++calls; });
|
|
CHECK(ept.on_event(EptEvent::EnterStandby)); // already in Standby
|
|
CHECK(calls == 0);
|
|
}
|
|
|
|
TEST_CASE("EPT: time buckets accumulate per state") {
|
|
EptStateMachine ept;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
ept.on_event(EptEvent::EnterStandby);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
ept.on_event(EptEvent::EnterProductive);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
ept.on_event(EptEvent::EnterStandby);
|
|
|
|
// Each bucket should have at least the slept time.
|
|
CHECK(ept.accumulated(EptState::NonScheduledTime).count() >= 5);
|
|
CHECK(ept.accumulated(EptState::Standby).count() >= 5);
|
|
CHECK(ept.accumulated(EptState::Productive).count() >= 5);
|
|
// Total = sum of buckets (modulo current-state in-progress dwell).
|
|
CHECK(ept.total_elapsed().count() >= 25);
|
|
}
|
|
|
|
TEST_CASE("EPT: current-state dwell is counted in accumulated()") {
|
|
EptStateMachine ept;
|
|
ept.on_event(EptEvent::EnterProductive);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(15));
|
|
// No transition out; accumulated should still include the live dwell.
|
|
CHECK(ept.accumulated(EptState::Productive).count() >= 10);
|
|
}
|
|
|
|
TEST_CASE("EPT: reset_history zeroes all buckets") {
|
|
EptStateMachine ept;
|
|
ept.on_event(EptEvent::EnterStandby);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
ept.on_event(EptEvent::EnterProductive);
|
|
REQUIRE(ept.accumulated(EptState::Standby).count() > 0);
|
|
|
|
ept.reset_history();
|
|
for (auto s : {EptState::NonScheduledTime, EptState::ScheduledDowntime,
|
|
EptState::UnscheduledDowntime, EptState::Engineering,
|
|
EptState::Standby}) {
|
|
CHECK(ept.accumulated(s).count() == 0);
|
|
}
|
|
// Current state (Productive) starts re-accumulating from zero.
|
|
CHECK(ept.accumulated(EptState::Productive).count() < 5);
|
|
}
|