1 #include "dbus-sdr/sensorutils.hpp"
2 
3 #include <cmath>
4 
5 #include "gtest/gtest.h"
6 
7 // There is a surprising amount of slop in the math,
8 // thanks to all the rounding and conversion.
9 // The "x" byte value can drift by up to 2 away, I have seen.
10 static constexpr int8_t expectedSlopX = 2;
11 
12 // Unlike expectedSlopX, this is a ratio, not an integer
13 // It scales based on the range of "y"
14 static constexpr double expectedSlopY = 0.01;
15 
16 // The algorithm here was copied from ipmitool
17 // sdr_convert_sensor_reading() function
18 // https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
19 double ipmitool_y_from_x(uint8_t x, int m, int k2_rExp, int b, int k1_bExp,
20                          bool bSigned)
21 {
22     double result;
23 
24     // Rename to exactly match names and types (except analog) from ipmitool
25     uint8_t val = x;
26     double k1 = k1_bExp;
27     double k2 = k2_rExp;
28     int analog = bSigned ? 2 : 0;
29 
30     // Begin paste here
31     // Only change is to comment out complicated structure in switch statement
32 
33     switch (/*sensor->cmn.unit.*/ analog)
34     {
35         case 0:
36             result = (double)(((m * val) + (b * pow(10, k1))) * pow(10, k2));
37             break;
38         case 1:
39             if (val & 0x80)
40                 val++;
41             /* Deliberately fall through to case 2. */
42             [[fallthrough]];
43         case 2:
44             result =
45                 (double)(((m * (int8_t)val) + (b * pow(10, k1))) * pow(10, k2));
46             break;
47         default:
48             /* Oops! This isn't an analog sensor. */
49             return 0.0;
50     }
51 
52     // End paste here
53     // Ignoring linearization curves and postprocessing that follows,
54     // assuming all sensors are perfectly linear
55     return result;
56 }
57 
58 void testValue(int x, double y, int16_t M, int8_t rExp, int16_t B, int8_t bExp,
59                bool bSigned, double yRange)
60 {
61     double yRoundtrip;
62     int result;
63 
64     // There is intentionally no exception catching here,
65     // because if getSensorAttributes() returned true,
66     // it is a promise that all of these should work.
67     if (bSigned)
68     {
69         int8_t expect = x;
70         int8_t actual = ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp,
71                                                        bSigned);
72 
73         result = actual;
74         yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);
75 
76         EXPECT_NEAR(actual, expect, expectedSlopX);
77     }
78     else
79     {
80         uint8_t expect = x;
81         uint8_t actual = ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp,
82                                                         bSigned);
83 
84         result = actual;
85         yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);
86 
87         EXPECT_NEAR(actual, expect, expectedSlopX);
88     }
89 
90     // Scale the amount of allowed slop in y based on range, so ratio similar
91     double yTolerance = yRange * expectedSlopY;
92     // double yError = std::abs(y - yRoundtrip);
93 
94     EXPECT_NEAR(y, yRoundtrip, yTolerance);
95 
96     char szFormat[1024];
97     sprintf(szFormat,
98             "Value | xExpect %4d | xResult %4d "
99             "| M %5d | rExp %3d "
100             "| B %5d | bExp %3d | bSigned %1d | y %18.3f | yRoundtrip %18.3f\n",
101             x, result, M, (int)rExp, B, (int)bExp, (int)bSigned, y, yRoundtrip);
102     std::cout << szFormat;
103 }
104 
105 void testBounds(double yMin, double yMax, bool bExpectedOutcome = true)
106 {
107     int16_t mValue;
108     int8_t rExp;
109     int16_t bValue;
110     int8_t bExp;
111     bool bSigned;
112     bool result;
113 
114     result = ipmi::getSensorAttributes(yMax, yMin, mValue, rExp, bValue, bExp,
115                                        bSigned);
116     EXPECT_EQ(result, bExpectedOutcome);
117 
118     if (!result)
119     {
120         return;
121     }
122 
123     char szFormat[1024];
124     sprintf(szFormat,
125             "Bounds | yMin %18.3f | yMax %18.3f | M %5d"
126             " | rExp %3d | B %5d | bExp %3d | bSigned %1d\n",
127             yMin, yMax, mValue, (int)rExp, bValue, (int)bExp, (int)bSigned);
128     std::cout << szFormat;
129 
130     double y50p = (yMin + yMax) / 2.0;
131 
132     // Average the average
133     double y25p = (yMin + y50p) / 2.0;
134     double y75p = (y50p + yMax) / 2.0;
135 
136     // This range value is only used for tolerance checking, not computation
137     double yRange = yMax - yMin;
138 
139     if (bSigned)
140     {
141         int8_t xMin = -128;
142         int8_t x25p = -64;
143         int8_t x50p = 0;
144         int8_t x75p = 64;
145         int8_t xMax = 127;
146 
147         testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
148         testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
149         testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
150         testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
151         testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
152     }
153     else
154     {
155         uint8_t xMin = 0;
156         uint8_t x25p = 64;
157         uint8_t x50p = 128;
158         uint8_t x75p = 192;
159         uint8_t xMax = 255;
160 
161         testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
162         testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
163         testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
164         testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
165         testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
166     }
167 }
168 
169 void testRanges(void)
170 {
171     // The ranges from the main TEST function
172     testBounds(0x0, 0xFF);
173     testBounds(-128, 127);
174     testBounds(0, 16000);
175     testBounds(0, 20);
176     testBounds(8000, 16000);
177     testBounds(-10, 10);
178     testBounds(0, 277);
179     testBounds(0, 0, false);
180     testBounds(10, 12);
181 
182     // Additional test cases recommended to me by hardware people
183     testBounds(-40, 150);
184     testBounds(0, 1);
185     testBounds(0, 2);
186     testBounds(0, 4);
187     testBounds(0, 8);
188     testBounds(35, 65);
189     testBounds(0, 18);
190     testBounds(0, 25);
191     testBounds(0, 80);
192     testBounds(0, 500);
193 
194     // Additional sanity checks
195     testBounds(0, 255);
196     testBounds(-255, 0);
197     testBounds(-255, 255);
198     testBounds(0, 1000);
199     testBounds(-1000, 0);
200     testBounds(-1000, 1000);
201     testBounds(0, 255000);
202     testBounds(-128000000, 127000000);
203     testBounds(-50000, 0);
204     testBounds(-40000, 10000);
205     testBounds(-30000, 20000);
206     testBounds(-20000, 30000);
207     testBounds(-10000, 40000);
208     testBounds(0, 50000);
209     testBounds(-1e3, 1e6);
210     testBounds(-1e6, 1e3);
211 
212     // Extreme ranges are now possible
213     testBounds(0, 1e10);
214     testBounds(0, 1e11);
215     testBounds(0, 1e12);
216     testBounds(0, 1e13, false);
217     testBounds(-1e10, 0);
218     testBounds(-1e11, 0);
219     testBounds(-1e12, 0);
220     testBounds(-1e13, 0, false);
221     testBounds(-1e9, 1e9);
222     testBounds(-1e10, 1e10);
223     testBounds(-1e11, 1e11);
224     testBounds(-1e12, 1e12, false);
225 
226     // Large multiplier but small offset
227     testBounds(1e4, 1e4 + 255);
228     testBounds(1e5, 1e5 + 255);
229     testBounds(1e6, 1e6 + 255);
230     testBounds(1e7, 1e7 + 255);
231     testBounds(1e8, 1e8 + 255);
232     testBounds(1e9, 1e9 + 255);
233     testBounds(1e10, 1e10 + 255, false);
234 
235     // Input validation against garbage
236     testBounds(0, INFINITY, false);
237     testBounds(-INFINITY, 0, false);
238     testBounds(-INFINITY, INFINITY, false);
239     testBounds(0, NAN, false);
240     testBounds(NAN, 0, false);
241     testBounds(NAN, NAN, false);
242 
243     // Noteworthy binary integers
244     testBounds(0, std::pow(2.0, 32.0) - 1.0);
245     testBounds(0, std::pow(2.0, 32.0));
246     testBounds(0.0 - std::pow(2.0, 31.0), std::pow(2.0, 31.0));
247     testBounds((0.0 - std::pow(2.0, 31.0)) - 1.0, std::pow(2.0, 31.0));
248 
249     // Similar but negative (note additional commented-out below)
250     testBounds(-1e1, (-1e1) + 255);
251     testBounds(-1e2, (-1e2) + 255);
252 
253     // Ranges of negative numbers (note additional commented-out below)
254     testBounds(-10400, -10000);
255     testBounds(-15000, -14000);
256     testBounds(-10000, -9000);
257     testBounds(-1000, -900);
258     testBounds(-1000, -800);
259     testBounds(-1000, -700);
260     testBounds(-1000, -740);
261 
262     // Very small ranges (note additional commented-out below)
263     testBounds(0, 0.1);
264     testBounds(0, 0.01);
265     testBounds(0, 0.001);
266     testBounds(0, 0.0001);
267     testBounds(0, 0.000001, false);
268 
269 #if 0
270     // TODO(): The algorithm in this module is better than it was before,
271     // but the resulting value of X is still wrong under certain conditions,
272     // such as when the range between min and max is around 255,
273     // and the offset is fairly extreme compared to the multiplier.
274     // Not sure why this is, but these ranges are contrived,
275     // and real-world examples would most likely never be this way.
276     testBounds(-10290, -10000);
277     testBounds(-10280, -10000);
278     testBounds(-10275,-10000);
279     testBounds(-10270,-10000);
280     testBounds(-10265,-10000);
281     testBounds(-10260,-10000);
282     testBounds(-10255,-10000);
283     testBounds(-10250,-10000);
284     testBounds(-10245,-10000);
285     testBounds(-10256,-10000);
286     testBounds(-10512, -10000);
287     testBounds(-11024, -10000);
288 
289     // TODO(): This also fails, due to extreme small range, loss of precision
290     testBounds(0, 0.00001);
291 
292     // TODO(): Interestingly, if bSigned is forced false,
293     // causing "x" to have range of (0,255) instead of (-128,127),
294     // these test cases change from failing to passing!
295     // Not sure why this is, perhaps a mathematician might know.
296     testBounds(-10300, -10000);
297     testBounds(-1000,-750);
298     testBounds(-1e3, (-1e3) + 255);
299     testBounds(-1e4, (-1e4) + 255);
300     testBounds(-1e5, (-1e5) + 255);
301     testBounds(-1e6, (-1e6) + 255);
302 #endif
303 }
304 
305 TEST(sensorutils, TranslateToIPMI)
306 {
307     /*bool getSensorAttributes(double maxValue, double minValue, int16_t
308        &mValue, int8_t &rExp, int16_t &bValue, int8_t &bExp, bool &bSigned); */
309     // normal unsigned sensor
310     double maxValue = 0xFF;
311     double minValue = 0x0;
312     int16_t mValue;
313     int8_t rExp;
314     int16_t bValue;
315     int8_t bExp;
316     bool bSigned;
317     bool result;
318 
319     uint8_t scaledVal;
320 
321     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
322                                        bExp, bSigned);
323     EXPECT_EQ(result, true);
324     if (result)
325     {
326         EXPECT_EQ(bSigned, false);
327         EXPECT_EQ(mValue, 1);
328         EXPECT_EQ(rExp, 0);
329         EXPECT_EQ(bValue, 0);
330         EXPECT_EQ(bExp, 0);
331     }
332     double expected = 0x50;
333     scaledVal = ipmi::scaleIPMIValueFromDouble(0x50, mValue, rExp, bValue, bExp,
334                                                bSigned);
335     EXPECT_NEAR(scaledVal, expected, expected * 0.01);
336 
337     // normal signed sensor
338     maxValue = 127;
339     minValue = -128;
340 
341     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
342                                        bExp, bSigned);
343     EXPECT_EQ(result, true);
344 
345     if (result)
346     {
347         EXPECT_EQ(bSigned, true);
348         EXPECT_EQ(mValue, 1);
349         EXPECT_EQ(rExp, 0);
350         EXPECT_EQ(bValue, 0);
351         EXPECT_EQ(bExp, 0);
352     }
353 
354     // check negative values
355     expected = 236; // 2s compliment -20
356     scaledVal = ipmi::scaleIPMIValueFromDouble(-20, mValue, rExp, bValue, bExp,
357                                                bSigned);
358     EXPECT_NEAR(scaledVal, expected, expected * 0.01);
359 
360     // fan example
361     maxValue = 16000;
362     minValue = 0;
363 
364     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
365                                        bExp, bSigned);
366     EXPECT_EQ(result, true);
367     if (result)
368     {
369         EXPECT_EQ(bSigned, false);
370         EXPECT_EQ(mValue, floor((16000.0 / 0xFF) + 0.5));
371         EXPECT_EQ(rExp, 0);
372         EXPECT_EQ(bValue, 0);
373         EXPECT_EQ(bExp, 0);
374     }
375 
376     // voltage sensor example
377     maxValue = 20;
378     minValue = 0;
379 
380     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
381                                        bExp, bSigned);
382     EXPECT_EQ(result, true);
383     if (result)
384     {
385         EXPECT_EQ(bSigned, false);
386         EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
387         EXPECT_EQ(rExp, -3);
388         EXPECT_EQ(bValue, 0);
389         EXPECT_EQ(bExp, 0);
390     }
391     scaledVal = ipmi::scaleIPMIValueFromDouble(12.2, mValue, rExp, bValue, bExp,
392                                                bSigned);
393 
394     expected = 12.2 / (mValue * std::pow(10, rExp));
395     EXPECT_NEAR(scaledVal, expected, expected * 0.01);
396 
397     // shifted fan example
398     maxValue = 16000;
399     minValue = 8000;
400 
401     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
402                                        bExp, bSigned);
403     EXPECT_EQ(result, true);
404 
405     if (result)
406     {
407         EXPECT_EQ(bSigned, false);
408         EXPECT_EQ(mValue, floor(((8000.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
409         EXPECT_EQ(rExp, -1);
410         EXPECT_EQ(bValue, 8);
411         EXPECT_EQ(bExp, 4);
412     }
413 
414     // signed voltage sensor example
415     maxValue = 10;
416     minValue = -10;
417 
418     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
419                                        bExp, bSigned);
420     EXPECT_EQ(result, true);
421     if (result)
422     {
423         EXPECT_EQ(bSigned, true);
424         EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
425         EXPECT_EQ(rExp, -3);
426         // Although this seems like a weird magic number,
427         // it is because the range (-128,127) is not symmetrical about zero,
428         // unlike the range (-10,10), so this introduces some distortion.
429         EXPECT_EQ(bValue, 392);
430         EXPECT_EQ(bExp, -1);
431     }
432 
433     scaledVal = ipmi::scaleIPMIValueFromDouble(5, mValue, rExp, bValue, bExp,
434                                                bSigned);
435 
436     expected = 5 / (mValue * std::pow(10, rExp));
437     EXPECT_NEAR(scaledVal, expected, expected * 0.01);
438 
439     // reading = max example
440     maxValue = 277;
441     minValue = 0;
442 
443     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
444                                        bExp, bSigned);
445     EXPECT_EQ(result, true);
446     if (result)
447     {
448         EXPECT_EQ(bSigned, false);
449     }
450 
451     scaledVal = ipmi::scaleIPMIValueFromDouble(maxValue, mValue, rExp, bValue,
452                                                bExp, bSigned);
453 
454     expected = 0xFF;
455     EXPECT_NEAR(scaledVal, expected, expected * 0.01);
456 
457     // 0, 0 failure
458     maxValue = 0;
459     minValue = 0;
460     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
461                                        bExp, bSigned);
462     EXPECT_EQ(result, false);
463 
464     // too close *success* (was previously failure!)
465     maxValue = 12;
466     minValue = 10;
467     result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
468                                        bExp, bSigned);
469     EXPECT_EQ(result, true);
470     if (result)
471     {
472         EXPECT_EQ(bSigned, false);
473         EXPECT_EQ(mValue, floor(((2.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
474         EXPECT_EQ(rExp, -4);
475         EXPECT_EQ(bValue, 1);
476         EXPECT_EQ(bExp, 5);
477     }
478 }
479 
480 TEST(sensorUtils, TestRanges)
481 {
482     // Additional test ranges, each running through a series of values,
483     // to make sure the values of "x" and "y" go together and make sense,
484     // for the resulting scaling attributes from each range.
485     // Unlike the TranslateToIPMI test, exact matches of the
486     // getSensorAttributes() results (the coefficients) are not required,
487     // because they are tested through actual use, relating "x" to "y".
488     testRanges();
489 }
490