| #!/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 |