350 lines
14 KiB
C++
350 lines
14 KiB
C++
#include "doctest/doctest.h"
|
|
#include "fgc/HardwareChecks.h"
|
|
|
|
using namespace fgc;
|
|
|
|
TEST_CASE("usbfs_memory_mb is checked against the value frames actually need") {
|
|
CHECK(checkUsbfsMemory("1000", 1000).pass);
|
|
CHECK(checkUsbfsMemory("2048\n", 1000).pass);
|
|
|
|
// The reboot default. This is the exact state that was found on the
|
|
// LattePanda and stalled frame delivery.
|
|
auto low = checkUsbfsMemory("16", 1000);
|
|
CHECK_FALSE(low.pass);
|
|
CHECK(low.detail.find("kernel cmdline") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("an unreadable usbfs node fails rather than passing by default") {
|
|
// A check that did not happen must not report PASS - that is how a silent
|
|
// precondition regression survives a green test run.
|
|
CHECK_FALSE(checkUsbfsMemory("", 1000).pass);
|
|
CHECK_FALSE(checkUsbfsMemory("not-a-number", 1000).pass);
|
|
}
|
|
|
|
TEST_CASE("USB3 LPM must be off, and anything unreadable is a failure") {
|
|
CHECK(checkUsb3Lpm("0").pass);
|
|
CHECK(checkUsb3Lpm(" 0\n").pass);
|
|
|
|
auto on = checkUsb3Lpm("1");
|
|
CHECK_FALSE(on.pass);
|
|
CHECK(on.detail.find("re-enumerate") != std::string::npos);
|
|
|
|
CHECK_FALSE(checkUsb3Lpm("").pass);
|
|
}
|
|
|
|
TEST_CASE("an SMBus-only adapter is caught before it looks like a dead sensor") {
|
|
CHECK(checkI2cFunctionality(kI2cFuncI2c).pass);
|
|
CHECK(checkI2cFunctionality(kI2cFuncI2c | 0xF0).pass);
|
|
|
|
auto smbus = checkI2cFunctionality(0xF0); // SMBus bits only
|
|
CHECK_FALSE(smbus.pass);
|
|
CHECK(smbus.detail.find("i2cdetect -F") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a tower on limp-home values is a fault, however healthy it looks") {
|
|
// The case this exists for: USE DEFAULTS fills the firmware's cfg_rx mask
|
|
// completely, so the axis leaves NEEDCFG, reports READY and passes every
|
|
// state check - while running values that describe no real drivetrain.
|
|
auto r = checkNotLimpHome("yaw", 1);
|
|
CHECK(r.ran);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("LIMP-HOME") != std::string::npos);
|
|
// It must send the reader to the cause, not to the symptom. "Re-send the
|
|
// values" is the wrong instruction when nobody ever sent any.
|
|
CHECK(r.detail.find("USE DEFAULTS") != std::string::npos);
|
|
CHECK(r.detail.find("Restart the host") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("an axis on the host's own values passes") {
|
|
auto r = checkNotLimpHome("yaw", 0);
|
|
CHECK(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("a firmware with no limp flag is a version skew, not a passing tower") {
|
|
// Absent must not read as limp=0. A firmware predating the flag genuinely
|
|
// cannot answer the question, and claiming PASS here would assert something
|
|
// the wire never said - the same -1-is-not-zero rule the current echo uses.
|
|
auto r = checkNotLimpHome("yaw", -1);
|
|
CHECK_FALSE(r.ran);
|
|
CHECK(r.detail.find("cannot be told apart") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("an undeclared axis has no drivetrain skew to find") {
|
|
DrivetrainExpectation want; // declared == false
|
|
DrivetrainEcho got; // all -1
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK_FALSE(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("a firmware that cannot echo the current is a version skew, not zero current") {
|
|
DrivetrainExpectation want{true, 80, 31, 7};
|
|
DrivetrainEcho got; // all absent
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK(r.ran);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("predates") != std::string::npos);
|
|
CHECK(r.detail.find("Re-flash") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("the IOTA tower's declared current, adopted") {
|
|
DrivetrainExpectation want{true, 80, 31, 7}; // 756 mA run, 189 mA homing at 75 mOhm
|
|
DrivetrainEcho got{80, 31, 7};
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("a homing current that did not land is named as the damaging case") {
|
|
// The exact failure that sheared the endstop: run current correct, homing
|
|
// current still at the firmware default.
|
|
DrivetrainExpectation want{true, 80, 31, 7};
|
|
DrivetrainEcho got{80, 31, 31};
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("homing IRUN wanted 7, firmware has 31") != std::string::npos);
|
|
CHECK(r.detail.find("endstop") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("every mismatched field is reported, not just the first") {
|
|
DrivetrainExpectation want{true, 80, 31, 7};
|
|
DrivetrainEcho got{255, 16, 31};
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("GLOBALSCALER") != std::string::npos);
|
|
CHECK(r.detail.find("IRUN wanted 31") != std::string::npos);
|
|
CHECK(r.detail.find("homing IRUN") != std::string::npos);
|
|
}
|
|
|
|
// --- the generalised adoption check (config-plan A.6) -----------------------
|
|
|
|
TEST_CASE("an axis with nothing declared beyond the current is not checked") {
|
|
const DeclaredValue none[] = {{"steps_per_rev", false, 0, -1}};
|
|
auto r = checkDeclaredValues("yaw", none, 1);
|
|
CHECK_FALSE(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("every declared value matching is a pass that says how many") {
|
|
const DeclaredValue v[] = {
|
|
{"steps_per_rev", true, 1210778, 1210778},
|
|
{"enc_deviation", true, 1500, 1500},
|
|
{"normal_speed", true, 39738, 39738},
|
|
};
|
|
auto r = checkDeclaredValues("yaw", v, 3);
|
|
CHECK(r.ran);
|
|
CHECK(r.pass);
|
|
CHECK(r.detail.find("all 3") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a firmware that cannot echo a declared value fails, and is named a skew") {
|
|
// Not a mismatch: the value cannot be confirmed either way, and a check that
|
|
// did not happen must never read as a pass.
|
|
const DeclaredValue v[] = {
|
|
{"steps_per_rev", true, 1210778, -1},
|
|
{"enc_deviation", true, 1500, -1},
|
|
};
|
|
auto r = checkDeclaredValues("yaw", v, 2);
|
|
CHECK(r.ran);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("does not echo 2") != std::string::npos);
|
|
CHECK(r.detail.find("Re-flash") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a lost SET is caught, and every wrong field is named") {
|
|
// The failure mode the handshake introduces: most SETs land, one does not,
|
|
// and the axis runs on a value nobody declared.
|
|
const DeclaredValue v[] = {
|
|
{"steps_per_rev", true, 1210778, 1210778},
|
|
{"enc_deviation", true, 1500, 177000}, // stale Delta value
|
|
{"homing_speed", true, 9958, 12000}, // firmware default
|
|
};
|
|
auto r = checkDeclaredValues("yaw", v, 3);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("enc_deviation wanted 1500, firmware has 177000") != std::string::npos);
|
|
CHECK(r.detail.find("homing_speed wanted 9958, firmware has 12000") != std::string::npos);
|
|
CHECK(r.detail.find("NEEDCFG") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("an undeclared value is skipped even when the firmware echoes one") {
|
|
// hardware.ini not declaring it means the firmware default IS the intent.
|
|
const DeclaredValue v[] = {
|
|
{"steps_per_rev", true, 1210778, 1210778},
|
|
{"sw_mode", false, 0, 3},
|
|
};
|
|
auto r = checkDeclaredValues("yaw", v, 2);
|
|
CHECK(r.pass);
|
|
CHECK(r.detail.find("all 1") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a partly echoed current is a version skew, not a wrong value") {
|
|
// The bug this caught: comparing an absent field against -1 reported
|
|
// "firmware has -1", which reads as a wrong value and sends someone to
|
|
// re-send a CURRENT that was never the problem.
|
|
DrivetrainExpectation want{true, 80, 31, 7};
|
|
DrivetrainEcho got;
|
|
got.global_scaler = 80; // echoed
|
|
got.run_irun = -1; // not echoed
|
|
got.homing_irun = -1;
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("2 of 3 fields absent") != std::string::npos);
|
|
CHECK(r.detail.find("firmware has -1") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a real mismatch alongside an absent field still names the mismatch") {
|
|
DrivetrainExpectation want{true, 80, 31, 7};
|
|
DrivetrainEcho got;
|
|
got.global_scaler = 255; // wrong
|
|
got.run_irun = 31; // right
|
|
got.homing_irun = -1; // absent
|
|
auto r = checkDrivetrainAdopted("yaw", want, got);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("GLOBALSCALER wanted 80, firmware has 255") != std::string::npos);
|
|
}
|
|
|
|
// --- the travel must fit in positive counts -------------------------------
|
|
|
|
TEST_CASE("the Delta's centre comfortably holds its travel") {
|
|
// ~89k half-travel against a 500000 centre: 411k .. 589k, all positive.
|
|
auto r = checkTravelFitsPositive("yaw", 500000, 89000);
|
|
CHECK(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("the IOTA's gear ratio pushes the same centre below zero") {
|
|
// The real case: half-travel 604749 against the inherited 500000 default
|
|
// gives lim_neg -104749, which is what tower 121 actually homed to.
|
|
auto r = checkTravelFitsPositive("yaw", 500000, 604749);
|
|
CHECK(r.ran);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("-104749") != std::string::npos);
|
|
CHECK(r.detail.find("yaw_center_count") != std::string::npos);
|
|
// The suggestion is the next round million above half the travel - 1000000
|
|
// here, which is the value actually adopted. Not 2000000: over-suggesting
|
|
// wastes range for no reason.
|
|
CHECK(r.detail.find("1000000") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("raising the centre fixes it") {
|
|
auto r = checkTravelFitsPositive("yaw", 1000000, 604749);
|
|
CHECK(r.pass);
|
|
CHECK(r.detail.find("395251") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("the suggestion scales with the drivetrain, not a fixed constant") {
|
|
// A ratio big enough that 1000000 would not be enough either.
|
|
auto r = checkTravelFitsPositive("yaw", 500000, 1400000);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("2000000") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("exactly touching zero is accepted, one count below is not") {
|
|
CHECK(checkTravelFitsPositive("yaw", 604749, 604749).pass);
|
|
CHECK_FALSE(checkTravelFitsPositive("yaw", 604748, 604749).pass);
|
|
}
|
|
|
|
TEST_CASE("an un-homed axis is not checked") {
|
|
auto r = checkTravelFitsPositive("pitch", 500000, 0);
|
|
CHECK_FALSE(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
// --- the wiring the image was built from ------------------------------------
|
|
|
|
TEST_CASE("pins built as declared pass") {
|
|
const DeclaredValue v[] = {{"cs", true, 1, 1}, {"en", true, 4, 4}};
|
|
auto r = checkPinsBuilt("yaw", v, 2);
|
|
CHECK(r.ran);
|
|
CHECK(r.pass);
|
|
}
|
|
|
|
TEST_CASE("a pin mismatch blames the build, not a lost SET") {
|
|
// Pins never travel over the wire, so "a SET was lost" would send someone
|
|
// looking in the wrong place. The only fix is a rebuild from this file.
|
|
const DeclaredValue v[] = {{"cs", true, 1, 5}, {"en", true, 4, 4}};
|
|
auto r = checkPinsBuilt("yaw", v, 2);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("cs declared 1, built with 5") != std::string::npos);
|
|
CHECK(r.detail.find("Rebuild and re-flash") != std::string::npos);
|
|
CHECK(r.detail.find("SET") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a firmware that does not echo its pins cannot pass") {
|
|
const DeclaredValue v[] = {{"cs", true, 1, -1}, {"en", true, 4, -1}};
|
|
auto r = checkPinsBuilt("yaw", v, 2);
|
|
CHECK(r.ran);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("predates the pin echo") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("no [Pins] declared is not checked") {
|
|
const DeclaredValue v[] = {{"cs", false, 0, 1}};
|
|
auto r = checkPinsBuilt("yaw", v, 1);
|
|
CHECK_FALSE(r.ran);
|
|
}
|
|
|
|
// --- test camera ------------------------------------------------------------
|
|
|
|
TEST_CASE("pixelStats counts each end of the range over every byte") {
|
|
std::vector<uint8_t> d(1000, 128);
|
|
for (int i = 0; i < 100; ++i) d[i] = 0;
|
|
for (int i = 100; i < 150; ++i) d[i] = 5; // still "low": the bound is inclusive
|
|
for (int i = 150; i < 200; ++i) d[i] = 250; // "high", inclusive
|
|
for (int i = 200; i < 210; ++i) d[i] = 255;
|
|
auto st = pixelStats(d);
|
|
CHECK(st.samples == 1000);
|
|
CHECK(st.pct_low == doctest::Approx(15.0));
|
|
CHECK(st.pct_high == doctest::Approx(6.0));
|
|
CHECK(pixelStats({}).samples == 0);
|
|
}
|
|
|
|
TEST_CASE("frames: all arriving whole and consistent passes, and says so") {
|
|
FrameRun run;
|
|
run.wanted = 5; run.got = 5; run.timeout_ms = 2000;
|
|
run.width = 2664; run.height = 1520; run.channels = 3;
|
|
auto r = checkFramesDelivered("rgb", run);
|
|
CHECK(r.pass);
|
|
CHECK(r.detail.find("5 complete frames, 2664x1520 x3") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("frames: a stall, a size change, bad depth and dropped frames each fail") {
|
|
FrameRun ok;
|
|
ok.wanted = 5; ok.got = 5; ok.timeout_ms = 2000;
|
|
ok.width = 640; ok.height = 480; ok.channels = 3;
|
|
|
|
FrameRun stall = ok; stall.got = 2;
|
|
CHECK_FALSE(checkFramesDelivered("rgb", stall).pass);
|
|
CHECK(checkFramesDelivered("rgb", stall).detail.find("2 of 5") != std::string::npos);
|
|
|
|
FrameRun changed = ok; changed.consistent = false;
|
|
CHECK_FALSE(checkFramesDelivered("rgb", changed).pass);
|
|
|
|
FrameRun depth = ok; depth.channels = 2;
|
|
CHECK_FALSE(checkFramesDelivered("rgb", depth).pass);
|
|
|
|
FrameRun drops = ok; drops.dropped = 1;
|
|
CHECK_FALSE(checkFramesDelivered("rgb", drops).pass);
|
|
CHECK(checkFramesDelivered("rgb", drops).detail.find("#19") != std::string::npos);
|
|
|
|
FrameRun none;
|
|
CHECK_FALSE(checkFramesDelivered("rgb", none).ran);
|
|
}
|
|
|
|
TEST_CASE("clipping: the forced end must be reached by the threshold share of pixels") {
|
|
PixelStats black; black.samples = 100; black.pct_low = 100.0; black.pct_high = 0.0;
|
|
PixelStats white; white.samples = 100; white.pct_low = 0.0; white.pct_high = 99.5;
|
|
PixelStats grey; grey.samples = 100; grey.pct_low = 1.0; grey.pct_high = 1.0; grey.mean = 128;
|
|
CHECK(checkClipped("rgb", false, black, 99.0).pass);
|
|
CHECK(checkClipped("rgb", true, white, 99.0).pass);
|
|
// A small bright patch at the dark end - the sun in frame - still passes.
|
|
PixelStats sun = black; sun.pct_low = 99.2;
|
|
CHECK(checkClipped("rgb", false, sun, 99.0).pass);
|
|
// A camera that did not follow the settings fails, with the reason.
|
|
auto r = checkClipped("rgb", true, grey, 99.0);
|
|
CHECK_FALSE(r.pass);
|
|
CHECK(r.detail.find("did not reach white") != std::string::npos);
|
|
CHECK_FALSE(checkClipped("rgb", false, PixelStats{}, 99.0).pass);
|
|
}
|