summaryrefslogtreecommitdiff
path: root/tools/perf/tests/shell
diff options
context:
space:
mode:
authorAlice Rogers <alice.mei.rogers@gmail.com>2026-10-02 11:26:18 -0700
committerArnaldo Carvalho de Melo <acme@redhat.com>2026-10-03 11:32:25 +0200
commit434735dfa2ae5b53ac4b73b00877d0124ccb2151 (patch)
tree0a982d49583e448741487cbad548f86e7c3fb921 /tools/perf/tests/shell
parentb379b70b7b7b41ff3a8ec91f1506ee66dca80df4 (diff)
perf test: Add a test for the treport script
Test launching treport through 'perf script', the error for a file that isn't a perf.data file, drive the textual app headless checking the profile matches one built without the UI and exercising the key bindings, and that quitting while loading stops the background load. Committer testing: root@x2:/home/acme/git/perf-tools-next# perf test "perf script treport test" 166: perf script treport test : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 root@x2:/home/acme/git/perf-tools-next# perf test -vv "perf script treport test" 166: perf script treport test: ---- start ---- test child forked, pid 2829708 treport plumbing test treport plumbing test [Success] treport bad file test treport bad file test [Success] treport headless UI test treport headless UI test [Success] treport cancel test treport cancel test [Success] ---- end(0) ---- 166: perf script treport test : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 root@x2:/home/acme/git/perf-tools-next# Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Alice Rogers <alice.mei.rogers@gmail.com> Co-developed-by: Ian Rogers <irogers@google.com> Signed-off-by: Ian Rogers <irogers@google.com> Tested-by: Arnaldo Carvalho de Melo <acme@redhat.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Diffstat (limited to 'tools/perf/tests/shell')
-rwxr-xr-xtools/perf/tests/shell/test_treport_python.sh247
1 files changed, 247 insertions, 0 deletions
diff --git a/tools/perf/tests/shell/test_treport_python.sh b/tools/perf/tests/shell/test_treport_python.sh
new file mode 100755
index 000000000000..155993b8be79
--- /dev/null
+++ b/tools/perf/tests/shell/test_treport_python.sh
@@ -0,0 +1,247 @@
+#!/bin/bash
+# perf script treport 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
+perfdata=
+output=
+badfile=
+
+cleanup() {
+ [ -n "${perfdata}" ] && rm -f "${perfdata}"*
+ rm -f "${output}" "${badfile}"
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+perfdata=$(mktemp /tmp/__perf_treport_test.perf.data.XXXXX)
+output=$(mktemp /tmp/__perf_treport_test.output.XXXXX)
+badfile=$(mktemp /tmp/__perf_treport_test.bad.XXXXX)
+
+test_plumbing() {
+ echo "treport plumbing test"
+ # A missing input file should be reported by the script launched
+ # through perf script, this doesn't need a terminal.
+ if perf script treport -i "${perfdata}.missing" > "${output}" 2>&1; then
+ echo "treport plumbing test [Failed: missing file not an error]"
+ err=1
+ return
+ fi
+ if ! grep -q "not found" "${output}"; then
+ echo "treport plumbing test [Failed: script not launched]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "treport plumbing test [Success]"
+}
+
+test_bad_file() {
+ echo "treport bad file test"
+ echo "not a perf.data file" > "${badfile}"
+ # The app starts before the file is read, so the error is reported by
+ # the app exiting with a failure.
+ if ! "$PYTHON" - "${badfile}" > "${output}" 2>&1 <<'EOF'
+import asyncio
+import sys
+import treport
+
+async def run() -> None:
+ app = treport.ReportApp(sys.argv[1])
+ async with app.run_test(size=(120, 40)) as pilot:
+ for _ in range(100):
+ if app.return_code is not None:
+ break
+ await pilot.pause(0.1)
+ if app.return_code != 1:
+ raise RuntimeError(f"Unexpected return code {app.return_code}")
+ print("bad file ok")
+
+asyncio.run(run())
+EOF
+ then
+ echo "treport bad file test [Failed: exception]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q "bad file ok" "${output}"; then
+ echo "treport bad file test [Failed: unexpected output]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "treport bad file test [Success]"
+}
+
+record() {
+ # A software event with callchains that doesn't need privileges.
+ perf record -e task-clock -g -o "${perfdata}" -- perf test -w noploop \
+ > /dev/null 2>&1
+}
+
+test_headless_ui() {
+ echo "treport headless UI test"
+ # Drive the textual app without a terminal, check the profile shown
+ # matches one built without the UI and exercise the key bindings.
+ if ! "$PYTHON" - "${perfdata}" > "${output}" 2>&1 <<'EOF'
+import asyncio
+import sys
+import treport
+
+def labels(tree, tnode, out):
+ for child in tnode.children:
+ out.append(str(child.label))
+ if child.is_expanded:
+ labels(tree, child, out)
+ return out
+
+async def run() -> None:
+ expected = treport.ProfileBuilder()
+ expected.read(sys.argv[1])
+ if not expected.root.children:
+ raise RuntimeError("No samples in the profile")
+
+ # The app starts before the profile is built in a background thread.
+ app = treport.ReportApp(sys.argv[1])
+ async with app.run_test(size=(120, 40)) as pilot:
+ for _ in range(600):
+ if app.loaded:
+ break
+ await pilot.pause(0.1)
+ if not app.loaded:
+ raise RuntimeError("Timed out loading data")
+ await pilot.pause()
+ if app.sub_title.startswith("Loading") or "samples" not in app.sub_title:
+ raise RuntimeError(f"Unexpected sub-title: {app.sub_title}")
+ totals = {name: node.value for name, node in app.root.children.items()}
+ want = {name: node.value for name, node in expected.root.children.items()}
+ if totals != want:
+ raise RuntimeError(f"Profile {totals} differs from {want}")
+ tree = app.query_one(treport.ProfileTree)
+ shown = labels(tree, tree.root, [])
+ if not any("noploop" in label for label in shown):
+ raise RuntimeError(f"noploop not shown in: {shown}")
+ await pilot.press("down", "down", "enter", "up", "enter")
+ tabs = app.query_one(treport.TabbedContent)
+ tabs.active = "flame"
+ await pilot.pause()
+ await pilot.press("down", "down", "right", "enter", "escape", "up", "left")
+ await pilot.pause()
+ print("headless UI ok")
+
+asyncio.run(run())
+EOF
+ then
+ echo "treport headless UI test [Failed: exception]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q "headless UI ok" "${output}"; then
+ echo "treport headless UI test [Failed: unexpected output]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "treport headless UI test [Success]"
+}
+
+test_cancel() {
+ echo "treport cancel test"
+ # Quitting while loading should stop the background thread without a
+ # "processing failed" error from the session.
+ if ! "$PYTHON" - "${perfdata}" > "${output}" 2>&1 <<'EOF'
+import asyncio
+import sys
+import time
+import treport
+
+# Check for cancellation often and slow processing each sample so that the
+# load can't finish before it is cancelled.
+treport.ProfileBuilder.PROGRESS_INTERVAL = 1
+process_event = treport.ProfileBuilder.process_event
+
+def slow_process_event(self, sample) -> None:
+ time.sleep(0.001)
+ process_event(self, sample)
+
+treport.ProfileBuilder.process_event = slow_process_event
+
+async def run() -> None:
+ app = treport.ReportApp(sys.argv[1])
+ async with app.run_test(size=(120, 40)) as pilot:
+ for _ in range(100):
+ loading = app.loading
+ if loading is not None and loading.nr_samples > 0:
+ break
+ await pilot.pause(0.1)
+ if loading is None or app.loaded:
+ raise RuntimeError("Background load not in progress")
+ await pilot.press("ctrl+q")
+ for _ in range(100):
+ if app.loading is None:
+ break
+ time.sleep(0.1)
+ if app.loading is not None:
+ raise RuntimeError("Background load didn't stop")
+ if not loading.cancelled or app.loaded:
+ raise RuntimeError("Background load wasn't cancelled")
+ print("cancel ok")
+
+asyncio.run(run())
+EOF
+ then
+ echo "treport cancel test [Failed: exception]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ if ! grep -q "cancel ok" "${output}" || grep -q "processing failed" "${output}"; then
+ echo "treport cancel test [Failed: unexpected output]"
+ cat "${output}"
+ err=1
+ return
+ fi
+ echo "treport cancel test [Success]"
+}
+
+test_plumbing
+test_bad_file
+
+if ! record; then
+ echo "Skipping remaining tests, failed to record samples"
+ if [ $err -eq 0 ]; then
+ err=2
+ fi
+ cleanup
+ exit $err
+fi
+
+test_headless_ui
+test_cancel
+
+cleanup
+exit $err