. "$BST_LIB" # BST-TIMEOUT: 1500 # FIVE python3 starts, and they are the whole cost. Every # # other assertion reads the filesystem or dpkg with shell # # builtins, or greps the output of one of those five. # # # # One is the probe, which takes a single start for ~56 # # measurements. One is a bare `import' -- kept separate # # because it is the cheapest possible statement of the # # deliverable and, on this image, the one most likely to # # fail. Two are the SHIPPED SCRIPTS, which cannot be folded # # into the probe: the point of them is that /usr/bin/pchrt # # and /usr/bin/ptaskset are separate executables reached # # through their `#!/usr/bin/env python3' line, so running # # their code in-process would assert the module a second # # time and the scripts not at all. The fifth is spare. # # # # The work inside each start is one syscall or one strcmp # # -- microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line runs. # # # # 1500 is 300s per start, against the ~60s a python3 start # # has measured at here. 99-python3-smartypants budgets 180s # # per start for ten; this is looser per start because there # # are fewer of them and a suite-timeout red is a batch-wide # # cost, so the margin is cheap. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. 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-schedutils 0.6 (meta-openembedded/meta-python # recipes-devtools/python/python3-schedutils_0.6.bb, `inherit setuptools3', a # git SRC_URI at git.kernel.org, SRCREV 46469f42) on the wasm32-hwjs glibc # image. # # ============================================================================ # THIS IS THE FIRST OUT-OF-TREE CPYTHON EXTENSION THIS IMAGE HAS EVER CARRIED, # AND THAT -- NOT THE SCHEDULER -- IS THE PORT'S CENTRAL QUESTION. # ============================================================================ # # python-schedutils is ONE C FILE and NOTHING ELSE. setup.py declares # `ext_modules=[schedutils]' and no py_modules and no packages, so what lands in # site-packages is a single compiled object, `schedutils.cpython-312.so', with # no Python-level file anywhere to fall back to. Either this interpreter can # load an out-of-tree extension or the package is inert; there is no third # outcome and no partial one. # # NOTHING THAT HAS RUN ON THIS IMAGE BEFORE SETTLES IT, and two facts point # opposite ways: # # FOR: the interpreter really does contain the loader. MEASURED off the built # python3-core .deb, from _sysconfigdata__linux_.py -- `HAVE_DYNAMIC_LOADING': # 1 and `DYNLOADFILE': 'dynload_shlib.o'. CPython's dlopen-based # ExtensionFileLoader is compiled in, and `EXT_SUFFIX': '.cpython-312.so' # matches the name this recipe installs EXACTLY, so the import machinery will # find the file and try. # # AGAINST: dlopen on this arm has been measured failing. 94-libldb records # `ldb: unable to dlopen /usr/lib/ldb/modules/ldb/libldb_module_asq.a' as a # platform statement rather than a port defect, and 91-vlock records the same # arm as static-only. lib-dynload being empty (99-python3 asserts it) is NOT # evidence either way -- that is about where the STDLIB extensions went, and # they went into the binary because configure picks MODULE_BUILDTYPE=static # from host_cpu=wasm32. A previous port read those two facts as settling this # question and dropped its own extension unbuilt on the strength of it # (99-isomd5sum, "a CPython built MODULE_BUILDTYPE=static that could not # import an out-of-tree .so even if it linked"); that inference does not # follow, and this suite is what replaces it with a measurement. # # So `python3-schedutils/import' below is asserted to SUCCEED. That is the deliverable, # and asserting the failure instead would ship a suite that goes green on a # broken port and red on a working one. If it reds, the cause is the arm's # dlopen and not this package -- and `import-error' prints the exact exception # into the transcript, which is the first direct measurement anyone here will # have of that question. # # EVERY FUNCTIONAL CASE BELOW IS GUARDED ON THAT ANSWER rather than left to red # on its own. Forty identical reds all saying "the module is not here" is not # forty findings, it is one finding and thirty-nine restatements, and it reads # to an integration lane exactly like a broken port. The one case that carries # the verdict is `import'; the rest skip with the reason. # # ============================================================================ # THE ONE PATCH, AND WHY IT IS A LINK ERROR RATHER THAN A RUNTIME ONE. # ============================================================================ # # `__get_max_number_of_cpus()' sizes a probe buffer from a mutable local the # loop doubles -- `unsigned long buffer[longsperbits(cpus)]' -- and hands its # address to syscall(SYS_sched_getaffinity, ...). That is a VLA, i.e. a dynamic # alloca; the function makes syscalls, so hwjs-cc's coroutinize pass rewrites it # into a coroutine, and the alloca's address then escapes the frame into an # opaque host sink. MEASURED 2026-09-01: # # [hwjs-coro] REFUSE fn=__get_max_number_of_cpus # reason=vla/dynamic-alloca-address-escape-opaque-sink # escaped=1 opaque_escape=1 ptrelem=0 across=0 dynalloca_total=1 # suspends=2 # wasmld: FATAL (HWJS-3 coro arm): the coroutinize pass REFUSED function(s) # # do_compile fails; no extension is produced at all. The bbappend's patch makes # the buffer a calloc()/free() pair -- heap memory is ordinary linear memory and # carries no such restriction -- and the recipe then builds clean. Every # affinity case below goes through that function, so they are also what says the # patch is correct rather than merely linkable. # # ============================================================================ # THE MODULE HAS TEN CALLABLES AND NO ioprio PAIR. # ============================================================================ # # Stated because the ticket that commissioned this port names one. 0.6's # PySchedutilsModuleMethods table is get_affinity, set_affinity, get_scheduler, # set_scheduler, get_priority, schedstr, schedfromstr, get_priority_min, # get_priority_max and get_max_number_of_cpus -- ten, MEASURED on the x86-64 # control and pinned by `surface' below. ioprio_get/ioprio_set are not in this # release and are not in this module; the two shipped scripts do not reference # them either. `get_priority' (sched_getparam) is the one the ticket did not # name and which is here. # # ============================================================================ # THE PLATFORM SPLITS THIS MODULE CLEANLY IN THREE, AND THE SUITE IS ORDERED # THAT WAY SO A RED SAYS WHICH THIRD IT IS IN. # ============================================================================ # # 1. PURE C, NO SYSCALL -- schedstr, schedfromstr, and the seven integer # constants. A switch and a strcmp chain compiled into the extension. If the # module imported at all these MUST pass, whatever the kernel does, so they # are the cases that separate "the extension's code works" from "the kernel # answers". Every value is pinned absolutely; all were measured identical on # the x86-64 control. # # 2. AFFINITY -- WIRED, AND ARCH-LOCAL. sched_setaffinity (203) and # sched_getaffinity (204) are in arch/wasm32's syscall table, implemented in # arch/wasm32/kernel/sys_wasm32.c rather than by upstream's handlers, and # documented as divergence D-AFFINITY in arch/wasm32/include/uapi/asm/unistd.h. # The headline behaviour is upstream's: a mask containing the caller's own CPU # is recorded and returns 0, an empty mask is EINVAL. So these are asserted as # real capabilities. # # They are asserted as AGREEMENT rather than as absolutes wherever the value # is the guest's own CPU count, which belongs to whatever booted this image # and not to this package: `affinity-agrees-os' compares the extension's # answer with os.sched_getaffinity()'s -- CPython's own binding over the same # syscall, linked INTO the interpreter -- and `affinity-agrees-proc' with # /proc/self/status Cpus_allowed_list, the kernel's own text. Two independent # oracles, neither of which can be wrong in the same direction as a # marshalling bug in this .so. # # ONE PART OF D-AFFINITY IS DELIBERATELY NOT EXERCISED. Setting a mask that # EXCLUDES the caller's current CPU asks the kernel for a migration, and the # divergence entry records that a glibc guest gets -EAGAIN rather than a move # ("A LIBC THAT DOES NOT ABSORB -517 AT RAW VENEERS -- glibc today"). This is # the glibc image. The call is bounded and safe, but its answer is a fact # about the libc's park ABI rather than about schedutils, and a case asserting # EAGAIN here would red on the day that arm is fixed while saying nothing # about this package on either side of it. `set-same' -- the mask the task # already has -- is the round trip that is genuinely about the module. # # 3. SCHEDULING POLICY AND PRIORITY -- NOT WIRED AT ALL, AND THIS IS WHERE THE # PORT'S REDS LIVE. The x86_64 numbers for the whole family are ABSENT from # arch/wasm32/kernel/syscall_table.c -- entries 142 through 148, verified by # reading the table -- and the dispatcher answers a NULL row with -ENOSYS, # which its own header calls "the linux-correct response for an unimplemented # syscall". That costs five of the ten callables: # # get_scheduler sched_getscheduler(145) brintos/linux#88 # set_scheduler sched_setscheduler(144) brintos/linux#88 # get_priority sched_getparam(143) -- see below # get_priority_min sched_get_priority_min(147) -- see below # get_priority_max sched_get_priority_max(146) -- see below # # #88 covers the POLICY PAIR ONLY, and says so; it is also the issue whose # "Not verified here" section asks for exactly the syscall-table reading # above. The other three are the same shape at three further numbers and are # NOT covered by #88, by #62 (getpriority/setpriority, 140/141 -- the # priority pair, a different thing again) or by anything else on the repo. # They are reported separately; the citation is in the test plan entry for # this ticket rather than inlined here, so that a suite file does not have to # be rebuilt to record an issue number. # # EXPECTED, NOT YET MEASURED. Phase A cannot build the image, so every claim # in this section is derived from arch/wasm32's syscall table and its # dispatcher, not observed on a guest. The cases below assert the ERRNO ALONE # (`enosys' extracts the bracket) rather than glibc's message text, because # the number is what the syscall table determines and the string is not. # # ============================================================================ # THE PACKAGING IS UNUSUAL IN TWO WAYS THAT ARE WORTH PINNING. # ============================================================================ # # `Root-Is-Purelib: false' and `Tag: cp312-cp312-linux_x86_64' -- MEASURED off # the built .deb's WHEEL. The tag is WRONG and harmlessly so: bdist_wheel names # the BUILD machine's platform, which under a Yocto cross build is the host, and # nothing on the target ever reads it because Yocto installs by path rather than # by resolving a wheel tag. It is pinned so that a reader who finds `x86_64' on # a wasm32 image is told it is expected, and so that a poky which started # honouring the tag would red here first. # # THE TWO SHIPPED SCRIPTS NEEDED NO PATCH OF OURS. Upstream's `#! /usr/bin/python' # names an interpreter this image does not have -- the same trap # 99-python3-smartypants records, where oe-core carries a patch for it -- but # setuptools' own build_scripts rewrites the line during the wheel build, and # what ships is `#!/usr/bin/env python3'. MEASURED off the .deb. Asserted # verbatim, because a change in that machinery ships two files that are still # executable, still pass every packaging case, and cannot run. P=python3-schedutils require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages SO=$SITE/schedutils.cpython-312.so DI=$SITE/schedutils-0.6.dist-info PCHRT=/usr/bin/pchrt PTASKSET=/usr/bin/ptaskset PKGLIST=/var/lib/dpkg/info/python3-schedutils.list PROBE=$BST_DATA/python3-schedutils-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-schedutils.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. The import further down is the functional # statement; these say WHICH object would have answered, so a package that # imported out of some other path on sys.path is distinguishable from one that # imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" check $P/ext-file test -s "$SO" # ONE COMPILED OBJECT, NO PYTHON. See the header: setup.py declares ext_modules # and nothing else, so there is no .py and no package directory to fall back on. # This is the case that fails a release which grew a Python shim -- which would # be good news, and should still be noticed rather than absorbed. check $P/no-package-dir test ! -d "$SITE/schedutils" check_eq $P/no-python-module "" "$(ls "$SITE"/schedutils.py 2>/dev/null)" # THE OBJECT IS A WASM MODULE, not an ELF. Measured off the built .deb: the # first four bytes are the \0asm magic and the next four the wasm version word, # 1. This is the case that says the extension really was cross-compiled for this # arm -- a host-built .so would carry \x7fELF here and every packaging case # above would still pass. `od -An -tx1' rather than `file', which this image does # not carry; the same form 54-pigz and 56-lz4 use for their magics. check_out $P/ext-is-wasm "00 61 73 6d 01 00 00 00" \ sh -c "od -An -tx1 -N8 '$SO' | tr -s ' '" # THE SHIPPED SCRIPTS, as files. The CLI cases below run them; these say they # are there and executable, so a red down there has somewhere to start. check $P/pchrt-present test -f "$PCHRT" check $P/pchrt-executable test -x "$PCHRT" check $P/ptaskset-present test -f "$PTASKSET" check $P/ptaskset-executable test -x "$PTASKSET" # THE REWRITTEN SHEBANGS, VERBATIM. See the header: upstream's line names # `python', which this image does not have, and setuptools' build_scripts is # what makes these runnable at all. A change there ships two files that pass # every case above and cannot run. check_eq $P/pchrt-shebang "#!/usr/bin/env python3" "$(head -n 1 "$PCHRT" 2>/dev/null)" check_eq $P/ptaskset-shebang "#!/usr/bin/env python3" "$(head -n 1 "$PTASKSET" 2>/dev/null)" # ...and the interpreter those shebangs go through. Another package's job, not # this one's; stated separately so that when the CLI group reds together, this # case says whether the cause is here. check $P/env-present test -x /usr/bin/env # THE dpkg FILE LIST. 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" # EVERY REGULAR FILE THE PACKAGE SHIPS IS THE EXTENSION, ITS METADATA, OR ONE OF # THE TWO SCRIPTS. The cases above name the failures that are expected; this one # catches the shape nobody predicted -- a second .so, a stray data file, a # Python module that arrived from somewhere -- without having to enumerate what # it might be. The scripts are exempted BY NAME rather than by living in # ${bindir}, so a package that grew a third executable is reported. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so each # entry is tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in "$PCHRT"|"$PTASKSET") continue ;; "$SO") continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ----------------------------------------------------------- the dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.6" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs, which is what # the import below has to match. check_out $P/dist-info-top-level "schedutils" cat "$DI/top_level.txt" # NOT PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. The packaging-side statement # of the header's central claim: this is a platform wheel with a compiled # object, the opposite of every pure-Python port on this image. check_out $P/dist-info-not-purelib "Root-Is-Purelib: false" cat "$DI/WHEEL" # ...AND THE TAG NAMES x86_64 ON A wasm32 IMAGE. See the header: bdist_wheel # reports the BUILD machine's platform and nothing on the target reads it. # Pinned so the oddity is documented rather than rediscovered, and so a poky # that started honouring the tag reds here first. check_out $P/dist-info-wheel-tag "Tag: cp312-cp312-linux_x86_64" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points. check $P/dist-info-copying test -s "$DI/COPYING" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files. The anchor is exact -- an unanchored # match would also take the -dev, -dbg or -doc stanza of the same family. sed -n '/^Package: python3-schedutils$/,/^$/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 DEPENDENCY, AS dpkg RECORDED IT -- and here it is the WHOLE of it. # MEASURED off the built .deb: `Depends: python3-core' and nothing more. The C # extension links libc and CPython and imports no Python module at all, which is # why this port waits on no other package. 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" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE, and it is the port's verdict. See # the header: this is the first out-of-tree CPython extension this image has # carried, the loader is compiled in (HAVE_DYNAMIC_LOADING=1) and dlopen on this # arm has been measured failing elsewhere (94-libldb). Asserted to SUCCEED, # because that is the deliverable; a red here is the arm's dlopen and not this # package, and `import-error' below prints what it actually said. check $P/import python3 -c 'import schedutils' # ---------------------------------------------------------------- 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 # ONE interpreter start for everything that follows. The probe CATCHES the # import itself and reports it as data, so this bound is about the interpreter # start and not about the question above. bst_probe keeps BOTH streams. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The errno a flattened exception carries, or the empty string. The probe writes # `OSError[38]: ...' for a kernel refusal and `OSError[-]: ...' for the two the # extension raises itself with PyErr_SetString, so this is also what tells those # two shapes apart. enosys() { printf '%s' "$1" | sed -n 's/^[A-Za-z]*\[\([0-9-]*\)\].*$/\1/p'; } # 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 library, 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 $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 # ...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 early cases green and the # late ones reading got:''. check_eq $P/probe-complete "yes" "$(pv probe_done)" # THE IMPORT, AS THE PROBE SAW IT. Two statements, not one: whether it worked, # and -- when it did not -- what it said. The second is not an assertion, it is # the first direct measurement this project will have of whether dlopen can load # an out-of-tree extension here, and it belongs in the transcript whichever way # the answer goes. IMPORTED=$(pv import_ok) check_eq $P/probe-import-ok "yes" "$IMPORTED" echo "==SCHEDUTILS-IMPORT==" echo "import_ok=$IMPORTED" echo "import_error=$(pv import_error)" echo "==SCHEDUTILS-IMPORT-END==" if [ "$IMPORTED" != "yes" ]; then # ONE finding, not forty. See the header: every case below is a statement # about a module that is not loaded, and emitting each as its own red would # bury the single case that carries the verdict under thirty-nine # restatements of it. `import' and `probe-import-ok' above are red and say # why; these skip and name the same cause. _why="schedutils did not import: $(pv import_error)" for _c in surface const-values fromstr-roundtrip schedstr-reset-on-fork \ schedstr-unknown fromstr-bogus module-answered is-extension \ loader max-cpus affinity-list affinity-nonempty affinity-agrees-os \ affinity-agrees-proc set-same set-same-readback set-empty-einval \ set-impossible-cpu get-scheduler-enosys set-scheduler-enosys \ get-priority-enosys priority-min-enosys priority-max-enosys \ probe-count cli-pchrt cli-ptaskset; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi check_eq $P/probe-count "56" "$(pv emitted_count)" # The extension answered out of the object this image installed, not a stray # copy on sys.path. This is what makes the payload cases above and the # behavioural cases below statements about the SAME schedutils. check_eq $P/module-answered "$SO" "$(pv module_file)" # ...and it really is a COMPILED EXTENSION reached through the dlopen loader. # On x86-64 the same three values are yes / no / ExtensionFileLoader. check_eq $P/is-extension "yes" "$(pv is_extension)" check_eq $P/loader "ExtensionFileLoader" "$(pv loader)" # ------------------------------------------------- 1. pure C, no syscall ----- # THE PUBLIC SURFACE, PINNED WHOLE. The module is a METH_VARARGS table in C, so # its callables are exactly what that table lists. See the header: ten names, # and no ioprio pair. x86-64 control: identical. check_eq $P/surface \ "get_affinity,get_max_number_of_cpus,get_priority,get_priority_max,get_priority_min,get_scheduler,schedfromstr,schedstr,set_affinity,set_scheduler" \ "$(pv callables)" # THE POLICY CONSTANTS, from the this extension was cross-compiled # against. SCHED_BATCH=3 and SCHED_IDLE=5 with 4 skipped is Linux's own # numbering, not a gap; SCHED_RESET_ON_FORK is a FLAG, 1<<30, not a policy. # A single case: seven separate ones would red together and say nothing extra. check_eq $P/const-values "0 1 2 3 5 6 1073741824" \ "$(pv const_SCHED_OTHER) $(pv const_SCHED_FIFO) $(pv const_SCHED_RR) $(pv const_SCHED_BATCH) $(pv const_SCHED_IDLE) $(pv const_SCHED_DEADLINE) $(pv const_SCHED_RESET_ON_FORK)" # schedfromstr/schedstr ROUND-TRIP EVERY POLICY NAME. This is the ticket's own # acceptance criterion for the pair, and it touches no syscall at all -- so if # the module imported, this must pass whatever the kernel does. That is exactly # what makes it the control for section 3 below. check_eq $P/fromstr-roundtrip \ "SCHED_OTHER:0:SCHED_OTHER SCHED_FIFO:1:SCHED_FIFO SCHED_RR:2:SCHED_RR SCHED_BATCH:3:SCHED_BATCH SCHED_IDLE:5:SCHED_IDLE SCHED_DEADLINE:6:SCHED_DEADLINE" \ "SCHED_OTHER:$(pv fromstr_SCHED_OTHER):$(pv roundtrip_SCHED_OTHER) SCHED_FIFO:$(pv fromstr_SCHED_FIFO):$(pv roundtrip_SCHED_FIFO) SCHED_RR:$(pv fromstr_SCHED_RR):$(pv roundtrip_SCHED_RR) SCHED_BATCH:$(pv fromstr_SCHED_BATCH):$(pv roundtrip_SCHED_BATCH) SCHED_IDLE:$(pv fromstr_SCHED_IDLE):$(pv roundtrip_SCHED_IDLE) SCHED_DEADLINE:$(pv fromstr_SCHED_DEADLINE):$(pv roundtrip_SCHED_DEADLINE)" # THE RESET_ON_FORK BIT IS MASKED OFF BEFORE THE SWITCH. Not decoration: a # policy read back from a task that set the flag carries 0x40000000, and a # caller passing that straight to schedstr gets the POLICY NAME rather than # UNKNOWN. A release that dropped the `& ~SCHED_RESET_ON_FORK' would change what # every such caller sees and nothing else here would notice. check_eq $P/schedstr-reset-on-fork "SCHED_FIFO" "$(pv schedstr_reset_on_fork)" # ...and the switch's default arm, for an integer that is no policy at all. check_eq $P/schedstr-unknown "UNKNOWN" "$(pv schedstr_unknown)" # THE ONE ERROR THE MODULE RAISES WITHOUT AN ERRNO, and the reason the probe # reports the errno separately. schedfromstr uses PyErr_SetString, so this is an # OSError carrying a message and errno None -- not a kernel refusal, and a # reader who went looking for one would be looking in the wrong place. check_eq $P/fromstr-bogus "OSError[-]: Unknown scheduler" "$(pv fromstr_bogus)" # --------------------------------------------------------- 2. affinity ------ # sched_getaffinity(2) THROUGH THE PATCHED FUNCTION. get_max_number_of_cpus is # __get_max_number_of_cpus() with a Python wrapper -- the very function the VLA # patch rewrote -- so a red here is the first thing to read if the patch is # suspected. The value is the guest's cpumask WIDTH, a property of the kernel # this image booted (x86-64 control: 64), so it is asserted as "a positive # integer" rather than pinned to a number that belongs to another machine. _maxcpus=$(pv max_cpus) case "$_maxcpus" in ''|*[!0-9]*) bst_notok $P/max-cpus "not a plain positive integer: '$_maxcpus'" ;; 0) bst_notok $P/max-cpus "0 -- sched_getaffinity(2) returned no bits" ;; *) bst_ok $P/max-cpus ;; esac # get_affinity(0) RETURNS A LIST OF CPU NUMBERS. Shape first, then contents. check_eq $P/affinity-list "list" "$(pv affinity_type)" _afflen=$(pv affinity_len) case "$_afflen" in ''|*[!0-9]*|0) bst_notok $P/affinity-nonempty "no CPU in the mask (len='$_afflen')" ;; *) bst_ok $P/affinity-nonempty ;; esac # THE TWO INDEPENDENT ORACLES. See the header: the CPU count belongs to whatever # booted this image, so what is asserted is AGREEMENT. os.sched_getaffinity() is # CPython's own binding over the same syscall, compiled INTO the interpreter -- # so if these two disagree the fault is in this .so's marshalling, and if they # agree every affinity result here is about the kernel rather than the package. check_eq $P/affinity-agrees-os "$(pv affinity_os)" "$(pv affinity_self)" # ...and the kernel's own text. Cpus_allowed_list is a RANGE form (`0-15'), so # the comparison is against the extension's list rendered the same way: first # and last, which is what a contiguous mask reduces to and what /proc prints. _af=$(pv affinity_self) _first=${_af%%,*}; _last=${_af##*,} if [ "$_first" = "$_last" ]; then _range="$_first"; else _range="$_first-$_last"; fi check_eq $P/affinity-agrees-proc "$(pv affinity_proc)" "$_range" # THE GET/SET ROUND TRIP, on the mask the task already has. See the header: this # is the one set_affinity call that is safe by construction -- the mask contains # the caller's own CPU, so D-AFFINITY's migration path is not entered and the # arch-local handler records it and returns 0, exactly as upstream would. check_eq $P/set-same "None" "$(pv set_same)" check_eq $P/set-same-readback "$(pv affinity_self)" "$(pv set_same_readback)" # AN EMPTY MASK IS EINVAL -- upstream's complaint, and the arch-local handler # reproduces it verbatim (`cpumask_empty(&new_mask)' -> -EINVAL). This one DOES # reach the kernel, which is what makes it the affinity family's proof that the # syscall is answered by the kernel rather than short-circuited in the # extension. Errno alone; glibc's message text is not this port's to pin. check_eq $P/set-empty-einval "$(pv errno_EINVAL)" "$(enosys "$(pv set_empty)")" # A CPU NUMBER THAT CANNOT EXIST NEVER REACHES THE KERNEL. The extension # compares each element against its own max and raises OSError("Invalid CPU") # with PyErr_SetString -- so no errno, and a DIFFERENT shape from the case # above. The ticket asks for "raises OSError with the documented errno rather # than appearing to succeed"; the measured truth is that this particular refusal # carries no errno at all, and that is what is asserted. check_eq $P/set-impossible-cpu "OSError[-]: Invalid CPU" "$(pv set_impossible_cpu)" # ------------------------------------- 3. scheduling policy and priority ---- # EXPECTED, NOT YET MEASURED -- see the header. Entries 142..148 are absent from # arch/wasm32/kernel/syscall_table.c and the dispatcher answers a NULL row with # -ENOSYS, so all five of these are expected to refuse with errno 38. The errno # alone is asserted, not glibc's message text. # # EACH IS ITS OWN CASE ON PURPOSE. They are five different syscall numbers at # three different attributions, so they go green one at a time as the numbers # are wired -- and a single combined case would hide four of those days. _E=$(pv errno_ENOSYS) # sched_getscheduler(145) -- brintos/linux#88. check_eq $P/get-scheduler-enosys "$_E" "$(enosys "$(pv get_scheduler)")" # sched_setscheduler(144) -- brintos/linux#88. SCHED_OTHER at priority 0 is the # policy the task is already under, so on a platform that answered this it would # change nothing; the refusal is therefore about the syscall and not the request. check_eq $P/set-scheduler-enosys "$_E" "$(enosys "$(pv set_scheduler_other)")" # sched_getparam(143) -- NOT covered by #88, which is the policy pair, nor by # #62, which is getpriority/setpriority (140/141). See the test plan entry. check_eq $P/get-priority-enosys "$_E" "$(enosys "$(pv get_priority)")" # sched_get_priority_min(147) and _max(146) -- same, and asserted across all # five policies at once because one syscall number serves them all: a platform # that wired it would answer every policy, and one that has not answers none. check_eq $P/priority-min-enosys "$_E $_E $_E $_E $_E" \ "$(enosys "$(pv get_priority_min_SCHED_OTHER)") $(enosys "$(pv get_priority_min_SCHED_FIFO)") $(enosys "$(pv get_priority_min_SCHED_RR)") $(enosys "$(pv get_priority_min_SCHED_BATCH)") $(enosys "$(pv get_priority_min_SCHED_IDLE)")" check_eq $P/priority-max-enosys "$_E $_E $_E $_E $_E" \ "$(enosys "$(pv get_priority_max_SCHED_OTHER)") $(enosys "$(pv get_priority_max_SCHED_FIFO)") $(enosys "$(pv get_priority_max_SCHED_RR)") $(enosys "$(pv get_priority_max_SCHED_BATCH)") $(enosys "$(pv get_priority_max_SCHED_IDLE)")" # ------------------------------------------------------------- the scripts --- # THE TWO SHIPPED EXECUTABLES, RUN. Each is a separate interpreter start reached # through its rewritten shebang, which is the whole point -- the probe cannot # stand in for them. With no arguments both print their usage and exit 0 # (measured on the x86-64 control), and both `import schedutils' at the top, so # these are also the only cases that exercise the extension from a process other # than the probe's. check_out $P/cli-pchrt "manipulate real-time attributes of a process" "$PCHRT" check_out $P/cli-ptaskset "set or get the affinity of a process" "$PTASKSET" # Raw payload, so a reader of the verify transcript can see every answer this # run produced and not only the ones a case names. This is where the exact # errno-and-message of each refusal lands. echo "==SCHEDUTILS-OBSERVED==" cat "$OUT" echo "==SCHEDUTILS-OBSERVED-END==" rm -rf "$W"