Skip to content

Commit 2ef270e

Browse files
committed
Create test_hysteresis.cpp
1 parent bba04c3 commit 2ef270e

File tree

1 file changed

+249
-0
lines changed

1 file changed

+249
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
#include <gtest/gtest.h>
2+
#include "sp140/simple_monitor.h"
3+
#include "sp140/monitor_config.h"
4+
5+
// Fake logger to capture log calls
6+
class FakeLogger : public ILogger {
7+
public:
8+
struct Entry {
9+
SensorID id;
10+
AlertLevel lvl;
11+
float fval;
12+
bool bval;
13+
bool isBool;
14+
};
15+
std::vector<Entry> entries;
16+
17+
void log(SensorID id, AlertLevel lvl, float v) override {
18+
entries.push_back({id, lvl, v, false, false});
19+
}
20+
void log(SensorID id, AlertLevel lvl, bool v) override {
21+
entries.push_back({id, lvl, 0.0f, v, true});
22+
}
23+
};
24+
25+
TEST(HysteresisMonitor, PreventsBouncingAroundWarningHigh) {
26+
FakeLogger logger;
27+
28+
// Create monitor that warns >50, crit > 80, with 2.0 hysteresis
29+
Thresholds thr{.warnLow = -100.0f, .warnHigh = 50.0f, .critLow = -200.0f, .critHigh = 80.0f, .hysteresis = 2.0f};
30+
31+
float sensorVal = 0.0f;
32+
HysteresisSensorMonitor mon(SensorID::CPU_Temp, SensorCategory::INTERNAL, thr, [&]() { return sensorVal; }, &logger);
33+
34+
// Start in OK
35+
mon.check();
36+
EXPECT_TRUE(logger.entries.empty());
37+
38+
// Oscillate around warning threshold - should NOT bounce
39+
sensorVal = 49.5f; // Still OK
40+
mon.check();
41+
EXPECT_TRUE(logger.entries.empty());
42+
43+
sensorVal = 50.2f; // Crosses into warning
44+
mon.check();
45+
ASSERT_EQ(logger.entries.size(), 1u);
46+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
47+
48+
// Oscillation around threshold - should NOT bounce back to OK
49+
sensorVal = 49.8f; // Within hysteresis deadband
50+
mon.check();
51+
EXPECT_EQ(logger.entries.size(), 1u); // No new entry
52+
53+
sensorVal = 50.1f; // Still within hysteresis
54+
mon.check();
55+
EXPECT_EQ(logger.entries.size(), 1u); // No new entry
56+
57+
sensorVal = 49.9f; // Still within hysteresis
58+
mon.check();
59+
EXPECT_EQ(logger.entries.size(), 1u); // No new entry
60+
61+
// Only when we go significantly below threshold should it clear
62+
sensorVal = 47.9f; // Below warnHigh - hysteresis (50 - 2 = 48)
63+
mon.check();
64+
ASSERT_EQ(logger.entries.size(), 2u);
65+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::OK);
66+
}
67+
68+
TEST(HysteresisMonitor, PreventsBouncingAroundWarningLow) {
69+
FakeLogger logger;
70+
71+
// Create monitor that warns <10, crit < 0, with 2.0 hysteresis
72+
Thresholds thr{.warnLow = 10.0f, .warnHigh = 200.0f, .critLow = 0.0f, .critHigh = 300.0f, .hysteresis = 2.0f};
73+
74+
float sensorVal = 50.0f;
75+
HysteresisSensorMonitor mon(SensorID::CPU_Temp, SensorCategory::INTERNAL, thr, [&]() { return sensorVal; }, &logger);
76+
77+
// Start in OK
78+
mon.check();
79+
EXPECT_TRUE(logger.entries.empty());
80+
81+
// Drop to warning
82+
sensorVal = 8.0f;
83+
mon.check();
84+
ASSERT_EQ(logger.entries.size(), 1u);
85+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_LOW);
86+
87+
// Oscillation around threshold - should NOT bounce
88+
sensorVal = 10.5f; // Within hysteresis deadband
89+
mon.check();
90+
EXPECT_EQ(logger.entries.size(), 1u); // No new entry
91+
92+
sensorVal = 9.8f; // Still within hysteresis
93+
mon.check();
94+
EXPECT_EQ(logger.entries.size(), 1u); // No new entry
95+
96+
// Only when we go significantly above threshold should it clear
97+
sensorVal = 12.1f; // Above warnLow + hysteresis (10 + 2 = 12)
98+
mon.check();
99+
ASSERT_EQ(logger.entries.size(), 2u);
100+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::OK);
101+
}
102+
103+
TEST(HysteresisMonitor, EscalatesToCriticalWhenAppropriate) {
104+
FakeLogger logger;
105+
106+
// Create monitor that warns >50, crit > 80, with 2.0 hysteresis
107+
Thresholds thr{.warnLow = -100.0f, .warnHigh = 50.0f, .critLow = -200.0f, .critHigh = 80.0f, .hysteresis = 2.0f};
108+
109+
float sensorVal = 0.0f;
110+
HysteresisSensorMonitor mon(SensorID::CPU_Temp, SensorCategory::INTERNAL, thr, [&]() { return sensorVal; }, &logger);
111+
112+
// Start in OK
113+
mon.check();
114+
EXPECT_TRUE(logger.entries.empty());
115+
116+
// Go to warning
117+
sensorVal = 55.0f;
118+
mon.check();
119+
ASSERT_EQ(logger.entries.size(), 1u);
120+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
121+
122+
// Escalate to critical
123+
sensorVal = 85.0f;
124+
mon.check();
125+
ASSERT_EQ(logger.entries.size(), 2u);
126+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::CRIT_HIGH);
127+
128+
// Try to de-escalate but stay within hysteresis
129+
sensorVal = 79.5f; // Within hysteresis (80 - 2 = 78)
130+
mon.check();
131+
EXPECT_EQ(logger.entries.size(), 2u); // Should NOT de-escalate
132+
133+
// Only de-escalate when significantly below critical threshold
134+
sensorVal = 77.9f; // Below critHigh - hysteresis (80 - 2 = 78)
135+
mon.check();
136+
ASSERT_EQ(logger.entries.size(), 3u);
137+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
138+
}
139+
140+
TEST(HysteresisMonitor, ZeroHysteresisBehavesLikeRegularMonitor) {
141+
FakeLogger logger;
142+
143+
// Create monitor with zero hysteresis - should behave like SensorMonitor
144+
Thresholds thr{.warnLow = -100.0f, .warnHigh = 50.0f, .critLow = -200.0f, .critHigh = 80.0f, .hysteresis = 0.0f};
145+
146+
float sensorVal = 0.0f;
147+
HysteresisSensorMonitor mon(SensorID::CPU_Temp, SensorCategory::INTERNAL, thr, [&]() { return sensorVal; }, &logger);
148+
149+
// Start in OK
150+
mon.check();
151+
EXPECT_TRUE(logger.entries.empty());
152+
153+
// Immediately go to warning
154+
sensorVal = 55.0f;
155+
mon.check();
156+
ASSERT_EQ(logger.entries.size(), 1u);
157+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
158+
159+
// Immediately go back to OK
160+
sensorVal = 45.0f;
161+
mon.check();
162+
ASSERT_EQ(logger.entries.size(), 2u);
163+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::OK);
164+
}
165+
166+
TEST(HysteresisMonitor, HandlesAllThresholdTransitions) {
167+
FakeLogger logger;
168+
169+
// Symmetric thresholds with hysteresis
170+
Thresholds thr{.warnLow = 20.0f, .warnHigh = 80.0f, .critLow = 10.0f, .critHigh = 90.0f, .hysteresis = 2.0f};
171+
172+
float sensorVal = 50.0f; // Start in middle
173+
HysteresisSensorMonitor mon(SensorID::CPU_Temp, SensorCategory::INTERNAL, thr, [&]() { return sensorVal; }, &logger);
174+
175+
// Start in OK
176+
mon.check();
177+
EXPECT_TRUE(logger.entries.empty());
178+
179+
// Test high side warning
180+
sensorVal = 85.0f;
181+
mon.check();
182+
ASSERT_EQ(logger.entries.size(), 1u);
183+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
184+
185+
// Test high side critical
186+
sensorVal = 95.0f;
187+
mon.check();
188+
ASSERT_EQ(logger.entries.size(), 2u);
189+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::CRIT_HIGH);
190+
191+
// Test de-escalation from critical to warning
192+
sensorVal = 87.9f; // Below critHigh - hysteresis (90 - 2 = 88)
193+
mon.check();
194+
ASSERT_EQ(logger.entries.size(), 3u);
195+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
196+
197+
// Test clearing warning high
198+
sensorVal = 77.9f; // Below warnHigh - hysteresis (80 - 2 = 78)
199+
mon.check();
200+
ASSERT_EQ(logger.entries.size(), 4u);
201+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::OK);
202+
203+
// Test low side warning
204+
sensorVal = 15.0f;
205+
mon.check();
206+
ASSERT_EQ(logger.entries.size(), 5u);
207+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_LOW);
208+
209+
// Test low side critical
210+
sensorVal = 5.0f;
211+
mon.check();
212+
ASSERT_EQ(logger.entries.size(), 6u);
213+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::CRIT_LOW);
214+
215+
// Test de-escalation from critical to warning low
216+
sensorVal = 12.1f; // Above critLow + hysteresis (10 + 2 = 12)
217+
mon.check();
218+
ASSERT_EQ(logger.entries.size(), 7u);
219+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_LOW);
220+
221+
// Test clearing warning low
222+
sensorVal = 22.1f; // Above warnLow + hysteresis (20 + 2 = 22)
223+
mon.check();
224+
ASSERT_EQ(logger.entries.size(), 8u);
225+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::OK);
226+
}
227+
228+
TEST(HysteresisMonitor, RespectsSensorIDAndCategory) {
229+
FakeLogger logger;
230+
231+
Thresholds thr{.warnLow = -100.0f, .warnHigh = 50.0f, .critLow = -200.0f, .critHigh = 80.0f, .hysteresis = 2.0f};
232+
233+
float sensorVal = 55.0f;
234+
HysteresisSensorMonitor mon(SensorID::ESC_MOS_Temp, SensorCategory::ESC, thr, [&]() { return sensorVal; }, &logger);
235+
236+
mon.check();
237+
ASSERT_EQ(logger.entries.size(), 1u);
238+
EXPECT_EQ(logger.entries.back().id, SensorID::ESC_MOS_Temp);
239+
EXPECT_EQ(logger.entries.back().lvl, AlertLevel::WARN_HIGH);
240+
241+
// Verify sensor properties
242+
EXPECT_EQ(mon.getSensorID(), SensorID::ESC_MOS_Temp);
243+
EXPECT_EQ(mon.getCategory(), SensorCategory::ESC);
244+
}
245+
246+
int main(int argc, char **argv) {
247+
testing::InitGoogleTest(&argc, argv);
248+
return RUN_ALL_TESTS();
249+
}

0 commit comments

Comments
 (0)