diff options
| author | Ian Rogers <irogers@google.com> | 2026-10-02 11:26:16 -0700 |
|---|---|---|
| committer | Arnaldo Carvalho de Melo <acme@redhat.com> | 2026-10-03 11:23:09 +0200 |
| commit | ba5992c94a828e7fee5fd6a139219a2c99e7cc92 (patch) | |
| tree | 904edb01c30724d0b19a50bc8caaf87fb9330eb0 /tools/perf/tests/shell | |
| parent | ab8834953ba082788d78bdf1af95f7a71db646a6 (diff) | |
perf test: Add a test for the ilist script
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@google.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Diffstat (limited to 'tools/perf/tests/shell')
| -rwxr-xr-x | tools/perf/tests/shell/test_ilist_python.sh | 181 |
1 files changed, 181 insertions, 0 deletions
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 |
