/** * @file TriggerEngineGTest.cpp * @brief Unit tests for the StreamHub hub-side trigger FSM. * * @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and * the Development of Fusion Energy ('Fusion for Energy'). * Licensed under the EUPL, Version 1.1 or - as soon they will be approved * by the European Commission - subsequent versions of the EUPL (the "Licence") * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl * * @warning Unless required by applicable law or agreed to in writing, * software distributed under the Licence is distributed on an "AS IS" * basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence permissions and limitations under the Licence. */ #include "TriggerEngine.h" #include using namespace StreamHub; using MARTe::float64; static TriggerConfig MakeConfig(TrigEdge edge, float64 threshold, float64 windowSec, float64 prePercent, TrigAcqMode mode = kTrigNormal) { TriggerConfig cfg; cfg.signalKey = "src:sig"; cfg.edge = edge; cfg.threshold = threshold; cfg.windowSec = windowSec; cfg.prePercent = prePercent; cfg.mode = mode; return cfg; } TEST(TriggerEngineGTest, TestInitialState) { TriggerEngine eng; EXPECT_EQ(kTrigIdle, eng.GetState()); EXPECT_FALSE(eng.GetStopped()); float64 tt, pre, post; EXPECT_FALSE(eng.GetFiredWindow(tt, pre, post)); } TEST(TriggerEngineGTest, TestIdleIgnoresSamples) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); /* would fire if armed */ EXPECT_EQ(kTrigIdle, eng.GetState()); } TEST(TriggerEngineGTest, TestRisingEdgeFires) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); EXPECT_EQ(kTrigArmed, eng.GetState()); eng.CheckSample(1.0, 0.0); /* primes prevValue */ eng.CheckSample(2.0, 1.0); /* 0.0 < 0.5 <= 1.0 → fire */ EXPECT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(2.0, tt); EXPECT_DOUBLE_EQ(0.2, pre); /* 1.0 s × 20 % */ EXPECT_DOUBLE_EQ(0.8, post); } TEST(TriggerEngineGTest, TestFirstSampleNeverFires) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); /* First sample above threshold must only prime the detector */ eng.CheckSample(1.0, 2.0); EXPECT_EQ(kTrigArmed, eng.GetState()); } TEST(TriggerEngineGTest, TestFallingEdgeFires) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeFalling, 0.5, 1.0, 50.0)); eng.Arm(); eng.CheckSample(1.0, 1.0); eng.CheckSample(2.0, 1.5); /* rising: must not fire */ EXPECT_EQ(kTrigArmed, eng.GetState()); eng.CheckSample(3.0, 0.2); /* 1.5 > 0.5 >= 0.2 → fire */ EXPECT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(3.0, tt); EXPECT_DOUBLE_EQ(0.5, pre); EXPECT_DOUBLE_EQ(0.5, post); } TEST(TriggerEngineGTest, TestBothEdgesFire) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeBoth, 0.0, 1.0, 0.0)); eng.Arm(); eng.CheckSample(1.0, -1.0); eng.CheckSample(2.0, 1.0); /* rising through 0 */ EXPECT_EQ(kTrigCollecting, eng.GetState()); eng.Arm(); eng.CheckSample(3.0, 1.0); eng.CheckSample(4.0, -1.0); /* falling through 0 */ EXPECT_EQ(kTrigCollecting, eng.GetState()); } TEST(TriggerEngineGTest, TestNoFireBelowThreshold) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 5.0, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); eng.CheckSample(3.0, 4.9); EXPECT_EQ(kTrigArmed, eng.GetState()); } TEST(TriggerEngineGTest, TestConfigClamping) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.0, 100.0, 150.0)); TriggerConfig cfg = eng.GetConfig(); EXPECT_DOUBLE_EQ(60.0, cfg.windowSec); EXPECT_DOUBLE_EQ(100.0, cfg.prePercent); eng.SetConfig(MakeConfig(kEdgeRising, 0.0, 1.0e-6, -5.0)); cfg = eng.GetConfig(); EXPECT_DOUBLE_EQ(1.0e-4, cfg.windowSec); EXPECT_DOUBLE_EQ(0.0, cfg.prePercent); } TEST(TriggerEngineGTest, TestConfigEpochIncrements) { TriggerEngine eng; MARTe::uint32 e0 = eng.GetConfigEpoch(); eng.SetConfig(MakeConfig(kEdgeRising, 0.0, 1.0, 20.0)); EXPECT_EQ(e0 + 1u, eng.GetConfigEpoch()); eng.SetConfig(MakeConfig(kEdgeFalling, 1.0, 2.0, 30.0)); EXPECT_EQ(e0 + 2u, eng.GetConfigEpoch()); } TEST(TriggerEngineGTest, TestMarkTriggeredOnlyFromCollecting) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); /* Not collecting: no-op */ eng.MarkTriggered(); EXPECT_EQ(kTrigIdle, eng.GetState()); eng.Arm(); eng.MarkTriggered(); EXPECT_EQ(kTrigArmed, eng.GetState()); /* Collecting → triggered */ eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); eng.MarkTriggered(); EXPECT_EQ(kTrigTriggered, eng.GetState()); } TEST(TriggerEngineGTest, TestDisarmResetsEverything) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.SetStopped(true); eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); eng.Disarm(); EXPECT_EQ(kTrigIdle, eng.GetState()); EXPECT_FALSE(eng.GetStopped()); float64 tt, pre, post; EXPECT_FALSE(eng.GetFiredWindow(tt, pre, post)); } TEST(TriggerEngineGTest, TestRearmResetsEdgeDetection) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); eng.MarkTriggered(); ASSERT_EQ(kTrigTriggered, eng.GetState()); /* Re-arm: first sample only primes even if it crosses the threshold * relative to the value seen before the re-arm. */ eng.Arm(); EXPECT_EQ(kTrigArmed, eng.GetState()); eng.CheckSample(3.0, 0.0); EXPECT_EQ(kTrigArmed, eng.GetState()); eng.CheckSample(4.0, 1.0); EXPECT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(4.0, tt); } /* An edge that arrives while a capture is still being collected, or while it is * being handed out, used to be dropped on the floor: CheckSample returned early * in every state but ARMED, and the automatic rearm then waited for a FRESH * edge. The engine is therefore deaf from its own trigger point until the * capture has been harvested — a post-window — and then for the holdoff on top. * * On a sparse pulse train that rounds the capture spacing up to a whole pulse * period: at the default 1 s window the blind stretch is 1 s, so a 1 Hz train * was caught at 0.5 Hz and a wider window lost whole multiples. Remembering the * edge costs nothing, because the capture is built from the edge's own * timestamp out of a ring that still holds everything around it. */ TEST(TriggerEngineGTest, TestEdgeDuringCaptureFiresOnRearm) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); /* post = 0.8 */ eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); /* A second pulse, clear of the capture in flight (1.1 + 0.8 = 1.9). */ eng.CheckSample(2.4, 0.0); eng.CheckSample(2.5, 1.0); eng.MarkTriggered(); eng.Rearm(); EXPECT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(2.5, tt); /* the remembered edge, not the rearm instant */ } /* The remembered edge must not be one the capture in flight already covers, nor * one inside the holdoff — that guard exists to stop the ringing of a single * event re-triggering on itself, and it is measured from the trigger point, so * the two overlap rather than add. */ TEST(TriggerEngineGTest, TestEdgeInsideOwnCaptureIsNotRemembered) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); /* post = 0.8 */ eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); /* Inside 1.1 + max(0.8, 0.2 holdoff) = 1.9: the capture owns this stretch. */ eng.CheckSample(1.4, 0.0); eng.CheckSample(1.5, 1.0); eng.MarkTriggered(); eng.Rearm(); EXPECT_EQ(kTrigArmed, eng.GetState()); } /* A holdoff longer than the post-window is what decides the guard interval. */ TEST(TriggerEngineGTest, TestHoldoffOutlastingPostWindowGovernsRearm) { TriggerEngine eng; TriggerConfig cfg = MakeConfig(kEdgeRising, 0.5, 1.0, 80.0); /* post = 0.2 */ cfg.holdoffSec = 2.0; eng.SetConfig(cfg); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); /* Past the post-window but inside the holdoff (1.1 + 2.0 = 3.1): ignored. */ eng.CheckSample(1.9, 0.0); eng.CheckSample(2.0, 1.0); /* Clear of it: remembered. */ eng.CheckSample(3.4, 0.0); eng.CheckSample(3.5, 1.0); eng.MarkTriggered(); eng.Rearm(); ASSERT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(3.5, tt); } /* Only the first qualifying edge is worth keeping; a later one would deliver * the same capture a pulse further on and skip the one in between. */ TEST(TriggerEngineGTest, TestFirstQualifyingEdgeWins) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); eng.CheckSample(2.4, 0.0); eng.CheckSample(2.5, 1.0); /* first past 1.9 */ eng.CheckSample(3.4, 0.0); eng.CheckSample(3.5, 1.0); /* later, must not displace it */ eng.MarkTriggered(); eng.Rearm(); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(2.5, tt); } /* Arm() is the user's own arm: it asks for the next event, not for one that has * already been and gone, so it drops anything remembered. */ TEST(TriggerEngineGTest, TestUserArmDiscardsRememberedEdge) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); eng.CheckSample(2.4, 0.0); eng.CheckSample(2.5, 1.0); eng.MarkTriggered(); eng.Arm(); EXPECT_EQ(kTrigArmed, eng.GetState()); } /* Reconfiguring drops it too: the edge would be latched against a window it was * never judged against. */ TEST(TriggerEngineGTest, TestSetConfigDiscardsRememberedEdge) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); eng.CheckSample(2.4, 0.0); eng.CheckSample(2.5, 1.0); eng.MarkTriggered(); eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 2.0, 20.0)); eng.Rearm(); EXPECT_EQ(kTrigArmed, eng.GetState()); } /* The comparator keeps running through the dead time, so the first sample after * an automatic rearm is measured against its real predecessor rather than being * spent seeding one. A rearm landing mid-pulse would otherwise miss that * pulse's edge as well as the ones it slept through. */ TEST(TriggerEngineGTest, TestRearmKeepsTrackingTheLevel) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); /* Falls back low during the capture: no rising edge, nothing remembered, * but the level is now known to be 0. */ eng.CheckSample(2.0, 0.0); eng.MarkTriggered(); eng.Rearm(); ASSERT_EQ(kTrigArmed, eng.GetState()); eng.CheckSample(2.1, 1.0); /* 0.0 → 1.0 across 0.5, on the very first sample */ EXPECT_EQ(kTrigCollecting, eng.GetState()); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(2.1, tt); } /* Idle is genuinely deaf: nothing is tracked and nothing is remembered, so a * disarmed hub cannot fire the moment it is armed again. */ TEST(TriggerEngineGTest, TestDisarmDiscardsRememberedEdge) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(1.1, 1.0); eng.CheckSample(2.4, 0.0); eng.CheckSample(2.5, 1.0); eng.MarkTriggered(); eng.Disarm(); eng.Rearm(); EXPECT_EQ(kTrigArmed, eng.GetState()); } TEST(TriggerEngineGTest, TestStoppedFlag) { TriggerEngine eng; EXPECT_FALSE(eng.GetStopped()); eng.SetStopped(true); EXPECT_TRUE(eng.GetStopped()); eng.SetStopped(false); EXPECT_FALSE(eng.GetStopped()); } TEST(TriggerEngineGTest, TestWindowLatchedAtFireTime) { TriggerEngine eng; eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); eng.Arm(); eng.CheckSample(1.0, 0.0); eng.CheckSample(2.0, 1.0); ASSERT_EQ(kTrigCollecting, eng.GetState()); /* Config edits after the fire must not change the latched window */ eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 10.0, 50.0)); float64 tt, pre, post; ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post)); EXPECT_DOUBLE_EQ(0.2, pre); EXPECT_DOUBLE_EQ(0.8, post); }