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