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