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