tools/testing/selftests/mm/Makefile | 1 + tools/testing/selftests/mm/run_vmtests.sh | 2 + tools/testing/selftests/mm/thp_sysfs_test.sh | 666 +++++++++++++++++++++++++++ 3 files changed, 669 insertions(+)
Add a shell-based selftest that exercises the full set of THP sysfs
knobs: enabled (global and per-size anon), defrag, use_zero_page,
hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
khugepaged/ tunables, and per-size stats files.
Each writable knob is tested for valid writes, invalid-input rejection,
idempotent writes, and mode transitions where applicable. All original
values are saved before testing and restored afterwards.
The test uses the kselftest KTAP framework (ktap_helpers.sh) for
structured TAP 13 output, making results parseable by the kselftest
harness. The test plan is printed at the end since the number of test
points is dynamic (depends on available hugepage sizes and sysfs files).
This is particularly useful for validating the refactoring of
enabled_store() and anon_enabled_store() to use sysfs_match_string()
and the new change_enabled()/change_anon_orders() helpers.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
While working on the THP sysfs interface, I noticed there were no
existing tests covering this functionality. This patch adds test
coverage that may be useful for upstream !?
---
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/run_vmtests.sh | 2 +
tools/testing/selftests/mm/thp_sysfs_test.sh | 666 +++++++++++++++++++++++++++
3 files changed, 669 insertions(+)
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 7a5de4e9bf520..90fcca53a561b 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -180,6 +180,7 @@ TEST_FILES += charge_reserved_hugetlb.sh
TEST_FILES += hugetlb_reparenting_test.sh
TEST_FILES += test_page_frag.sh
TEST_FILES += run_vmtests.sh
+TEST_FILES += thp_sysfs_test.sh
# required by charge_reserved_hugetlb.sh
TEST_FILES += write_hugetlb_memory.sh
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index afdcfd0d7cef7..9e0e2089214a3 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -491,6 +491,8 @@ CATEGORY="thp" run_test ./khugepaged -s 4 all:shmem
CATEGORY="thp" run_test ./transhuge-stress -d 20
+CATEGORY="thp" run_test ./thp_sysfs_test.sh
+
# Try to create XFS if not provided
if [ -z "${SPLIT_HUGE_PAGE_TEST_XFS_PATH}" ]; then
if [ "${HAVE_HUGEPAGES}" = "1" ]; then
diff --git a/tools/testing/selftests/mm/thp_sysfs_test.sh b/tools/testing/selftests/mm/thp_sysfs_test.sh
new file mode 100755
index 0000000000000..407f306a0989f
--- /dev/null
+++ b/tools/testing/selftests/mm/thp_sysfs_test.sh
@@ -0,0 +1,666 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test THP sysfs interface.
+#
+# Exercises the full set of THP sysfs knobs: enabled (global and
+# per-size anon), defrag, use_zero_page, hpage_pmd_size, shmem_enabled
+# (global and per-size), shrink_underused, khugepaged/ tunables, and
+# per-size stats files. Each writable knob is tested for valid writes,
+# invalid-input rejection, idempotent writes, and mode transitions
+# where applicable. All original values are saved before testing and
+# restored afterwards.
+#
+# Author: Breno Leitao <leitao@debian.org>
+
+DIR="$(dirname "$(readlink -f "$0")")"
+. "${DIR}"/../kselftest/ktap_helpers.sh
+
+THP_SYSFS=/sys/kernel/mm/transparent_hugepage
+
+# Read the currently active mode from a sysfs enabled file.
+# The active mode is enclosed in brackets, e.g. "always [madvise] never"
+get_active_mode() {
+ local path="$1"
+ local content
+
+ content=$(cat "$path")
+ echo "$content" | grep -o '\[.*\]' | tr -d '[]'
+}
+
+# Test that writing a mode and reading it back gives the expected result.
+test_mode() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+ local active
+
+ if ! echo "$mode" > "$path" 2>/dev/null; then
+ ktap_test_fail "$label: write '$mode'"
+ return
+ fi
+
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$mode" ]; then
+ ktap_test_pass "$label: write '$mode'"
+ else
+ ktap_test_fail "$label: write '$mode', read back '$active'"
+ fi
+}
+
+# Test that writing an invalid mode is rejected.
+test_invalid() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+ local saved
+
+ saved=$(get_active_mode "$path")
+
+ if echo "$mode" > "$path" 2>/dev/null; then
+ # Write succeeded -- check if mode actually changed (it shouldn't)
+ local active
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$saved" ]; then
+ # Some shells don't propagate the error, but mode unchanged
+ ktap_test_pass "$label: reject '$mode'"
+ else
+ ktap_test_fail "$label: '$mode' should have been rejected but mode changed to '$active'"
+ fi
+ else
+ ktap_test_pass "$label: reject '$mode'"
+ fi
+}
+
+# Test that writing the same mode twice doesn't crash or change state.
+test_idempotent() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+
+ echo "$mode" > "$path" 2>/dev/null
+ echo "$mode" > "$path" 2>/dev/null
+ local active
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$mode" ]; then
+ ktap_test_pass "$label: idempotent '$mode'"
+ else
+ ktap_test_fail "$label: idempotent '$mode', got '$active'"
+ fi
+}
+
+# Write a numeric value, read it back, verify match.
+test_numeric() {
+ local path="$1"
+ local value="$2"
+ local label="$3"
+ local readback
+
+ if ! echo "$value" > "$path" 2>/dev/null; then
+ ktap_test_fail "$label: write '$value'"
+ return
+ fi
+
+ readback=$(cat "$path" 2>/dev/null)
+ if [ "$readback" = "$value" ]; then
+ ktap_test_pass "$label: write '$value'"
+ else
+ ktap_test_fail "$label: write '$value', read back '$readback'"
+ fi
+}
+
+# Verify that an out-of-range or invalid numeric value is rejected.
+test_numeric_invalid() {
+ local path="$1"
+ local value="$2"
+ local label="$3"
+ local saved readback
+
+ saved=$(cat "$path" 2>/dev/null)
+
+ if echo "$value" > "$path" 2>/dev/null; then
+ readback=$(cat "$path" 2>/dev/null)
+ if [ "$readback" = "$saved" ]; then
+ ktap_test_pass "$label: reject '$value'"
+ else
+ ktap_test_fail "$label: '$value' should have been rejected but value changed to '$readback'"
+ fi
+ else
+ ktap_test_pass "$label: reject '$value'"
+ fi
+}
+
+# Verify a read-only file: readable, returns a numeric value, rejects writes.
+test_readonly() {
+ local path="$1"
+ local label="$2"
+ local val
+
+ val=$(cat "$path" 2>/dev/null)
+ if [ -z "$val" ]; then
+ ktap_test_fail "$label: read returned empty"
+ return
+ fi
+
+ if ! echo "$val" | grep -qE '^[0-9]+$'; then
+ ktap_test_fail "$label: expected numeric, got '$val'"
+ return
+ fi
+
+ if echo "0" > "$path" 2>/dev/null; then
+ local after
+ after=$(cat "$path" 2>/dev/null)
+ if [ "$after" = "$val" ]; then
+ ktap_test_pass "$label: read-only (value=$val)"
+ else
+ ktap_test_fail "$label: write should have been rejected but value changed"
+ fi
+ else
+ ktap_test_pass "$label: read-only (value=$val)"
+ fi
+}
+
+# --- Precondition checks ---
+
+ktap_print_header
+
+if [ ! -d "$THP_SYSFS" ]; then
+ ktap_skip_all "THP sysfs not found at $THP_SYSFS"
+ exit "$KSFT_SKIP"
+fi
+
+if [ "$(id -u)" -ne 0 ]; then
+ ktap_skip_all "must be run as root"
+ exit "$KSFT_SKIP"
+fi
+
+# --- Test global THP enabled ---
+
+GLOBAL_ENABLED="$THP_SYSFS/enabled"
+
+if [ ! -f "$GLOBAL_ENABLED" ]; then
+ ktap_test_skip "global enabled file not found"
+else
+ ktap_print_msg "Testing global THP enabled ($GLOBAL_ENABLED)"
+
+ # Save current setting
+ saved_global=$(get_active_mode "$GLOBAL_ENABLED")
+
+ # Valid modes for global
+ test_mode "$GLOBAL_ENABLED" "always" "global"
+ test_mode "$GLOBAL_ENABLED" "madvise" "global"
+ test_mode "$GLOBAL_ENABLED" "never" "global"
+
+ # "inherit" is not valid for global THP
+ test_invalid "$GLOBAL_ENABLED" "inherit" "global"
+
+ # Invalid strings
+ test_invalid "$GLOBAL_ENABLED" "bogus" "global"
+ test_invalid "$GLOBAL_ENABLED" "" "global (empty)"
+
+ # Idempotent writes
+ test_idempotent "$GLOBAL_ENABLED" "always" "global"
+ test_idempotent "$GLOBAL_ENABLED" "never" "global"
+
+ # Restore
+ echo "$saved_global" > "$GLOBAL_ENABLED" 2>/dev/null
+fi
+
+# --- Test global defrag ---
+
+GLOBAL_DEFRAG="$THP_SYSFS/defrag"
+
+if [ ! -f "$GLOBAL_DEFRAG" ]; then
+ ktap_test_skip "defrag file not found"
+else
+ ktap_print_msg "Testing global THP defrag ($GLOBAL_DEFRAG)"
+
+ saved_defrag=$(get_active_mode "$GLOBAL_DEFRAG")
+
+ # Valid modes
+ test_mode "$GLOBAL_DEFRAG" "always" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "defer" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "madvise" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "never" "defrag"
+
+ # Invalid
+ test_invalid "$GLOBAL_DEFRAG" "bogus" "defrag"
+ test_invalid "$GLOBAL_DEFRAG" "" "defrag (empty)"
+ test_invalid "$GLOBAL_DEFRAG" "inherit" "defrag"
+
+ # Idempotent
+ test_idempotent "$GLOBAL_DEFRAG" "always" "defrag"
+ test_idempotent "$GLOBAL_DEFRAG" "never" "defrag"
+
+ # Mode transitions: cycle through all 5
+ echo "always" > "$GLOBAL_DEFRAG"
+ test_mode "$GLOBAL_DEFRAG" "defer" "defrag (always->defer)"
+ test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag (defer->defer+madvise)"
+ test_mode "$GLOBAL_DEFRAG" "madvise" "defrag (defer+madvise->madvise)"
+ test_mode "$GLOBAL_DEFRAG" "never" "defrag (madvise->never)"
+ test_mode "$GLOBAL_DEFRAG" "always" "defrag (never->always)"
+
+ # Restore
+ echo "$saved_defrag" > "$GLOBAL_DEFRAG" 2>/dev/null
+fi
+
+# --- Test use_zero_page ---
+
+USE_ZERO_PAGE="$THP_SYSFS/use_zero_page"
+
+if [ ! -f "$USE_ZERO_PAGE" ]; then
+ ktap_test_skip "use_zero_page file not found"
+else
+ ktap_print_msg "Testing use_zero_page ($USE_ZERO_PAGE)"
+
+ saved_uzp=$(cat "$USE_ZERO_PAGE" 2>/dev/null)
+
+ # Valid values
+ test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page"
+ test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page"
+
+ # Invalid values
+ test_numeric_invalid "$USE_ZERO_PAGE" "2" "use_zero_page"
+ test_numeric_invalid "$USE_ZERO_PAGE" "-1" "use_zero_page"
+ test_numeric_invalid "$USE_ZERO_PAGE" "bogus" "use_zero_page"
+
+ # Idempotent
+ echo "1" > "$USE_ZERO_PAGE" 2>/dev/null
+ test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page (idempotent)"
+ echo "0" > "$USE_ZERO_PAGE" 2>/dev/null
+ test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page (idempotent)"
+
+ # Restore
+ echo "$saved_uzp" > "$USE_ZERO_PAGE" 2>/dev/null
+fi
+
+# --- Test hpage_pmd_size ---
+
+HPAGE_PMD_SIZE_FILE="$THP_SYSFS/hpage_pmd_size"
+
+if [ ! -f "$HPAGE_PMD_SIZE_FILE" ]; then
+ ktap_test_skip "hpage_pmd_size file not found"
+else
+ ktap_print_msg "Testing hpage_pmd_size ($HPAGE_PMD_SIZE_FILE)"
+
+ test_readonly "$HPAGE_PMD_SIZE_FILE" "hpage_pmd_size"
+fi
+
+# --- Test global shmem_enabled ---
+
+SHMEM_ENABLED="$THP_SYSFS/shmem_enabled"
+
+if [ ! -f "$SHMEM_ENABLED" ]; then
+ ktap_test_skip "shmem_enabled file not found (CONFIG_SHMEM not set?)"
+else
+ ktap_print_msg "Testing global shmem_enabled ($SHMEM_ENABLED)"
+
+ saved_shmem=$(get_active_mode "$SHMEM_ENABLED")
+
+ # Valid modes
+ test_mode "$SHMEM_ENABLED" "always" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "never" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "force" "shmem_enabled"
+
+ # Invalid
+ test_invalid "$SHMEM_ENABLED" "bogus" "shmem_enabled"
+ test_invalid "$SHMEM_ENABLED" "inherit" "shmem_enabled"
+ test_invalid "$SHMEM_ENABLED" "" "shmem_enabled (empty)"
+
+ # Idempotent
+ test_idempotent "$SHMEM_ENABLED" "always" "shmem_enabled"
+ test_idempotent "$SHMEM_ENABLED" "never" "shmem_enabled"
+
+ # Mode transitions: cycle through all 6
+ echo "always" > "$SHMEM_ENABLED"
+ test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled (always->within_size)"
+ test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled (within_size->advise)"
+ test_mode "$SHMEM_ENABLED" "never" "shmem_enabled (advise->never)"
+ test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled (never->deny)"
+ test_mode "$SHMEM_ENABLED" "force" "shmem_enabled (deny->force)"
+ test_mode "$SHMEM_ENABLED" "always" "shmem_enabled (force->always)"
+
+ # Restore
+ echo "$saved_shmem" > "$SHMEM_ENABLED" 2>/dev/null
+fi
+
+# --- Test shrink_underused ---
+
+SHRINK_UNDERUSED="$THP_SYSFS/shrink_underused"
+
+if [ ! -f "$SHRINK_UNDERUSED" ]; then
+ ktap_test_skip "shrink_underused file not found"
+else
+ ktap_print_msg "Testing shrink_underused ($SHRINK_UNDERUSED)"
+
+ saved_shrink=$(cat "$SHRINK_UNDERUSED" 2>/dev/null)
+
+ # Valid values
+ test_numeric "$SHRINK_UNDERUSED" "0" "shrink_underused"
+ test_numeric "$SHRINK_UNDERUSED" "1" "shrink_underused"
+
+ # Invalid values
+ test_numeric_invalid "$SHRINK_UNDERUSED" "2" "shrink_underused"
+ test_numeric_invalid "$SHRINK_UNDERUSED" "bogus" "shrink_underused"
+
+ # Restore
+ echo "$saved_shrink" > "$SHRINK_UNDERUSED" 2>/dev/null
+fi
+
+# --- Test per-size anon THP enabled ---
+
+found_anon=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+
+ ANON_ENABLED="$dir/enabled"
+ [ -f "$ANON_ENABLED" ] || continue
+
+ found_anon=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size anon THP enabled ($size)"
+
+ # Save current setting
+ saved_anon=$(get_active_mode "$ANON_ENABLED")
+
+ # Valid modes for per-size anon (includes inherit)
+ test_mode "$ANON_ENABLED" "always" "$size"
+ test_mode "$ANON_ENABLED" "inherit" "$size"
+ test_mode "$ANON_ENABLED" "madvise" "$size"
+ test_mode "$ANON_ENABLED" "never" "$size"
+
+ # Invalid strings
+ test_invalid "$ANON_ENABLED" "bogus" "$size"
+
+ # Idempotent writes
+ test_idempotent "$ANON_ENABLED" "always" "$size"
+ test_idempotent "$ANON_ENABLED" "inherit" "$size"
+ test_idempotent "$ANON_ENABLED" "never" "$size"
+
+ # Mode transitions: verify each mode clears the others
+ echo "always" > "$ANON_ENABLED"
+ test_mode "$ANON_ENABLED" "madvise" "$size (always->madvise)"
+ test_mode "$ANON_ENABLED" "inherit" "$size (madvise->inherit)"
+ test_mode "$ANON_ENABLED" "never" "$size (inherit->never)"
+ test_mode "$ANON_ENABLED" "always" "$size (never->always)"
+
+ # Restore
+ echo "$saved_anon" > "$ANON_ENABLED" 2>/dev/null
+
+ # Only test one size in detail to keep output manageable,
+ # but do a quick smoke test on the rest
+ break
+done
+
+if [ $found_anon -eq 0 ]; then
+ ktap_test_skip "no per-size anon THP directories found"
+fi
+
+# Quick smoke test: all other sizes accept valid modes
+first=1
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ ANON_ENABLED="$dir/enabled"
+ [ -f "$ANON_ENABLED" ] || continue
+
+ # Skip the first one (already tested in detail)
+ if [ $first -eq 1 ]; then
+ first=0
+ continue
+ fi
+
+ size=$(basename "$dir")
+ saved=$(get_active_mode "$ANON_ENABLED")
+
+ smoke_failed=0
+ for mode in always inherit madvise never; do
+ echo "$mode" > "$ANON_ENABLED" 2>/dev/null
+ active=$(get_active_mode "$ANON_ENABLED")
+ if [ "$active" != "$mode" ]; then
+ ktap_test_fail "$size: smoke test '$mode' got '$active'"
+ smoke_failed=1
+ break
+ fi
+ done
+ [ $smoke_failed -eq 0 ] && ktap_test_pass "$size: smoke test all modes"
+
+ echo "$saved" > "$ANON_ENABLED" 2>/dev/null
+done
+
+# --- Test per-size shmem_enabled ---
+
+found_shmem=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+
+ SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
+ [ -f "$SHMEM_SIZE_ENABLED" ] || continue
+
+ found_shmem=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size shmem_enabled ($size)"
+
+ # Save current setting
+ saved_shmem_size=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+
+ # Valid modes for per-size shmem
+ test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem"
+
+ # Invalid: deny and force are not valid for per-size
+ test_invalid "$SHMEM_SIZE_ENABLED" "bogus" "$size shmem"
+ test_invalid "$SHMEM_SIZE_ENABLED" "deny" "$size shmem"
+ test_invalid "$SHMEM_SIZE_ENABLED" "force" "$size shmem"
+
+ # Mode transitions
+ echo "always" > "$SHMEM_SIZE_ENABLED"
+ test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem (always->inherit)"
+ test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem (inherit->within_size)"
+ test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem (within_size->advise)"
+ test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem (advise->never)"
+ test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem (never->always)"
+
+ # Restore
+ echo "$saved_shmem_size" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+
+ # Only test one size in detail
+ break
+done
+
+if [ $found_shmem -eq 0 ]; then
+ ktap_test_skip "no per-size shmem_enabled files found"
+fi
+
+# Quick smoke test: remaining sizes with shmem_enabled
+first=1
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
+ [ -f "$SHMEM_SIZE_ENABLED" ] || continue
+
+ if [ $first -eq 1 ]; then
+ first=0
+ continue
+ fi
+
+ size=$(basename "$dir")
+ saved=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+
+ smoke_failed=0
+ for mode in always inherit within_size advise never; do
+ echo "$mode" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+ active=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+ if [ "$active" != "$mode" ]; then
+ ktap_test_fail "$size shmem: smoke test '$mode' got '$active'"
+ smoke_failed=1
+ break
+ fi
+ done
+ [ $smoke_failed -eq 0 ] && ktap_test_pass "$size shmem: smoke test all modes"
+
+ echo "$saved" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+done
+
+# --- Test khugepaged tunables ---
+
+KHUGEPAGED="$THP_SYSFS/khugepaged"
+
+if [ ! -d "$KHUGEPAGED" ]; then
+ ktap_test_skip "khugepaged directory not found"
+else
+ ktap_print_msg "Testing khugepaged tunables ($KHUGEPAGED)"
+
+ # Compute HPAGE_PMD_NR for boundary tests
+ pmd_size=$(cat "$HPAGE_PMD_SIZE_FILE" 2>/dev/null)
+ page_size=$(getconf PAGE_SIZE)
+ if [ -n "$pmd_size" ] && [ -n "$page_size" ] && [ "$page_size" -gt 0 ]; then
+ hpage_pmd_nr=$((pmd_size / page_size))
+ else
+ hpage_pmd_nr=512
+ fi
+
+ # Save all tunable values
+ saved_khp_defrag=$(cat "$KHUGEPAGED/defrag" 2>/dev/null)
+ saved_khp_max_ptes_none=$(cat "$KHUGEPAGED/max_ptes_none" 2>/dev/null)
+ saved_khp_max_ptes_swap=$(cat "$KHUGEPAGED/max_ptes_swap" 2>/dev/null)
+ saved_khp_max_ptes_shared=$(cat "$KHUGEPAGED/max_ptes_shared" 2>/dev/null)
+ saved_khp_pages_to_scan=$(cat "$KHUGEPAGED/pages_to_scan" 2>/dev/null)
+ saved_khp_scan_sleep=$(cat "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null)
+ saved_khp_alloc_sleep=$(cat "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null)
+
+ # khugepaged/defrag (0/1 flag)
+ if [ -f "$KHUGEPAGED/defrag" ]; then
+ test_numeric "$KHUGEPAGED/defrag" "0" "khugepaged/defrag"
+ test_numeric "$KHUGEPAGED/defrag" "1" "khugepaged/defrag"
+ test_numeric_invalid "$KHUGEPAGED/defrag" "2" "khugepaged/defrag"
+ test_numeric_invalid "$KHUGEPAGED/defrag" "bogus" "khugepaged/defrag"
+ fi
+
+ # khugepaged/max_ptes_none (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_none" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_none" "0" "khugepaged/max_ptes_none"
+ test_numeric "$KHUGEPAGED/max_ptes_none" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_none"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_none" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_none (boundary)"
+ fi
+
+ # khugepaged/max_ptes_swap (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_swap" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_swap" "0" "khugepaged/max_ptes_swap"
+ test_numeric "$KHUGEPAGED/max_ptes_swap" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_swap"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_swap" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_swap (boundary)"
+ fi
+
+ # khugepaged/max_ptes_shared (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_shared" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_shared" "0" "khugepaged/max_ptes_shared"
+ test_numeric "$KHUGEPAGED/max_ptes_shared" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_shared"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_shared" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_shared (boundary)"
+ fi
+
+ # khugepaged/pages_to_scan (1 .. UINT_MAX, 0 rejected)
+ if [ -f "$KHUGEPAGED/pages_to_scan" ]; then
+ test_numeric "$KHUGEPAGED/pages_to_scan" "1" "khugepaged/pages_to_scan"
+ test_numeric "$KHUGEPAGED/pages_to_scan" "8" "khugepaged/pages_to_scan"
+ test_numeric_invalid "$KHUGEPAGED/pages_to_scan" "0" \
+ "khugepaged/pages_to_scan (reject 0)"
+ fi
+
+ # khugepaged/scan_sleep_millisecs
+ if [ -f "$KHUGEPAGED/scan_sleep_millisecs" ]; then
+ test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "0" \
+ "khugepaged/scan_sleep_millisecs"
+ test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "1000" \
+ "khugepaged/scan_sleep_millisecs"
+ fi
+
+ # khugepaged/alloc_sleep_millisecs
+ if [ -f "$KHUGEPAGED/alloc_sleep_millisecs" ]; then
+ test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "0" \
+ "khugepaged/alloc_sleep_millisecs"
+ test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "1000" \
+ "khugepaged/alloc_sleep_millisecs"
+ fi
+
+ # khugepaged/pages_collapsed (read-only)
+ if [ -f "$KHUGEPAGED/pages_collapsed" ]; then
+ test_readonly "$KHUGEPAGED/pages_collapsed" "khugepaged/pages_collapsed"
+ fi
+
+ # khugepaged/full_scans (read-only)
+ if [ -f "$KHUGEPAGED/full_scans" ]; then
+ test_readonly "$KHUGEPAGED/full_scans" "khugepaged/full_scans"
+ fi
+
+ # Restore all values
+ [ -n "$saved_khp_defrag" ] && \
+ echo "$saved_khp_defrag" > "$KHUGEPAGED/defrag" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_none" ] && \
+ echo "$saved_khp_max_ptes_none" > "$KHUGEPAGED/max_ptes_none" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_swap" ] && \
+ echo "$saved_khp_max_ptes_swap" > "$KHUGEPAGED/max_ptes_swap" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_shared" ] && \
+ echo "$saved_khp_max_ptes_shared" > "$KHUGEPAGED/max_ptes_shared" 2>/dev/null
+ [ -n "$saved_khp_pages_to_scan" ] && \
+ echo "$saved_khp_pages_to_scan" > "$KHUGEPAGED/pages_to_scan" 2>/dev/null
+ [ -n "$saved_khp_scan_sleep" ] && \
+ echo "$saved_khp_scan_sleep" > "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null
+ [ -n "$saved_khp_alloc_sleep" ] && \
+ echo "$saved_khp_alloc_sleep" > "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null
+fi
+
+# --- Test per-size stats files ---
+
+found_stats=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ [ -d "$dir/stats" ] || continue
+
+ found_stats=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size stats ($size)"
+
+ for stat_file in "$dir"/stats/*; do
+ [ -f "$stat_file" ] || continue
+ stat_name=$(basename "$stat_file")
+ val=$(cat "$stat_file" 2>/dev/null)
+
+ if [ -z "$val" ]; then
+ ktap_test_fail "$size/stats/$stat_name: read returned empty"
+ continue
+ fi
+
+ if echo "$val" | grep -qE '^[0-9]+$'; then
+ ktap_test_pass "$size/stats/$stat_name: readable (value=$val)"
+ else
+ ktap_test_fail "$size/stats/$stat_name: expected numeric, got '$val'"
+ fi
+ done
+
+ # Only test one size
+ break
+done
+
+if [ $found_stats -eq 0 ]; then
+ ktap_test_skip "no per-size stats directories found"
+fi
+
+# --- Done ---
+
+# The test count is dynamic (depends on available sysfs files and hugepage
+# sizes), so print the plan at the end. TAP 13 allows trailing plans.
+KSFT_NUM_TESTS=$((KTAP_CNT_PASS + KTAP_CNT_FAIL + KTAP_CNT_SKIP))
+echo "1..$KSFT_NUM_TESTS"
+ktap_finished
---
base-commit: dc420ab7b435928a467b8d03fd3ed80e7d01d720
change-id: 20260309-thp_selftest_v2-4d32eba70441
Best regards,
--
Breno Leitao <leitao@debian.org>
On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> Add a shell-based selftest that exercises the full set of THP sysfs
> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> khugepaged/ tunables, and per-size stats files.
>
> Each writable knob is tested for valid writes, invalid-input rejection,
> idempotent writes, and mode transitions where applicable. All original
> values are saved before testing and restored afterwards.
>
> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> structured TAP 13 output, making results parseable by the kselftest
> harness. The test plan is printed at the end since the number of test
> points is dynamic (depends on available hugepage sizes and sysfs files).
>
> This is particularly useful for validating the refactoring of
> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> and the new change_enabled()/change_anon_orders() helpers.
>
> Signed-off-by: Breno Leitao <leitao@debian.org>
The test is broken locally for me, returning error code 127.
I do appreciate the effort here, so I'm sorry to push back negatively, but I
feel a bash script here is pretty janky, and frankly if any of these interfaces
were as broken as this it'd be a major failure that would surely get picked up
far sooner elsewhere.
So while I think this might be useful as a local test for your sysfs interface
changes, I don't think this is really suited to the mm selftests.
Andrew - can we drop this change please? Thanks!
Thanks, Lorenzo
> ---
> While working on the THP sysfs interface, I noticed there were no
> existing tests covering this functionality. This patch adds test
> coverage that may be useful for upstream !?
> ---
> tools/testing/selftests/mm/Makefile | 1 +
> tools/testing/selftests/mm/run_vmtests.sh | 2 +
> tools/testing/selftests/mm/thp_sysfs_test.sh | 666 +++++++++++++++++++++++++++
> 3 files changed, 669 insertions(+)
>
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> index 7a5de4e9bf520..90fcca53a561b 100644
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -180,6 +180,7 @@ TEST_FILES += charge_reserved_hugetlb.sh
> TEST_FILES += hugetlb_reparenting_test.sh
> TEST_FILES += test_page_frag.sh
> TEST_FILES += run_vmtests.sh
> +TEST_FILES += thp_sysfs_test.sh
>
> # required by charge_reserved_hugetlb.sh
> TEST_FILES += write_hugetlb_memory.sh
> diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
> index afdcfd0d7cef7..9e0e2089214a3 100755
> --- a/tools/testing/selftests/mm/run_vmtests.sh
> +++ b/tools/testing/selftests/mm/run_vmtests.sh
> @@ -491,6 +491,8 @@ CATEGORY="thp" run_test ./khugepaged -s 4 all:shmem
>
> CATEGORY="thp" run_test ./transhuge-stress -d 20
>
> +CATEGORY="thp" run_test ./thp_sysfs_test.sh
> +
> # Try to create XFS if not provided
> if [ -z "${SPLIT_HUGE_PAGE_TEST_XFS_PATH}" ]; then
> if [ "${HAVE_HUGEPAGES}" = "1" ]; then
> diff --git a/tools/testing/selftests/mm/thp_sysfs_test.sh b/tools/testing/selftests/mm/thp_sysfs_test.sh
> new file mode 100755
> index 0000000000000..407f306a0989f
> --- /dev/null
> +++ b/tools/testing/selftests/mm/thp_sysfs_test.sh
> @@ -0,0 +1,666 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Test THP sysfs interface.
> +#
> +# Exercises the full set of THP sysfs knobs: enabled (global and
> +# per-size anon), defrag, use_zero_page, hpage_pmd_size, shmem_enabled
> +# (global and per-size), shrink_underused, khugepaged/ tunables, and
> +# per-size stats files. Each writable knob is tested for valid writes,
> +# invalid-input rejection, idempotent writes, and mode transitions
> +# where applicable. All original values are saved before testing and
> +# restored afterwards.
> +#
> +# Author: Breno Leitao <leitao@debian.org>
> +
> +DIR="$(dirname "$(readlink -f "$0")")"
> +. "${DIR}"/../kselftest/ktap_helpers.sh
> +
> +THP_SYSFS=/sys/kernel/mm/transparent_hugepage
> +
> +# Read the currently active mode from a sysfs enabled file.
> +# The active mode is enclosed in brackets, e.g. "always [madvise] never"
> +get_active_mode() {
> + local path="$1"
> + local content
> +
> + content=$(cat "$path")
> + echo "$content" | grep -o '\[.*\]' | tr -d '[]'
> +}
> +
> +# Test that writing a mode and reading it back gives the expected result.
> +test_mode() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> + local active
> +
> + if ! echo "$mode" > "$path" 2>/dev/null; then
> + ktap_test_fail "$label: write '$mode'"
> + return
> + fi
> +
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$mode" ]; then
> + ktap_test_pass "$label: write '$mode'"
> + else
> + ktap_test_fail "$label: write '$mode', read back '$active'"
> + fi
> +}
> +
> +# Test that writing an invalid mode is rejected.
> +test_invalid() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> + local saved
> +
> + saved=$(get_active_mode "$path")
> +
> + if echo "$mode" > "$path" 2>/dev/null; then
> + # Write succeeded -- check if mode actually changed (it shouldn't)
> + local active
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$saved" ]; then
> + # Some shells don't propagate the error, but mode unchanged
> + ktap_test_pass "$label: reject '$mode'"
> + else
> + ktap_test_fail "$label: '$mode' should have been rejected but mode changed to '$active'"
> + fi
> + else
> + ktap_test_pass "$label: reject '$mode'"
> + fi
> +}
> +
> +# Test that writing the same mode twice doesn't crash or change state.
> +test_idempotent() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> +
> + echo "$mode" > "$path" 2>/dev/null
> + echo "$mode" > "$path" 2>/dev/null
> + local active
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$mode" ]; then
> + ktap_test_pass "$label: idempotent '$mode'"
> + else
> + ktap_test_fail "$label: idempotent '$mode', got '$active'"
> + fi
> +}
> +
> +# Write a numeric value, read it back, verify match.
> +test_numeric() {
> + local path="$1"
> + local value="$2"
> + local label="$3"
> + local readback
> +
> + if ! echo "$value" > "$path" 2>/dev/null; then
> + ktap_test_fail "$label: write '$value'"
> + return
> + fi
> +
> + readback=$(cat "$path" 2>/dev/null)
> + if [ "$readback" = "$value" ]; then
> + ktap_test_pass "$label: write '$value'"
> + else
> + ktap_test_fail "$label: write '$value', read back '$readback'"
> + fi
> +}
> +
> +# Verify that an out-of-range or invalid numeric value is rejected.
> +test_numeric_invalid() {
> + local path="$1"
> + local value="$2"
> + local label="$3"
> + local saved readback
> +
> + saved=$(cat "$path" 2>/dev/null)
> +
> + if echo "$value" > "$path" 2>/dev/null; then
> + readback=$(cat "$path" 2>/dev/null)
> + if [ "$readback" = "$saved" ]; then
> + ktap_test_pass "$label: reject '$value'"
> + else
> + ktap_test_fail "$label: '$value' should have been rejected but value changed to '$readback'"
> + fi
> + else
> + ktap_test_pass "$label: reject '$value'"
> + fi
> +}
> +
> +# Verify a read-only file: readable, returns a numeric value, rejects writes.
> +test_readonly() {
> + local path="$1"
> + local label="$2"
> + local val
> +
> + val=$(cat "$path" 2>/dev/null)
> + if [ -z "$val" ]; then
> + ktap_test_fail "$label: read returned empty"
> + return
> + fi
> +
> + if ! echo "$val" | grep -qE '^[0-9]+$'; then
> + ktap_test_fail "$label: expected numeric, got '$val'"
> + return
> + fi
> +
> + if echo "0" > "$path" 2>/dev/null; then
> + local after
> + after=$(cat "$path" 2>/dev/null)
> + if [ "$after" = "$val" ]; then
> + ktap_test_pass "$label: read-only (value=$val)"
> + else
> + ktap_test_fail "$label: write should have been rejected but value changed"
> + fi
> + else
> + ktap_test_pass "$label: read-only (value=$val)"
> + fi
> +}
> +
> +# --- Precondition checks ---
> +
> +ktap_print_header
> +
> +if [ ! -d "$THP_SYSFS" ]; then
> + ktap_skip_all "THP sysfs not found at $THP_SYSFS"
> + exit "$KSFT_SKIP"
> +fi
> +
> +if [ "$(id -u)" -ne 0 ]; then
> + ktap_skip_all "must be run as root"
> + exit "$KSFT_SKIP"
> +fi
> +
> +# --- Test global THP enabled ---
> +
> +GLOBAL_ENABLED="$THP_SYSFS/enabled"
> +
> +if [ ! -f "$GLOBAL_ENABLED" ]; then
> + ktap_test_skip "global enabled file not found"
> +else
> + ktap_print_msg "Testing global THP enabled ($GLOBAL_ENABLED)"
> +
> + # Save current setting
> + saved_global=$(get_active_mode "$GLOBAL_ENABLED")
> +
> + # Valid modes for global
> + test_mode "$GLOBAL_ENABLED" "always" "global"
> + test_mode "$GLOBAL_ENABLED" "madvise" "global"
> + test_mode "$GLOBAL_ENABLED" "never" "global"
> +
> + # "inherit" is not valid for global THP
> + test_invalid "$GLOBAL_ENABLED" "inherit" "global"
> +
> + # Invalid strings
> + test_invalid "$GLOBAL_ENABLED" "bogus" "global"
> + test_invalid "$GLOBAL_ENABLED" "" "global (empty)"
> +
> + # Idempotent writes
> + test_idempotent "$GLOBAL_ENABLED" "always" "global"
> + test_idempotent "$GLOBAL_ENABLED" "never" "global"
> +
> + # Restore
> + echo "$saved_global" > "$GLOBAL_ENABLED" 2>/dev/null
> +fi
> +
> +# --- Test global defrag ---
> +
> +GLOBAL_DEFRAG="$THP_SYSFS/defrag"
> +
> +if [ ! -f "$GLOBAL_DEFRAG" ]; then
> + ktap_test_skip "defrag file not found"
> +else
> + ktap_print_msg "Testing global THP defrag ($GLOBAL_DEFRAG)"
> +
> + saved_defrag=$(get_active_mode "$GLOBAL_DEFRAG")
> +
> + # Valid modes
> + test_mode "$GLOBAL_DEFRAG" "always" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "defer" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "madvise" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "never" "defrag"
> +
> + # Invalid
> + test_invalid "$GLOBAL_DEFRAG" "bogus" "defrag"
> + test_invalid "$GLOBAL_DEFRAG" "" "defrag (empty)"
> + test_invalid "$GLOBAL_DEFRAG" "inherit" "defrag"
> +
> + # Idempotent
> + test_idempotent "$GLOBAL_DEFRAG" "always" "defrag"
> + test_idempotent "$GLOBAL_DEFRAG" "never" "defrag"
> +
> + # Mode transitions: cycle through all 5
> + echo "always" > "$GLOBAL_DEFRAG"
> + test_mode "$GLOBAL_DEFRAG" "defer" "defrag (always->defer)"
> + test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag (defer->defer+madvise)"
> + test_mode "$GLOBAL_DEFRAG" "madvise" "defrag (defer+madvise->madvise)"
> + test_mode "$GLOBAL_DEFRAG" "never" "defrag (madvise->never)"
> + test_mode "$GLOBAL_DEFRAG" "always" "defrag (never->always)"
> +
> + # Restore
> + echo "$saved_defrag" > "$GLOBAL_DEFRAG" 2>/dev/null
> +fi
> +
> +# --- Test use_zero_page ---
> +
> +USE_ZERO_PAGE="$THP_SYSFS/use_zero_page"
> +
> +if [ ! -f "$USE_ZERO_PAGE" ]; then
> + ktap_test_skip "use_zero_page file not found"
> +else
> + ktap_print_msg "Testing use_zero_page ($USE_ZERO_PAGE)"
> +
> + saved_uzp=$(cat "$USE_ZERO_PAGE" 2>/dev/null)
> +
> + # Valid values
> + test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page"
> + test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page"
> +
> + # Invalid values
> + test_numeric_invalid "$USE_ZERO_PAGE" "2" "use_zero_page"
> + test_numeric_invalid "$USE_ZERO_PAGE" "-1" "use_zero_page"
> + test_numeric_invalid "$USE_ZERO_PAGE" "bogus" "use_zero_page"
> +
> + # Idempotent
> + echo "1" > "$USE_ZERO_PAGE" 2>/dev/null
> + test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page (idempotent)"
> + echo "0" > "$USE_ZERO_PAGE" 2>/dev/null
> + test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page (idempotent)"
> +
> + # Restore
> + echo "$saved_uzp" > "$USE_ZERO_PAGE" 2>/dev/null
> +fi
> +
> +# --- Test hpage_pmd_size ---
> +
> +HPAGE_PMD_SIZE_FILE="$THP_SYSFS/hpage_pmd_size"
> +
> +if [ ! -f "$HPAGE_PMD_SIZE_FILE" ]; then
> + ktap_test_skip "hpage_pmd_size file not found"
> +else
> + ktap_print_msg "Testing hpage_pmd_size ($HPAGE_PMD_SIZE_FILE)"
> +
> + test_readonly "$HPAGE_PMD_SIZE_FILE" "hpage_pmd_size"
> +fi
> +
> +# --- Test global shmem_enabled ---
> +
> +SHMEM_ENABLED="$THP_SYSFS/shmem_enabled"
> +
> +if [ ! -f "$SHMEM_ENABLED" ]; then
> + ktap_test_skip "shmem_enabled file not found (CONFIG_SHMEM not set?)"
> +else
> + ktap_print_msg "Testing global shmem_enabled ($SHMEM_ENABLED)"
> +
> + saved_shmem=$(get_active_mode "$SHMEM_ENABLED")
> +
> + # Valid modes
> + test_mode "$SHMEM_ENABLED" "always" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "never" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "force" "shmem_enabled"
> +
> + # Invalid
> + test_invalid "$SHMEM_ENABLED" "bogus" "shmem_enabled"
> + test_invalid "$SHMEM_ENABLED" "inherit" "shmem_enabled"
> + test_invalid "$SHMEM_ENABLED" "" "shmem_enabled (empty)"
> +
> + # Idempotent
> + test_idempotent "$SHMEM_ENABLED" "always" "shmem_enabled"
> + test_idempotent "$SHMEM_ENABLED" "never" "shmem_enabled"
> +
> + # Mode transitions: cycle through all 6
> + echo "always" > "$SHMEM_ENABLED"
> + test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled (always->within_size)"
> + test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled (within_size->advise)"
> + test_mode "$SHMEM_ENABLED" "never" "shmem_enabled (advise->never)"
> + test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled (never->deny)"
> + test_mode "$SHMEM_ENABLED" "force" "shmem_enabled (deny->force)"
> + test_mode "$SHMEM_ENABLED" "always" "shmem_enabled (force->always)"
> +
> + # Restore
> + echo "$saved_shmem" > "$SHMEM_ENABLED" 2>/dev/null
> +fi
> +
> +# --- Test shrink_underused ---
> +
> +SHRINK_UNDERUSED="$THP_SYSFS/shrink_underused"
> +
> +if [ ! -f "$SHRINK_UNDERUSED" ]; then
> + ktap_test_skip "shrink_underused file not found"
> +else
> + ktap_print_msg "Testing shrink_underused ($SHRINK_UNDERUSED)"
> +
> + saved_shrink=$(cat "$SHRINK_UNDERUSED" 2>/dev/null)
> +
> + # Valid values
> + test_numeric "$SHRINK_UNDERUSED" "0" "shrink_underused"
> + test_numeric "$SHRINK_UNDERUSED" "1" "shrink_underused"
> +
> + # Invalid values
> + test_numeric_invalid "$SHRINK_UNDERUSED" "2" "shrink_underused"
> + test_numeric_invalid "$SHRINK_UNDERUSED" "bogus" "shrink_underused"
> +
> + # Restore
> + echo "$saved_shrink" > "$SHRINK_UNDERUSED" 2>/dev/null
> +fi
> +
> +# --- Test per-size anon THP enabled ---
> +
> +found_anon=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> +
> + ANON_ENABLED="$dir/enabled"
> + [ -f "$ANON_ENABLED" ] || continue
> +
> + found_anon=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size anon THP enabled ($size)"
> +
> + # Save current setting
> + saved_anon=$(get_active_mode "$ANON_ENABLED")
> +
> + # Valid modes for per-size anon (includes inherit)
> + test_mode "$ANON_ENABLED" "always" "$size"
> + test_mode "$ANON_ENABLED" "inherit" "$size"
> + test_mode "$ANON_ENABLED" "madvise" "$size"
> + test_mode "$ANON_ENABLED" "never" "$size"
> +
> + # Invalid strings
> + test_invalid "$ANON_ENABLED" "bogus" "$size"
> +
> + # Idempotent writes
> + test_idempotent "$ANON_ENABLED" "always" "$size"
> + test_idempotent "$ANON_ENABLED" "inherit" "$size"
> + test_idempotent "$ANON_ENABLED" "never" "$size"
> +
> + # Mode transitions: verify each mode clears the others
> + echo "always" > "$ANON_ENABLED"
> + test_mode "$ANON_ENABLED" "madvise" "$size (always->madvise)"
> + test_mode "$ANON_ENABLED" "inherit" "$size (madvise->inherit)"
> + test_mode "$ANON_ENABLED" "never" "$size (inherit->never)"
> + test_mode "$ANON_ENABLED" "always" "$size (never->always)"
> +
> + # Restore
> + echo "$saved_anon" > "$ANON_ENABLED" 2>/dev/null
> +
> + # Only test one size in detail to keep output manageable,
> + # but do a quick smoke test on the rest
> + break
> +done
> +
> +if [ $found_anon -eq 0 ]; then
> + ktap_test_skip "no per-size anon THP directories found"
> +fi
> +
> +# Quick smoke test: all other sizes accept valid modes
> +first=1
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + ANON_ENABLED="$dir/enabled"
> + [ -f "$ANON_ENABLED" ] || continue
> +
> + # Skip the first one (already tested in detail)
> + if [ $first -eq 1 ]; then
> + first=0
> + continue
> + fi
> +
> + size=$(basename "$dir")
> + saved=$(get_active_mode "$ANON_ENABLED")
> +
> + smoke_failed=0
> + for mode in always inherit madvise never; do
> + echo "$mode" > "$ANON_ENABLED" 2>/dev/null
> + active=$(get_active_mode "$ANON_ENABLED")
> + if [ "$active" != "$mode" ]; then
> + ktap_test_fail "$size: smoke test '$mode' got '$active'"
> + smoke_failed=1
> + break
> + fi
> + done
> + [ $smoke_failed -eq 0 ] && ktap_test_pass "$size: smoke test all modes"
> +
> + echo "$saved" > "$ANON_ENABLED" 2>/dev/null
> +done
> +
> +# --- Test per-size shmem_enabled ---
> +
> +found_shmem=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> +
> + SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
> + [ -f "$SHMEM_SIZE_ENABLED" ] || continue
> +
> + found_shmem=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size shmem_enabled ($size)"
> +
> + # Save current setting
> + saved_shmem_size=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> +
> + # Valid modes for per-size shmem
> + test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem"
> +
> + # Invalid: deny and force are not valid for per-size
> + test_invalid "$SHMEM_SIZE_ENABLED" "bogus" "$size shmem"
> + test_invalid "$SHMEM_SIZE_ENABLED" "deny" "$size shmem"
> + test_invalid "$SHMEM_SIZE_ENABLED" "force" "$size shmem"
> +
> + # Mode transitions
> + echo "always" > "$SHMEM_SIZE_ENABLED"
> + test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem (always->inherit)"
> + test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem (inherit->within_size)"
> + test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem (within_size->advise)"
> + test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem (advise->never)"
> + test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem (never->always)"
> +
> + # Restore
> + echo "$saved_shmem_size" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> +
> + # Only test one size in detail
> + break
> +done
> +
> +if [ $found_shmem -eq 0 ]; then
> + ktap_test_skip "no per-size shmem_enabled files found"
> +fi
> +
> +# Quick smoke test: remaining sizes with shmem_enabled
> +first=1
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
> + [ -f "$SHMEM_SIZE_ENABLED" ] || continue
> +
> + if [ $first -eq 1 ]; then
> + first=0
> + continue
> + fi
> +
> + size=$(basename "$dir")
> + saved=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> +
> + smoke_failed=0
> + for mode in always inherit within_size advise never; do
> + echo "$mode" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> + active=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> + if [ "$active" != "$mode" ]; then
> + ktap_test_fail "$size shmem: smoke test '$mode' got '$active'"
> + smoke_failed=1
> + break
> + fi
> + done
> + [ $smoke_failed -eq 0 ] && ktap_test_pass "$size shmem: smoke test all modes"
> +
> + echo "$saved" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> +done
> +
> +# --- Test khugepaged tunables ---
> +
> +KHUGEPAGED="$THP_SYSFS/khugepaged"
> +
> +if [ ! -d "$KHUGEPAGED" ]; then
> + ktap_test_skip "khugepaged directory not found"
> +else
> + ktap_print_msg "Testing khugepaged tunables ($KHUGEPAGED)"
> +
> + # Compute HPAGE_PMD_NR for boundary tests
> + pmd_size=$(cat "$HPAGE_PMD_SIZE_FILE" 2>/dev/null)
> + page_size=$(getconf PAGE_SIZE)
> + if [ -n "$pmd_size" ] && [ -n "$page_size" ] && [ "$page_size" -gt 0 ]; then
> + hpage_pmd_nr=$((pmd_size / page_size))
> + else
> + hpage_pmd_nr=512
> + fi
> +
> + # Save all tunable values
> + saved_khp_defrag=$(cat "$KHUGEPAGED/defrag" 2>/dev/null)
> + saved_khp_max_ptes_none=$(cat "$KHUGEPAGED/max_ptes_none" 2>/dev/null)
> + saved_khp_max_ptes_swap=$(cat "$KHUGEPAGED/max_ptes_swap" 2>/dev/null)
> + saved_khp_max_ptes_shared=$(cat "$KHUGEPAGED/max_ptes_shared" 2>/dev/null)
> + saved_khp_pages_to_scan=$(cat "$KHUGEPAGED/pages_to_scan" 2>/dev/null)
> + saved_khp_scan_sleep=$(cat "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null)
> + saved_khp_alloc_sleep=$(cat "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null)
> +
> + # khugepaged/defrag (0/1 flag)
> + if [ -f "$KHUGEPAGED/defrag" ]; then
> + test_numeric "$KHUGEPAGED/defrag" "0" "khugepaged/defrag"
> + test_numeric "$KHUGEPAGED/defrag" "1" "khugepaged/defrag"
> + test_numeric_invalid "$KHUGEPAGED/defrag" "2" "khugepaged/defrag"
> + test_numeric_invalid "$KHUGEPAGED/defrag" "bogus" "khugepaged/defrag"
> + fi
> +
> + # khugepaged/max_ptes_none (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_none" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_none" "0" "khugepaged/max_ptes_none"
> + test_numeric "$KHUGEPAGED/max_ptes_none" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_none"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_none" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_none (boundary)"
> + fi
> +
> + # khugepaged/max_ptes_swap (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_swap" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_swap" "0" "khugepaged/max_ptes_swap"
> + test_numeric "$KHUGEPAGED/max_ptes_swap" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_swap"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_swap" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_swap (boundary)"
> + fi
> +
> + # khugepaged/max_ptes_shared (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_shared" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_shared" "0" "khugepaged/max_ptes_shared"
> + test_numeric "$KHUGEPAGED/max_ptes_shared" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_shared"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_shared" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_shared (boundary)"
> + fi
> +
> + # khugepaged/pages_to_scan (1 .. UINT_MAX, 0 rejected)
> + if [ -f "$KHUGEPAGED/pages_to_scan" ]; then
> + test_numeric "$KHUGEPAGED/pages_to_scan" "1" "khugepaged/pages_to_scan"
> + test_numeric "$KHUGEPAGED/pages_to_scan" "8" "khugepaged/pages_to_scan"
> + test_numeric_invalid "$KHUGEPAGED/pages_to_scan" "0" \
> + "khugepaged/pages_to_scan (reject 0)"
> + fi
> +
> + # khugepaged/scan_sleep_millisecs
> + if [ -f "$KHUGEPAGED/scan_sleep_millisecs" ]; then
> + test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "0" \
> + "khugepaged/scan_sleep_millisecs"
> + test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "1000" \
> + "khugepaged/scan_sleep_millisecs"
> + fi
> +
> + # khugepaged/alloc_sleep_millisecs
> + if [ -f "$KHUGEPAGED/alloc_sleep_millisecs" ]; then
> + test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "0" \
> + "khugepaged/alloc_sleep_millisecs"
> + test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "1000" \
> + "khugepaged/alloc_sleep_millisecs"
> + fi
> +
> + # khugepaged/pages_collapsed (read-only)
> + if [ -f "$KHUGEPAGED/pages_collapsed" ]; then
> + test_readonly "$KHUGEPAGED/pages_collapsed" "khugepaged/pages_collapsed"
> + fi
> +
> + # khugepaged/full_scans (read-only)
> + if [ -f "$KHUGEPAGED/full_scans" ]; then
> + test_readonly "$KHUGEPAGED/full_scans" "khugepaged/full_scans"
> + fi
> +
> + # Restore all values
> + [ -n "$saved_khp_defrag" ] && \
> + echo "$saved_khp_defrag" > "$KHUGEPAGED/defrag" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_none" ] && \
> + echo "$saved_khp_max_ptes_none" > "$KHUGEPAGED/max_ptes_none" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_swap" ] && \
> + echo "$saved_khp_max_ptes_swap" > "$KHUGEPAGED/max_ptes_swap" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_shared" ] && \
> + echo "$saved_khp_max_ptes_shared" > "$KHUGEPAGED/max_ptes_shared" 2>/dev/null
> + [ -n "$saved_khp_pages_to_scan" ] && \
> + echo "$saved_khp_pages_to_scan" > "$KHUGEPAGED/pages_to_scan" 2>/dev/null
> + [ -n "$saved_khp_scan_sleep" ] && \
> + echo "$saved_khp_scan_sleep" > "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null
> + [ -n "$saved_khp_alloc_sleep" ] && \
> + echo "$saved_khp_alloc_sleep" > "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null
> +fi
> +
> +# --- Test per-size stats files ---
> +
> +found_stats=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + [ -d "$dir/stats" ] || continue
> +
> + found_stats=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size stats ($size)"
> +
> + for stat_file in "$dir"/stats/*; do
> + [ -f "$stat_file" ] || continue
> + stat_name=$(basename "$stat_file")
> + val=$(cat "$stat_file" 2>/dev/null)
> +
> + if [ -z "$val" ]; then
> + ktap_test_fail "$size/stats/$stat_name: read returned empty"
> + continue
> + fi
> +
> + if echo "$val" | grep -qE '^[0-9]+$'; then
> + ktap_test_pass "$size/stats/$stat_name: readable (value=$val)"
> + else
> + ktap_test_fail "$size/stats/$stat_name: expected numeric, got '$val'"
> + fi
> + done
> +
> + # Only test one size
> + break
> +done
> +
> +if [ $found_stats -eq 0 ]; then
> + ktap_test_skip "no per-size stats directories found"
> +fi
> +
> +# --- Done ---
> +
> +# The test count is dynamic (depends on available sysfs files and hugepage
> +# sizes), so print the plan at the end. TAP 13 allows trailing plans.
> +KSFT_NUM_TESTS=$((KTAP_CNT_PASS + KTAP_CNT_FAIL + KTAP_CNT_SKIP))
> +echo "1..$KSFT_NUM_TESTS"
> +ktap_finished
>
> ---
> base-commit: dc420ab7b435928a467b8d03fd3ed80e7d01d720
> change-id: 20260309-thp_selftest_v2-4d32eba70441
>
> Best regards,
> --
> Breno Leitao <leitao@debian.org>
>
On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: > On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: > > Add a shell-based selftest that exercises the full set of THP sysfs > > knobs: enabled (global and per-size anon), defrag, use_zero_page, > > hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, > > khugepaged/ tunables, and per-size stats files. > > > > Each writable knob is tested for valid writes, invalid-input rejection, > > idempotent writes, and mode transitions where applicable. All original > > values are saved before testing and restored afterwards. > > > > The test uses the kselftest KTAP framework (ktap_helpers.sh) for > > structured TAP 13 output, making results parseable by the kselftest > > harness. The test plan is printed at the end since the number of test > > points is dynamic (depends on available hugepage sizes and sysfs files). > > > > This is particularly useful for validating the refactoring of > > enabled_store() and anon_enabled_store() to use sysfs_match_string() > > and the new change_enabled()/change_anon_orders() helpers. > > > > Signed-off-by: Breno Leitao <leitao@debian.org> > > The test is broken locally for me, returning error code 127. > > I do appreciate the effort here, so I'm sorry to push back negatively, but I > feel a bash script here is pretty janky, and frankly if any of these interfaces > were as broken as this it'd be a major failure that would surely get picked up > far sooner elsewhere. > > So while I think this might be useful as a local test for your sysfs interface > changes, I don't think this is really suited to the mm selftests. That is totally fine. This test is what I have been using to test the changes, and I decide to share it in case someone find it useful. Let's drop it.
On 3/16/26 14:47, Breno Leitao wrote: > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: >>> Add a shell-based selftest that exercises the full set of THP sysfs >>> knobs: enabled (global and per-size anon), defrag, use_zero_page, >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, >>> khugepaged/ tunables, and per-size stats files. >>> >>> Each writable knob is tested for valid writes, invalid-input rejection, >>> idempotent writes, and mode transitions where applicable. All original >>> values are saved before testing and restored afterwards. >>> >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for >>> structured TAP 13 output, making results parseable by the kselftest >>> harness. The test plan is printed at the end since the number of test >>> points is dynamic (depends on available hugepage sizes and sysfs files). >>> >>> This is particularly useful for validating the refactoring of >>> enabled_store() and anon_enabled_store() to use sysfs_match_string() >>> and the new change_enabled()/change_anon_orders() helpers. >>> >>> Signed-off-by: Breno Leitao <leitao@debian.org> >> >> The test is broken locally for me, returning error code 127. >> >> I do appreciate the effort here, so I'm sorry to push back negatively, but I >> feel a bash script here is pretty janky, and frankly if any of these interfaces >> were as broken as this it'd be a major failure that would surely get picked up >> far sooner elsewhere. >> >> So while I think this might be useful as a local test for your sysfs interface >> changes, I don't think this is really suited to the mm selftests. > > That is totally fine. This test is what I have been using to test the > changes, and I decide to share it in case someone find it useful. > > Let's drop it. Out of interest, to we know why the test is failing for Lorenzo? I agree that the test is a bit excessive, in particular when it comes to invalid/idempotent values etc. I could see some value for testing whether setting the modes keeps working, but also then I wonder if that is really something we'll be changing frequently (and that breaks easily). -- Cheers, David
On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote: > On 3/16/26 14:47, Breno Leitao wrote: > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: > >>> Add a shell-based selftest that exercises the full set of THP sysfs > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page, > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, > >>> khugepaged/ tunables, and per-size stats files. > >>> > >>> Each writable knob is tested for valid writes, invalid-input rejection, > >>> idempotent writes, and mode transitions where applicable. All original > >>> values are saved before testing and restored afterwards. > >>> > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for > >>> structured TAP 13 output, making results parseable by the kselftest > >>> harness. The test plan is printed at the end since the number of test > >>> points is dynamic (depends on available hugepage sizes and sysfs files). > >>> > >>> This is particularly useful for validating the refactoring of > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string() > >>> and the new change_enabled()/change_anon_orders() helpers. > >>> > >>> Signed-off-by: Breno Leitao <leitao@debian.org> > >> > >> The test is broken locally for me, returning error code 127. > >> > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I > >> feel a bash script here is pretty janky, and frankly if any of these interfaces > >> were as broken as this it'd be a major failure that would surely get picked up > >> far sooner elsewhere. > >> > >> So while I think this might be useful as a local test for your sysfs interface > >> changes, I don't think this is really suited to the mm selftests. > > > > That is totally fine. This test is what I have been using to test the > > changes, and I decide to share it in case someone find it useful. > > > > Let's drop it. > > Out of interest, to we know why the test is failing for Lorenzo? I really don't know, but, it sounds like ktap was not found? Then the first early-exit path hit: ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP" # expands to: exit "" → exits with last $? = 127 > I agree that the test is a bit excessive, in particular when it comes to > invalid/idempotent values etc. I could see some value for testing > whether setting the modes keeps working, but also then I wonder if that > is really something we'll be changing frequently (and that breaks easily). yea, I make it very excessive, because there were some intrinsics in those sysfs that I was gettingit wrong when doing the intial conversion. So, the test is something that I trust now, and I found it useful when finding regressiosn. Is is something that will chagne frequently? probably not! That said, would you like to have a simplified/different version of this test?
On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote: > On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote: > > On 3/16/26 14:47, Breno Leitao wrote: > > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: > > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: > > >>> Add a shell-based selftest that exercises the full set of THP sysfs > > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page, > > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, > > >>> khugepaged/ tunables, and per-size stats files. > > >>> > > >>> Each writable knob is tested for valid writes, invalid-input rejection, > > >>> idempotent writes, and mode transitions where applicable. All original > > >>> values are saved before testing and restored afterwards. > > >>> > > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for > > >>> structured TAP 13 output, making results parseable by the kselftest > > >>> harness. The test plan is printed at the end since the number of test > > >>> points is dynamic (depends on available hugepage sizes and sysfs files). > > >>> > > >>> This is particularly useful for validating the refactoring of > > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string() > > >>> and the new change_enabled()/change_anon_orders() helpers. > > >>> > > >>> Signed-off-by: Breno Leitao <leitao@debian.org> > > >> > > >> The test is broken locally for me, returning error code 127. > > >> > > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I > > >> feel a bash script here is pretty janky, and frankly if any of these interfaces > > >> were as broken as this it'd be a major failure that would surely get picked up > > >> far sooner elsewhere. > > >> > > >> So while I think this might be useful as a local test for your sysfs interface > > >> changes, I don't think this is really suited to the mm selftests. > > > > > > That is totally fine. This test is what I have been using to test the > > > changes, and I decide to share it in case someone find it useful. > > > > > > Let's drop it. > > > > Out of interest, to we know why the test is failing for Lorenzo? > > I really don't know, but, it sounds like ktap was not found? Yeah CONFIG_KUNIT is not set so could be :) > > Then the first early-exit path hit: > ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP" > # expands to: exit "" → exits with last $? = 127 > > > I agree that the test is a bit excessive, in particular when it comes to > > invalid/idempotent values etc. I could see some value for testing > > whether setting the modes keeps working, but also then I wonder if that > > is really something we'll be changing frequently (and that breaks easily). > > yea, I make it very excessive, because there were some intrinsics in > those sysfs that I was gettingit wrong when doing the intial conversion. > > So, the test is something that I trust now, and I found it useful when > finding regressiosn. > > Is is something that will chagne frequently? probably not! > > That said, would you like to have a simplified/different version of this > test? In an ideal world we'd use kunit or something to assert it internal to the kernel I guess, but if we do have something scaled down it'd at least be nice to have in C? :) I am not sure how useful it'd be though overall, I don't see us changing this too often and really we're more interested in asserting behaviour. Sadly THP is inherently tricky to test generally because of its very nature, I wish we could have better test isolation etc. See tools/testing/vma for a forlorn dream of kernel code being run in userland (but oh how the stubs/duplicate declarations/etc. are a pain). I suspect THP could never be given the same treatment though! :) Cheers, Lorenzo
On Mon, Mar 16, 2026 at 07:53:46PM +0000, Lorenzo Stoakes (Oracle) wrote: > On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote: > > On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote: > > > On 3/16/26 14:47, Breno Leitao wrote: > > > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: > > > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: > > > >>> Add a shell-based selftest that exercises the full set of THP sysfs > > > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page, > > > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, > > > >>> khugepaged/ tunables, and per-size stats files. > > > >>> > > > >>> Each writable knob is tested for valid writes, invalid-input rejection, > > > >>> idempotent writes, and mode transitions where applicable. All original > > > >>> values are saved before testing and restored afterwards. > > > >>> > > > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for > > > >>> structured TAP 13 output, making results parseable by the kselftest > > > >>> harness. The test plan is printed at the end since the number of test > > > >>> points is dynamic (depends on available hugepage sizes and sysfs files). > > > >>> > > > >>> This is particularly useful for validating the refactoring of > > > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string() > > > >>> and the new change_enabled()/change_anon_orders() helpers. > > > >>> > > > >>> Signed-off-by: Breno Leitao <leitao@debian.org> > > > >> > > > >> The test is broken locally for me, returning error code 127. > > > >> > > > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I > > > >> feel a bash script here is pretty janky, and frankly if any of these interfaces > > > >> were as broken as this it'd be a major failure that would surely get picked up > > > >> far sooner elsewhere. > > > >> > > > >> So while I think this might be useful as a local test for your sysfs interface > > > >> changes, I don't think this is really suited to the mm selftests. > > > > > > > > That is totally fine. This test is what I have been using to test the > > > > changes, and I decide to share it in case someone find it useful. > > > > > > > > Let's drop it. > > > > > > Out of interest, to we know why the test is failing for Lorenzo? > > > > I really don't know, but, it sounds like ktap was not found? > > Yeah CONFIG_KUNIT is not set so could be :) Nah, CONFIG_KUNIT has nothing to do with ktap_helpers.sh, probably your environment does not bring in tools/testing/selftests/kselftest > > > > Then the first early-exit path hit: > > ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP" > > # expands to: exit "" → exits with last $? = 127 > > > > > I agree that the test is a bit excessive, in particular when it comes to > > > invalid/idempotent values etc. I could see some value for testing > > > whether setting the modes keeps working, but also then I wonder if that > > > is really something we'll be changing frequently (and that breaks easily). > > > > yea, I make it very excessive, because there were some intrinsics in > > those sysfs that I was gettingit wrong when doing the intial conversion. > > > > So, the test is something that I trust now, and I found it useful when > > finding regressiosn. > > > > Is is something that will chagne frequently? probably not! > > > > That said, would you like to have a simplified/different version of this > > test? > > In an ideal world we'd use kunit or something to assert it internal to the > kernel I guess, but if we do have something scaled down it'd at least be nice to > have in C? :) > > I am not sure how useful it'd be though overall, I don't see us changing this > too often and really we're more interested in asserting behaviour. > > Sadly THP is inherently tricky to test generally because of its very nature, I > wish we could have better test isolation etc. > > See tools/testing/vma for a forlorn dream of kernel code being run in userland > (but oh how the stubs/duplicate declarations/etc. are a pain). > > I suspect THP could never be given the same treatment though! :) > > Cheers, Lorenzo -- Sincerely yours, Mike.
On Tue, Mar 17, 2026 at 07:43:41AM +0200, Mike Rapoport wrote: > On Mon, Mar 16, 2026 at 07:53:46PM +0000, Lorenzo Stoakes (Oracle) wrote: > > On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote: > > > On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote: > > > > On 3/16/26 14:47, Breno Leitao wrote: > > > > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote: > > > > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote: > > > > >>> Add a shell-based selftest that exercises the full set of THP sysfs > > > > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page, > > > > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused, > > > > >>> khugepaged/ tunables, and per-size stats files. > > > > >>> > > > > >>> Each writable knob is tested for valid writes, invalid-input rejection, > > > > >>> idempotent writes, and mode transitions where applicable. All original > > > > >>> values are saved before testing and restored afterwards. > > > > >>> > > > > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for > > > > >>> structured TAP 13 output, making results parseable by the kselftest > > > > >>> harness. The test plan is printed at the end since the number of test > > > > >>> points is dynamic (depends on available hugepage sizes and sysfs files). > > > > >>> > > > > >>> This is particularly useful for validating the refactoring of > > > > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string() > > > > >>> and the new change_enabled()/change_anon_orders() helpers. > > > > >>> > > > > >>> Signed-off-by: Breno Leitao <leitao@debian.org> > > > > >> > > > > >> The test is broken locally for me, returning error code 127. > > > > >> > > > > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I > > > > >> feel a bash script here is pretty janky, and frankly if any of these interfaces > > > > >> were as broken as this it'd be a major failure that would surely get picked up > > > > >> far sooner elsewhere. > > > > >> > > > > >> So while I think this might be useful as a local test for your sysfs interface > > > > >> changes, I don't think this is really suited to the mm selftests. > > > > > > > > > > That is totally fine. This test is what I have been using to test the > > > > > changes, and I decide to share it in case someone find it useful. > > > > > > > > > > Let's drop it. > > > > > > > > Out of interest, to we know why the test is failing for Lorenzo? > > > > > > I really don't know, but, it sounds like ktap was not found? > > > > Yeah CONFIG_KUNIT is not set so could be :) > > Nah, CONFIG_KUNIT has nothing to do with ktap_helpers.sh, probably your > environment does not bring in tools/testing/selftests/kselftest Yeah I copy the mm tests to /mnt in vng before running to avoid ro filesystem issues. Not a fan of us relying on the entire tree structure being there either then :) Cheers, Lorenzo
© 2016 - 2026 Red Hat, Inc.