From 99af1359005d0b4b692dc16204058e1f92013b9a Mon Sep 17 00:00:00 2001 From: zhangkun Date: Thu, 10 Sep 2026 10:14:43 +0800 Subject: [PATCH] test(power): add battery device unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add a self-contained QtTest executable that drives BatteryDevice through a fake sysfs tree, covering present/absent detection, energy/charge conversion, percentage, capacity, status mapping and time-to-empty/full 2. Cover the D-Bus object-path escaping rules for unusual battery names 3. Register the test under BUILD_TESTING in CMake Log: Add a battery-device sysfs parsing unit test without touching production source Influence: Test runs headless and requires no production code changes test(power): 增加电池设备单元测试 1. 新增自包含 QtTest 用例,通过伪造 sysfs 目录驱动 BatteryDevice,覆盖在位/不在位、能量/电荷换算、百分比、容量、状态映射及剩余/充满时间 2. 覆盖异常电池名对应的 D-Bus 对象路径转义规则 3. 在 CMake 中按 BUILD_TESTING 接入测试 Log: 在不改动生产源码的前提下增加电池设备 sysfs 解析单元测试 PMS: TASK-395399 Influence: 测试无图形运行,不改动任何生产代码 Change-Id: Ie9c8b16ebfe346f1df2220cf81511ba2847192f7 --- src/plugin-qt/power/CMakeLists.txt | 4 + src/plugin-qt/power/tests/CMakeLists.txt | 27 ++ .../power/tests/tst_batterydevice.cpp | 272 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 src/plugin-qt/power/tests/CMakeLists.txt create mode 100644 src/plugin-qt/power/tests/tst_batterydevice.cpp diff --git a/src/plugin-qt/power/CMakeLists.txt b/src/plugin-qt/power/CMakeLists.txt index 6a19076f..036f6147 100644 --- a/src/plugin-qt/power/CMakeLists.txt +++ b/src/plugin-qt/power/CMakeLists.txt @@ -5,6 +5,10 @@ add_subdirectory(session) add_subdirectory(system) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/misc/org.deepin.dde.daemon.power.json DESTINATION ${CMAKE_INSTALL_DATADIR}/dsg/configs/org.deepin.dde.daemon/ ) diff --git a/src/plugin-qt/power/tests/CMakeLists.txt b/src/plugin-qt/power/tests/CMakeLists.txt new file mode 100644 index 00000000..ffaa9134 --- /dev/null +++ b/src/plugin-qt/power/tests/CMakeLists.txt @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +find_package(Qt6 REQUIRED COMPONENTS Core DBus Test) + +set(CMAKE_AUTOMOC ON) + +# BatteryDevice (system): battery sysfs parsing math + D-Bus object-path escaping. +# Self-contained — compiles batterydevice.cpp directly, no production source changes. +add_executable(tst-batterydevice + tst_batterydevice.cpp + ../system/batterydevice.cpp +) + +target_include_directories(tst-batterydevice PRIVATE + ../system + .. +) + +target_link_libraries(tst-batterydevice PRIVATE + Qt6::Core + Qt6::DBus + Qt6::Test +) + +add_test(NAME power-batterydevice COMMAND tst-batterydevice) diff --git a/src/plugin-qt/power/tests/tst_batterydevice.cpp b/src/plugin-qt/power/tests/tst_batterydevice.cpp new file mode 100644 index 00000000..ac6f798f --- /dev/null +++ b/src/plugin-qt/power/tests/tst_batterydevice.cpp @@ -0,0 +1,272 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "../system/batterydevice.h" + +#include +#include +#include + +// BatteryDevice reads Linux power_supply sysfs entries. These tests drive the +// parsing/math through a fake sysfs tree, covering the critical battery-info +// path that feeds warn levels and the UI. + +class TestBatteryDevice : public QObject +{ + Q_OBJECT + +private: + // Builds a fake sysfs directory; each QPair is (filename, contents). + static void makeSysfs(QTemporaryDir &dir, + std::initializer_list> entries) + { + for (const auto &e : entries) { + QFile f(dir.path() + '/' + e.first); + if (!f.open(QIODevice::WriteOnly)) { + QFAIL(qPrintable(QStringLiteral("failed to create ") + e.first)); + } + f.write(e.second.toUtf8()); + } + } + +private Q_SLOTS: + void absentWithoutTypeFile(); + void absentWhenPresentIsZero(); + void energyBasedBattery(); + void dischargingTimeToEmpty(); + void chargingTimeToFull(); + void chargeBasedFallback(); + void percentageFromCapacityFile(); + void energyFullFallsBackToDesign(); + void energyAboveFullClampsFull(); + void statusMapping_data(); + void statusMapping(); + void objectPath_data(); + void objectPath(); + void setStatusEmitsChange(); +}; + +// sysfs numeric files are µ-units; readScaled divides by 1e6. +static QString uValue(double units) { return QString::number(units * 1'000'000.0, 'f', 0); } + +void TestBatteryDevice::absentWithoutTypeFile() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + BatteryDevice device(dir.path()); + QVERIFY(!device.isPresent()); +} + +void TestBatteryDevice::absentWhenPresentIsZero() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("0")}}); + BatteryDevice device(dir.path()); + QVERIFY(!device.isPresent()); +} + +void TestBatteryDevice::energyBasedBattery() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_now"), uValue(50)}, + {QStringLiteral("energy_full"), uValue(100)}, + {QStringLiteral("energy_full_design"), uValue(100)}}); + BatteryDevice device(dir.path()); + + QVERIFY(device.isPresent()); + QCOMPARE(device.energy(), 50.0); + QCOMPARE(device.energyFull(), 100.0); + QCOMPARE(device.energyFullDesign(), 100.0); + QCOMPARE(device.percentage(), 50.0); + QCOMPARE(device.capacity(), 100.0); + QCOMPARE(device.status(), uint(0)); + QCOMPARE(device.timeToEmpty(), quint64(0)); + QCOMPARE(device.timeToFull(), quint64(0)); +} + +void TestBatteryDevice::dischargingTimeToEmpty() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_now"), uValue(50)}, + {QStringLiteral("energy_full"), uValue(100)}, + {QStringLiteral("power_now"), uValue(10)}, + {QStringLiteral("status"), QStringLiteral("Discharging")}}); + BatteryDevice device(dir.path()); + + QCOMPARE(device.status(), uint(2)); + QCOMPARE(device.energyRate(), 10.0); + // 3600 * 50 Wh / 10 W = 18000 s + QCOMPARE(device.timeToEmpty(), quint64(18000)); +} + +void TestBatteryDevice::chargingTimeToFull() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_now"), uValue(50)}, + {QStringLiteral("energy_full"), uValue(100)}, + {QStringLiteral("power_now"), uValue(10)}, + {QStringLiteral("status"), QStringLiteral("Charging")}}); + BatteryDevice device(dir.path()); + + QCOMPARE(device.status(), uint(1)); + // 3600 * (100 - 50) Wh / 10 W = 18000 s + QCOMPARE(device.timeToFull(), quint64(18000)); +} + +void TestBatteryDevice::chargeBasedFallback() +{ + // No energy_* files; energy is derived from charge_now * voltage_design and + // energy_full from charge_full * voltage_design (µAh × V = Wh). + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("charge_now"), uValue(5)}, + {QStringLiteral("charge_full"), uValue(10)}, + {QStringLiteral("charge_full_design"), uValue(10)}}); + BatteryDevice device(dir.path()); + + QCOMPARE(device.energy(), 50.0); + QCOMPARE(device.energyFull(), 100.0); + QCOMPARE(device.percentage(), 50.0); +} + +void TestBatteryDevice::percentageFromCapacityFile() +{ + // A hardware-provided `capacity` percentage wins over energy math, and + // missing energy_now is back-filled from energyFull × percentage. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_full"), uValue(100)}, + {QStringLiteral("capacity"), QStringLiteral("80")}}); + BatteryDevice device(dir.path()); + + QCOMPARE(device.percentage(), 80.0); + QCOMPARE(device.energy(), 80.0); +} + +void TestBatteryDevice::energyFullFallsBackToDesign() +{ + // energy_full missing and charge_full missing, but charge_full_design present: + // energy_full is back-filled from charge_full_design × voltage_design (µAh × V = Wh). + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_now"), uValue(40)}, + {QStringLiteral("charge_full_design"), uValue(10)}}); + BatteryDevice device(dir.path()); + + QCOMPARE(device.energyFull(), 100.0); + QCOMPARE(device.energyFullDesign(), 100.0); + QCOMPARE(device.percentage(), 40.0); +} + +void TestBatteryDevice::energyAboveFullClampsFull() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("voltage_max_design"), uValue(10)}, + {QStringLiteral("energy_now"), uValue(120)}, + {QStringLiteral("energy_full"), uValue(100)}, + {QStringLiteral("energy_full_design"), uValue(100)}}); + BatteryDevice device(dir.path()); + + // energy_full is raised to energy, so percentage clamps to 100. + QCOMPARE(device.energyFull(), 120.0); + QCOMPARE(device.percentage(), 100.0); +} + +void TestBatteryDevice::statusMapping_data() +{ + QTest::addColumn("state"); + QTest::addColumn("expected"); + + QTest::newRow("Charging") << QStringLiteral("Charging") << uint(1); + QTest::newRow("Discharging") << QStringLiteral("Discharging") << uint(2); + QTest::newRow("Not charging") << QStringLiteral("Not charging") << uint(3); + QTest::newRow("Full") << QStringLiteral("Full") << uint(4); + QTest::newRow("FullCharging") << QStringLiteral("FullCharging") << uint(5); + QTest::newRow("Unknown") << QStringLiteral("Unknown") << uint(0); +} + +void TestBatteryDevice::statusMapping() +{ + QFETCH(QString, state); + QFETCH(uint, expected); + + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}, + {QStringLiteral("status"), state}}); + BatteryDevice device(dir.path()); + QCOMPARE(device.status(), expected); +} + +void TestBatteryDevice::objectPath_data() +{ + QTest::addColumn("dirName"); + QTest::addColumn("expectedPath"); + + QTest::newRow("plain") << QStringLiteral("BAT0") << QStringLiteral("/org/deepin/dde/Power1/battery_BAT0"); + QTest::newRow("underscore")<< QStringLiteral("AC_ADAPTER") << QStringLiteral("/org/deepin/dde/Power1/battery_AC_ADAPTER"); + QTest::newRow("dash") << QStringLiteral("BAT-1") << QStringLiteral("/org/deepin/dde/Power1/battery_BAT_x01"); + QTest::newRow("dot") << QStringLiteral("psu.0") << QStringLiteral("/org/deepin/dde/Power1/battery_psu_x10"); + QTest::newRow("colon") << QStringLiteral("battery:1") << QStringLiteral("/org/deepin/dde/Power1/battery_battery_x21"); + QTest::newRow("space") << QStringLiteral("A B") << QStringLiteral("/org/deepin/dde/Power1/battery_A_x20B"); +} + +void TestBatteryDevice::objectPath() +{ + QFETCH(QString, dirName); + QFETCH(QString, expectedPath); + + QTemporaryDir parent; + QVERIFY(parent.isValid()); + const QString fullPath = parent.path() + '/' + dirName; + QVERIFY(QDir().mkpath(fullPath)); + + BatteryDevice device(fullPath); + QCOMPARE(device.objectPath().path(), expectedPath); +} + +void TestBatteryDevice::setStatusEmitsChange() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + makeSysfs(dir, {{QStringLiteral("type"), QStringLiteral("Battery")}, + {QStringLiteral("present"), QStringLiteral("1")}}); + BatteryDevice device(dir.path()); + + QSignalSpy spy(&device, &BatteryDevice::statusChanged); + device.setStatus(1); + device.setStatus(1); // no-op, no second emission + QCOMPARE(spy.count(), 1); + QCOMPARE(device.status(), uint(1)); +} + +QTEST_GUILESS_MAIN(TestBatteryDevice) +#include "tst_batterydevice.moc"