. "$BST_LIB" require htop htop/present # htop is a CURSES program with no batch mode, and the option that makes it # testable at all is -n/--max-iterations: ScreenManager.c decrements the counter # immediately after drawing a frame and quits BEFORE the first Panel_getCh(), so # `htop -n 1` renders one frame and exits without ever reading a key. Every frame # case below therefore runs with stdin on /dev/null and cannot block on input -- # which matters here because the runner is typed onto the guest's own tty, and a # tool that waited for a keystroke would wedge the whole sweep rather than just # this suite. # # -C (monochrome) and -U (plain ASCII) keep the escape traffic down to cursor # motion, which is what lets the needles below survive as CONTIGUOUS substrings # of the raw capture; "Mem[" and the function-key bar do not survive and are not # used. TERM/LINES/COLUMNS are pinned so the frame is the same shape wherever it # runs, and the 200-column width is what puts a whole command line in the Command # column instead of a truncation. # # THE WHOLE-/proc WALK IS THE RISK ON THIS ARM, NOT THE CURSES LAYER. procps' # top, vmstat, pgrep and full `ps -e` hang here on brintos/linux#9 (/proc reads # return EINTR and are not restarted) and 85-procps.test records them as skips # WITHOUT running them; psmisc's pstree/fuser walk the same /proc and are clean # 8/8, so the class is not "any scan hangs". htop is measured rather than # assumed: the CLI cases below touch no /proc at all and come FIRST, so they are # emitted even if a later frame case runs into the suite bound. htop_out=""; htop_rc=0 # hrun : one frame, stdin closed, pinned geometry. Sets # $htop_out (raw capture) and $htop_rc. hrun() { htop_out=$(TERM=xterm LINES=40 COLUMNS=200 HOME="$hhome" htop "$@" &1) htop_rc=$? } # hdetail : the capture with the escape traffic stripped, for a failure detail. hdetail() { printf '%s' "$htop_out" | tr -cd '\11\12\40-\176' | tr '\n\t' ' ' | cut -c1-100; } # hrcout : ok iff exits AND says # . check_out cannot express this -- it requires rc 0 as well as the # needle -- and every refusal htop has is a non-zero status carrying the message # that explains it, so asserting the two separately would leave the message # unchecked or the status unasserted. hrcout() { _id=$1; _want=$2; _needle=$3; shift 3 _o=$("$@" 2>&1); _r=$? case "$_o" in *"$_needle"*) _h=1 ;; *) _h=0 ;; esac if [ "$_r" -eq "$_want" ] && [ "$_h" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$_o"; then bst_unrun "$_id" "$_o" else bst_notok "$_id" "rc=$_r want=$_want needle:'$_needle' got:'$(_bst_1line "$_o")'"; fi } # hframe : ok iff the frame exits 0 and carries the needle. hframe() { _id=$1; _needle=$2; shift 2 hrun "$@" case "$htop_out" in *"$_needle"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$htop_rc" -eq 0 ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$htop_out"; then bst_unrun "$_id" "$htop_out" else bst_notok "$_id" "rc=$htop_rc want:'$_needle' got:'$(hdetail)'"; fi } hhome=${TMPDIR:-/tmp}/bst-htop.$$ mkdir -p "$hhome" # --- 1. the CLI surface: no terminal, no /proc ------------------------------ # --version and --help answer out of CommandLine.c before Platform_init(), and # --sort-key=help walks the static column table and exits from inside getopt # processing. None of the three opens /proc or initscr(), so they are the cases # that stay meaningful even where the display cannot be reached at all. check_out htop/version "htop 3.3.0" htop --version check_out htop/help-iter "--max-iterations" htop --help check_out htop/sortkey-help "PROCESSOR" htop --sort-key=help # --- 2. the CLI refusals, which are the only non-zero statuses htop has ----- check_rc htop/bad-option 1 htop --nosuchopt check_rc htop/bad-sortkey 1 htop --sort-key=NOSUCHCOLUMN check_rc htop/bad-user 1 htop --user=nosuchuser99 check_rc htop/bad-iter 1 htop --max-iterations=0 hrcout htop/bad-sortkey-msg 1 "invalid column" htop --sort-key=NOSUCHCOLUMN hrcout htop/bad-user-msg 1 "invalid user" htop --user=nosuchuser99 hrcout htop/bad-option-msg 1 "unrecognized option" htop --nosuchopt # --- 3. one frame: curses came up and the /proc walk completed -------------- # Each needle is a different reader. Tasks: is ProcessTable's own count of what # the walk collected, Load average: is /proc/loadavg, Uptime: is /proc/uptime, # and "PID USER" is the column header the table draws once it has rows. hframe htop/frame-runs "Tasks:" -C -U -n 1 hframe htop/frame-load "Load average:" -C -U -n 1 hframe htop/frame-uptime "Uptime:" -C -U -n 1 hframe htop/frame-header "PID USER" -C -U -n 1 # kthr is the kernel-thread count in the same Tasks meter -- present only when # the walk actually distinguished kernel threads, which needs /proc//stat # per process rather than just the directory listing. hframe htop/frame-kthr "kthr" -C -U -n 1 # --- 4. the axes that prove it is reading, not just painting ---------------- # -t adds the ASCII tree drawing (`- with -U); the flat frame has none of it, so # the needle tells the two renderers apart rather than merely re-asserting a # frame was drawn. hframe htop/tree '`-' -C -U -n 1 -t # Two frames means the refresh loop re-read /proc and redrew, which is the shape # top(1) cannot survive here. hframe htop/two-frames "Tasks:" -C -U -n 2 -d 1 hframe htop/readonly "Tasks:" -C -U -n 1 --readonly hframe htop/sort-pid "Tasks:" -C -U -n 1 -s PID # -p and -F are the two filters, and both are asserted against a string read out # of /proc at run time rather than a pinned one: this kernel runs no init at all # (arch/wasm32's boot path ends in run_init_process(/bin/sh)), so pid 1 is the # shell and any hardcoded "systemd"/"init" would assert a process that does not # exist here. Same reasoning as 85-procps.test's ps-pid1 case. cmd1=$(tr '\0' ' ' < /proc/1/cmdline 2>/dev/null | awk '{print $1}') [ -n "$cmd1" ] || cmd1=$(cat /proc/1/comm 2>/dev/null) if [ -n "$cmd1" ]; then hframe htop/pid-filter "$cmd1" -C -U -n 1 -p 1 hframe htop/comm-filter "$cmd1" -C -U -n 1 -F "$cmd1" else bst_skip htop/pid-filter "/proc/1/cmdline and /proc/1/comm are both empty, so there is no name to filter on" bst_skip htop/comm-filter "/proc/1/cmdline and /proc/1/comm are both empty, so there is no name to filter on" fi # --- 5. the settings writer, which is the only thing htop writes ------------ # htop saves its configuration on exit, creating $HOME/.config/htop/ as it goes. # It is the one filesystem write in the package and it is silent either way -- # an unwritable HOME still exits 0 -- so nothing but the file itself says whether # it worked. (--readonly does NOT suppress it: that option disables killing and # renicing, not persistence.) if [ -f "$hhome/.config/htop/htoprc" ]; then bst_ok htop/config-write else bst_notok htop/config-write "no settings file at $hhome/.config/htop/htoprc after the frames above"; fi # The version stamp is written from the running binary, so this pins the file to # THIS htop rather than to any htoprc that happened to be lying around. check_out htop/config-content "htop_version=3.3.0" cat "$hhome/.config/htop/htoprc" rm -rf "$hhome"