| #include "sensorutils.hpp" |
| |
| #include <cmath> |
| |
| #include "gtest/gtest.h" |
| |
| // There is a surprising amount of slop in the math, |
| // thanks to all the rounding and conversion. |
| // The "x" byte value can drift by up to 2 away, I have seen. |
| static constexpr int8_t expectedSlopX = 2; |
| |
| // Unlike expectedSlopX, this is a ratio, not an integer |
| // It scales based on the range of "y" |
| static constexpr double expectedSlopY = 0.01; |
| |
| // The algorithm here was copied from ipmitool |
| // sdr_convert_sensor_reading() function |
| // https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360 |
| double ipmitool_y_from_x(uint8_t x, int m, int k2_rExp, int b, int k1_bExp, |
| bool bSigned) |
| { |
| double result; |
| |
| // Rename to exactly match names and types (except analog) from ipmitool |
| uint8_t val = x; |
| double k1 = k1_bExp; |
| double k2 = k2_rExp; |
| int analog = bSigned ? 2 : 0; |
| |
| // Begin paste here |
| // Only change is to comment out complicated structure in switch statement |
| |
| switch (/*sensor->cmn.unit.*/ analog) |
| { |
| case 0: |
| result = (double)(((m * val) + (b * pow(10, k1))) * pow(10, k2)); |
| break; |
| case 1: |
| if (val & 0x80) |
| val++; |
| /* Deliberately fall through to case 2. */ |
| [[fallthrough]]; |
| case 2: |
| result = |
| (double)(((m * (int8_t)val) + (b * pow(10, k1))) * pow(10, k2)); |
| break; |
| default: |
| /* Oops! This isn't an analog sensor. */ |
| return 0.0; |
| } |
| |
| // End paste here |
| // Ignoring linearization curves and postprocessing that follows, |
| // assuming all sensors are perfectly linear |
| return result; |
| } |
| |
| void testValue(int x, double y, int16_t M, int8_t rExp, int16_t B, int8_t bExp, |
| bool bSigned, double yRange) |
| { |
| double yRoundtrip; |
| int result; |
| |
| // There is intentionally no exception catching here, |
| // because if getSensorAttributes() returned true, |
| // it is a promise that all of these should work. |
| if (bSigned) |
| { |
| int8_t expect = x; |
| int8_t actual = ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, |
| bSigned); |
| |
| result = actual; |
| yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned); |
| |
| EXPECT_NEAR(actual, expect, expectedSlopX); |
| } |
| else |
| { |
| uint8_t expect = x; |
| uint8_t actual = ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, |
| bSigned); |
| |
| result = actual; |
| yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned); |
| |
| EXPECT_NEAR(actual, expect, expectedSlopX); |
| } |
| |
| // Scale the amount of allowed slop in y based on range, so ratio similar |
| double yTolerance = yRange * expectedSlopY; |
| |
| EXPECT_NEAR(y, yRoundtrip, yTolerance); |
| |
| char szFormat[1024]; |
| sprintf(szFormat, |
| "Value | xExpect %4d | xResult %4d " |
| "| M %5d | rExp %3d " |
| "| B %5d | bExp %3d | bSigned %1d | y %18.3f | yRoundtrip %18.3f\n", |
| x, result, M, (int)rExp, B, (int)bExp, (int)bSigned, y, yRoundtrip); |
| std::cout << szFormat; |
| } |
| |
| void testBounds(double yMin, double yMax, bool bExpectedOutcome = true) |
| { |
| int16_t mValue; |
| int8_t rExp; |
| int16_t bValue; |
| int8_t bExp; |
| bool bSigned; |
| bool result; |
| |
| result = ipmi::getSensorAttributes(yMax, yMin, mValue, rExp, bValue, bExp, |
| bSigned); |
| EXPECT_EQ(result, bExpectedOutcome); |
| |
| if (!result) |
| { |
| return; |
| } |
| |
| char szFormat[1024]; |
| sprintf(szFormat, |
| "Bounds | yMin %18.3f | yMax %18.3f | M %5d" |
| " | rExp %3d | B %5d | bExp %3d | bSigned %1d\n", |
| yMin, yMax, mValue, (int)rExp, bValue, (int)bExp, (int)bSigned); |
| std::cout << szFormat; |
| |
| double y50p = (yMin + yMax) / 2.0; |
| |
| // Average the average |
| double y25p = (yMin + y50p) / 2.0; |
| double y75p = (y50p + yMax) / 2.0; |
| |
| // This range value is only used for tolerance checking, not computation |
| double yRange = yMax - yMin; |
| |
| if (bSigned) |
| { |
| int8_t xMin = -128; |
| int8_t x25p = -64; |
| int8_t x50p = 0; |
| int8_t x75p = 64; |
| int8_t xMax = 127; |
| |
| testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange); |
| } |
| else |
| { |
| uint8_t xMin = 0; |
| uint8_t x25p = 64; |
| uint8_t x50p = 128; |
| uint8_t x75p = 192; |
| uint8_t xMax = 255; |
| |
| testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange); |
| testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange); |
| } |
| } |
| |
| void testRanges(void) |
| { |
| // The ranges from the main TEST function |
| testBounds(0x0, 0xFF); |
| testBounds(-128, 127); |
| testBounds(0, 16000); |
| testBounds(0, 20); |
| testBounds(8000, 16000); |
| testBounds(-10, 10); |
| testBounds(0, 277); |
| testBounds(0, 0, false); |
| testBounds(10, 12); |
| |
| // Additional test cases recommended to me by hardware people |
| testBounds(-40, 150); |
| testBounds(0, 1); |
| testBounds(0, 2); |
| testBounds(0, 4); |
| testBounds(0, 8); |
| testBounds(35, 65); |
| testBounds(0, 18); |
| testBounds(0, 25); |
| testBounds(0, 80); |
| testBounds(0, 500); |
| |
| // Additional sanity checks |
| testBounds(0, 255); |
| testBounds(-255, 0); |
| testBounds(-255, 255); |
| testBounds(0, 1000); |
| testBounds(-1000, 0); |
| testBounds(-1000, 1000); |
| testBounds(0, 255000); |
| testBounds(-128000000, 127000000); |
| testBounds(-50000, 0); |
| testBounds(-40000, 10000); |
| testBounds(-30000, 20000); |
| testBounds(-20000, 30000); |
| testBounds(-10000, 40000); |
| testBounds(0, 50000); |
| testBounds(-1e3, 1e6); |
| testBounds(-1e6, 1e3); |
| |
| // Extreme ranges are now possible |
| testBounds(0, 1e10); |
| testBounds(0, 1e11); |
| testBounds(0, 1e12); |
| testBounds(0, 1e13, false); |
| testBounds(-1e10, 0); |
| testBounds(-1e11, 0); |
| testBounds(-1e12, 0); |
| testBounds(-1e13, 0, false); |
| testBounds(-1e9, 1e9); |
| testBounds(-1e10, 1e10); |
| testBounds(-1e11, 1e11); |
| testBounds(-1e12, 1e12, false); |
| |
| // Large multiplier but small offset |
| testBounds(1e4, 1e4 + 255); |
| testBounds(1e5, 1e5 + 255); |
| testBounds(1e6, 1e6 + 255); |
| testBounds(1e7, 1e7 + 255); |
| testBounds(1e8, 1e8 + 255); |
| testBounds(1e9, 1e9 + 255); |
| testBounds(1e10, 1e10 + 255, false); |
| |
| // Input validation against garbage |
| testBounds(0, INFINITY, false); |
| testBounds(-INFINITY, 0, false); |
| testBounds(-INFINITY, INFINITY, false); |
| testBounds(0, NAN, false); |
| testBounds(NAN, 0, false); |
| testBounds(NAN, NAN, false); |
| |
| // Noteworthy binary integers |
| testBounds(0, std::pow(2.0, 32.0) - 1.0); |
| testBounds(0, std::pow(2.0, 32.0)); |
| testBounds(0.0 - std::pow(2.0, 31.0), std::pow(2.0, 31.0)); |
| testBounds((0.0 - std::pow(2.0, 31.0)) - 1.0, std::pow(2.0, 31.0)); |
| |
| // Similar but negative (note additional commented-out below) |
| testBounds(-1e1, (-1e1) + 255); |
| testBounds(-1e2, (-1e2) + 255); |
| |
| // Ranges of negative numbers (note additional commented-out below) |
| testBounds(-10400, -10000); |
| testBounds(-15000, -14000); |
| testBounds(-10000, -9000); |
| testBounds(-1000, -900); |
| testBounds(-1000, -800); |
| testBounds(-1000, -700); |
| testBounds(-1000, -740); |
| |
| // Very small ranges (note additional commented-out below) |
| testBounds(0, 0.1); |
| testBounds(0, 0.01); |
| testBounds(0, 0.001); |
| testBounds(0, 0.0001); |
| testBounds(0, 0.000001, false); |
| |
| #if 0 |
| // TODO(): The algorithm in this module is better than it was before, |
| // but the resulting value of X is still wrong under certain conditions, |
| // such as when the range between min and max is around 255, |
| // and the offset is fairly extreme compared to the multiplier. |
| // Not sure why this is, but these ranges are contrived, |
| // and real-world examples would most likely never be this way. |
| testBounds(-10290, -10000); |
| testBounds(-10280, -10000); |
| testBounds(-10275,-10000); |
| testBounds(-10270,-10000); |
| testBounds(-10265,-10000); |
| testBounds(-10260,-10000); |
| testBounds(-10255,-10000); |
| testBounds(-10250,-10000); |
| testBounds(-10245,-10000); |
| testBounds(-10256,-10000); |
| testBounds(-10512, -10000); |
| testBounds(-11024, -10000); |
| |
| // TODO(): This also fails, due to extreme small range, loss of precision |
| testBounds(0, 0.00001); |
| |
| // TODO(): Interestingly, if bSigned is forced false, |
| // causing "x" to have range of (0,255) instead of (-128,127), |
| // these test cases change from failing to passing! |
| // Not sure why this is, perhaps a mathematician might know. |
| testBounds(-10300, -10000); |
| testBounds(-1000,-750); |
| testBounds(-1e3, (-1e3) + 255); |
| testBounds(-1e4, (-1e4) + 255); |
| testBounds(-1e5, (-1e5) + 255); |
| testBounds(-1e6, (-1e6) + 255); |
| #endif |
| } |
| |
| TEST(sensorutils, TranslateToIPMI) |
| { |
| /*bool getSensorAttributes(double maxValue, double minValue, int16_t |
| &mValue, int8_t &rExp, int16_t &bValue, int8_t &bExp, bool &bSigned); */ |
| // normal unsigned sensor |
| double maxValue = 0xFF; |
| double minValue = 0x0; |
| int16_t mValue; |
| int8_t rExp; |
| int16_t bValue; |
| int8_t bExp; |
| bool bSigned; |
| bool result; |
| |
| uint8_t scaledVal; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| EXPECT_EQ(mValue, 1); |
| EXPECT_EQ(rExp, 0); |
| EXPECT_EQ(bValue, 0); |
| EXPECT_EQ(bExp, 0); |
| } |
| double expected = 0x50; |
| scaledVal = ipmi::scaleIPMIValueFromDouble(0x50, mValue, rExp, bValue, bExp, |
| bSigned); |
| EXPECT_NEAR(scaledVal, expected, expected * 0.01); |
| |
| // normal signed sensor |
| maxValue = 127; |
| minValue = -128; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| |
| if (result) |
| { |
| EXPECT_EQ(bSigned, true); |
| EXPECT_EQ(mValue, 1); |
| EXPECT_EQ(rExp, 0); |
| EXPECT_EQ(bValue, 0); |
| EXPECT_EQ(bExp, 0); |
| } |
| |
| // check negative values |
| expected = 236; // 2s compliment -20 |
| scaledVal = ipmi::scaleIPMIValueFromDouble(-20, mValue, rExp, bValue, bExp, |
| bSigned); |
| EXPECT_NEAR(scaledVal, expected, expected * 0.01); |
| |
| // fan example |
| maxValue = 16000; |
| minValue = 0; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| EXPECT_EQ(mValue, floor((16000.0 / 0xFF) + 0.5)); |
| EXPECT_EQ(rExp, 0); |
| EXPECT_EQ(bValue, 0); |
| EXPECT_EQ(bExp, 0); |
| } |
| |
| // voltage sensor example |
| maxValue = 20; |
| minValue = 0; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5)); |
| EXPECT_EQ(rExp, -3); |
| EXPECT_EQ(bValue, 0); |
| EXPECT_EQ(bExp, 0); |
| } |
| scaledVal = ipmi::scaleIPMIValueFromDouble(12.2, mValue, rExp, bValue, bExp, |
| bSigned); |
| |
| expected = 12.2 / (mValue * std::pow(10, rExp)); |
| EXPECT_NEAR(scaledVal, expected, expected * 0.01); |
| |
| // shifted fan example |
| maxValue = 16000; |
| minValue = 8000; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| EXPECT_EQ(mValue, floor(((8000.0 / 0xFF) / std::pow(10, rExp)) + 0.5)); |
| EXPECT_EQ(rExp, -1); |
| EXPECT_EQ(bValue, 8); |
| EXPECT_EQ(bExp, 4); |
| } |
| |
| // signed voltage sensor example |
| maxValue = 10; |
| minValue = -10; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, true); |
| EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5)); |
| EXPECT_EQ(rExp, -3); |
| // Although this seems like a weird magic number, |
| // it is because the range (-128,127) is not symmetrical about zero, |
| // unlike the range (-10,10), so this introduces some distortion. |
| EXPECT_EQ(bValue, 392); |
| EXPECT_EQ(bExp, -1); |
| } |
| |
| scaledVal = ipmi::scaleIPMIValueFromDouble(5, mValue, rExp, bValue, bExp, |
| bSigned); |
| |
| expected = 5 / (mValue * std::pow(10, rExp)); |
| EXPECT_NEAR(scaledVal, expected, expected * 0.01); |
| |
| // reading = max example |
| maxValue = 277; |
| minValue = 0; |
| |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| } |
| |
| scaledVal = ipmi::scaleIPMIValueFromDouble(maxValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| |
| expected = 0xFF; |
| EXPECT_NEAR(scaledVal, expected, expected * 0.01); |
| |
| // 0, 0 failure |
| maxValue = 0; |
| minValue = 0; |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, false); |
| |
| // too close *success* (was previously failure!) |
| maxValue = 12; |
| minValue = 10; |
| result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue, |
| bExp, bSigned); |
| EXPECT_EQ(result, true); |
| if (result) |
| { |
| EXPECT_EQ(bSigned, false); |
| EXPECT_EQ(mValue, floor(((2.0 / 0xFF) / std::pow(10, rExp)) + 0.5)); |
| EXPECT_EQ(rExp, -4); |
| EXPECT_EQ(bValue, 1); |
| EXPECT_EQ(bExp, 5); |
| } |
| } |
| |
| TEST(sensorUtils, TestRanges) |
| { |
| // Additional test ranges, each running through a series of values, |
| // to make sure the values of "x" and "y" go together and make sense, |
| // for the resulting scaling attributes from each range. |
| // Unlike the TranslateToIPMI test, exact matches of the |
| // getSensorAttributes() results (the coefficients) are not required, |
| // because they are tested through actual use, relating "x" to "y". |
| testRanges(); |
| } |