# BST-TIMEOUT: 600 . "$BST_LIB" require atop atop/present # atop 2.4.0 (meta-oe recipes-support) -- the system and process monitor whose # NON-INTERACTIVE surfaces are the whole of what this suite drives: -P writes # labelled parseable records to stdout and -w/-r write and replay a raw log, so # there is no curses layer here, no expect payload and no pty. 85-htop covers the # frame-drawing shape; this file deliberately does not. # # THE SUITE SPLITS IN TWO AND THE SPLIT IS THE POINT. Everything above the # "SAMPLING" banner is green and stays green: argument handling, the refusals, # and -- most importantly -- that atop STARTS at all. Everything below it is RED # on this image, for a platform reason atop cannot defend against, and is written # to assert what a WORKING platform produces so the cases flip green by # themselves when the kernel is fixed (the 99-sharutils / 85-nmon convention). # # WHY EVERY SAMPLE IS RED: brintos/linux#50 -- reading /proc/cpuinfo traps the # kernel (null function or function signature mismatch in seq_read_iter) and the # reader is handed ENOSYS. That file is not optional on atop's sample path. # photosyst.c takes per-CPU frequency from /sys/devices/system/cpu/cpu*/cpufreq/ # first; this image has no cpufreq sysfs, so `didone` stays 0 and the code falls # through to its documented fallback, fopen("cpuinfo") at photosyst.c:540. There # is no flag that skips it, and the frequency probe is part of the system-level # sample that -P CPU, -P ALL and -w all take -- so not one sample completes. # Measured, not assumed: in the same boot /proc/stat, /proc/meminfo, # /proc/vmstat, /proc/loadavg, /proc/uptime, the whole per-pid /proc//stat # walk and ALL of /proc/net/* read correctly, and re-reading /proc/stat 2000 # times in one process is clean -- cpuinfo is the only file that trips it. # # A SECOND BLOCKER SITS BEHIND IT and is recorded so that fixing #50 alone is not # mistaken for a full fix: atop's sample loop waits for its interval with # alarm(interval) + pause() (atop.c:778, :831), and signal delivery does not work # on this arm at all (brintos/linux#21). The FIRST sample does not pause -- # `if (sampcnt > 0 && awaittrigger) pause()` -- so the cases below are held only # by #50 and will pass when it lands. That is why every one asks for exactly ONE # sample; a multi-sample run would need #21 as well and would confuse the two. # # ONE RED PER SURFACE, NOT ONE PER LABEL. Every sampling failure here has the # same single cause and is hit at the same point, so a case per -P label would # cost a bound each and report the same fact repeatedly. The three below are the # three distinct SURFACES -- parseable stdout, the raw log writer, the converter # -- which is the granularity at which a partial platform fix would show up. # # WHY THE GREEN HALF RUNS BARE AND ONLY THE RED HALF IS BOUNDED. Every case above # the banner is an argument-handling path that exits inside getopt or prusage, # before atop opens a single /proc file, so none of them can reach the hang and # none needs protecting. The sampling cases do, and they get `arun` -- a # background subshell behind a bounded poll -- so a run that never produces its # record costs its own bound and NOT the suite's, and reports a real `not ok` # carrying that bound instead of losing the file to atop/suite-timeout. # NOT timeout(1), for the reason 85-nmon states: coreutils timeout arms a SIGALRM # and waits, and that path is itself broken here (#21). # Bounding is not free on this runtime and that is why it is rationed: the parent # poll and the child compete for one cooperative lane, so a backgrounded `atop -V` # takes tens of seconds where the same command run bare takes about two. A first # draft bounded all thirteen cases and spent its whole 600 s bound before the # sampling half finished -- 12 cases, atop/suite-timeout, and two surfaces never # reached. Measured after this split, on a fresh guest: 13 cases in 102 s # (==BST-SUITE atop elapsed=102s cases=13==), of which ~135 s of bound is the red # half's to spend. The 600 s marker above is that measurement with ~6x headroom. work=${TMPDIR:-/tmp}/bst-atop.$$ rm -rf "$work"; mkdir -p "$work" || { bst_notok atop/workdir "cannot create $work"; exit 0; } # rcout : ok iff exits AND says # . check_out cannot express this -- it requires rc 0 as well as the # needle -- and every refusal atop 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. rcout() { _id=$1; _want=$2; _needle=$3; shift 3 _o=$("$@" &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 } # arun : run in the background with stdin # closed, behind a bounded poll. Sets $arun_rc (the exit status) and $arun_out, # or $arun_rc=-1 when the bound expired or the child vanished without leaving a # status -- which is what a trapped or wedged atop looks like from here. arun_rc=0; arun_out="" arun() { _t=$1; _lim=$2; shift 2 rm -f "$work/rc.$_t" ( "$@" "$work/o.$_t" 2>&1; echo $? > "$work/rc.$_t" ) & _p=$!; _i=0 while [ "$_i" -lt "$_lim" ]; do [ -f "$work/rc.$_t" ] && break kill -0 "$_p" 2>/dev/null || break sleep 1; _i=$((_i+1)) done if [ -f "$work/rc.$_t" ]; then arun_rc=$(cat "$work/rc.$_t") else kill -9 "$_p" 2>/dev/null; arun_rc=-1; fi arun_out=$(cat "$work/o.$_t" 2>/dev/null) } # ---- the port itself: it is installed, and it STARTS ------------------------- # atop -V is the cheapest proof that main() got past its FIRST statement, which # on this image is not a given: main() drops root privileges before parsing any # argument and exits 42 on any failure that is not EPERM, and this kernel wires # no credential setters at all (brintos/linux#19), so a STOCK build answers # "not possible to drop root privs" to every invocation, `atop -V` included. The # recipe patches droprootprivs() to accept ENOSYS for the identity transition # only; these two cases are what hold that patch honest, and the second is # written against the refusal TEXT so a regression names itself. check_out atop/version "Version: 2.4.0" atop -V _v=$(atop -V &1) case "$_v" in *"not possible to drop root privs"*) bst_notok atop/no-privdrop-refusal "brintos/linux#19: the privilege-drop patch is not in effect -- $(_bst_1line "$_v")" ;; *) bst_ok atop/no-privdrop-refusal ;; esac # The other two programs the package installs. atopsar is the SAME binary reached # through argv[0], which is why it is asserted as a symlink rather than by size. if [ -L /usr/bin/atopsar ]; then bst_ok atop/sar-is-symlink else bst_notok atop/sar-is-symlink "/usr/bin/atopsar is not a symlink to atop"; fi if [ -x /usr/bin/atopconvert ]; then bst_ok atop/convert-present else bst_notok atop/convert-present "/usr/bin/atopconvert missing or not executable"; fi # ---- argument handling and the refusals ------------------------------------- # atop exits 1 through prusage() for every argument it will not take, so each # case asserts status AND message together: a case that checked only the status # would pass against the wrong diagnostic. rcout atop/badlabel 1 "label NOSUCHLABEL not found" atop -P NOSUCHLABEL 1 1 # The usage text names the parseable flag this suite drives, so it doubles as the # check that this build carries it. rcout atop/usage 1 "Usage: atop [-flags] [interval [samples]]" atop -\? rcout atop/usage-names-P 1 "-P generate parseable output for specified" atop -\? # UPSTREAM, PINNED: atopsar has its OWN flag set and -V is not in it, so # `atopsar -V` is an invalid option where `atop -V` prints the version. That # reads like a broken symlink and is not one. Pinned so an upstream bump that # unifies the two flag sets becomes visible rather than silent. rcout atop/sar-rejects-V 1 "atopsar: invalid option" atopsar -V rcout atop/sar-usage 1 "Usage: atopsar [-flags]" atopsar -\? # atopconvert touches no /proc at all -- it only rewrites a raw log between # versions -- so its refusal is green even though the case that gives it a real # log to convert is red below. rcout atop/convert-usage 1 "Usage: atopconvert" atopconvert # ---- SAMPLING: red on this image, brintos/linux#50 -------------------------- # Each of the three asserts the record a WORKING platform emits, and each is a # different surface. Exactly one sample is requested, so #50 is the only thing # holding them (see the header on #21). # Surface 1 -- parseable stdout. atop's own record shape: a line whose first # field is the label and whose second is the node name, terminated by SEP. arun sample-cpu 45 atop -P CPU 1 1 case "$arun_out" in *"CPU "*) _h=1 ;; *) _h=0 ;; esac if [ "$arun_rc" = 0 ] && [ "$_h" = 1 ]; then bst_ok atop/sample-cpu elif [ "$arun_rc" = -1 ]; then bst_notok atop/sample-cpu "brintos/linux#50 (/proc/cpuinfo read traps; atop's cpufreq fallback needs it): no record in 45s -- got:'$(_bst_1line "$arun_out")'" else bst_notok atop/sample-cpu "brintos/linux#50: rc=$arun_rc want a 'CPU ' record -- got:'$(_bst_1line "$arun_out")'" fi # Surface 2 -- the raw log writer. Asserted on the FILE, not on stdout, because # -w writes nothing to stdout even when it works. raw="$work/raw.atop" arun rawwrite 45 atop -w "$raw" 1 1 if [ "$arun_rc" = 0 ] && [ -s "$raw" ]; then bst_ok atop/rawlog-write else bst_notok atop/rawlog-write "brintos/linux#50: rc=$arun_rc, raw log $( [ -s "$raw" ] && echo written || echo 'not written' ) -- $(_bst_1line "$arun_out")"; fi # Surface 3 -- the converter, against that log. Its own code path is green # (atop/convert-usage above); what it lacks here is an input. Kept separate so # that when #50 lands this case confirms the raw format round-trips, and so its # failure detail distinguishes "no input" from "convert refused". arun convert 45 atopconvert "$raw" "$work/raw2.atop" if [ "$arun_rc" = 0 ] && [ -s "$work/raw2.atop" ]; then bst_ok atop/convert-roundtrip else bst_notok atop/convert-roundtrip "brintos/linux#50 (no raw log to convert): rc=$arun_rc -- $(_bst_1line "$arun_out")"; fi rm -rf "$work"