Files
fedora-tools/framework-laptop-tools/tests/test-hardware.cpp
T

366 lines
23 KiB
C++

// SPDX-License-Identifier: MIT
#include "hardware.h"
#include "fan.h"
#include "cpu.h"
#include "power.h"
#include <QTest>
#include <QTemporaryDir>
#include <QDir>
#include <QFile>
class HardwareTest : public QObject {
Q_OBJECT
void put(const QString &path, const QByteArray &value) {
QVERIFY(QDir().mkpath(QFileInfo(path).absolutePath()));
QFile file(path); QVERIFY(file.open(QIODevice::WriteOnly)); QCOMPARE(file.write(value), value.size());
}
private Q_SLOTS:
void coreGroupFrequencies() {
QTemporaryDir root;
const QString base = root.path() + "/devices/system/cpu/cpufreq/";
for (int i = 0; i < 3; ++i) {
const auto policy = base + "policy" + QString::number(i) + "/";
put(policy + "related_cpus", QByteArray::number(i));
put(policy + "scaling_cur_freq", QByteArray::number((i + 1) * 1000000));
}
// No core-type metadata: retain the original combined reading.
auto sensors = frequencySensors(root.path());
QCOMPARE(sensors.size(), 1);
QCOMPARE(sensors[0].id, QString("cpu"));
QVERIFY(sensors[0].primary);
QCOMPARE(sensorValue(sensors[0]), std::optional<double>(2000));
put(root.path() + "/bus/event_source/devices/cpu_core/cpus", "2");
put(root.path() + "/bus/event_source/devices/cpu_atom/cpus", "0-1");
sensors = frequencySensors(root.path());
QCOMPARE(sensors.size(), 5);
QCOMPARE(sensors[0].id, QString("cpu/p"));
QCOMPARE(sensors[1].id, QString("cpu/e"));
QCOMPARE(sensors[2].id, QString("cpu"));
QVERIFY(sensors[0].primary && sensors[1].primary && !sensors[2].primary);
QCOMPARE(sensorValue(sensors[0]), std::optional<double>(3000));
QCOMPARE(sensorValue(sensors[1]), std::optional<double>(1500));
QCOMPARE(sensorValue(sensors[2]), std::optional<double>(2000));
QCOMPARE(sensors[3].id, QString("cpu/p/max"));
QCOMPARE(sensors[4].id, QString("cpu/e/max"));
QVERIFY(!sensors[3].primary && !sensors[4].primary);
QCOMPARE(sensorValue(sensors[3]), std::optional<double>(3000));
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000));
put(base + "policy0/scaling_cur_freq", "4000000");
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(4000));
put(base + "policy0/scaling_cur_freq", "1000000");
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000)); // Not a historical peak.
// Missing readings aren't zeroes, and membership isn't guessed by speed.
put(base + "policy0/scaling_cur_freq", "unavailable");
QCOMPARE(sensorValue(sensors[1]), std::optional<double>(2000));
QCOMPARE(sensorValue(sensors[4]), std::optional<double>(2000));
put(base + "policy1/scaling_cur_freq", "unavailable");
QVERIFY(!sensorValue(sensors[1]));
QVERIFY(!sensorValue(sensors[4]));
QCOMPARE(sensorValue(sensors[2]), std::optional<double>(3000));
put(base + "policy2/related_cpus", "1 2");
sensors = frequencySensors(root.path());
QCOMPARE(sensors.size(), 1); // A policy spanning types cannot be split.
QVERIFY(sensors[0].primary);
}
void energyCounters() {
EnergyCounter counter;
const quint64 range = 262143328850ULL;
QVERIFY(!counter.sample(1000000, range, 0, 3000));
QCOMPARE(counter.sample(6000000, range, 1000, 3000), std::optional<double>(5));
QCOMPARE(counter.sample(16000000, range, 3000, 3000), std::optional<double>(5));
QVERIFY(!counter.sample(100, range, 4000, 3000)); // Reset, not a wrap.
QCOMPARE(counter.sample(100, range, 5000, 3000), std::optional<double>(0));
QVERIFY(!counter.sample(200, range, 10000, 3000)); // Long gaps establish a new baseline.
QCOMPARE(counter.sample(1000200, range, 11000, 3000), std::optional<double>(1));
EnergyCounter wrapped;
QVERIFY(!wrapped.sample(range - 3000000, range, 0, 3000));
QCOMPARE(wrapped.sample(2000000, range, 1000, 3000), std::optional<double>(5));
QVERIFY(!wrapped.sample(100, 100000000, 2000, 3000)); // Changed range.
QVERIFY(!wrapped.sample(100, 0, 3000, 3000));
QVERIFY(!wrapped.sample(range + 1, range, 4000, 3000));
EnergyCounter large;
const quint64 base = 1ULL << 60;
QVERIFY(!large.sample(base, base * 2, 0, 3000));
QCOMPARE(large.sample(base + 1000, base * 2, 1000, 3000), std::optional<double>(.001));
}
void powerDiscoveryAndSampling() {
QTemporaryDir root;
const QString rapl = root.path() + "/class/powercap/intel-rapl:0/";
const QString mmio = root.path() + "/class/powercap/intel-rapl-mmio:0/";
for (const auto &path : {rapl, mmio}) {
put(path + "name", "package-0"); put(path + "energy_uj", "1000000");
put(path + "max_energy_range_uj", "262143328850");
}
const QString fan = root.path() + "/class/hwmon/hwmon9/";
put(fan + "name", "power_meter"); put(fan + "power1_input", "250000"); put(fan + "power1_average", "500000");
const QString acpiFan = root.path() + "/class/hwmon/hwmon10/";
put(acpiFan + "name", "acpi_fan"); put(acpiFan + "power1_input", "0");
PowerMonitor monitor(root.path()); QCOMPARE(monitor.sensors().size(), 2);
QCOMPARE(monitor.sensors()[0].name, QString("CPU package"));
QCOMPARE(monitor.sensors()[0].path, rapl + "energy_uj");
const QString cpu = monitor.sensors()[0].id, fanId = monitor.sensors()[1].id;
auto values = monitor.sample(1000, 0);
QVERIFY(!values.contains(cpu)); QCOMPARE(values.value(fanId), .25);
put(rapl + "energy_uj", "6000000");
values = monitor.sample(1000, 1000); QCOMPARE(values.value(cpu), 5.);
put(rapl + "energy_uj", "unavailable"); QVERIFY(!monitor.sample(1000, 2000).contains(cpu));
put(rapl + "energy_uj", "11000000"); QVERIFY(!monitor.sample(1000, 3000).contains(cpu));
put(rapl + "energy_uj", "16000000"); QCOMPARE(monitor.sample(1000, 4000).value(cpu), 5.);
monitor.reset(); QVERIFY(!monitor.sample(1000, 5000).contains(cpu));
put(fan + "power1_input", "0"); QCOMPARE(monitor.sample(1000, 6000).value(fanId), 0.);
QTemporaryDir fallback;
const QString fallbackZone = fallback.path() + "/class/powercap/intel-rapl-mmio:0/";
put(fallbackZone + "name", "package-0"); put(fallbackZone + "energy_uj", "0");
PowerMonitor mmioOnly(fallback.path()); QCOMPARE(mmioOnly.sensors().size(), 1);
QCOMPARE(mmioOnly.sensors()[0].id, cpu);
QVERIFY(mmioOnly.sample(1000, 0).isEmpty()); // Missing range is not a zero reading.
}
void platformPowerIsExcluded() {
for (const auto &prefix : {"intel-rapl:", "intel-rapl-mmio:"}) {
QTemporaryDir root;
const QStringList names{"package-0", "core", "uncore", "dram", "psys", "psys-0"};
for (int i = 0; i < names.size(); ++i) {
const QString path = root.path() + "/class/powercap/" + prefix + QString::number(i) + "/";
put(path + "name", names[i].toUtf8()); put(path + "energy_uj", "0");
put(path + "max_energy_range_uj", "1000000");
}
PowerMonitor monitor(root.path());
QStringList discovered;
for (const auto &sensor : monitor.sensors()) discovered.append(sensor.name);
QCOMPARE(discovered, QStringList({"CPU package", "CPU cores", "Uncore", "Memory (RAPL)"}));
QVERIFY(monitor.sample(1000, 0).isEmpty());
QCOMPARE(monitor.sample(1000, 1000).size(), 4);
}
}
void modelGate() {
QTemporaryDir root;
QVERIFY(!compatibilityError(root.path()).isEmpty());
put(root.path() + "/class/dmi/id/sys_vendor", "Framework\n");
put(root.path() + "/class/dmi/id/product_name", "Laptop 13\n");
QVERIFY(!compatibilityError(root.path()).isEmpty());
put(root.path() + "/class/dmi/id/product_name", "Laptop 13 Pro (Intel Core Ultra Series 3)\n");
QVERIFY(compatibilityError(root.path()).isEmpty());
}
void missingReadingsAreNotZero() {
QTemporaryDir root;
QVERIFY(!readNumber(root.filePath("absent")));
put(root.filePath("value"), "invalid\n"); QVERIFY(!readNumber(root.filePath("value")));
put(root.filePath("value"), "0\n"); QCOMPARE(*readNumber(root.filePath("value")), 0.);
}
void batteryUnits() {
QTemporaryDir root; const QString battery = root.path() + "/class/power_supply/BAT1/";
put(battery + "type", "Battery"); put(battery + "current_now", "2000000");
put(battery + "voltage_now", "16000000"); put(battery + "capacity", "60");
QCOMPARE(batteryStatus(root.path())["watts"].toDouble(), 32.);
put(battery + "power_now", "12300000");
QCOMPARE(batteryStatus(root.path())["watts"].toDouble(), 12.3);
QCOMPARE(batteryRate({{"watts", 12.3}, {"state", "Discharging"}}), std::optional<double>(-12.3));
QCOMPARE(batteryRate({{"watts", 12.3}, {"state", "Charging"}}), std::optional<double>(12.3));
QCOMPARE(batteryRate({{"watts", 0}, {"state", "Full"}}), std::optional<double>(0));
QVERIFY(!batteryRate({{"watts", 12.3}, {"state", "Unknown"}}));
put(battery + "charge_full", "4500000"); put(battery + "charge_full_design", "5000000");
put(battery + "voltage_min_design", "16000000");
put(battery + "charge_now", "3000000");
QCOMPARE(batteryStatus(root.path())["remainingMWh"].toDouble(), 48000.);
QCOMPARE(batteryStatus(root.path())["fullMWh"].toDouble(), 72000.);
QCOMPARE(batteryStatus(root.path())["health"].toDouble(), 90.);
QVERIFY(batteryStatus(root.path())["capacityEstimated"].toBool());
put(battery + "energy_full", "75000000"); put(battery + "energy_full_design", "75000000");
put(battery + "energy_now", "56250000");
QCOMPARE(batteryStatus(root.path())["remainingMWh"].toDouble(), 56250.);
QCOMPARE(batteryStatus(root.path())["fullMWh"].toDouble(), 75000.);
QCOMPARE(batteryStatus(root.path())["health"].toDouble(), 100.);
QVERIFY(!batteryStatus(root.path()).contains("capacityEstimated"));
}
void mainTemperatureSensors() {
QTemporaryDir root;
const QList<QPair<QString, QString>> fixtures{{"nvme", "Composite"}, {"nvme", "Sensor 1"},
{"cros_ec", "peci-temp"}, {"cros_ec", "cpu_f75303@4d"}, {"coretemp", "Core 0"}, {"spd5118", ""}};
for (int i = 0; i < fixtures.size(); ++i) {
const QString dir = root.path() + "/class/hwmon/hwmon" + QString::number(i) + "/";
put(dir + "name", fixtures[i].first.toUtf8()); put(dir + "temp1_label", fixtures[i].second.toUtf8());
put(dir + "temp1_input", "45000");
}
const auto sensors = temperatureSensors(root.path()); QCOMPARE(sensors.size(), 6);
for (const auto &sensor : sensors) {
const bool main = sensor.name == "NVMe (composite)" || sensor.name == "CPU (PECI)"
|| sensor.name == "Memory (SPD)";
QCOMPARE(sensor.primary, main); QCOMPARE(sensorValue(sensor), std::optional<double>(45.));
}
}
void fanValidationAndCurve() {
QVERIFY(validateFan({{"mode", "auto"}}).isEmpty());
QVERIFY(validateFan({{"mode", "manual"}, {"duty", 0}}).isEmpty());
QVERIFY(!validateFan({{"mode", "manual"}, {"duty", -1}}).isEmpty());
QVERIFY(validateFan({{"mode", "curve"}, {"curve", QVariantList{0, 0, 20, 100}}}).isEmpty());
QCOMPARE(curveDuty(40, {0, 0, 20, 100}), 0.);
QVERIFY(!validateFan({{"mode", "curve"}, {"curve", QVariantList{30, 70, 40, 100}}}).isEmpty());
QVERIFY(!validateFan({{"mode", "curve"}, {"curve", QVariantList{30, 40, 70, 90}}}).isEmpty());
QVERIFY(validateFan({{"mode", "curve"}, {"curve", QVariantList{30, 40, 70, 100}}}).isEmpty());
QCOMPARE(curveDuty(55, {30, 40, 70, 100}), 40.);
QCOMPARE(curveDuty(62.5, {30, 40, 70, 100}), 55.);
QCOMPARE(curveDuty(100, {30, 40, 70, 100}), 100.);
QCOMPARE(curveDuty(60, {30, 40, 70, 100}, {40, 60, 75, 85}), 40.);
QCOMPARE(curveDuty(67.5, {30, 40, 70, 100}, {40, 60, 75, 85}), 55.);
QVariantMap config{{"mode", "curve"}, {"curve", QVariantList{30, 40, 70, 100}}, {"temperatures", QVariantList{40, 60, 75, 85}}};
QVERIFY(validateFan(config).isEmpty());
config["temperatures"] = QVariantList{40, 60, 60, 85}; QVERIFY(!validateFan(config).isEmpty());
config["temperatures"] = QVariantList{40, 60, 75, 90}; QVERIFY(!validateFan(config).isEmpty());
}
void cpuRangeAndApply() {
QTemporaryDir root; const QString policy = root.path() + "/devices/system/cpu/cpufreq/policy0/";
put(policy + "cpuinfo_min_freq", "400000"); put(policy + "cpuinfo_max_freq", "4800000");
put(policy + "scaling_min_freq", "400000"); put(policy + "scaling_max_freq", "3000000");
QVERIFY(!setCpuLimits(3500, 3000, root.path()).isEmpty());
QCOMPARE(readText(policy + "scaling_max_freq"), QString("3000000"));
QVERIFY(setCpuLimits(1000, 4000, root.path()).isEmpty());
QCOMPARE(readText(policy + "scaling_min_freq"), QString("1000000"));
QCOMPARE(readText(policy + "scaling_max_freq"), QString("4000000"));
}
void hybridCpuProfiles() {
QTemporaryDir root;
const QString base = root.path() + "/devices/system/cpu/cpufreq/";
for (int i = 0; i < 2; ++i) {
const QString p = base + "policy" + QString::number(i) + "/";
put(p + "cpuinfo_min_freq", "400000"); put(p + "cpuinfo_max_freq", i ? "3300000" : "4800000");
put(p + "scaling_min_freq", "400000"); put(p + "scaling_max_freq", "3000000");
put(p + "scaling_driver", "intel_pstate");
put(p + "scaling_governor", "powersave"); put(p + "energy_performance_preference", "balance_power");
put(p + "scaling_available_governors", "powersave performance");
put(p + "energy_performance_available_preferences", "performance balance_performance balance_power power");
}
QVariantMap profile{{"frequencyOverride", true}, {"minimum", 1000}, {"maximum", 4500}, {"governor", "powersave"}, {"preference", "balance_power"}};
QCOMPARE(cpuLimits(root.path())["high"].toInt(), 4800);
QCOMPARE(cpuLimits(root.path())["capped"].toInt(), 2);
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(base + "policy0/scaling_max_freq"), QString("4500000"));
QCOMPARE(readText(base + "policy1/scaling_max_freq"), QString("3300000"));
QCOMPARE(cpuLimits(root.path())["capped"].toInt(), 1); // An E-core at its own ceiling isn't capped.
QCOMPARE(readText(base + "policy1/energy_performance_preference"), QString("balance_power"));
profile["minimum"] = 4000;
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(base + "policy1/scaling_min_freq"), QString("3300000"));
profile["governor"] = "performance";
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(base + "policy0/scaling_governor"), QString("powersave"));
profile["preference"] = "performance";
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
profile["maximum"] = 99999;
QVERIFY(!applyCpuProfile(profile, root.path()).isEmpty());
}
void independentCpuOverrides() {
QTemporaryDir root; const QString p = root.path() + "/devices/system/cpu/cpufreq/policy0/";
const QVariantMap automatic{{"frequencyOverride", false}, {"governor", "auto"}, {"preference", "auto"}};
QVERIFY(applyCpuProfile(automatic, root.path()).isEmpty()); // No CPU files are required for a no-op.
put(p + "scaling_driver", "intel_pstate"); put(p + "scaling_governor", "powersave");
put(p + "energy_performance_preference", "balance_power");
put(p + "scaling_available_governors", "powersave performance");
put(p + "energy_performance_available_preferences", "performance balance_power balance_performance power");
put(p + "scaling_min_freq", "555000"); put(p + "scaling_max_freq", "2345000");
put(p + "cpuinfo_min_freq", "400000"); put(p + "cpuinfo_max_freq", "4800000");
auto profile = automatic; profile["governor"] = "performance";
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(p + "scaling_governor"), QString("powersave"));
QCOMPARE(readText(p + "energy_performance_preference"), QString("balance_power"));
put(p + "energy_performance_preference", "performance");
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(p + "scaling_governor"), QString("performance"));
// Simulate another controller switching governor/EPP after a power-profile change.
put(p + "scaling_governor", "powersave"); put(p + "energy_performance_preference", "power");
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(p + "scaling_governor"), QString("powersave"));
QCOMPARE(readText(p + "energy_performance_preference"), QString("power"));
QCOMPARE(readText(p + "scaling_min_freq"), QString("555000"));
QCOMPARE(readText(p + "scaling_max_freq"), QString("2345000"));
profile = automatic; profile["preference"] = "balance_power";
put(p + "scaling_governor", "performance");
QVERIFY(!applyCpuProfile(profile, root.path()).isEmpty()); // Auto governor must not be silently changed.
QCOMPARE(readText(p + "scaling_governor"), QString("performance"));
QCOMPARE(readText(p + "energy_performance_preference"), QString("power"));
profile = automatic; profile["frequencyOverride"] = true; profile["minimum"] = 1000; profile["maximum"] = 4000;
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(p + "scaling_governor"), QString("performance"));
QCOMPARE(readText(p + "energy_performance_preference"), QString("power"));
QCOMPARE(readText(p + "scaling_max_freq"), QString("4000000"));
QVERIFY(applyCpuProfile(automatic, root.path()).isEmpty());
QCOMPARE(readText(p + "scaling_max_freq"), QString("4000000")); // Releasing control is not a reset.
}
void cpuConfigMigration() {
const QVariantMap profile{{"minimum", 1000}, {"maximum", 3000}, {"governor", "performance"}, {"preference", "performance"}};
QVariantMap old{{"enabled", false}, {"separate", true}, {"battery", profile}, {"ac", profile}};
auto config = normalizeCpuConfig(old); QVERIFY(!config.contains("enabled")); QVERIFY(!hasCpuOverrides(config));
QCOMPARE(config["battery"].toMap()["governor"].toString(), QString("auto"));
old["enabled"] = true; config = normalizeCpuConfig(old); QVERIFY(hasCpuOverrides(config));
QVERIFY(config["battery"].toMap()["frequencyOverride"].toBool());
QCOMPARE(normalizeCpuConfig(config), config);
}
void independentCoreGroups() {
QTemporaryDir root;
put(root.path() + "/bus/event_source/devices/cpu_core/cpus", "0");
put(root.path() + "/bus/event_source/devices/cpu_atom/cpus", "1-2");
const QString base = root.path() + "/devices/system/cpu/cpufreq/";
for (int i = 0; i < 3; ++i) {
const auto path = base + "policy" + QString::number(i) + "/";
put(path + "related_cpus", QByteArray::number(i));
put(path + "cpuinfo_min_freq", "400000"); put(path + "cpuinfo_max_freq", i == 0 ? "4800000" : i == 1 ? "3700000" : "3300000");
put(path + "scaling_min_freq", "400000"); put(path + "scaling_max_freq", "3000000");
}
QCOMPARE(cpuPolicyGroups(root.path())["e"].size(), 2);
QVariantMap bounds{{"p", QVariantMap{{"frequencyOverride", true}, {"minimum", 1000}, {"maximum", 4500}}},
{"e", QVariantMap{{"frequencyOverride", false}, {"minimum", 400}, {"maximum", 3500}}}};
QVariantMap profile{{"bounds", bounds}, {"governor", "auto"}, {"preference", "auto"}};
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(base + "policy0/scaling_max_freq"), QString("4500000"));
QCOMPARE(readText(base + "policy1/scaling_max_freq"), QString("3000000"));
bounds["p"] = QVariantMap{{"frequencyOverride", false}};
bounds["e"] = QVariantMap{{"frequencyOverride", true}, {"minimum", 500}, {"maximum", 3500}};
profile["bounds"] = bounds;
QVERIFY(applyCpuProfile(profile, root.path()).isEmpty());
QCOMPARE(readText(base + "policy0/scaling_max_freq"), QString("4500000"));
QCOMPARE(readText(base + "policy1/scaling_max_freq"), QString("3500000"));
QCOMPARE(readText(base + "policy2/scaling_max_freq"), QString("3300000"));
QVERIFY(hasCpuOverrides({{"battery", profile}}));
QCOMPARE(normalizeCpuConfig({{"battery", profile}})["battery"].toMap()["bounds"], QVariant(bounds));
put(base + "policy1/related_cpus", "0 1");
QVERIFY(cpuPolicyGroups(root.path()).isEmpty());
QVERIFY(!applyCpuProfile(profile, root.path()).isEmpty());
}
void powerSourceAndProfileSelection() {
QTemporaryDir root;
QVERIFY(!onAcPower(root.path()).has_value());
const QString ac = root.path() + "/class/power_supply/ACAD/";
put(ac + "type", "Mains"); put(ac + "online", "0");
QCOMPARE(onAcPower(root.path()), std::optional<bool>(false));
put(ac + "online", "1");
QCOMPARE(onAcPower(root.path()), std::optional<bool>(true));
const QVariantMap battery{{"maximum", 2500}}, plugged{{"maximum", 4500}};
QVariantMap config{{"enabled", true}, {"separate", true}, {"battery", battery}, {"ac", plugged}};
QCOMPARE(cpuProfile(config, false), battery); QCOMPARE(cpuProfile(config, true), plugged);
config["separate"] = false;
QCOMPARE(cpuProfile(config, true), battery);
}
void fanRefusesMissingOrHotSensors() {
QTemporaryDir root; const QString ec = root.path() + "/class/hwmon/hwmon7/";
put(ec + "name", "cros_ec"); put(ec + "pwm1_enable", "2"); put(ec + "pwm1", "0"); put(ec + "fan1_fault", "0");
const QVariantMap config{{"mode", "manual"}, {"duty", 50}};
double last = -1;
QVERIFY(!updateFan({{"mode", "auto"}}, last, root.path()).isEmpty());
QVERIFY(!updateFan(config, last, root.path()).isEmpty());
QCOMPARE(readText(ec + "pwm1_enable"), QString("2"));
for (int i = 1; i <= 5; ++i) {
const QString stem = ec + "temp" + QString::number(i);
put(stem + "_input", "40000"); put(stem + "_fault", "0"); put(stem + "_crit", "90000");
}
QVERIFY(updateFan(config, last, root.path()).isEmpty());
QCOMPARE(readText(ec + "pwm1_enable"), QString("1"));
QCOMPARE(last, 50.);
put(ec + "temp3_input", "89000");
QVERIFY(!updateFan(config, last, root.path()).isEmpty());
put(ec + "temp3_input", "40000"); put(ec + "temp1_fault", "1");
QVERIFY(!updateFan(config, last, root.path()).isEmpty());
}
};
QTEST_GUILESS_MAIN(HardwareTest)
#include "test-hardware.moc"