// SPDX-License-Identifier: MIT #include "hardware.h" #include "fan.h" #include "cpu.h" #include #include #include #include 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 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(-12.3)); QCOMPARE(batteryRate({{"watts", 12.3}, {"state", "Charging"}}), std::optional(12.3)); QCOMPARE(batteryRate({{"watts", 0}, {"state", "Full"}}), std::optional(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"); 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"); 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> 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(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(false)); put(ac + "online", "1"); QCOMPARE(onAcPower(root.path()), std::optional(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"