[PATCH v1 07/13] perf test: Add a test for the ilist script

From: Ian Rogers

Date: Fri Oct 02 2026 - 14:31:04 EST


Test launching ilist through perf script and perf list --tui, and drive
the textual app headless: check the tree of PMUs and metrics, search for
and select the software task-clock event checking its counters are
shown, check a failed search shows an error and select a metric. Failing
to open counters, for example for want of permissions, is tolerated as
an error dialog is shown.

Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
tools/perf/tests/shell/test_ilist_python.sh | 181 ++++++++++++++++++++
1 file changed, 181 insertions(+)
create mode 100755 tools/perf/tests/shell/test_ilist_python.sh

diff --git a/tools/perf/tests/shell/test_ilist_python.sh b/tools/perf/tests/shell/test_ilist_python.sh
new file mode 100755
index 000000000000..83048474710d
--- /dev/null
+++ b/tools/perf/tests/shell/test_ilist_python.sh
@@ -0,0 +1,181 @@
+#!/bin/bash
+# perf script ilist and perf list --tui test
+# SPDX-License-Identifier: GPL-2.0
+
+set -e
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/setup_python.sh
+. "${shelldir}"/lib/setup_python.sh
+
+if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then
+ echo "Skipping test, perf python module not found"
+ exit 2
+fi
+
+if ! "$PYTHON" -c 'import textual' > /dev/null 2>&1; then
+ echo "Skipping test, python textual library not found"
+ exit 2
+fi
+
+err=0
+output=$(mktemp /tmp/__perf_ilist_test.output.XXXXX)
+
+cleanup() {
+ rm -f "${output}"
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+test_plumbing() {
+ echo "ilist plumbing test"
+ # The script should be found and run by perf script, --help doesn't
+ # need a terminal.
+ if ! perf script ilist --help > "${output}" 2>&1; then
+ echo "ilist plumbing test [Failed: script failed]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q -- "--interval" "${output}"; then
+ echo "ilist plumbing test [Failed: script not launched]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "ilist plumbing test [Success]"
+}
+
+test_list_tui() {
+ echo "perf list --tui plumbing test"
+ # Arguments after -- are passed to the script launched by perf list.
+ if ! perf list --tui -- --help > "${output}" 2>&1; then
+ echo "perf list --tui plumbing test [Failed: script failed]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q -- "--interval" "${output}"; then
+ echo "perf list --tui plumbing test [Failed: script not launched]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "perf list --tui plumbing test [Success]"
+}
+
+test_headless_ui() {
+ echo "ilist headless UI test"
+ # Drive the textual app without a terminal. Opening counters may fail
+ # for want of permissions, in which case an error dialog is shown.
+ if ! "$PYTHON" > "${output}" 2>&1 <<'EOF'
+import asyncio
+import ilist
+from textual.widgets import Label, Tree
+
+async def dismiss_error(app, pilot) -> bool:
+ """Close the error dialog if shown, returning whether it was."""
+ if not isinstance(app.screen, ilist.ErrorScreen):
+ return False
+ await pilot.click("#error")
+ await pilot.pause()
+ if isinstance(app.screen, ilist.ErrorScreen):
+ raise RuntimeError("Error dialog not dismissed")
+ return True
+
+async def search(pilot, text: str) -> None:
+ await pilot.press("s")
+ await pilot.pause()
+ await pilot.press(*text, "enter")
+ await pilot.pause()
+
+def find_leaf(node, cls):
+ """Find the first node whose data is of type cls."""
+ if isinstance(node.data, cls):
+ return node
+ for child in node.children:
+ found = find_leaf(child, cls)
+ if found:
+ return found
+ return None
+
+async def check_counting(app, pilot, name: str) -> None:
+ """Check the selected event is shown and counting, or that it failed to open."""
+ await pilot.pause(0.5)
+ if await dismiss_error(app, pilot):
+ print(f"{name}: failed to open")
+ return
+ shown = str(app.query_one("#event_name", Label).render())
+ if shown != name:
+ raise RuntimeError(f"Selected {shown} rather than {name}")
+ if not app.query(ilist.CounterSparkline) or not app.query("#counter_total"):
+ raise RuntimeError(f"No counters shown for {name}")
+ print(f"{name}: total {app.query_one('#counter_total', Label).render()}")
+
+async def run() -> None:
+ app = ilist.IListApp(0.1)
+ async with app.run_test(size=(120, 40)) as pilot:
+ await pilot.pause()
+ tree = app.query_one("#root", Tree)
+ top = [str(node.label) for node in tree.root.children]
+ if top != ["PMUs", "Metrics"]:
+ raise RuntimeError(f"Unexpected tree: {top}")
+ pmus = [str(node.label) for node in tree.root.children[0].children]
+ if "software" not in pmus:
+ raise RuntimeError(f"No software PMU in: {pmus}")
+
+ # Search for a software event, which exists everywhere. The search
+ # result is selected which opens the event.
+ await search(pilot, "task-clock")
+ found = app.cur_search_result
+ if not found or not isinstance(found.data, ilist.PmuEvent) or \
+ "task-clock" not in str(found.label):
+ raise RuntimeError(f"Search didn't find task-clock: {found}")
+ await check_counting(app, pilot, found.data.name())
+ if not app.query_one("#active_search", Label).display:
+ raise RuntimeError("Active search not shown")
+ await pilot.press("n", "p", "down", "up", "c")
+ await pilot.pause(0.2)
+ await dismiss_error(app, pilot)
+
+ # Searching for something that doesn't exist shows an error.
+ await search(pilot, "no-such-event-xyzzy")
+ if not await dismiss_error(app, pilot):
+ raise RuntimeError("No error for a failed search")
+
+ # Select a metric, if there are any.
+ metric = find_leaf(tree.root, ilist.Metric)
+ if metric:
+ app.expand_and_select(metric)
+ await check_counting(app, pilot, metric.data.name())
+ print("headless UI ok")
+
+asyncio.run(run())
+EOF
+ then
+ echo "ilist headless UI test [Failed: exception]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q "headless UI ok" "${output}"; then
+ echo "ilist headless UI test [Failed: unexpected output]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "ilist headless UI test [Success]"
+}
+
+test_plumbing
+test_list_tui
+test_headless_ui
+
+cleanup
+exit $err
--
2.56.0.rc1.315.gc6ed9934b7-goog