[LTP] [PATCH v2 14/31] testcases: sysfs: sys_hwmon01

Cyril Hrubis chrubis@suse.cz
Wed Aug 19 18:16:17 CEST 2026


Add test for /sys/class/hwmon/*/* files.

Signed-off-by: Cyril Hrubis <chrubis@suse.cz>
---
 runtest/sysfs                                 |   1 +
 testcases/kernel/sysfs/class/hwmon/.gitignore |   1 +
 testcases/kernel/sysfs/class/hwmon/Makefile   |   7 +
 .../kernel/sysfs/class/hwmon/sys_hwmon01.c    | 173 ++++++++++++++++++
 4 files changed, 182 insertions(+)
 create mode 100644 testcases/kernel/sysfs/class/hwmon/.gitignore
 create mode 100644 testcases/kernel/sysfs/class/hwmon/Makefile
 create mode 100644 testcases/kernel/sysfs/class/hwmon/sys_hwmon01.c

diff --git a/runtest/sysfs b/runtest/sysfs
index c0983a5be..23c7adc7a 100644
--- a/runtest/sysfs
+++ b/runtest/sysfs
@@ -10,3 +10,4 @@ sys_cpu_cache01 sys_cpu_cache01
 sys_clockevents01 sys_clockevents01
 sys_ata01 sys_ata01
 sys_bdi01 sys_bdi01
+sys_hwmon01 sys_hwmon01
diff --git a/testcases/kernel/sysfs/class/hwmon/.gitignore b/testcases/kernel/sysfs/class/hwmon/.gitignore
new file mode 100644
index 000000000..f619181d7
--- /dev/null
+++ b/testcases/kernel/sysfs/class/hwmon/.gitignore
@@ -0,0 +1 @@
+/sys_hwmon01
diff --git a/testcases/kernel/sysfs/class/hwmon/Makefile b/testcases/kernel/sysfs/class/hwmon/Makefile
new file mode 100644
index 000000000..034038061
--- /dev/null
+++ b/testcases/kernel/sysfs/class/hwmon/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 Cyril Hrubis <chrubis@suse.cz>
+
+top_srcdir		?= ../../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/sysfs/class/hwmon/sys_hwmon01.c b/testcases/kernel/sysfs/class/hwmon/sys_hwmon01.c
new file mode 100644
index 000000000..4c9797bd6
--- /dev/null
+++ b/testcases/kernel/sysfs/class/hwmon/sys_hwmon01.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Cyril Hrubis <chrubis@suse.cz>
+ */
+
+/*\
+ * Walks all hardware monitoring devices under /sys/class/hwmon/ and performs
+ * sanity and cross-checks on the sensor channels exported per the stable
+ * hwmon sysfs ABI (Documentation/hwmon/sysfs-interface.rst).
+ *
+ * For every discovered channel it verifies that:
+ *
+ * - tempN_input is within a plausible range (-55000 to 200000 millidegree
+ *   Celsius, i.e. within the extended-industrial silicon sensor rating floor
+ *   and below a generously high bound)
+ * - tempN_alarm, tempN_crit_alarm, tempN_min_alarm, tempN_max_alarm and
+ *   tempN_fault are booleans, when present
+ * - fanN_input is within a plausible range (0 to 100000 RPM, comfortably
+ *   above even exotic small high-speed blower fans)
+ * - fanN_alarm, fanN_fault, fanN_min_alarm and fanN_max_alarm are booleans,
+ *   when present
+ * - inN_input is within a plausible range (-100000 to 100000 millivolt,
+ *   allowing for negative supply rails, not just positive ones, inN
+ *   channels are numbered from 0, unlike the other channel types which
+ *   start at 1)
+ * - inN_alarm and related alarm attributes are booleans, when present
+ * - currN_input is within a plausible range (-500000 to 500000 milliampere,
+ *   allowing for negative values on bidirectional/battery charge-discharge
+ *   current sensors)
+ * - powerN_input is within a plausible range (0 to 2000000000 microwatt,
+ *   i.e. 2 kW, chosen to stay within LONG_MAX on 32bit architectures as well)
+ * - pwmN is in the documented range [0, 255]
+ * - update_interval, when present, is within a plausible range (1 to
+ *   3600000 millisecond, i.e. up to an hour)
+ *
+ * All checks skip silently when a particular channel or attribute is not
+ * present, since the set of exposed sensors varies wildly between chips and
+ * platforms and up to MAX_CHANNELS indices are probed for every channel type
+ * regardless of how many are actually implemented by a given chip.
+ *
+ * Note: per Documentation/ABI/testing/sysfs-class-hwmon, tempN/inN/currN
+ * min/max/crit/lcrit are plain independent RW attributes. The kernel does
+ * not cross-validate them against each other (only tempN_crit/tempN_lcrit
+ * even mention a "typically greater/lower than" relation to max/min, and
+ * that is explicitly not a guarantee), so this test deliberately does not
+ * assert any ordering between them, e.g. a firmware default of minN > maxN
+ * is not a kernel or driver bug.
+ */
+
+#include <string.h>
+#include <dirent.h>
+
+#include "tst_test.h"
+#include "tst_sysfs_assert.h"
+
+#define HWMON "/sys/class/hwmon"
+#define MAX_CHANNELS 32
+
+static void check_temp_channel(const char *hwmon, int idx)
+{
+	TST_SYSFS_ASSERT_RANGELL_SILENT(-55000, 200000,
+					HWMON "/%s/temp%d_input", hwmon, idx);
+
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/temp%d_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/temp%d_crit_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/temp%d_min_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/temp%d_max_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/temp%d_fault", hwmon, idx);
+}
+
+static void check_fan_channel(const char *hwmon, int idx)
+{
+	TST_SYSFS_ASSERT_RANGELL_SILENT(0, 100000,
+					HWMON "/%s/fan%d_input", hwmon, idx);
+
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/fan%d_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/fan%d_min_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/fan%d_max_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/fan%d_fault", hwmon, idx);
+}
+
+static void check_in_channel(const char *hwmon, int idx)
+{
+	TST_SYSFS_ASSERT_RANGELL_SILENT(-100000, 100000,
+					HWMON "/%s/in%d_input", hwmon, idx);
+
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/in%d_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/in%d_min_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/in%d_max_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/in%d_crit_alarm", hwmon, idx);
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/in%d_lcrit_alarm", hwmon, idx);
+}
+
+static void check_curr_channel(const char *hwmon, int idx)
+{
+	/*
+	 * Unlike fan/power, current can be negative on bidirectional sensors,
+	 * e.g. a battery charge/discharge current monitor.
+	 */
+	TST_SYSFS_ASSERT_RANGELL_SILENT(-500000, 500000,
+					HWMON "/%s/curr%d_input", hwmon, idx);
+
+	TST_SYSFS_ASSERT_BOOL_SILENT(HWMON "/%s/curr%d_alarm", hwmon, idx);
+}
+
+static void check_power_channel(const char *hwmon, int idx)
+{
+	/*
+	 * 2000000000 uW (2 kW) comfortably covers any single-component power
+	 * sensor (CPU, GPU, PSU rail, ...) while still fitting within
+	 * LONG_MAX on 32bit architectures.
+	 */
+	TST_SYSFS_ASSERT_RANGELL_SILENT(0, 2000000000,
+					HWMON "/%s/power%d_input", hwmon, idx);
+}
+
+static void check_pwm_channel(const char *hwmon, int idx)
+{
+	TST_SYSFS_ASSERT_RANGELL_SILENT(0, 255, HWMON "/%s/pwm%d", hwmon, idx);
+}
+
+static void check_hwmon_device(const char *hwmon)
+{
+	char name[64];
+	int i;
+
+	if (TST_SYSFS_READ_STR(name, sizeof(name), HWMON "/%s/name", hwmon))
+		tst_res(TINFO, "%s: name = '%s'", hwmon, name);
+
+	TST_SYSFS_ASSERT_RANGELL_SILENT(1, 3600000,
+					HWMON "/%s/update_interval", hwmon);
+
+	/* inN channels are numbered from 0, per the hwmon ABI */
+	for (i = 0; i <= MAX_CHANNELS; i++)
+		check_in_channel(hwmon, i);
+
+	for (i = 1; i <= MAX_CHANNELS; i++) {
+		check_temp_channel(hwmon, i);
+		check_fan_channel(hwmon, i);
+		check_curr_channel(hwmon, i);
+		check_power_channel(hwmon, i);
+		check_pwm_channel(hwmon, i);
+	}
+}
+
+static void do_test(void)
+{
+	DIR *d;
+	struct dirent *ent;
+	int found = 0;
+
+	if (!tst_sysfs_exists(HWMON))
+		tst_brk(TCONF, HWMON ": not present");
+
+	d = SAFE_OPENDIR(HWMON);
+
+	while ((ent = SAFE_READDIR(d))) {
+		if (strncmp(ent->d_name, "hwmon", 5))
+			continue;
+
+		found = 1;
+		check_hwmon_device(ent->d_name);
+	}
+
+	SAFE_CLOSEDIR(d);
+
+	if (!found)
+		tst_res(TCONF, "No hwmon device found");
+}
+
+static struct tst_test test = {
+	.test_all = do_test,
+};
-- 
2.54.0



More information about the ltp mailing list