. "$BST_LIB" # BST-TIMEOUT: 1800 # TWO python3 starts (the probe, and one bare `import'), plus # # one `pflags' run, which is a third. Every assertion below # # either reads the filesystem/dpkg with shell builtins or # # greps the single probe run's output, so the bound is three # # interpreter loads plus a WHOLE-/proc WALK -- and that walk # # is what makes this suite's bound 1800 rather than the 1200 # # the pure-Python ports use: procfs.pidstats() opens and # # parses /proc//stat for every process on the machine, # # and pflags does it a second time. NOT MEASURED ON TARGET # # YET -- this port's first on-target run is the batch verify, # # and the measured elapsed replaces this note in the test # # plan rather than in this comment, which would cost an image # # build to keep the shipped suite byte-identical. # # python3-linux-procfs 0.7.3 (meta-openembedded/meta-python, # python3-linux-procfs_0.7.3.bb, a git SRC_URI at SRCREV 59ecd1ba, `inherit # setuptools3') -- THE /proc ABSTRACTION FROM THE tuna FAMILY, on the wasm32-hwjs # glibc image. The distribution is `python-linux-procfs'; the importable package # is `procfs'. Both strings appear below and they are not typos for each other. # # ============================================================================ # WHAT THE PACKAGE IS, AND THEREFORE WHAT THIS SUITE ASSERTS. # ============================================================================ # # It is a set of PARSERS with a /proc path baked into each one. That is two # separable things and they fail separately, so they are asserted separately: # # 1. THE PARSING. Given the bytes of a /proc file, produce the right dict. # Nothing about this depends on the platform, so it is driven over files # this suite STAGES and a fixture it SHIPS, and every value is asserted # exactly. # # 2. THE READING. Open the real file on this kernel and get bytes at all. # That is entirely a statement about the platform, and it is where the reds # on this image, if any, will be. # # A suite that only did (2) would report a platform gap and say nothing about # whether the port works; one that only did (1) would pass on an image where # /proc did not exist. # # ============================================================================ # /proc/cpuinfo IS NEVER READ, AND THAT IS DELIBERATE. # ============================================================================ # # Reading /proc/cpuinfo TRAPS THIS KERNEL -- "null function or function # signature mismatch" in seq_read_iter, brintos/linux#50 -- and the trap has been # observed to take the whole guest down mid-run, not merely to hand the reader # ENOSYS (77-tokyocabinet's header records exactly that happening, and # 85-atop.test carries the same citation for its sampling half). A suite that # called the bare `procfs.cpuinfo()' would therefore not produce an attributable # red; it would delete its own remaining evidence and everything scheduled after # it in the same boot. # # So the cpuinfo PARSER is driven over a committed fixture instead -- # `cpuinfo(filename=...)' takes the path as an argument, which is what makes that # possible -- and the fixture's expected nr_cpus/nr_sockets/nr_cores are known # exactly rather than being whatever this machine reports. What is NOT asserted, # and cannot be by anyone until #50 closes, is what this kernel's own # /proc/cpuinfo contains. # # BRINTOS-WORKAROUND: brintos/linux#50 revisit # # ============================================================================ # THE CASE ORDER IS LOAD-BEARING, FOR THE SAME REASON. # ============================================================================ # # The probe runs everything that touches no live /proc file first -- import, # pure functions, staged parsers, the absence paths -- and the live walks last. # A kernel trap during the live section then costs only the keys below it, and # every case above is already scored. Ordered the other way one trap silently # deletes the whole green half of this port's evidence, which is what happened on # 77-tokyocabinet's first run. # # ============================================================================ # WHAT IS PREDICTED HERE AND NOT YET MEASURED. # ============================================================================ # # THE RECIPE PHASE OF A BATCHED PASS CANNOT BOOT A GUEST, so nothing below has # run on this arm at the time of writing. Every asserted value was measured on # x86-64 CPython 3.12.3 against THIS EXACT PROBE and against the upstream tree at # SRCREV 59ecd1ba (cloned and checked out, not read off a release page). The # image ships python3.12 and six 1.16.0 and the control had both, so there is no # version gap to declare. # # What transfers from that control and what does not divides exactly along the # (1)/(2) line above: # # - THE STAGED AND FIXTURE CASES TRANSFER. They are string and integer # operations over bytes this suite writes; nothing in them depends on # pointer width, byte order or the platform string. # - THE LIVE-/proc CASES ARE A PREDICTION. Specifically: # * /proc//stat, /proc//status and the whole-/proc walk are # EXPECTED GREEN: 85-atop.test measured that the per-pid /proc//stat # walk reads correctly on this kernel, and pidstats() is that walk. # * /proc/cmdline and /proc/interrupts ARE NOT KNOWN EITHER WAY. No suite # in this layer reads either one on this image, and the recipe phase # cannot find out. They are asserted as a WORKING kernel produces them, # so that a kernel which does not provide them reds here -- attributably, # with `raw-interrupts-readable' / `raw-kcmdline-readable' saying which # of the two halves refused -- and flips green by itself when it does # (the 99-sharutils / 85-nmon convention). # # ============================================================================ # ONE UPSTREAM LIMITATION IS PINNED RATHER THAN AVOIDED. # ============================================================================ # # `pidstat.load' recovers the comm field by splitting the raw line on the two # characters `) '. A comm containing a SPACE is handled; a comm containing `) ' # is not -- the split yields three parts, the numeric fields are read out of the # middle of the name, and every key after comm is simply absent, so `ppid' raises # KeyError. `paren-comm' and `paren-ppid' assert that measured behaviour, which # makes an upstream fix visible here as a red instead of passing unnoticed. # Nothing is filed for it: we file nothing against third-party public # repositories (docs/conventions/no-public-upstream-reports.md), and it is not # caused by, and does not affect, this port. # # A SECOND UPSTREAM ODDITY IS RECORDED AND NOT ASSERTED AS A DEFECT. pidstat, # pidstatus and pidstats all take a `basedir', but `process.load_cmdline', # `load_environ', `load_cgroups' and `load_threads' hardcode /proc// and # ignore it. That is why the cmdline and environ cases below are LIVE while the # stat and status cases have staged twins: there is no staged path for them to # take. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE LIVE /proc/cpuinfo READ. See above; it is a guest-killer, not a red. # - smaps / smaps_lib. They parse /proc//smaps, a file whose presence on # this kernel is unknown and whose contents are entirely allocator state; # an assertion over them would be a statement about this boot rather than # about the parser. `has-smaps' asserts the names exist and stops there. # - cpusstats / cpustat. They read /proc/stat, which 85-atop measured as # readable here -- but every value in it is a monotonically rising counter, # so nothing about it can be asserted by value, and its SHAPE is already # covered by the same argument the interrupts cross-check uses. Named in # `has-cpusstats' and left there. # - THE ptest PACKAGE. The recipe does not `inherit ptest', so none is built. # Upstream ships one test script, `bitmasklist_test.py', which is not # installed by setup.py and is not on the image; the two bitmasklist cases # below assert the same function directly. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-linux-procfs # THE CANARY, which asserts nothing about python3-linux-procfs. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PF=$SITE/procfs DI=$SITE/python_linux_procfs-0.7.3.dist-info PKGLIST=/var/lib/dpkg/info/python3-linux-procfs.list D=$BST_DATA PROBE=$D/python3-linux-procfs-probe.py FIX=$D/python3-linux-procfs-cpuinfo.fixture tmp=${TMPDIR:-/tmp} W=$tmp/bst-procfs.$$ mkdir -p "$W/stage" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import procfs'` further down is the # functional statement; these are the ones that say WHICH copy answered, so a # module that imported out of some other path is distinguishable from one that # imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$PF" # The whole of the module, named individually rather than counted: a count moves # on any upstream refactor, while each of these is a distinct part of the # package. procfs.py is every parser, utilist.py is the two bit functions, and # __init__.py is the star-import that makes both reachable as `procfs.*'. check $P/module-init test -f "$PF/__init__.py" check $P/module-procfs test -f "$PF/procfs.py" check $P/module-utilist test -f "$PF/utilist.py" # THE SCRIPT. setup.py declares `scripts=['pflags']', so this package ships an # executable as well as a module -- which most of this image's Python ports do # not. A recipe that packaged the module and dropped the script would pass every # import case below and still be an incomplete port. check $P/script-present test -f /usr/bin/pflags check $P/script-executable test -x /usr/bin/pflags # THE SHEBANG IS THE DISPATCH. `pflags' is a plain script, not a console_scripts # entry point, so there is no generated launcher -- the first line IS how it # reaches an interpreter. Upstream's reads `#! /usr/bin/python3', which this # image does not have; setuptools rewrites it at install time. The rewritten # line is asserted verbatim because a rebuild that lost the rewrite ships a file # that is still executable, still passes every packaging case above, and cannot # run. Whether /usr/bin/env resolves `python3' is another package's job, and # `script-rc' below is where a failure of THAT would show. check_eq $P/script-shebang "#!/usr/bin/env python3" "$(head -n 1 /usr/bin/pflags)" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. This image's python3 is ONE # STATIC BINARY with an EMPTY lib-dynload and no dynamic loader (BRINTOS-1476; # 99-python3 asserts both directly), so a .so could not be loaded here at all -- # not "would be slow", could not be loaded. This package is pure Python, which is # the entire reason it ports; the case fails a version bump that grew an # extension, loudly, here, instead of at `import' on a later ticket. check_eq $P/module-no-so "" \ "$(find "$PF" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # A find sees what is on disk; the dpkg list sees what the package CLAIMS. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. It is scoped to site-packages # precisely because /usr/bin/pflags is NOT python-or-metadata by extension and is # supposed to be there; it has its own cases above. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ---------------------------------------------------------- the dist-info ---- # What makes the installed module discoverable by importlib.metadata; a package # whose module landed but whose metadata did not would import fine here and be # invisible to any dependency resolver on the image -- and `dist-version' below, # which reads the metadata through importlib, would be the case that goes red. # # The DIRECTORY name carries the distribution name with its hyphens normalised to # underscores, which is what a PEP 503 name normalisation does and what # importlib.metadata then reverses; asserting the directory and the METADATA # `Name:' line separately is what would catch a packaging change that normalised # one and not the other. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" check_out $P/dist-info-version "Version: 0.7.3" cat "$DI/METADATA" check_out $P/dist-info-name "Name: python-linux-procfs" cat "$DI/METADATA" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-linux-procfs is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list. The # anchor is exact because a package named `python3-linux-procfs-' # tomorrow would otherwise be read as this one. sed -n '/^Package: python3-linux-procfs$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES dpkg RECORDED. One case per dependency, against the # extracted line, rather than one case asserting the whole list as a string: the # recipe's RDEPENDS order is not dpkg's to preserve and the separator spacing is # dpkg's own, so an exact-string case would be asserting a FORMAT rather than a # fact. Split, each red also names which dependency went missing. # # python3-six IS THE ONE THAT MATTERS. It is not an optional accelerator: both # procfs/procfs.py and procfs/utilist.py do `from six.moves import range' at # MODULE SCOPE, so an image without it cannot import procfs at all. The recipe's # `RDEPENDS:${PN} += "python3-six"' is the only thing that puts it on the image, # and this case is what says that line survived. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-six "python3-six" cat "$W/dep" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If procfs cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It names `pidstats' specifically # because that is the class the package is FOR, so a module that imported without # its parsers is caught here rather than thirty cases later. check $P/import python3 -c 'from procfs import pidstats, pidstat, interrupts' # ------------------------------------------------------------- the probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present check $P/fixture-present test -s "$FIX" # The marker the environ case reads back out of /proc//environ. It is # exported HERE so that the value asserted below is one this suite put there, # rather than a variable that happened to be in the guest's environment -- an # environ parser that returned a stale or empty dict would otherwise still match # whatever was already set. BST_PROCFS_MARKER=brintos-1981 export BST_PROCFS_MARKER # ONE interpreter start for everything that follows. 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 actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins, and because the probe walks the whole of /proc. bst_probe $P/probe-run "$W/probe" 1200 python3 "$PROBE" "$FIX" "$W/stage" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `version' distinct from # `dist_version', and `live_status_name' from `live_status_name_matches_comm'. 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 parser, 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 in a comment. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE VERSION THE RECIPE PINS, twice over. `version' is procfs.VERSION, a # constant in procfs/procfs.py; `dist-version' is what importlib.metadata reads # out of the dist-info. The two disagreeing means a mixed install -- a dist-info # from one release beside a module from another -- and every expectation in this # file would then be describing a different package. check_eq $P/version "0.7.3" "$(pv version)" check_eq $P/dist-version "0.7.3" "$(pv dist_version)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME procfs. check_eq $P/module-file "$PF/__init__.py" "$(pv module_file)" check_eq $P/submodule-file "$PF/procfs.py" "$(pv procfs_submodule_file)" # six ANSWERED, AND FROM THE IMAGE'S OWN PACKAGE. The import case above already # proves six resolved -- procfs cannot import without it -- so what these two add # is WHICH six, and at what version. python3-six is BRINTOS-1744's port and is # already on this image; a procfs that imported against some other copy would be # a packaging fault this names. check_eq $P/six-version "1.16.0" "$(pv six_version)" check_eq $P/six-file "$SITE/six.py" "$(pv six_file)" # THE PUBLIC SURFACE. `procfs/__init__.py' is two star-imports, so what the # package promises is the union of two modules' top-level names; each documented # one is asserted on its own rather than as a count. check_eq $P/has-pidstat "yes" "$(pv has_pidstat)" check_eq $P/has-pidstatus "yes" "$(pv has_pidstatus)" check_eq $P/has-process "yes" "$(pv has_process)" check_eq $P/has-pidstats "yes" "$(pv has_pidstats)" check_eq $P/has-interrupts "yes" "$(pv has_interrupts)" check_eq $P/has-cmdline "yes" "$(pv has_cmdline)" check_eq $P/has-cpuinfo "yes" "$(pv has_cpuinfo)" check_eq $P/has-smaps "yes" "$(pv has_smaps)" check_eq $P/has-smaps-lib "yes" "$(pv has_smaps_lib)" check_eq $P/has-cpustat "yes" "$(pv has_cpustat)" check_eq $P/has-cpusstats "yes" "$(pv has_cpusstats)" check_eq $P/has-process-cmdline "yes" "$(pv has_process_cmdline)" check_eq $P/has-is-s390 "yes" "$(pv has_is_s390)" check_eq $P/has-bitmasklist "yes" "$(pv has_bitmasklist)" check_eq $P/has-hexbitmask "yes" "$(pv has_hexbitmask)" # ====================================================================== # THE PURE FUNCTIONS. No filesystem, so these are the cases that would still be # true on a machine with no /proc at all. # ====================================================================== # # bitmasklist turns an smp_affinity line into a CPU list and hexbitmask turns a # CPU list back into 32-bit words. Both answers are fixed by arithmetic, so both # are asserted by value. # # The four bitmasklist cases are four different questions: `f' over 4 entries is # the ordinary case; `f' over 2 is the TRUNCATION at nr_entries, which is the # only thing that stops the parser reporting CPUs the machine does not have; # `5' over 8 is a sparse mask, which is what catches a reversed bit order; and # the comma case is the grouping the kernel writes into a wide smp_affinity, # which must be STRIPPED rather than parsed as a separator -- a regression there # would silently halve every mask on a machine with more than 32 CPUs. check_eq $P/bitmasklist-f-4 "0,1,2,3" "$(pv bitmasklist_f_4)" check_eq $P/bitmasklist-f-2 "0,1" "$(pv bitmasklist_f_2)" check_eq $P/bitmasklist-sparse "0,2" "$(pv bitmasklist_5_8)" check_eq $P/bitmasklist-comma "0,1" "$(pv bitmasklist_comma)" # ...and the inverse. [0,1] over 4 entries is one word with the low two bits set; # [0,32] over 64 is the case that must produce TWO words, which is the only one # that exercises the 32-bit rollover in the loop. check_eq $P/hexbitmask-narrow "3" "$(pv hexbitmask_0_1)" check_eq $P/hexbitmask-wide "1,1" "$(pv hexbitmask_wide)" # is_s390() is one line, but it GATES a branch of cpuinfo.parse() that counts # CPUs differently, so a wrong answer would change how the parser reads every # cpuinfo. This arm is not an s390 and never will be. check_eq $P/is-s390 "no" "$(pv is_s390)" # ====================================================================== # THE cpuinfo PARSER, OVER THE COMMITTED FIXTURE. /proc/cpuinfo is never opened # -- see the header, brintos/linux#50. # ====================================================================== # # The fixture is a two-CPU, one-socket, two-core x86-shaped cpuinfo, so every # derived number below is known by construction rather than measured: # nr_cpus counts `processor' lines, sockets collects distinct `physical id' # values, nr_sockets is len(sockets), and nr_cores is `cpu cores' times # nr_sockets. check_eq $P/cpuinfo-nr-cpus "2" "$(pv cpuinfo_nr_cpus)" check_eq $P/cpuinfo-nr-sockets "1" "$(pv cpuinfo_nr_sockets)" check_eq $P/cpuinfo-nr-cores "2" "$(pv cpuinfo_nr_cores)" check_eq $P/cpuinfo-sockets "0" "$(pv cpuinfo_sockets)" # __getitem__ lowercases the key it is handed, so `Model Name' and `model name' # are the same lookup. That is the documented calling convention and it is worth # a case: a parser that stored the raw case would answer one and not the other. check_eq $P/cpuinfo-model-name "Brintos Reference CPU 1.00GHz" "$(pv cpuinfo_model_name)" check_eq $P/cpuinfo-vendor "BrintosReferenceVendor" "$(pv cpuinfo_vendor)" check_eq $P/cpuinfo-cpu-cores "2" "$(pv cpuinfo_cpu_cores)" # The whole key set, which is the case that catches a tag the parser started or # stopped keeping. `core id' and `processor' are absent BY DESIGN -- the first is # per-CPU and would only ever hold the last CPU's value, the second is consumed # as the counter -- so their absence gets its own two cases rather than being # left implied by this list. check_eq $P/cpuinfo-keys \ "bogomips,cpu cores,cpu family,flags,model,model name,physical id,siblings,vendor_id" \ "$(pv cpuinfo_keys)" check_eq $P/cpuinfo-drops-core-id "no" "$(pv cpuinfo_has_core_id)" check_eq $P/cpuinfo-drops-processor "no" "$(pv cpuinfo_has_processor)" # ====================================================================== # THE pidstat / pidstatus PARSERS, OVER A STAGED /proc-SHAPED TREE. # ====================================================================== # # THE COMM FIELD IS WHY THIS IS STAGED. /proc//stat's second field is the # executable name in parentheses and it may contain SPACES, which is why the # parser splits on `) ' and rejoins rather than using str.split(). No process on # this image is named that way, so the only way to assert the split is to write # the name into a file -- and the staged name `we ird name' has two spaces in it # for exactly that reason. check_eq $P/staged-comm "we ird name" "$(pv staged_comm)" check_eq $P/staged-state "S" "$(pv staged_state)" check_eq $P/staged-ppid "4241" "$(pv staged_ppid)" check_eq $P/staged-num-threads "3" "$(pv staged_num_threads)" check_eq $P/staged-starttime "987654" "$(pv staged_starttime)" # The TYPES, which are what say the dictionary is usable for arithmetic rather # than being a bag of strings: load() int()s what it can and keeps the rest. check_eq $P/staged-ppid-type "int" "$(pv staged_ppid_type)" check_eq $P/staged-comm-type "str" "$(pv staged_comm_type)" # The mapping protocol the class hand-rolls -- `in', has_key(), keys() -- because # it is a plain class rather than a dict subclass and every one of those is code # that could be wrong on its own. check_eq $P/staged-contains "yes" "$(pv staged_contains_comm)" check_eq $P/staged-has-key "yes" "$(pv staged_has_key_state)" check_eq $P/staged-nr-keys "41" "$(pv staged_nr_keys)" # is_bound_to_cpu() masks the `flags' field against PF_THREAD_BOUND (0x04000000). # The staged value 4194560 is 0x400100 and does not carry that bit, so the answer # is False -- computed from the fixture, not observed. check_eq $P/staged-flags "4194560" "$(pv staged_flags)" check_eq $P/staged-bound-to-cpu "no" "$(pv staged_bound_to_cpu)" # /proc//status, the other per-process file, over the same staged pid. Its # parser is a different one -- colon-split key/value, int() where it can -- and # the two views of one process must agree about Name and PPid. check_eq $P/staged-status-name "we ird name" "$(pv staged_status_name)" check_eq $P/staged-status-state "S (sleeping)" "$(pv staged_status_state)" check_eq $P/staged-status-ppid "4241" "$(pv staged_status_ppid)" check_eq $P/staged-status-threads "3" "$(pv staged_status_threads)" # A value that is not an int stays a string, including the ` kB' suffix the # kernel writes -- a parser that stripped it would be changing the units. check_eq $P/staged-status-vmrss "3600 kB" "$(pv staged_status_vmrss)" check_eq $P/staged-status-ppid-type "int" "$(pv staged_status_ppid_type)" # A line with no colon is DROPPED rather than crashing the parse. The staged file # carries one on purpose; a live /proc//status never would. check_eq $P/staged-status-drops-junk "yes" "$(pv staged_status_dropped_junk)" # `process' is LAZY: it holds a pid and materialises stat/status on first access. # Asserting that the object exists before any file is read is what says the # laziness is real -- and it is what makes the absence cases below meaningful, # since a class that loaded eagerly would raise in its constructor instead. check_eq $P/staged-process-pid "4242" "$(pv staged_process_pid)" check_eq $P/staged-process-lazy "yes" "$(pv staged_process_lazy)" check_eq $P/staged-process-stat-comm "we ird name" "$(pv staged_process_stat_comm)" check_eq $P/staged-process-status-name "we ird name" "$(pv staged_process_status_name)" check_eq $P/staged-process-cached "yes" "$(pv staged_process_cached)" # process_cmdline() falls back to the stat comm when cmdline is empty, which is # the branch a live process with a readable cmdline never takes. check_eq $P/staged-process-cmdline-fallback "we ird name" "$(pv staged_process_cmdline_fallback)" # pidstats over the staged tree: the same whole-directory walk the live case runs # below, but over a directory with exactly two entries, so its result is exact. check_eq $P/staged-pidstats-keys "4242" "$(pv staged_pidstats_keys)" check_eq $P/staged-pidstats-contains "yes" "$(pv staged_pidstats_contains)" # find_by_name compares against comm, truncated to the 15 characters the kernel # stores. Both arms are asserted: a hit and a miss, because a lookup that # returned everything would pass a hit-only case. check_eq $P/staged-find-by-name "4242" "$(pv staged_find_by_name)" check_eq $P/staged-find-by-name-miss "" "$(pv staged_find_by_name_miss)" # THE UPSTREAM LIMITATION, PINNED. See the header: a comm containing `) ' defeats # the split, comm comes back truncated at the paren and every numeric key after # it is absent. Asserted as measured so an upstream fix shows up as a red here # rather than passing unnoticed. check_eq $P/paren-comm "odd" "$(pv paren_comm)" check_eq $P/paren-ppid "KeyError" "$(pv paren_ppid)" # ====================================================================== # THE ABSENCE PATHS. # ====================================================================== # # Two of them, and they are genuinely different questions. # # FIRST: AN UNUSED PID MUST RAISE, and raise the DOCUMENTED exception, rather # than returning a half-built object a caller would then read garbage out of. # pidstat.__init__ catches FileNotFoundError only to re-raise it -- the source # comment says "propagate the error to the user to handle" -- so the exception # TYPE is the assertion. An IOError, an empty object or a None would each be a # different contract. check_eq $P/unused-pid-absent "yes" "$(pv unused_pid_absent)" check_eq $P/absent-pidstat "FileNotFoundError" "$(pv absent_pidstat)" check_eq $P/absent-pidstatus "FileNotFoundError" "$(pv absent_pidstatus)" # ...and on `process', where the laziness moves WHERE it raises. Two cases, # because a class that raised in its constructor would make the second # unreachable and the difference is the contract. check_eq $P/absent-process-ctor "no-raise" "$(pv absent_process_ctor)" check_eq $P/absent-process-stat "FileNotFoundError" "$(pv absent_process_stat)" # SECOND: A /proc FILE THAT IS NOT THERE MUST GIVE THE DOCUMENTED EMPTY RESULT. # `process.load_cmdline' is one of only two places in this package that documents # one: it catches FileNotFoundError and sets cmdline to None. Not [], not "", and # not an exception -- a caller distinguishes "no cmdline" from "empty cmdline" by # exactly that None, and process_cmdline() branches on it. check_eq $P/absent-process-cmdline "None" "$(pv absent_process_cmdline)" # ====================================================================== # LIVE /proc. Everything above has already been printed by the time these run. # ====================================================================== # # THE CROSS-CHECKS ARE THE POINT. Each `*-match' key was computed inside the # probe by comparing the library's answer against the SAME FILE read directly a # few microseconds earlier -- so a green says the parser agrees with the bytes, # not merely that it agrees with itself. The raw read is a control, not a second # implementation: it does the minimum (`partition(") ")', then split) and nothing # else. check_eq $P/live-pid-matches "yes" "$(pv live_pid_matches)" check_eq $P/live-comm-match "yes" "$(pv live_comm_match)" check_eq $P/live-state-match "yes" "$(pv live_state_match)" check_eq $P/live-ppid-match "yes" "$(pv live_ppid_match)" # ...and the ppid the parser read is the one the KERNEL told this process it has, # via getppid(2). That is a different statement from the one above: the first # says the parse is right, this says the file is about this process. check_eq $P/live-ppid-is-ppid "yes" "$(pv live_ppid_is_ppid)" check_eq $P/live-flags-type "int" "$(pv live_flags_type)" # The parsed field count is REPORTED, not pinned: nr_fields is min(what the file # has, what the parser knows), so a kernel that writes fewer fields than Linux # x86 does is a legitimate difference rather than a defect. What must hold is # that enough of them arrived for the keys asserted above to be real. _n=$(pv live_nr_keys) case "$_n" in ''|*[!0-9]*) bst_notok $P/live-nr-keys "not a number -- got:'$_n'" ;; *) if [ "$_n" -ge 20 ]; then bst_ok $P/live-nr-keys else bst_notok $P/live-nr-keys "only $_n fields parsed from /proc//stat; want >= 20"; fi ;; esac # /proc//status for the same live process, and the third statement of the # same two facts. status truncates Name to the same 15 characters comm holds, so # the two files must agree -- a disagreement means one of the two parsers is # reading the wrong file for this pid. check_eq $P/live-status-pid-matches "yes" "$(pv live_status_pid_matches)" check_eq $P/live-status-ppid-matches "yes" "$(pv live_status_ppid_matches)" check_eq $P/live-status-name-matches-comm "yes" "$(pv live_status_name_matches_comm)" # THE WHOLE-/proc WALK, which is the call the ticket names first and the # expensive one in the package: it lists /proc and builds a `process' for every # numeric entry. It is asserted to contain THIS process and its PARENT -- the two # pids this suite can name with certainty -- and NOT to contain the unused pid, # which is what says the walk is reading the directory rather than inventing it. check_eq $P/pidstats-has-self "yes" "$(pv pidstats_has_self)" check_eq $P/pidstats-has-parent "yes" "$(pv pidstats_has_parent)" check_eq $P/pidstats-has-init "yes" "$(pv pidstats_has_init)" check_eq $P/pidstats-lacks-unused "no" "$(pv pidstats_has_unused)" # Non-numeric entries in /proc (self, thread-self, meminfo, ...) are skipped by # the int() guard in reload(). A key that was not an int would mean the guard has # stopped working and every caller indexing by pid would break. check_eq $P/pidstats-keys-are-ints "yes" "$(pv pidstats_all_int)" # ...and the walk's own view of this process agrees with the direct read, field # by field. This is the ticket's `name/state/ppid cross-checked against # /proc//stat read directly', asserted through pidstats() rather than # through pidstat() -- a different code path to the same file. check_eq $P/pidstats-self-comm-match "yes" "$(pv pidstats_self_comm_match)" check_eq $P/pidstats-self-state-match "yes" "$(pv pidstats_self_state_match)" check_eq $P/pidstats-self-ppid-match "yes" "$(pv pidstats_self_ppid_match)" # find_by_name over the LIVE table, using this process's own comm. It is the # documented lookup and the one /usr/bin/pflags uses; a table that held every pid # but could not find one by name would be useless to every caller of it. check_eq $P/pidstats-find-self "yes" "$(pv pidstats_find_self)" # The walk found more than a handful of processes. Pinning a count would be # pinning what else was running, so the floor is what is asserted -- and a walk # that returned one or zero would mean /proc listed but did not parse. _n=$(pv pidstats_count) case "$_n" in ''|*[!0-9]*) bst_notok $P/pidstats-count "not a number -- got:'$_n'" ;; *) if [ "$_n" -ge 3 ]; then bst_ok $P/pidstats-count else bst_notok $P/pidstats-count "walk found only $_n processes; want >= 3"; fi ;; esac # process() over the live tree: cmdline and environ, which are the two the staged # tree could NOT stand in for -- load_cmdline() and load_environ() hardcode # /proc// and ignore `basedir' (see the header). # # The probe was invoked as `python3 ', so its cmdline is # four elements and the fixture path is the third. Asserting an ELEMENT OTHER # THAN THE FIRST is what says the whole NUL-separated vector was split, rather # than only its head being readable. check_eq $P/live-cmdline-type "list" "$(pv live_cmdline_type)" check_eq $P/live-cmdline-len "4" "$(pv live_cmdline_len)" check_eq $P/live-cmdline-argv0 "python3" "$(pv live_cmdline_0)" check_eq $P/live-cmdline-has-fixture "yes" "$(pv live_cmdline_has_fixture)" # ...against the same file read directly, which is the control for the two above. check_eq $P/raw-cmdline-len "4" "$(pv raw_cmdline_len)" check_eq $P/raw-cmdline-argv0 "python3" "$(pv raw_cmdline_0)" # /proc//environ. The marker was exported by this suite a few lines above, # so a green says the parser read THIS process's environment now -- an environ # dict that was stale, empty or from another pid could not carry it. check_eq $P/live-environ-type "dict" "$(pv live_environ_type)" check_eq $P/live-environ-marker "brintos-1981" "$(pv live_environ_marker_value)" check_eq $P/live-environ-has-marker "yes" "$(pv live_environ_has_marker)" # ---- /proc/cmdline: THE KERNEL command line, a different file ----------------- # # NOT KNOWN ON THIS ARM (see the header). What this image's boot line CONTAINS is # not assertable -- it is a property of the boot, not of the port -- so what is # asserted is that the parser's key set equals the file's own, computed inside # the probe over one reading of each. `raw-kcmdline-readable' is the case that # says which half refused if this reds. if [ -n "$(pv raw_kcmdline_error)" ]; then bst_notok $P/raw-kcmdline-readable "/proc/cmdline could not be read: $(pv raw_kcmdline_error)" else bst_ok $P/raw-kcmdline-readable fi check_eq $P/kcmdline-keys-match "yes" "$(pv kcmdline_keys_match)" # Every bare option parses to the boolean True and every `k=v' to the string v. # That is the whole of the class's contract and it is the one thing about # /proc/cmdline that does not depend on what this kernel was booted with. check_eq $P/kcmdline-bare-are-true "yes" "$(pv kcmdline_bare_are_true)" # ---- /proc/interrupts: the last live file, and the least certain -------------- # # NOT KNOWN ON THIS ARM (see the header). The parser needs a `CPU0 CPU1 ...' # header line to learn nr_cpus before any entry can be parsed, so a file with a # different shape raises rather than returning an empty table. # # The two cross-checks compare the parsed table against the same file read # directly, on COUNTS ONLY -- the counters in /proc/interrupts move between two # reads, but the number of CPU columns and the number of IRQ lines do not. if [ -n "$(pv raw_interrupts_error)" ]; then bst_notok $P/raw-interrupts-readable "/proc/interrupts could not be read: $(pv raw_interrupts_error)" else bst_ok $P/raw-interrupts-readable fi check_eq $P/interrupts-nr-cpus-match "yes" "$(pv interrupts_nr_cpus_match)" check_eq $P/interrupts-entries-match "yes" "$(pv interrupts_entries_match)" # __getitem__ str()s its key, so an int and a str index the same entry. That is # the documented calling convention -- the class's own docstring uses the int # form -- and `n/a-non-numeric' is the legitimate answer on a machine whose # lowest-sorting IRQ name is a letter code rather than a number. _v=$(pv interrupts_int_key_ok) case "$_v" in yes|n/a-non-numeric) bst_ok $P/interrupts-int-key ;; *) bst_notok $P/interrupts-int-key "want yes or n/a-non-numeric -- got:'$_v'" ;; esac # ------------------------------------------------------------- the script ---- # /usr/bin/pflags, the executable this package ships, driven against pid 1. It is # a THIRD interpreter start and it is worth it: it is the only end-to-end user of # the library on this image, it exercises pidstats() + process_flags() through # code this suite does not otherwise call, and a port that shipped an importable # module with an unusable script would pass every case above. # # pid 1 is chosen rather than this suite's own pid because pflags prints one line # per pid and pid 1 exists on every image; the assertion is on the LEADING pid # column, since both the comm and the flag names depend on what pid 1 is and what # it is doing at that instant. `%6d' right-aligns that column, hence the squeeze. pflags 1 > "$W/pflags" 2>&1; _pfrc=$? check_eq $P/script-rc "0" "$_pfrc" _pfpid=$(head -n 1 "$W/pflags" | tr -s ' ' | sed 's/^ *//' | cut -d' ' -f1) check_eq $P/script-pid1 "1" "$_pfpid" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"