# BST-TIMEOUT: 900 . "$BST_LIB" require nmon nmon/present # nmon 16m -- the single-source-file performance monitor. Unlike htop (85-htop), # whose only testable surface is a frame drawn by -n 1, nmon has a REAL BATCH # MODE: -f/-F write a CSV capture file and take no terminal at all # (go_background() sets cursed=0, so the initscr() in main() is never reached). # That puts every /proc reader, the whole snapshot loop and the CSV writer on the # far side of a pipe, which is why this suite asserts file CONTENT rather than # scraped screen text for all but the pty leg at the end. # # NMONDEBUG=1 IS WHAT MAKES THE BATCH MODE SYNCHRONOUS, and without it this suite # could not exist in this shape. Capture mode DAEMONIZES: main() does # `if (!debug && (childpid = fork()) != 0) exit(0)`, so the parent returns # immediately and an orphaned child writes the file on its own schedule. The only # switch for that is the NMONDEBUG environment variable (there is no option -- # -D is extended_disk); it suppresses the fork and the close(0/1/2)+setpgrp() and # NOTHING ELSE, because its three other uses in the source are all inside # #ifdef DEBUG, which this recipe does not define. So `NMONDEBUG=1 nmon -F f -s 1 # -c N` runs the whole capture in the foreground and exits when it is done. # # NOTHING HERE IS WRAPPED IN `timeout`, DELIBERATELY, and the suite's own bound # above is the whole of its protection. Coreutils timeout arms a SIGALRM and # wait()s, and on this runtime that signal path itself hangs (brintos/linux#9; # recorded on-target by 85-procps.test and 92-dumb-init.test) -- wrapping wedges # even working tools, and the runner already bounds each suite by running it in # the background, which is the mechanism that actually works. What keeps nmon # safe to run bare is instead a property of the invocations chosen: # - capture mode terminates on its own after exactly -c snapshots, and with # cursed=0 it never calls getch(), so it cannot block on input; # - stdin is redirected from /dev/null anyway, for the interactive-mode cases; # - -t IS NEVER USED. It is the one option that walks /proc//stat for # every process, which is precisely the many-read shape that hangs here # (85-procps records top/vmstat/pgrep/`ps -e` as skips against #9 for it). # Default capture reads a bounded handful of /proc files -- the shape that # works -- and that distinction is the reason this suite can exist at all. # # THE popen(3) CASES AT THE END ARE RED ON THIS ARM ON PURPOSE. nmon's BBBP # section is ~60 popen()s of external commands (cat /proc/cpuinfo, lscpu, lsblk, # ...) and popen is ENOSYS here (brintos/linux#14: glibc builds it on # posix_spawn, whose clone(CLONE_VM|CLONE_VFORK) shape cannot be serviced where a # child is a separate Worker). Following 99-sharutils, those cases assert the # value a WORKING platform gives and are left FAILING rather than skipped, so # they flip green by themselves when the platform fix lands and there is nothing # to rewrite at that point. Every one of nmon's seven popen call sites is # NULL-guarded, so the gap costs data and never a crash -- linux_bbbp() even # writes its own "failed to run" line into the capture, which is what # nmon/bbbp-no-failed reads. # # fork() ITSELF WORKS HERE and nmon/fork-daemonizes is the proof, deliberately # kept beside the popen reds: it runs capture mode WITHOUT NMONDEBUG, so the # daemonizing fork above must succeed and the child must produce the file. That # separates "process creation is broken" from "popen's particular clone shape is # unserviceable", which is the whole of #14. work=${TMPDIR:-/tmp}/bst-nmon.$$ rm -rf "$work"; mkdir -p "$work" || { bst_notok nmon/workdir "cannot create $work"; exit 0; } # ncap : one foreground capture. Sets $ncap_rc. ncap_rc=0 ncap() { _f=$1; shift NMONDEBUG=1 nmon -F "$_f" "$@" /dev/null 2>&1 ncap_rc=$? } # field : the AAA value for , e.g. field f version -> 16k field() { awk -F, -v t="$2" '$1=="AAA" && $2==t {print $3; exit}' "$1" 2>/dev/null; } # rows : data rows for a section (the T#### lines, header excluded). rows() { awk -F, -v t="$1" '$1==t && $2 ~ /^T[0-9][0-9][0-9][0-9]$/' "$2" 2>/dev/null | wc -l | tr -d ' '; } # ---- CLI surfaces: no /proc, no terminal, no file -- so they come first ------- check_out nmon/version "nmon version 16k" nmon -V check_rc nmon/hint-rc 0 nmon -\? check_out nmon/hint-text "Data Collection:" nmon -\? check_out nmon/help-full "Full Help Information for nmon 16k" nmon -h # UPSTREAM, PINNED: nmon has no failing exit for an unusable argument. getopt's # default case falls through to hint() and returns 0, exactly as -? does, so a # typo'd option is indistinguishable from a request for help. Same class as # dhex's usage-error 0 (BRINTOS-531); measured identically on x86-64, so it is # pinned here rather than reported, and an upstream bump that changes it becomes # visible instead of silent. check_rc nmon/badopt-rc 0 nmon -Q # ---- the capture file: structure, arithmetic and the readers ----------------- cap="$work/c1.nmon" ncap "$cap" -s 1 -c 3 if [ "$ncap_rc" -eq 0 ]; then bst_ok nmon/capture-rc else bst_notok nmon/capture-rc "rc=$ncap_rc (want 0)"; fi if [ -s "$cap" ]; then bst_ok nmon/capture-nonempty else bst_notok nmon/capture-nonempty "no capture file written at $cap"; fi # The AAA header is nmon's own account of the run: what it was asked for and what # it found. interval/snapshots echo -s/-c, so they are the arithmetic assertion. check_eq nmon/aaa-version 16k "$(field "$cap" version)" check_eq nmon/aaa-interval 1 "$(field "$cap" interval)" check_eq nmon/aaa-snapshots 3 "$(field "$cap" snapshots)" check_eq nmon/aaa-os Linux "$(awk -F, '$1=="AAA" && $2=="OS" {print $3; exit}' "$cap" 2>/dev/null)" # AAA,command echoes argv, which proves the file belongs to THIS invocation and # not to a stale one left by an earlier case. check_eq nmon/aaa-command 1 "$(grep -c '^AAA,command,.*-c 3' "$cap" 2>/dev/null | tr -d ' ')" # cpus is read from /proc/stat's per-CPU lines, so a positive count is the first # evidence that the /proc walk produced anything at all. cpus=$(field "$cap" cpus) case "$cpus" in ''|*[!0-9]*) bst_notok nmon/aaa-cpus "not a number: '$cpus'" ;; *) if [ "$cpus" -ge 1 ]; then bst_ok nmon/aaa-cpus else bst_notok nmon/aaa-cpus "cpus=$cpus (want >=1)"; fi ;; esac # ZZZZ is the snapshot index: exactly one per -c, numbered from T0001. A port # that lost the loop, or ran it the wrong number of times, shows up here first. check_eq nmon/zzzz-count 3 "$(grep -c '^ZZZZ,' "$cap" 2>/dev/null | tr -d ' ')" check_eq nmon/zzzz-first 1 "$(grep -c '^ZZZZ,T0001,' "$cap" 2>/dev/null | tr -d ' ')" check_eq nmon/zzzz-last 1 "$(grep -c '^ZZZZ,T0003,' "$cap" 2>/dev/null | tr -d ' ')" # Each reader writes one header line and then one row per snapshot. Asserting # BOTH is what separates "the section exists" from "the section produced data" -- # a header with no rows is exactly what a /proc reader that returned nothing # would leave behind, and it is the failure this arm is most likely to have. for sec in CPU_ALL MEM PROC VM NET; do if grep -q "^$sec," "$cap" 2>/dev/null; then bst_ok "nmon/$sec-header" else bst_notok "nmon/$sec-header" "no $sec section in the capture"; fi check_eq "nmon/$sec-rows" 3 "$(rows "$sec" "$cap")" done # The CPU percentages are the one place the arithmetic can be wrong while the # shape stays right, so they are range-checked rather than merely counted: # User%+Sys%+Wait%+Idle% is a partition of one CPU-second and each part is a # percentage. A negative or >100 value means the /proc/stat deltas were misread # (a 32-bit truncation of a 64-bit counter would land here). bad=$(awk -F, '$1=="CPU_ALL" && $2 ~ /^T/ {for(i=3;i<=6;i++) if ($i<0 || $i>100) c++} END{print c+0}' "$cap" 2>/dev/null) check_eq nmon/cpu-pct-range 0 "$bad" # memtotal is MemTotal from /proc/meminfo in MB; any running system has some. mt=$(awk -F, '$1=="MEM" && $2 ~ /^T/ {print ($3>0)?"pos":"nonpos"; exit}' "$cap" 2>/dev/null) check_eq nmon/mem-total-positive pos "$mt" # -s is the second half of the arithmetic and is asserted separately, because a # port that ignored the interval entirely would still pass every -c assertion. cap2="$work/c2.nmon" ncap "$cap2" -s 2 -c 2 check_eq nmon/interval-2 2 "$(field "$cap2" interval)" check_eq nmon/snapshots-2 2 "$(field "$cap2" snapshots)" # -F stdout is a distinct path through the file opener (fdopen(1) rather than # fopen), and it is the one capture mode that needs no writable filesystem -- # worth having on an image whose rootfs may be mounted read-only. so=$(NMONDEBUG=1 nmon -F stdout -s 1 -c 1 /dev/null) case "$so" in *"AAA,version,16k"*) bst_ok nmon/stdout-mode ;; *) bst_notok nmon/stdout-mode "no AAA header on stdout: $(_bst_1line "$so")" ;; esac # The refusal nmon does have, and it is a distinct status rather than the 0 that # every argument error returns: fopen() of the output file fails -> exit(42). check_rc nmon/badfile-rc 42 sh -c 'NMONDEBUG=1 nmon -F /no/such/dir/x.nmon -s 1 -c 1 /dev/null 2>&1' # ---- fork(2) works here; popen(3) does not ---------------------------------- # Run capture WITHOUT NMONDEBUG so the daemonizing fork is exercised: the parent # must return 0 immediately and the CHILD must go on to write the file. This is # the case that keeps the popen reds below honest -- if it is green and they are # red, the gap is popen's clone shape and not process creation. # # The wait loop is a bounded poll rather than a timer for the same reason nothing # here is timeout-wrapped, and it is bounded well inside the suite's own bound. fk="$work/fk.nmon" rm -f "$fk" nmon -F "$fk" -s 1 -c 1 /dev/null 2>&1 fkrc=$? i=0 while [ $i -lt 90 ]; do [ -s "$fk" ] && break; sleep 1; i=$((i+1)); done if [ "$fkrc" -eq 0 ] && [ -s "$fk" ]; then bst_ok nmon/fork-daemonizes else bst_notok nmon/fork-daemonizes "parent rc=$fkrc, child file $( [ -s "$fk" ] && echo written || echo missing )"; fi # RED ON THIS ARM BY DESIGN -- brintos/linux#14. Each asserts what a working # platform produces: linux_bbbp() popens the command and writes its output as # BBBP,NNN,,"" rows; on ENOSYS it writes one # BBBP,NNN, failed to run WARNING line instead. Left failing so they flip # green by themselves when popen is serviceable. if grep -q '^BBBP,' "$cap" 2>/dev/null; then bst_ok nmon/bbbp-present else bst_notok nmon/bbbp-present "no BBBP section at all in the capture"; fi check_eq nmon/bbbp-cpuinfo 1 "$( [ "$(grep -c '^BBBP,[0-9]*,/proc/cpuinfo,"' "$cap" 2>/dev/null | tr -d ' ')" -gt 0 ] && echo 1 || echo 0 )" check_eq nmon/bbbp-meminfo 1 "$( [ "$(grep -c '^BBBP,[0-9]*,/proc/meminfo,"' "$cap" 2>/dev/null | tr -d ' ')" -gt 0 ] && echo 1 || echo 0 )" check_eq nmon/bbbp-no-failed 0 "$(grep -c 'failed to run' "$cap" 2>/dev/null | tr -d ' ')" # ---- the curses layer, over a real pty -------------------------------------- # Everything above ran with cursed=0, so none of it says that initscr() resolves # $TERM out of /etc/terminfo, that the box and the panels paint, or that a # keystroke reaches the dispatcher. A build whose ncurses was a stub would pass # every case above. The needles are each a single contiguous ncurses write and # were measured against a native build of this same source. expect is run BARE # (58-hexedit does the same): its own `set timeout` is a select() inside tcl, not # a SIGALRM, so it is the one bound here that is safe on this runtime. require expect nmon/expect-present out=$(TERM=xterm expect "$BST_DATA/selftest_nmon.exp" 2>&1); rc=$? case "$out" in *ALL-NMON-OK*) all=1 ;; *) all=0 ;; esac if [ "$rc" -eq 0 ] && [ "$all" -eq 1 ]; then bst_ok nmon/pty-all-ok else bst_notok nmon/pty-all-ok "rc=$rc $(_bst_1line "$out")"; fi for tag in N1welcome N2keybar N3quithint N4acsbox N5cpuview N6quit; do case "$out" in *"N-$tag OK"*) bst_ok "nmon/$tag" ;; *) bst_notok "nmon/$tag" "not OK in payload output (rc=$rc)" ;; esac done rm -rf "$work"