[PATCH v2 6/6] selftests/cpuidle: add idle-state disable selftest
From: Yaxiong Tian
Date: Wed Jul 29 2026 - 02:27:10 EST
Verify that writing 1 to cpuidle stateX/disable stops that state from
gaining usage while other states continue to idle, then restore the
previous disable value.
Signed-off-by: Yaxiong Tian <tianyaxiong@xxxxxxxxxx>
---
tools/testing/selftests/cpuidle/Makefile | 2 +-
.../cpuidle/cpuidle_disable_state.py | 133 ++++++++++++++++++
tools/testing/selftests/cpuidle/settings | 2 +-
3 files changed, 135 insertions(+), 2 deletions(-)
create mode 100755 tools/testing/selftests/cpuidle/cpuidle_disable_state.py
diff --git a/tools/testing/selftests/cpuidle/Makefile b/tools/testing/selftests/cpuidle/Makefile
index 116afec7fc0f..0e054199acc5 100644
--- a/tools/testing/selftests/cpuidle/Makefile
+++ b/tools/testing/selftests/cpuidle/Makefile
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
all:
-TEST_PROGS := cpuidle_latency_req_qos.py
+TEST_PROGS := cpuidle_latency_req_qos.py cpuidle_disable_state.py
TEST_FILES := cpuidle_lib.py
include ../lib.mk
diff --git a/tools/testing/selftests/cpuidle/cpuidle_disable_state.py b/tools/testing/selftests/cpuidle/cpuidle_disable_state.py
new file mode 100755
index 000000000000..72ad5278c53c
--- /dev/null
+++ b/tools/testing/selftests/cpuidle/cpuidle_disable_state.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+cpuidle: verify user disable of an idle state blocks selection.
+
+Writes 1 to /sys/devices/system/cpu/cpuN/cpuidle/stateX/disable and checks
+that stateX does not gain usage while other states still idle.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Dict, Optional
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import cpuidle_lib as cl
+import ksft
+
+
+def pick_disable_target(states: Dict[int, cl.IdleState]) -> Optional[int]:
+ """Prefer deepest user-enabled state; keep at least one shallower enabled."""
+ ordered = sorted(states)
+ if len(ordered) < 2:
+ return None
+ for idx in reversed(ordered):
+ if idx == ordered[0]:
+ continue
+ s = states[idx]
+ if s.disable_path and s.disable == 0:
+ return idx
+ return None
+
+
+class DisableStateGuard:
+ def __init__(self, path: str):
+ self.path = path
+ self.prev: Optional[str] = None
+
+ def __enter__(self):
+ self.prev = open(self.path).read().strip()
+ with open(self.path, "w") as f:
+ f.write("1\n")
+ return self
+
+ def __exit__(self, *args):
+ if self.prev is None:
+ return
+ with open(self.path, "w") as f:
+ f.write(f"{self.prev}\n")
+
+
+def run_disable_case(desc: str, states: Dict[int, cl.IdleState],
+ target: int) -> None:
+ s = states[target]
+ ksft.print_msg(f"=== {desc} ===")
+ ksft.print_msg(f"disable state{target}:{s.name} (lat={s.latency_us})")
+
+ with DisableStateGuard(s.disable_path):
+ ud = cl.measure_idle()
+
+ total = sum(ud.values())
+ cl.print_usage_table(states, ud, {target})
+ ksft.print_msg(f"total_usage+={total} disabled_usage+={ud[target]}")
+
+ if total <= 0:
+ ksft.test_result_fail(f"{desc}: too little idle activity ({total})")
+ return
+ if ud[target] > 0:
+ ksft.test_result_fail(
+ f"{desc}: state{target}({s.name}) usage+={ud[target]} "
+ "while user-disabled"
+ )
+ return
+ ksft.test_result_pass(desc)
+
+
+def run_reenable_case(desc: str, states: Dict[int, cl.IdleState],
+ target: int) -> None:
+ """After restore, confirm disable sysfs is cleared and idle still works."""
+ s = states[target]
+ ksft.print_msg(f"=== {desc} ===")
+ ksft.print_msg(f"re-enabled state{target}:{s.name}")
+
+ ud = cl.measure_idle()
+ total = sum(ud.values())
+ cl.print_usage_table(states, ud)
+ ksft.print_msg(f"total_usage+={total} target_usage+={ud[target]}")
+
+ if total <= 0:
+ ksft.test_result_fail(f"{desc}: too little idle activity ({total})")
+ return
+ cur = int(open(s.disable_path).read())
+ if cur != 0:
+ ksft.test_result_fail(f"{desc}: disable sysfs still {cur}")
+ return
+ ksft.test_result_pass(desc)
+
+
+def main() -> None:
+ ksft.print_header()
+
+ states = cl.require_root_and_cpuidle(min_states=2)
+ # Need disable sysfs on states we might target.
+ states = {i: s for i, s in states.items() if s.disable_path}
+ if len(states) < 2:
+ ksft.set_plan(1)
+ ksft.test_result_skip("need at least 2 cpuidle states with disable")
+ ksft.finished()
+
+ cl.log_states(states, extra=True)
+ cl.warn_preexisting_qos()
+
+ target = pick_disable_target(states)
+ if target is None:
+ ksft.set_plan(1)
+ ksft.test_result_skip("no user-enabled deep state to disable")
+ ksft.finished()
+
+ ksft.set_plan(2)
+ run_disable_case(
+ f"disable state{target} blocks selection", states, target
+ )
+ states = cl.read_states()
+ run_reenable_case(
+ f"restore state{target} disable", states, target
+ )
+
+ ksft.finished()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/cpuidle/settings b/tools/testing/selftests/cpuidle/settings
index 1d2078f92780..1bbc754b982b 100644
--- a/tools/testing/selftests/cpuidle/settings
+++ b/tools/testing/selftests/cpuidle/settings
@@ -1,2 +1,2 @@
-# QoS ceilings x paths, plus no-QoS baselines.
+# QoS ceilings x paths, no-QoS baselines, plus disable-state windows.
timeout=180
--
2.43.0