. "$BST_LIB" # BST-TIMEOUT: 1800 # SEVEN python3 starts, and they are the whole cost. The # # interpreter here is one large static image, so a start is # # tens of seconds before any work begins; six of the seven # # are CLI runs that cannot be folded together, because the # # point of each is a different argv, and the seventh is the # # probe, which takes thirty-odd measurements in one. # # hwlatdetect 2.6 -- the Python front end to the kernel's ftrace `hwlat' tracer. # # WHAT THIS SUITE IS ABOUT. The tool's whole job is to touch debugfs, and this # kernel has none: wasm32_defconfig carries "# CONFIG_FTRACE is not set", so # there is no tracing/ directory and no hwlat tracer to find in it -- the same # absence 89-blktrace asserts against for the block tracer. So the interesting # question is not "does it measure latency" (it cannot, anywhere on this # platform) but "what does it do when it cannot", and the answer the SOURCE # gives is: nothing graceful. # # * DetectorNotAvailable is defined at line 113, raised in Tracer.__init__ at # line 272, and caught NOWHERE. # * The RuntimeError DebugFS.mount raises one step earlier, when # `/bin/mount -t debugfs none /sys/kernel/debug' fails, is not caught either. # * __main__ has no try/except at all. # # So the refusal arrives as an UNHANDLED PYTHON TRACEBACK and status 1. This # suite asserts that -- what the source does, not what a reader would hope it # does -- and section 6 would go red if a future upstream added the handler, # which is the correct signal for a change of that size. The three refusal # points are enumerated rather than pinned to one, because which of them fires # depends on the guest's uid and on whether /bin/mount can register a debugfs; # what is asserted is that the refusal is ONE OF THEM and not something nobody # predicted. # # WHY THE ARGUMENT LAYER IS ASSERTED THROUGH A PROBE. __main__ runs, in order: # parse_args, then `detect = Tracer()', and only THEN converts --threshold, # --window, --width and --duration. The constructor is what fails here, so no # unit suffix is ever converted through the command line on this image, and a # malformed --duration is refused by the constructor's traceback rather than by # the parser -- which is asserted as itself, in section 6. The conversions the # ticket names are measured where they live, at module level, by # data/hwlatdetect-probe.py. That is what "importable and runnable by the ported # interpreter" buys, and it is the only route to them. # # ...AND WHY THE IMPORT NEEDS A PATH. poky's recipe forces # PYLIB=${libdir}/python${PYTHON_BASEVERSION}/dist-packages while this # interpreter is built with site-packages, so the module is NOT on the default # sys.path. That is packaging, not a defect, and both halves are pinned (section # 5) so a future change to either is visible instead of silently rescuing the # other. # # WHY IT READS A MANIFEST INSTEAD OF CARRYING DIGESTS. The bytes have to survive # four legs -- do_install, do_package, the .deb, do_rootfs -- and only the first # can see ${D}. The bbappend asserts at BUILD time that what the recipe declares # was staged, and generates ${libexecdir}/hwlatdetect-tests/install-manifest from # ${D}; this suite asserts on TARGET that the image equals that manifest. # Together they say the shipped file is the declared file. A digest typed in here # would pin poky's content rather than the pipeline, and would go red on a # legitimate poky bump while saying nothing about the three legs that lose bytes. S=hwlatdetect R=${BST_CC_ROOT:-} BIN_DECL=/usr/bin/hwlatdetect BIN=$R$BIN_DECL MAN=$R/usr/libexec/hwlatdetect-tests/install-manifest PROBE=$BST_DATA/hwlatdetect-probe.py W=${TMPDIR:-/tmp}/bst-$S.$$ mkdir -p "$W" 2>/dev/null || { bst_notok $S/workdir "cannot create $W"; exit 0; } # --- a bounded run whose VERDICT is the caller's ------------------------------ # bst_probe cannot be used for the runs below: it is ok iff rc==0, and every run # in section 6 but --help is expected to FAIL. The bounding is bst_probe's, for # its reasons and they are not obvious -- a completion sentinel rather than # `kill -0' (a fast child's zombie is not reaped inside a tight poll), # /proc/uptime rather than an elapsed counter (it is the clock that advances # here), and `sleep 1' with builtins only in the loop body (without the sleep the # poll grows the HOST process without bound, brintos/hardwarejs#20; and a # relative sleep placed directly after reaping a foreground child returns # instantly, brintos/linux-in-the-browser#120). # # The bound matters for a second reason here: if this kernel ever DID grow the # hwlat tracer, a bare `hwlatdetect' runs for its default 120 seconds. Nothing # below would hang the suite even then. # # Sets hw_rc, hw_out (stdout), hw_err (stderr) and hw_all; returns 124 on # overrun, with hw_rc=124. hw_run() { _hr_t=$1; _hr_b=$2 shift 2 _hr_o=$W/$_hr_t.out; _hr_e=$W/$_hr_t.err; _hr_s=$W/$_hr_t.rc rm -f "$_hr_o" "$_hr_e" "$_hr_s" { "$@" >"$_hr_o" 2>"$_hr_e"; echo $? >"$_hr_s"; } & _hr_p=$! _hr_0= if [ -r /proc/uptime ] && read -r _hr_u _ < /proc/uptime; then _hr_0=${_hr_u%.*}; fi _hr_k=0 while [ ! -e "$_hr_s" ]; do _hr_d=$_hr_k if [ -n "$_hr_0" ] && read -r _hr_u _ < /proc/uptime; then _hr_d=$(( ${_hr_u%.*} - _hr_0 )) fi if [ "$_hr_d" -ge "$_hr_b" ]; then kill -9 "$_hr_p" 2>/dev/null hw_rc=124; hw_out=; hw_err=; hw_all= return 124 fi sleep 1 _hr_k=$(( _hr_k + 1 )) done hw_rc=0 [ -s "$_hr_s" ] && read -r hw_rc < "$_hr_s" 2>/dev/null case "$hw_rc" in ''|*[!0-9]*) hw_rc=0 ;; esac hw_out=$(cat "$_hr_o" 2>/dev/null) hw_err=$(cat "$_hr_e" 2>/dev/null) hw_all=$hw_out$_BST_NL$hw_err return 0 } # Which of the three refusal points the module reached. Enumerated rather than # pinned: Detector.__init__ checks uid FIRST, then mounts debugfs, and only then # does Tracer.__init__ look for the tracer -- so the earliest one that fails is # the one that speaks, and all three mean "this platform has no hwlat". hw_class() { case "$1" in *DetectorNotAvailable*) hw_cls=no-tracer ;; *"Failed to mount debugfs"*) hw_cls=no-debugfs ;; *"Must be root"*) hw_cls=not-root ;; *"'/bin/mount'"*) hw_cls=no-mount ;; *) hw_cls=unknown ;; esac } require python3 $S/python3-present require hwlatdetect $S/present # --- section 1: the -tests package arrived, and its manifest header ----------- check $S/manifest-present test -f "$MAN" if [ ! -f "$MAN" ]; then bst_notok $S/manifest "no $MAN: hwlatdetect-tests is not on this image, so nothing below has a subject" rm -rf "$W" 2>/dev/null exit 0 fi # `bst_local' first: the manifest is read with `while ... done < file', which has # to stay in this shell to keep the variables it sets, and a top-level stdin # redirect FROM THE VIRTIOFS ROOT makes dup2(2) return the kernel's restart-park # sentinel as 513 -- ash treats that as fatal and exits, taking every case below # with it while the run still reads as a clean pass (brintos/linux#73). MANL=$(bst_local "$MAN") man_root=$(LC_ALL=C awk '$1=="root"{print $2; exit}' "$MANL") man_ver=$(LC_ALL=C awk '$1=="version"{print $2; exit}' "$MANL") man_sheb=$(LC_ALL=C awk '$1=="shebang"{print $2; exit}' "$MANL") man_lnk=$(LC_ALL=C awk '$1=="link"{print $2; exit}' "$MANL") man_tgt=$(LC_ALL=C awk '$1=="link"{print $3; exit}' "$MANL") man_cnt=$(LC_ALL=C awk '$1=="count"{print $2; exit}' "$MANL") check_eq $S/manifest-root / "$man_root" check_eq $S/manifest-version 2.6 "$man_ver" check_eq $S/manifest-shebang '#!/usr/bin/python3' "$man_sheb" check_eq $S/manifest-link "$BIN_DECL" "$man_lnk" # EXACT, not a floor. The recipe installs exactly one regular file plus one # symlink; the rootfs is read-only at browser runtime so nothing here can repair # a short install, and an exact count is the only form that catches a package # that shipped its manifest and lost its content. A poky bump that installs a # second file is expected to move this number, deliberately and visibly -- and # would fail the bbappend's own assertions at build time first. check_eq $S/manifest-count 1 "$man_cnt" # The count agrees with the body it heads. A manifest whose header had drifted # from its own entries would satisfy every per-file case below while claiming to # cover something it does not list. NR>5 skips the five header lines. check_eq $S/manifest-count-agrees "$man_cnt" \ "$(LC_ALL=C awk 'NR>5' "$MANL" | LC_ALL=C awk 'END{print NR+0}')" # --- section 2: every file the manifest names, as it names it ----------------- if ! command -v sha256sum >/dev/null 2>&1 || ! command -v stat >/dev/null 2>&1; then bst_skip $S/per-file "no sha256sum and/or stat on this image; the manifest cannot be checked against the tree" PYFILE= else LC_ALL=C awk 'NR>5' "$MANL" > "$W/body" BODY=$(bst_local "$W/body") PYFILE= while IFS=' ' read -r mode sha rel; do [ -n "$rel" ] || continue PYFILE=$rel if [ ! -f "$R$rel" ]; then bst_notok $S/installed:$rel "manifest names it, image does not have it" continue fi bst_ok $S/installed:$rel check_eq $S/mode:$rel "$mode" "$(stat -c %a "$R$rel" 2>/dev/null)" check_eq $S/sha256:$rel "$sha" \ "$(sha256sum "$R$rel" 2>/dev/null | LC_ALL=C awk '{print $1; exit}')" done < "$BODY" fi # The manifest named a body line at all. Without this, a manifest whose count # header said 1 and whose body was empty would leave section 2 emitting nothing # and section 5 reading an empty path -- silently, since a `while' over no lines # is not an error. if [ -n "$PYFILE" ]; then bst_ok $S/manifest-body else bst_notok $S/manifest-body "the manifest body named no file; sections 2 and 5 have no subject" PYFILE=/usr/lib/python3.12/dist-packages/hwlatdetect.py fi # It is where the packaging says it is, and not somewhere else that happens to # work: ${PYLIB} is dist-packages by poky's own EXTRA_OEMAKE. case "$PYFILE" in */dist-packages/hwlatdetect.py) bst_ok $S/pydist-path ;; *) bst_notok $S/pydist-path "the module shipped at '$PYFILE', which is not a dist-packages path -- poky's EXTRA_OEMAKE sets PYLIB to one" ;; esac PYDIST=${PYFILE%/hwlatdetect.py} # The shebang the recipe's sed wrote, read off the image rather than off ${D}. # It is the DISPATCH for every run in section 6 that goes through the symlink: # a rebuild that lost the sed ships a file that is still executable, still # passes every packaging case, and cannot run. check_eq $S/shebang "$man_sheb" "$(head -n 1 "$R$PYFILE" 2>/dev/null)" # --- section 3: the symlink --------------------------------------------------- # `make install_hwlatdetect' links ${bindir}/hwlatdetect at the ABSOLUTE ${PYLIB} # path, and the .deb still carries it that way -- but THE IMAGE DOES NOT. # meta-brintos/scripts/stage-machine.sh step 1 rewrites every absolute symlink in # the staged rootfs to a relative one ("ABSOLUTE symlinks trap the browser FUSE # bridge/kernel -- relativize all"), so what ships here is # ../lib/python3.12/dist-packages/hwlatdetect.py. Measured over the whole staged # image: 1717 relative symlinks and ZERO absolute ones. That is an image-wide # platform policy, not something this package chose or could opt out of. # # So what is asserted is where the link POINTS, not how it spells it: the target # is normalised against the link's own directory and compared to the manifest's. # Pinning the literal text asserts stage-machine.sh's output rather than this # package's packaging, and goes red on every image for a rewrite the platform # makes on purpose. A digest would only re-measure the file it points at. check $S/bin-is-symlink test -L "$BIN" # Pure text, no filesystem lookup: it says what the LINK CLAIMS rather than what # happens to exist -- which is bin-resolves' job below -- and it is therefore # unaffected by $BST_CC_ROOT, whose prefix a resolved path would carry. hwlat_normpath() { _out= IFS=/ for _c in $1; do case $_c in ''|.) continue ;; ..) _out=${_out%/*} ;; *) _out=$_out/$_c ;; esac done printf '%s\n' "${_out:-/}" } bin_lnk=$(readlink "$BIN" 2>/dev/null) case $bin_lnk in /*) bin_tgt=$(hwlat_normpath "$bin_lnk") ;; *) bin_tgt=$(hwlat_normpath "${BIN_DECL%/*}/$bin_lnk") ;; esac check_eq $S/bin-target "$man_tgt" "$bin_tgt" check $S/bin-resolves test -f "$BIN" check $S/bin-executable test -x "$BIN" # --- section 4: the .deb, as dpkg sees it ------------------------------------- if ! command -v dpkg-query >/dev/null 2>&1; then bst_skip $S/dpkg "no dpkg-query on this image" else check_eq $S/dpkg-version "1:2.6-r0" \ "$(dpkg-query -W -f '${Version}' hwlatdetect 2>/dev/null)" check_eq $S/dpkg-tests-version "1:2.6-r0" \ "$(dpkg-query -W -f '${Version}' hwlatdetect-tests 2>/dev/null)" # RDEPENDS is python3-core alone -- a sub-package of the ported python3 # (BRINTOS-1476). Asserted exactly, because the ticket's claim that nothing # about this port waits on another package is exactly this line. check_eq $S/dpkg-depends "python3-core" \ "$(dpkg-query -W -f '${Depends}' hwlatdetect 2>/dev/null)" check_eq $S/dpkg-tests-depends "hwlatdetect" \ "$(dpkg-query -W -f '${Depends}' hwlatdetect-tests 2>/dev/null)" # The bbappend's one divergence, asserted on the IMAGE rather than in the # build log: upstream RRECOMMENDS kernel-module-hwlat-detector, which is # unsatisfiable on a kernel built with "# CONFIG_MODULES is not set" and # stopped bitbake before a single task ran. A rebuild that lost the # RRECOMMENDS:remove would not reach an image at all -- but if the kernel # ever grows modules, this is the case that says the recommendation is still # withdrawn and the decision needs revisiting. rec=$(dpkg-query -W -f '${Recommends}' hwlatdetect 2>/dev/null) case "$rec" in *kernel-module-hwlat-detector*) bst_notok $S/dpkg-no-kernel-module "Recommends still names kernel-module-hwlat-detector: '$rec'" ;; *) bst_ok $S/dpkg-no-kernel-module ;; esac # Ownership: the module belongs to ${PN} and the manifest to ${PN}-tests, so # an image that wants the tool does not have to take a test manifest with it. check_eq $S/dpkg-owner-module "hwlatdetect" \ "$(dpkg -S "$PYFILE" 2>/dev/null | LC_ALL=C awk -F: 'NR==1{print $1; exit}')" check_eq $S/dpkg-owner-manifest "hwlatdetect-tests" \ "$(dpkg -S /usr/libexec/hwlatdetect-tests/install-manifest 2>/dev/null | LC_ALL=C awk -F: 'NR==1{print $1; exit}')" fi # --- section 5: the module, through the ported interpreter -------------------- if [ ! -f "$PROBE" ]; then bst_notok $S/probe-present "$PROBE did not ship" else bst_ok $S/probe-present # ONE interpreter start for everything in this section. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the traceback nowhere. # $R-prefixed: on target R is empty and this is $PYDIST, but under a # BST_CC_ROOT control run it is the shadowed tree -- so the probe measures # the module this suite's own packaging cases just measured, either way. HWLAT_PYDIST=$R$PYDIST export HWLAT_PYDIST bst_probe $S/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken module, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite # that prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not a comment. if [ -s "$OUT" ]; then bst_ok $S/probe-output else bst_notok $S/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial module rather than a # partial run. check_eq $S/probe-complete "yes" "$(pv probe_done)" check_eq $S/probe-count "42" "$(pv emitted_count)" # It imported at all, and it imported the file this image installed rather than # a stray copy on sys.path. This is what makes the packaging cases above and the # behaviour cases below statements about the SAME module. check_eq $S/import-clean "" "$(pv import_error)" check_eq $S/module-answered "$R$PYFILE" "$(pv module_file)" # The interpreter that answered is the one whose libdir the module shipped into. # A module installed under python3.12/dist-packages and imported by a 3.13 # interpreter would pass every other case in this section. check_eq $S/py-version "$(printf '%s' "$PYDIST" | LC_ALL=C sed -n 's,.*/python\([0-9.]*\)/dist-packages$,\1,p')" \ "$(pv py_version)" # ${PYLIB} IS NOT ON THE DEFAULT sys.path, and that is the packaging fact rather # than a defect: poky's EXTRA_OEMAKE sets PYLIB to dist-packages while this # interpreter is built with site-packages. Pinned in both directions -- if a # future python3 recipe adds dist-packages to the default path this goes red and # the probe's own path insertion becomes redundant, which is worth knowing. check_eq $S/pydist-off-default-path "no" "$(pv pydist_on_default_path)" # The module's own version constant. hwlatdetect has NO --version option -- the # constant is not exposed on the command line at all -- so this and the .deb's # 1:2.6-r0 above are the two versions there are, and they are different numbers # about different things: 0.8 is the script's, 2.6 is rt-tests'. check_eq $S/module-version "0.8" "$(pv version)" # The names the CLI is built out of. A release that renamed or dropped one would # still import, and section 6 would then fail with no indication of why. for n in DebugFS Detector Tracer DetectorNotAvailable seconds milliseconds microseconds debug info; do check_eq $S/has:$n "yes" "$(pv "has_$n")" done check_eq $S/dna-is-exception "yes" "$(pv dna_is_exception)" # --- the unit conversions, which the command line cannot reach here ----------- # `seconds()' is what --duration uses. The help string documents {smdw}; `h' # works too and is asserted because it is in the source. check_eq $S/seconds:10 "10" "$(pv sec_10)" check_eq $S/seconds:10s "10" "$(pv sec_10s)" check_eq $S/seconds:10m "600" "$(pv sec_10m)" check_eq $S/seconds:10h "36000" "$(pv sec_10h)" check_eq $S/seconds:10d "864000" "$(pv sec_10d)" check_eq $S/seconds:10w "6048000" "$(pv sec_10w)" # `microseconds()' is what --threshold, --window and --width use, and it takes # only {us,ms,s} plus a bare integer. THE MAN PAGE IS WRONG ABOUT THIS: it # documents --window and --width as