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>
This commit is contained in:
Alice Rogers 2026-10-02 11:26:18 -07:00 committed by Arnaldo Carvalho de Melo
parent b379b70b7b
commit 434735dfa2

View File

@ -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