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