# BST-TIMEOUT: 120 # ~110 cases, every one a short-lived fork or an awk over a # # file the probe already wrote. Nothing here can block: the # # probe issues no syscall that waits, and the CLI cases all # # end in a refusal or a usage banner. 120 is headroom for a # # loaded nightly host, not a reflection of any slow case. # # . "$BST_LIB" is the first executable line; everything above it is the # reader's brief. # numactl 2.0.18 (poky meta/recipes-support/numactl). What ships: libnuma.a, # numa.h / numaif.h / numacompat1.h, numa.pc, the six programs numactl, # numastat, numademo, memhog, migratepages and migspeed, the man pages, and one # probe under /usr/libexec/numactl-tests. # # --------------------------------------------------------------------------- # THIS KERNEL CARRIES NO MEMORY POLICY, AND THE SUITE ASSERTS THAT AS A VALUE. # # arch/wasm32/configs/wasm32_defconfig sets `# CONFIG_NUMA is not set', so # mm/mempolicy.c is not built and arch/wasm32/kernel/syscall_table.c has no # entry for get_mempolicy, set_mempolicy or mbind. numa_available() probes # exactly that -- get_mempolicy(...) < 0 && errno == ENOSYS -- and answers -1. # # That is not a defect and nothing is filed for it: a WebAssembly guest has one # linear address space and no memory controllers to be near or far from, so # there is no topology for a kernel to describe. # # THE REFUSAL IS ASSERTED, NOT SKIPPED -- and it is asserted against the LIVE # numa_available() answer rather than against a constant. The R group below # branches on what the probe just measured and makes a real assertion on either # side: with no policy, that --hardware and --show print their documented # refusals and exit 1; with one, that they enumerate nodes and exit 0. So the # suite is correct today AND stays correct the day this machine gains a # topology, which is what the ticket asked for. A hardcoded expected string # would go red on that day for no reason, and a `skip' would say nothing at all. # # --------------------------------------------------------------------------- # THE FOUR GROUPS, AND WHY THE REFUSAL ALONE WOULD BE WORTHLESS. # # A suite that only drove --hardware and watched it refuse would pass against a # libnuma.a miscompiled into rubble, because not one of those cases reaches a # line of library code that computes anything. So: # # PACKAGING -- the archive, three headers, .pc, six programs, two man pages # and the probe are present and the programs EXECUTE. `numactl # --version' is not a formality: it is the exit-127 discrimination # from BRINTOS-739, a program that was never built and a program that # fails being different findings that only the exit status separates. # # O group -- ORACLE-SCORED. Eleven cases whose output cannot depend on the # machine: --version and six getopt/usage refusals, and the usage # banners of memhog, migratepages, migspeed and numademo. Compared # BYTE FOR BYTE -- stdout, stderr and exit status together, nothing # normalised -- against data/numactl-oracle.txt, produced from a # NATIVE x86-64 build of this same source and patch series by # src/numactl_mknative.sh. Every command is invoked by its bare name, # on both sides, because getopt puts argv[0] in its diagnostics. # # A group -- THE ARCHIVE IS ALIVE. Entry points that reach no memory policy: # numa_parse_bitmap() is pure computation, and # numa_sched_getaffinity()/setaffinity() run on syscalls this kernel # does carry. Thirteen parse inputs -- including the three distinct # refusals, and the undocumented trailing-newline requirement -- and # an affinity round trip. NOTHING HERE MAY BE RED, today or after the # kernel gap closes. # # V group -- THE FIFTEEN BINDINGS, BEHAVIOURALLY. Every public name this port # repaired exists upstream only as an ELF symbol-version alias and is # produced here by __attribute__((alias)). The CLI cannot see one # bound to the WRONG implementation -- _v1 and _v2 differ in argument # type, not in anything a caller observes from a refused policy call # -- so the probe calls each public name AND its own _v2 with # identical arguments and compares what each did. This is the group # that actually guards the patch. # # IT IS NOT AN ADDRESS COMPARISON, and the first cut of this suite was # wrong to make it one. Whether an alias shares its target's address # is a property of the object format: on ELF the two are one symbol # and it reads `same', on wasm32 a function address is an index into # __indirect_function_table and an alias gets its own entry, so all # fifteen read `differ' while both entries reach the same body. The # addresses are still reported, and the suite asserts only that the # fifteen answers AGREE WITH EACH OTHER -- a mixture would mean some # aliases resolved one way and some another, which no correct build # produces on either format. # # R group -- the refusal, described above. # # ATTRIB: no case here is expected red, and no case is attributed to an open # issue. The absent memory policy is a deliberate property of a single-address- # space guest, not a gap awaiting a fix. . "$BST_LIB" require numactl numactl/present PROBE=/usr/libexec/numactl-tests/numactl-probe ORACLE="$BST_DATA/numactl-oracle.txt" W=${TMPDIR:-/tmp}/bst-numactl.$$ mkdir -p "$W" 2>/dev/null # ---------------------------------------------------------------- PACKAGING for p in numactl numastat numademo memhog migratepages migspeed; do check numactl/have-$p test -x /usr/bin/$p done check numactl/have-libnuma-a test -f /usr/lib/libnuma.a check numactl/have-numa-h test -f /usr/include/numa.h check numactl/have-numaif-h test -f /usr/include/numaif.h check numactl/have-numacompat-h test -f /usr/include/numacompat1.h check numactl/have-numa-pc test -f /usr/lib/pkgconfig/numa.pc check numactl/have-man8 test -f /usr/share/man/man8/numactl.8 check numactl/have-man3 test -f /usr/share/man/man3/numa.3 check numactl/have-probe test -f "$PROBE" check numactl/have-oracle test -f "$ORACLE" # ------------------------------------------------------------------ O group # The oracle block is `rc=N' followed by the command's combined output. awk # opens the file itself -- this is not a shell redirect, so it is not the # rootfs-stdin hazard the packager README describes. oracle_case() { _c=$1; shift _want=$(awk -v c="$_c" '$0 == "==CASE " c { f = 1; next } f && $0 == "==END" { exit } f { print }' "$ORACLE") if _out=$( "$@" 2>&1 ); then _rc=0; else _rc=$?; fi _got=$(printf 'rc=%s\n%s\n' "$_rc" "$_out") if [ -z "$_want" ]; then bst_notok numactl/$_c "case absent from the oracle -- data/numactl-oracle.txt is short" elif [ "$_got" = "$_want" ]; then bst_ok numactl/$_c else bst_notok numactl/$_c "got: $(_bst_1line "$_got")" fi } oracle_case version numactl --version oracle_case usage-noargs numactl oracle_case usage-badlong numactl --no-such-option oracle_case usage-badshort numactl -Z oracle_case usage-noarg numactl --interleave oracle_case usage-shm-noarg numactl --shm oracle_case memhog-help memhog --help oracle_case migratepages-noargs migratepages oracle_case migspeed-noargs migspeed oracle_case numademo-help numademo -h # The generator and this file must name the same set. A case present in the # oracle and run by nothing is coverage that reads as present and is not, and # nothing else here would notice: every oracle_case above would still pass. check_eq numactl/oracle-case-count 10 \ "$(awk '/^==CASE /{ n++ } END { print n + 0 }' "$ORACLE")" # --------------------------------------------------------------- THE PROBE if [ -x "$PROBE" ]; then "$PROBE" > "$W/probe.out" 2> "$W/probe.err" prc=$? else prc=127 : > "$W/probe.out" : > "$W/probe.err" fi check_eq numactl/probe-rc 0 "$prc" check numactl/probe-begin grep -q "^NP-BEGIN$" "$W/probe.out" check numactl/probe-end grep -q "^NP-END$" "$W/probe.out" pfield() { awk -v k="$1" -v t="$2" '$1 == k && $2 == t { print $3 }' "$W/probe.out"; } # ------------------------------------------------------------------ V group # The binding, behaviourally: each public name and its own _v2 called with # identical arguments, results compared inside the probe. for v in numa_max_possible_node numa_get_interleave_mask numa_get_membind \ numa_get_run_node_mask numa_parse_bitmap-rc numa_parse_bitmap \ numa_node_to_cpus-rc numa_node_to_cpus numa_sched_getaffinity-rc \ numa_sched_getaffinity numa_sched_setaffinity numa_run_on_node_mask \ numa_alloc_interleaved_subset numa_set_interleave_mask \ numa_set_membind numa_bind numa_interleave_memory \ numa_tonodemask_memory; do check_eq numactl/bind-$v match "$(pfield NP-vpair $v)" done # The address answers are an object-format property, so the assertion is that # all fifteen agree -- not what they agree on. alias_answers=$(awk '$1 == "NP-alias" { print $3 }' "$W/probe.out" | sort -u | tr '\n' ' ') alias_count=$(awk '$1 == "NP-alias" { n++ } END { print n + 0 }' "$W/probe.out") check_eq numactl/alias-count 15 "$alias_count" case "$alias_answers" in "same "|"differ ") bst_ok numactl/alias-consistent ;; *) bst_notok numactl/alias-consistent "fifteen aliases gave mixed answers: $alias_answers" ;; esac # ------------------------------------------------------------------ A group # numa_parse_bitmap: comma-grouped 32-bit hex words, most significant first, # and a MANDATORY trailing newline (strchr(line,'\n') is its first statement). # Pure computation, so these answers are the same on every machine -- which is # what makes them the half of the probe that proves the archive computes. parse_case() { check_eq numactl/parse-$1-rc "rc=$2" "$(pfield NP-parse "$1")" check_eq numactl/parse-$1-mask "$3" "$(pfield NP-mask "$1")" } parse_case bm-zero 0 - parse_case bm-one 0 0 parse_case bm-f 0 0,1,2,3 parse_case bm-alt 0 0,2 parse_case bm-high 0 31 parse_case bm-word2 0 32 parse_case bm-full64 0 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63 parse_case bm-word3 0 64 parse_case bm-wide 0 4,5,6,7,64,65,66,67 parse_case bm-junk -1 - parse_case bm-nonewline -1 - parse_case bm-bare-nl 0 - # The one parse case whose answer is a property of the ARM rather than of the # input. numa_parse_bitmap walks its fields from the right and pairs two 32-bit # words into one long only when sizeof(unsigned long) == 8, so a REJECTED parse # stops at a different field index on a 32-bit long and leaves a different # partial mask behind. Every accepted parse above agrees across both widths; # only this residue does not, so it is asserted against the width the probe # reports rather than against the host's. longw=$(awk '$1 == "NP-sizeof-long" { print $2 }' "$W/probe.out") check numactl/sizeof-long-reported test -n "$longw" check_eq numactl/parse-bm-overflow-rc "rc=-1" "$(pfield NP-parse bm-overflow)" case "$longw" in 8) check_eq numactl/parse-bm-overflow-mask 0,4 "$(pfield NP-mask bm-overflow)" ;; 4) check_eq numactl/parse-bm-overflow-mask 0,32 "$(pfield NP-mask bm-overflow)" ;; *) bst_notok numactl/parse-bm-overflow-mask "unexpected sizeof(unsigned long)=$longw" ;; esac # The affinity round trip. These run on sched_getaffinity/sched_setaffinity, # which this kernel does carry, so they must SUCCEED -- and they are what keeps # a green run from being green because everything refused. aff_rc=$(pfield NP-rc sched-getaffinity) aff_mask=$(pfield NP-mask sched-getaffinity) case "$aff_rc" in -*|"") bst_notok numactl/affinity-get "numa_sched_getaffinity returned '$aff_rc'" ;; *) bst_ok numactl/affinity-get ;; esac check numactl/affinity-mask-nonempty test "$aff_mask" != "-" check_eq numactl/affinity-set 0 "$(pfield NP-rc sched-setaffinity)" # arch/wasm32 documents (DIVERGENCES D-AFFINITY) that sched_getaffinity always # includes the caller's own CPU and deliberately does NOT intersect with # cpu_active_mask -- only CPU 0 is ever marked active while process Workers # genuinely run as CPUs 1..n-1, so intersecting would report {0} to a task # provably executing on CPU 3. The mask therefore carries a second bit that # CHANGES FROM BOOT TO BOOT (0,5 and 0,3 on two consecutive runs here), and the # assertable contract is not its value but that it contains this CPU. getcpu() # is wired for exactly this check. check_eq numactl/affinity-contains-own-cpu yes \ "$(awk '$1 == "NP-aff-has-self" { print $2 }' "$W/probe.out")" # Everything else the probe drove: recorded as a reached call, so a probe that # died half way through cannot pass by omission. for c in run-node-mask membind interleave-mask interleave-after-set \ membind-after-set; do check numactl/reached-$c grep -q "^NP-mask $c " "$W/probe.out" done check numactl/reached-run-on-node-mask grep -q "^NP-rc run-on-node-mask " "$W/probe.out" check numactl/reached-bind grep -q "^NP-called numa_bind$" "$W/probe.out" check numactl/reached-alloc grep -q "^NP-alloc interleaved-subset " "$W/probe.out" check numactl/reached-interleave-mem grep -q "^NP-called numa_interleave_memory$" "$W/probe.out" check numactl/reached-tonodemask-mem grep -q "^NP-called numa_tonodemask_memory$" "$W/probe.out" check numactl/mmap-ok grep -q "^NP-mmap ok$" "$W/probe.out" # numa_max_possible_node() is a compile-time ceiling, not a topology read, so # it must agree with itself across the whole probe run. mp1=$(awk '$1 == "NP-maxpossible" { print $2 }' "$W/probe.out") mp2=$(awk '$1 == "NP-maxpossible-again" { print $2 }' "$W/probe.out") check_eq numactl/maxpossible-stable "$mp1" "$mp2" check numactl/maxpossible-positive test "${mp1:-0}" -gt 0 # ------------------------------------------------------------------ R group # Branch on what the probe just measured, and assert on BOTH sides. avail=$(awk '$1 == "NP-available" { print $2 }' "$W/probe.out") check numactl/available-reported test -n "$avail" expect_exact() { # id want-rc want-text cmd... _id=$1; _wrc=$2; _wtxt=$3; shift 3 if _o=$( "$@" 2>&1 ); then _r=0; else _r=$?; fi if [ "$_r" = "$_wrc" ] && [ "$_o" = "$_wtxt" ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_r want=$_wrc out: $(_bst_1line "$_o")" fi } if [ "$avail" = "-1" ]; then # No memory policy. Each of these is numactl's own documented path for # that state, asserted by text and exit status together. expect_exact numactl/hardware-refused 1 \ "No NUMA available on this system" numactl --hardware expect_exact numactl/hardware-compress-refused 1 \ "No NUMA available on this system" numactl --cpu-compress --hardware # --show prints the affinity line BEFORE it refuses, which is the one # piece of real information it can still give -- and that line's second # CPU varies from boot to boot (see D-AFFINITY above), so the two halves # are asserted separately rather than as one exact string. check_rc numactl/show-rc 1 numactl --show show_out=$(numactl --show 2>&1) case "$show_out" in *"No NUMA support available on this system."*) bst_ok numactl/show-refused ;; *) bst_notok numactl/show-refused "$(_bst_1line "$show_out")" ;; esac case "$show_out" in physcpubind:*[0-9]*) bst_ok numactl/show-physcpubind ;; *) bst_notok numactl/show-physcpubind "no physcpubind line: $(_bst_1line "$show_out")" ;; esac # Every policy flag, including the two that are argument errors on a # machine WITH a policy: checknuma() runs before the node argument is # parsed and before the conflicting-policy check, so all of them reach # the same refusal here. That ordering is the finding -- an out-of-range # node, a malformed node, two conflicting policies and a misplaced --all # are all indistinguishable on this image, and only this branch says so. for f in "--interleave=all" "--interleave=0" "--interleave=99" \ "--interleave=notanode" "--membind=0" "--membind=99" \ "--preferred=0" "--preferred=99" "--localalloc" \ "--cpunodebind=0"; do id=$(echo "$f" | tr -d "\-=") expect_exact numactl/policy-refused-$id 1 \ "numactl: This system does not support NUMA policy" \ numactl "$f" /bin/true done expect_exact numactl/conflict-refused-first 1 \ "numactl: This system does not support NUMA policy" \ numactl --preferred=0 --membind=0 /bin/true expect_exact numactl/all-too-late-refused-first 1 \ "numactl: This system does not support NUMA policy" \ numactl --membind=0 --all /bin/true # libnuma's own view of the same absence. check_eq numactl/maxnode-degenerate 0 \ "$(awk '$1 == "NP-maxnode" { print $2 }' "$W/probe.out")" # numa_node_to_cpus reads /sys/devices/system/node, which this kernel # does not create -- /sys/devices/system carries clockevents, # clocksource, container and cpu, and no node. So the read fails, and # that is a consequence of the same missing CONFIG_NUMA rather than a # second finding. check_eq numactl/node-to-cpus-refused -1 "$(pfield NP-rc node-to-cpus-0)" check numactl/sysfs-has-no-node test ! -d /sys/devices/system/node else # The machine gained a topology. The same questions, answered the other # way -- a real assertion, not a skip. check_rc numactl/hardware-ok 0 numactl --hardware check_out numactl/hardware-lists "available:" numactl --hardware check_rc numactl/show-ok 0 numactl --show check_out numactl/show-policy "policy:" numactl --show check numactl/policy-accepted numactl --localalloc /bin/true check numactl/policy-membind0 numactl --membind=0 /bin/true # With a policy these reach their own diagnostics instead of # checknuma()'s, which is the whole reason they are not in the oracle. check_out numactl/conflict-policies "Conflicting policies" \ numactl --preferred=0 --membind=0 /bin/true check_out numactl/all-too-late \ "--all/-a option must be before all cpu/node specifications" \ numactl --membind=0 --all /bin/true check_out numactl/badpolicy-parse "unparseable node description" \ numactl --interleave=notanode /bin/true check_eq numactl/node-to-cpus-ok 0 "$(pfield NP-rc node-to-cpus-0)" check numactl/sysfs-has-node test -d /sys/devices/system/node fi # numastat reads /sys/devices/system/node/node*/numastat, which this kernel does # not create. It does not go quiet about that: it names the reason and exits 1, # which is the better of the two behaviours and is asserted as such. A tool that # prints nothing and a tool that was never built look identical from a summary, # and only the exit status separates them (BRINTOS-739) -- so both halves are # pinned here, on both sides of the branch. numastat_out=$(numastat 2>&1); numastat_rc=$? if [ -d /sys/devices/system/node ]; then check_eq numactl/numastat-rc 0 "$numastat_rc" check numactl/numastat-reports test -n "$numastat_out" else check_eq numactl/numastat-rc 1 "$numastat_rc" check_eq numactl/numastat-names-the-reason \ "sysfs not mounted or system not NUMA aware: No such file or directory" \ "$numastat_out" fi rm -rf "$W"