. "$BST_LIB" # BST-TIMEOUT: 1500 # THREE python3 starts, and nothing else that costs. # # # # A bare `import', a second one whose STDERR is the # # assertion (`import-quiet'), and the probe, which takes # # ONE start for 151 measurements. 1500 is 400s per start -- # # the per-start budget the neighbouring python3 suites use # # -- plus 300s of margin. # # # # There is no exec traffic here and no fixture bigger than # # a few bytes: the probe's syscall load is a couple of dozen # # open/ioctl/write/close pairs on tmpfs. So unlike # # 99-python3-editor next door, the interpreter start IS the # # whole cost. # # # # 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-watchdogdev 1.0.0 (meta-openembedded meta-python/recipes-devtools/python, # python3-watchdogdev_1.0.0.bb, `inherit pypi setuptools3', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # NOTHING IN THIS SUITE ARMS A WATCHDOG, AND THAT IS THE FIRST THING TO SAY. # ============================================================================ # # A watchdog is the one device class where a SUCCESSFUL open has consequences: # the kernel starts a timer and reboots the machine if nothing pings it. So no # case here opens a watchdog node. The probe lists /dev/watchdog* before it does # anything else and, if any node exists, declines the open outright and reports # `skipped-device-present' in place of a result -- see `dev-nodes' and the # `open-nodev-*' family below, which are written to be read either way. # # On this image there is no node to decline. wasm32_defconfig sets no # CONFIG_WATCHDOG, so drivers/watchdog is not built, no character device is # registered and devtmpfs publishes nothing -- which is why the open CAN be # attempted: there is no timer for it to start, and what is measured is that it # is refused in the documented way. # # NO PING LOOP, NO TIMEOUT IS SET ON A DEVICE, AND NO MAGIC-CLOSE SEQUENCE RUNS # AGAINST ONE. The behavioural half of this suite runs against A REGULAR FILE -- # see the next section -- so `set_timeout', `keep_alive' and `magic_close' are # all exercised with nothing behind the descriptor. # # ============================================================================ # WHY A REGULAR FILE IS A LEGITIMATE STAND-IN, AND WHAT IT BUYS. # ============================================================================ # # watchdog.__new__ is `open(filename, O_WRONLY)' and NOTHING ELSE -- it does not # check that the path is a device, that the fd is a character device, or that # any driver is behind it. So every method on the type can be reached against an # ordinary file under $TMPDIR, and that is exactly how a port with no hardware # gets tested rather than merely imported: # # the ioctls come back from the kernel's generic path with an errno, which # is the "documented error when handed a file descriptor that is # not a watchdog" this ticket asks for; # write() writes bytes we then read back out of the file; # magic_close() writes its `V' to the file and closes, so the sequence is # measured rather than described. # # Without this the suite would be constants and an import. With it, 151 of the # probe's measurements are behaviour. # # ============================================================================ # THE PORT TURNS ON A dlopen, AND THAT IS RECENT. # ============================================================================ # # `watchdogdev' IS a C extension. The distribution contains no Python source at # all -- one 557-line src/watchdogdev.c, no fallback, no pure-Python half -- so # `import watchdogdev' is the load of a wasm side module against the # interpreter's exported C API and there is nothing to fall back to if it fails. # That was impossible on this arm until BRINTOS-2046: the interpreter exported # thirty-five symbols of hwjs/wasm32 runtime ABI and not one `Py*', so every such # load died on an unbound `env.PyModule_Create2'. # # So `import' and `so-present' are not routine packaging cases here, they are the # port. Measured off this recipe's own artifacts before the suite was written: # the module imports 21 C API names -- 17 functions and the four GOT.mem DATA # imports `PyExc_IOError', `_Py_NoneStruct', `_Py_TrueStruct', `_Py_FalseStruct' # -- and all 21 are in the 1715 the target /usr/bin/python3.12 exports. The DATA # half is worth naming because BRINTOS-2046 recorded it as the open question of # the mechanism; this module needs it, so a green `import' here answers it. # # ============================================================================ # THE RECIPE CARRIES ONE PATCH, AND THIS SUITE IS WHERE IT IS PROVED. # ============================================================================ # # watchdog.write() -- the documented way to keep the timer ticking -- parses # with "s#" and upstream never defined PY_SSIZE_T_CLEAN, which 3.10 made # mandatory for every `#' format. On a stock build every `write-*' case below is # a SystemError raised before the argument is even looked at. # # The obvious fix makes it worse, which is why the patch does something else: # PY_SSIZE_T_CLEAN works by #define'ing PyArg_ParseTuple to the private alias # `_PyArg_ParseTuple_SizeT', and the interpreter's export list is derived from # PyAPI_FUNC/PyAPI_DATA declarations, which that alias does not carry. Built # that way the module imports a symbol the interpreter defines and does not # export, and fails at dlopen instead of at write(). So the patch parses an # object and measures it afterwards, binding only exported API. # # `write-bytes' is therefore the case that fails first if the patch is ever # lost, and `magic-close-wrote' is its control: magic_close() reaches write(2) # directly, never went through PyArg_ParseTuple, and works on a stock build -- # so the two together say the defect was in the argument parser and not in the # descriptor. # # ============================================================================ # FIVE THINGS A CALLER WILL GET WRONG. THESE ARE THE POINT OF THE SUITE. # ============================================================================ # # THE REQUEST NUMBERS ARE NEGATIVE HERE AND POSITIVE ON A 64-BIT HOST. # `_IOC' builds an unsigned int with bit 31 set for every # read direction, and PyModule_AddIntConstant takes a `long'. # On this ILP32 arm a long is 32 bits, so all eleven WDIOC_* # constants arrive as NEGATIVE Python ints -- WDIOC_GETSUPPORT # is -2144839936 here and 2150127360 on x86-64, off the same # source. Neither is wrong and neither is a literal this suite # may assert on its own: what is asserted is the value MASKED # TO 32 BITS, which is 0x80285700 on both, plus the sign # pattern DERIVED from the width the interpreter reports. # `long-bits', `negative-ioctl-count' and the `ioc-*' family # are that split. # # THE STRUCTURE SIZE IS CARRIED IN THE REQUEST NUMBER, NOT IN A CONSTANT. # The module exports no sizeof. `_IOC' packs size into bits # 16-29, so WDIOC_GETSUPPORT's size field IS the compiler's # own sizeof(struct watchdog_info) on this arm, and # WDIOC_GETSTATUS's is sizeof(int). That is the check that # catches an _IOC width mismatch on wasm32, and it reads the # COMPILED MODULE rather than the header: # `watchdog-info-size' is 40 and `int-size-from-ioctl' is 4, # which is __u32 + __u32 + __u8[32] with 4-byte ints and no # tail padding. # # options, firmware_version AND identity ARE None UNTIL get_support() RUNS... # They read a struct the object has not allocated yet, so a # caller who follows README.txt's `wdt.options & # WDIOF_MAGICCLOSE' without calling get_support() first gets a # TypeError on None rather than an error naming the mistake. # # ...AND A FAILED get_support() LEAVES UNINITIALISED MEMORY BEHIND IT. # get_support() mallocs the struct BEFORE the ioctl and frees # nothing when the ioctl fails, so afterwards those three # getters stop answering None and start answering whatever was # in that allocation. A caller who ignores the exception then # reads uninitialised memory as a flags word, which is strictly # worse than the None they had before they called. This is an # upstream defect. `getsupport-leaves-buffer' states the # transition; NO case asserts the contents, which are not a # fact about anything. # # close() IS IDEMPOTENT AND fileno() KEEPS ANSWERING. # close() sets fd to -1 and returns None however many times it # is called, and fileno() then returns -1 rather than raising. # So a caller cannot tell a closed object from a live one by # asking for its descriptor -- only `closed' says. The write # and ioctl paths do NOT check, and hand the -1 to the kernel, # which is why `write-after-close' is an EBADF from write(2) # rather than a ValueError from the module. # # ============================================================================ # WHAT IS DELIBERATELY NOT HERE. # ============================================================================ # # EVERY SUCCESSFUL IOCTL PATH. get_support(), keep_alive(), get_timeout() and # the rest return a value only against a driver, and there is none -- so the # eleven wrappers are measured by the errno they produce and by the fact that # they all produce the SAME one, never by a reading. The `options'/`identity' # decode after a real GETSUPPORT is unreachable on this image and is not faked. # # THE `name' USE-AFTER-FREE. watchdog_new stores the `char *' PyArg_ParseTuple # handed it and takes no reference to the str that owns it, so `wdt.name' reads # freed memory once the caller's path string is collected. It is real and it is # not testable without relying on allocator behaviour, so it is recorded here # and asserted nowhere. `name-survives-close' holds only because the probe keeps # its path in a live variable, which is the ordinary case and not the bug. P=python3-watchdogdev bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages SO=$SITE/watchdogdev.cpython-312.so DI=$SITE/watchdogdev-1.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-watchdogdev.list PROBE=$BST_DATA/python3-watchdogdev-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-watchdogdev-suite.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ on the rootfs -- check $P/site-packages-dir test -d "$SITE" # THE MODULE IS THE SHARED OBJECT. This is the one line that distinguishes this # port from every pure-Python neighbour in this directory. check $P/so-present test -s "$SO" check $P/so-executable test -x "$SO" check $P/not-a-package test ! -d "$SITE/watchdogdev" # ...AND THERE IS NO PYTHON SOURCE ANYWHERE IN IT. watchdogdev has no .py, so # there is no __pycache__ either -- a byte-compiled file here would mean the # package grew a Python half nobody expects. check $P/no-py-module test ! -e "$SITE/watchdogdev.py" check $P/no-pycache test ! -e "$SITE/__pycache__/watchdogdev.cpython-312.pyc" # IT IS A WASM SIDE MODULE, not an ELF that happens to be named .so. The magic # is `\0asm' followed by version 1 -- 00 61 73 6d 01 00 00 00. check_eq $P/so-is-wasm "0061736d01000000" \ "$(od -An -tx1 -N8 "$SO" 2>/dev/null | tr -d ' \n')" # ------------------------------------------------------------- the package --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # EXACTLY ONE SHARED OBJECT SHIPS, and it is the module. A second .so would mean # a split or a stray build artifact. check_eq $P/pkg-one-so "$SO" \ "$(grep -E '\.so$' "$W/pkgf" 2>/dev/null)" # NOTHING LANDS IN A BIN DIRECTORY: this package has no console script and no # helper program. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(s?bin)/' "$W/pkgf" 2>/dev/null)" # ...AND NOTHING OUTSIDE site-packages AT ALL. check_eq $P/pkg-only-site-packages "" \ "$(grep -v -e "^$SITE" -e '^/usr/lib/python3\.12$' -e '^/usr/lib$' \ -e '^/usr$' -e '^/\.$' "$W/pkgf" 2>/dev/null)" # THE DEBUG AND DEV SPLITS TOOK THEIR SHARE. A `.debug' directory under # site-packages in the RUNTIME package would mean the split did not happen and # the image is carrying DWARF it cannot use. check $P/no-debug-in-runtime test ! -d "$SITE/.debug" # ---------------------------------------------------------------- metadata --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: watchdogdev" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 1.0.0" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check_out $P/dist-info-top-level "watchdogdev" cat "$DI/top_level.txt" # THE WHEEL IS TAGGED FOR THE BUILD HOST, NOT FOR THE TARGET, and that is # upstream tooling rather than a packaging error: bdist_wheel names the platform # it RAN on, and it ran on x86-64. `Root-Is-Purelib: false' is the half that is # true here and the half that matters -- it says the payload is an extension. # The same pair is recorded for python3-markupsafe's accelerator arm next door. check_out $P/wheel-not-purelib "Root-Is-Purelib: false" cat "$DI/WHEEL" check_out $P/wheel-tag-host "Tag: cp312-cp312-linux_x86_64" cat "$DI/WHEEL" # UPSTREAM SHIPS NO LICENSE FILE IN THE WHEEL, which is why there is no # dist-info-license case here and there is one in the pure-Python suites: the # licence text reaches the image through the recipe's LIC_FILES_CHKSUM on # setup.py, not through the distribution's metadata. check $P/dist-info-no-license test ! -e "$DI/LICENSE" sed -n '/^Package: python3-watchdogdev$/,/^$/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" check_out $P/pkg-version "Version: 1.0.0-r0" cat "$W/st" # THE DEPENDS LINE IS python3-core ALONE, and it is correct rather than thin. # The module imports nothing: it is C, its only runtime requirement is an # interpreter that can dlopen it, and setuptools3-base's python3-core is exactly # that. Asserted verbatim so a change to it is a deliberate act. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- # THE LOAD. See the header: this is the port. check $P/import python3 -c 'import watchdogdev' # A dlopen diagnostic, a DeprecationWarning or a stray print would be invisible # to the case above, which scores only the exit status. check_eq $P/import-quiet "" \ "$(python3 -c 'import watchdogdev' 2>&1)" # ----------------------------------------------------------------- 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 bst_probe $P/probe-run "$W/probe" 1200 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or the extension did not load; every CASE below is unscorable" fi # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "151" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED, and that it is the extension rather than something on the # path with the same name. check_eq $P/module-answered "$SO" "$(pv module_file)" check_eq $P/is-extension "yes" "$(pv is_extension)" check_eq $P/is-package "no" "$(pv is_package)" # THE MODULE STATES NO VERSION. Nothing in watchdogdev.c adds `__version__', so # the only version this package declares anywhere is its .dist-info, asserted # above. A caller cannot ask the module what it is. check_eq $P/no-version-attr "no" "$(pv has_version_attr)" # NO `__all__' EITHER, so `from watchdogdev import *' -- which is what README.txt # tells a user to write -- leaks every public name. `public-names' is that list, # and it is also the whole API surface. check_eq $P/no-dunder-all "no" "$(pv has_dunder_all)" check_eq $P/public-count "28" "$(pv public_count)" check_eq $P/public-names \ "WDIOC_GETBOOTSTATUS,WDIOC_GETPRETIMEOUT,WDIOC_GETSTATUS,WDIOC_GETSUPPORT,WDIOC_GETTEMP,WDIOC_GETTIMELEFT,WDIOC_GETTIMEOUT,WDIOC_KEEPALIVE,WDIOC_SETOPTIONS,WDIOC_SETPRETIMEOUT,WDIOC_SETTIMEOUT,WDIOF_CARDRESET,WDIOF_EXTERN1,WDIOF_EXTERN2,WDIOF_FANFAULT,WDIOF_KEEPALIVEPING,WDIOF_MAGICCLOSE,WDIOF_OVERHEAT,WDIOF_POWEROVER,WDIOF_POWERUNDER,WDIOF_PRETIMEOUT,WDIOF_SETTIMEOUT,WDIOF_UNKNOWN,WDIOS_DISABLECARD,WDIOS_ENABLECARD,WDIOS_TEMPPANIC,WDIOS_UNKNOWN,watchdog" \ "$(pv public_names)" # THERE IS NO MODULE-LEVEL FUNCTION. watchdogdev.c's module_functions[] is the # sentinel alone, so the type is the entire callable surface. check_eq $P/only-callable-is-the-type "watchdog" "$(pv callable_names)" check_eq $P/module-doc "This module implements watchdog API." "$(pv doc_first_line)" # ------------------------------------------------------------------ type ----- check_eq $P/type-is-type "yes" "$(pv type_is_type)" check_eq $P/type-name "watchdog" "$(pv type_name)" check_eq $P/type-module "watchdogdev" "$(pv type_module)" check_eq $P/type-base "object" "$(pv type_base)" # NOT SUBCLASSABLE: tp_flags is Py_TPFLAGS_DEFAULT with no Py_TPFLAGS_BASETYPE, # so a caller who wants to wrap this object has to compose rather than inherit. check_eq $P/not-subclassable "!TypeError" "$(pv subclassable)" # EVERY DOCUMENTED FUNCTION IS PRESENT -- the roster read off the built object # rather than off README.txt, in one comparison, so an added or a lost method is # a red here and not a silent difference. check_eq $P/methods \ "close,fileno,get_boot_status,get_pretimeout,get_status,get_support,get_temp,get_time_left,get_timeout,keep_alive,magic_close,set_options,set_pretimeout,set_timeout,write" \ "$(pv methods)" check_eq $P/method-count "15" "$(pv method_count)" check_eq $P/properties "closed,firmware_version,identity,name,options" "$(pv properties)" # THE THREE FILE METHODS CARRY NO DOCSTRING and the twelve ioctl wrappers do. # `help(watchdogdev)' therefore documents the device API and says nothing about # the three calls a user reaches for first. check_eq $P/undocumented-methods "close,fileno,write" "$(pv undocumented_methods)" # ------------------------------------------------- the ioctl request numbers -- # MASKED TO 32 BITS, which is the same value on this arm and on a 64-bit host -- # see the header. Each is what _IOR/_IOWR computes from with # WATCHDOG_IOCTL_BASE 'W'. check_eq $P/ioc-getsupport "0x80285700" "$(pv ioc_wdioc_getsupport)" check_eq $P/ioc-getstatus "0x80045701" "$(pv ioc_wdioc_getstatus)" check_eq $P/ioc-getbootstatus "0x80045702" "$(pv ioc_wdioc_getbootstatus)" check_eq $P/ioc-gettemp "0x80045703" "$(pv ioc_wdioc_gettemp)" check_eq $P/ioc-setoptions "0x80045704" "$(pv ioc_wdioc_setoptions)" check_eq $P/ioc-keepalive "0x80045705" "$(pv ioc_wdioc_keepalive)" check_eq $P/ioc-settimeout "0xc0045706" "$(pv ioc_wdioc_settimeout)" check_eq $P/ioc-gettimeout "0x80045707" "$(pv ioc_wdioc_gettimeout)" check_eq $P/ioc-setpretimeout "0xc0045708" "$(pv ioc_wdioc_setpretimeout)" check_eq $P/ioc-getpretimeout "0x80045709" "$(pv ioc_wdioc_getpretimeout)" check_eq $P/ioc-gettimeleft "0x8004570a" "$(pv ioc_wdioc_gettimeleft)" # ...and the same claim made once more by the probe against its own computation, # so a transcription error in the eleven literals above cannot pass unnoticed. check_eq $P/ioc-agree-with-header "yes" "$(pv ioctls_agree_with_header)" # THE DECODED FIELDS. `_IOC' packs dir<<30 | size<<16 | type<<8 | nr, so these # four lines are the whole encoding read back out of the compiled module. check_eq $P/ioc-dirs \ "WDIOC_GETSUPPORT:2,WDIOC_GETSTATUS:2,WDIOC_GETBOOTSTATUS:2,WDIOC_GETTEMP:2,WDIOC_SETOPTIONS:2,WDIOC_KEEPALIVE:2,WDIOC_SETTIMEOUT:3,WDIOC_GETTIMEOUT:2,WDIOC_SETPRETIMEOUT:3,WDIOC_GETPRETIMEOUT:2,WDIOC_GETTIMELEFT:2" \ "$(pv ioc_dirs)" check_eq $P/ioc-nrs \ "WDIOC_GETSUPPORT:0,WDIOC_GETSTATUS:1,WDIOC_GETBOOTSTATUS:2,WDIOC_GETTEMP:3,WDIOC_SETOPTIONS:4,WDIOC_KEEPALIVE:5,WDIOC_SETTIMEOUT:6,WDIOC_GETTIMEOUT:7,WDIOC_SETPRETIMEOUT:8,WDIOC_GETPRETIMEOUT:9,WDIOC_GETTIMELEFT:10" \ "$(pv ioc_nrs)" check_eq $P/ioc-type-is-W "yes" "$(pv ioc_types_all_W)" # THE SIZE FIELD OF EACH REQUEST, which is where the width check actually lives # -- GETSUPPORT carries sizeof(struct watchdog_info) and the other ten carry # sizeof(int), both as the wasm32 compiler measured them. check_eq $P/ioc-sizes \ "WDIOC_GETSUPPORT:40,WDIOC_GETSTATUS:4,WDIOC_GETBOOTSTATUS:4,WDIOC_GETTEMP:4,WDIOC_SETOPTIONS:4,WDIOC_KEEPALIVE:4,WDIOC_SETTIMEOUT:4,WDIOC_GETTIMEOUT:4,WDIOC_SETPRETIMEOUT:4,WDIOC_GETPRETIMEOUT:4,WDIOC_GETTIMELEFT:4" \ "$(pv ioc_sizes)" # TWO OF THE ELEVEN ARE _IOWR AND NINE ARE _IOR, which is upstream's kernel # header and not a mistake: SETOPTIONS and KEEPALIVE are declared _IOR even # though they carry a value IN, while SETTIMEOUT and SETPRETIMEOUT are _IOWR # because they also report back what was actually set. A caller comparing the # direction bits against the verb names will find them disagreeing. check_eq $P/ioc-setoptions-is-ior "WDIOC_SETOPTIONS:2" \ "$(pv ioc_dirs | tr ',' '\n' | grep '^WDIOC_SETOPTIONS:')" # ------------------------------------------------------- struct watchdog_info - # THE WIDTH CHECK. The size field of GETSUPPORT is the compiler's own # sizeof(struct watchdog_info) on this arm -- __u32 + __u32 + __u8[32] -- and # GETSTATUS's is sizeof(int). If wasm32 ever disagreed with the header about # either, these two are where it would show, ahead of any ioctl being issued. check_eq $P/watchdog-info-size "40" "$(pv watchdog_info_size)" check_eq $P/int-size-from-ioctl "4" "$(pv int_size_from_ioctl)" # The same size reached independently, through Python's own struct module, and # the field offsets that size implies. check_eq $P/watchdog-info-calcsize "40" "$(pv watchdog_info_calcsize)" check_eq $P/watchdog-info-offsets "0,4,8" "$(pv watchdog_info_offsets)" # ---------------------------------------------------------- the sign pattern -- # DERIVED, NOT TRANSCRIBED -- see the header. The width the interpreter reports # is what decides whether the eleven constants are negative, so the two are # asserted together and this suite states the same fact on either arm. _bits=$(pv long_bits) check_eq $P/long-bits "32" "$_bits" case "$_bits" in 32) check_eq $P/negative-ioctl-count "11" "$(pv negative_ioctl_count)" check_eq $P/ioc-getsupport-raw "-2144839936" "$(pv ioc_wdioc_getsupport_raw)" ;; 64) check_eq $P/negative-ioctl-count "0" "$(pv negative_ioctl_count)" check_eq $P/ioc-getsupport-raw "2150127360" "$(pv ioc_wdioc_getsupport_raw)" ;; *) bst_notok $P/negative-ioctl-count "the probe reported long_bits='$_bits', which is neither 32 nor 64, so the sign of the request numbers cannot be predicted from it" bst_notok $P/ioc-getsupport-raw "same -- see $P/negative-ioctl-count" ;; esac # ------------------------------------------------------------ the flag bits --- # Verbatim from . These are the values a caller ANDs against # `options', so a wrong one is a silently wrong capability test rather than an # error. check_eq $P/flag-unknown "-1" "$(pv flag_wdiof_unknown)" check_eq $P/flag-status-unknown "-1" "$(pv flag_wdios_unknown)" check_eq $P/flag-overheat "1" "$(pv flag_wdiof_overheat)" check_eq $P/flag-fanfault "2" "$(pv flag_wdiof_fanfault)" check_eq $P/flag-extern1 "4" "$(pv flag_wdiof_extern1)" check_eq $P/flag-extern2 "8" "$(pv flag_wdiof_extern2)" check_eq $P/flag-powerunder "16" "$(pv flag_wdiof_powerunder)" check_eq $P/flag-cardreset "32" "$(pv flag_wdiof_cardreset)" check_eq $P/flag-powerover "64" "$(pv flag_wdiof_powerover)" check_eq $P/flag-settimeout "128" "$(pv flag_wdiof_settimeout)" check_eq $P/flag-magicclose "256" "$(pv flag_wdiof_magicclose)" check_eq $P/flag-pretimeout "512" "$(pv flag_wdiof_pretimeout)" check_eq $P/flag-keepaliveping "32768" "$(pv flag_wdiof_keepaliveping)" check_eq $P/flag-disablecard "1" "$(pv flag_wdios_disablecard)" check_eq $P/flag-enablecard "2" "$(pv flag_wdios_enablecard)" check_eq $P/flag-temppanic "4" "$(pv flag_wdios_temppanic)" # THEY ARE DISJOINT SINGLE BITS, which is the property every `options & WDIOF_x' # rests on, asserted as a property rather than as sixteen more literals. check_eq $P/flags-single-bit "yes" "$(pv wdiof_all_single_bit)" check_eq $P/flags-disjoint "no" "$(pv wdiof_overlap)" check_eq $P/flags-union "0x83ff" "$(pv wdiof_union)" # WDIOF_ALARMONLY (0x0400) IS IN THIS IMAGE'S HEADER AND NOT IN THE MODULE. # The kernel added it for a watchdog that raises an alarm instead of resetting; # watchdogdev 1.0.0 is from 2018 and exports the other twelve. A caller decoding # `options' cannot name that bit through this module, and the hole in # `flags-union' at 0x0400 is the same fact stated as a number. check_eq $P/no-wdiof-alarmonly "no" "$(pv has_wdiof_alarmonly)" # ============================================================ the device ===== # WHAT IS ON /dev, measured before anything was opened. This image has no # CONFIG_WATCHDOG, so there is no node -- and that is what makes the open case # below safe to run at all. check_eq $P/dev-nodes "none" "$(pv dev_nodes)" check_eq $P/dev-watchdog-absent "no" "$(pv dev_watchdog_exists)" # THE OPEN THAT MUST FAIL. With no node, `watchdog("/dev/watchdog")' raises # FileNotFoundError -- an OSError subclass carrying ENOENT -- and the filename # it was handed. Nothing is armed and nothing is left open. _dev=$(pv dev_watchdog_exists) if [ "$_dev" = "yes" ]; then # Not reachable on this image. If a node ever appears, the probe declines the # open and these four say so rather than reporting a result nobody measured. check_eq $P/open-nodev "skipped-device-present" "$(pv open_nodev)" check_eq $P/open-nodev-errno "skipped-device-present" "$(pv open_nodev_errno)" check_eq $P/open-nodev-filename "skipped-device-present" "$(pv open_nodev_filename)" bst_ok $P/open-nodev-prompt else check_eq $P/open-nodev "!FileNotFoundError" "$(pv open_nodev)" check_eq $P/open-nodev-errno "FileNotFoundError/ENOENT" "$(pv open_nodev_errno)" # THE FILENAME IS CARRIED ON THE EXCEPTION, because the constructor uses # PyErr_SetFromErrnoWithFilename rather than the bare form -- so a caller who # opens several nodes learns which one failed. check_eq $P/open-nodev-filename "/dev/watchdog" "$(pv open_nodev_filename)" check_eq $P/open-nodev-is-oserror "yes" "$(pv open_nodev_is_oserror)" # PROMPTLY: the refusal is one failed open(2), so it must not be measurable in # seconds. A bound rather than a number, because this is an unloaded-guest # timing and 30s is three orders of magnitude above what it should cost. _ms=$(pv open_nodev_ms) case "$_ms" in ''|*[!0-9]*) bst_notok $P/open-nodev-prompt "the probe reported no elapsed time for the failed open (got '$_ms')" ;; *) if [ "$_ms" -lt 30000 ]; then bst_ok $P/open-nodev-prompt else bst_notok $P/open-nodev-prompt "the failed open took ${_ms}ms, which is not a refused open(2) -- something is retrying or blocking"; fi ;; esac fi # ...AND THE INTERPRETER SURVIVED IT. The probe kept running to its last line, # which `probe-complete' already says; this is that fact named where the ticket # asks for it. check_eq $P/open-nodev-no-abort "yes" "$(pv probe_done)" # ========================================================= the constructor === # "s:__new__" TAKES ONE str AND NOTHING ELSE. bytes -- the natural spelling for # a device path in a program that also uses os.open -- is a TypeError before # anything is opened. check_eq $P/ctor-no-args "!TypeError" "$(pv ctor_no_args)" check_eq $P/ctor-two-args "!TypeError" "$(pv ctor_two_args)" check_eq $P/ctor-int "!TypeError" "$(pv ctor_int)" check_eq $P/ctor-none "!TypeError" "$(pv ctor_none)" check_eq $P/ctor-bytes "!TypeError" "$(pv ctor_bytes)" check_eq $P/ctor-embedded-nul "!ValueError" "$(pv ctor_embedded_nul)" # NO KEYWORD IS ACCEPTED, and not because one is rejected by name: tp_new parses # `args' and IGNORES `kwds' entirely, so `watchdog(filename=...)' is an # empty-tuple TypeError rather than an unexpected-keyword one. check_eq $P/ctor-keyword "!TypeError" "$(pv ctor_keyword)" # open(2)'s OWN FAILURES, each reached through the constructor. The module opens # O_WRONLY, which is why a directory is EISDIR here rather than opening fine. check_eq $P/ctor-missing "FileNotFoundError/ENOENT" "$(pv ctor_missing)" check_eq $P/ctor-directory "IsADirectoryError/EISDIR" "$(pv ctor_directory)" # A MODE-0444 FILE IS EACCES -- unless the suite is running as root, which on # this image it is, and root's open(2) is not refused by mode bits. Branched # rather than predicted, because the answer is a property of the runner and not # of the package. if [ "$(pv euid_is_root)" = "yes" ]; then check_eq $P/ctor-readonly "skipped-running-as-root" "$(pv ctor_readonly)" else check_eq $P/ctor-readonly "PermissionError/EACCES" "$(pv ctor_readonly)" fi # ================================================ a regular file as the fd === # See the header for why this is legitimate. Everything below is a real, # fully-constructed watchdog object over an ordinary file. check_eq $P/obj-type "watchdog" "$(pv obj_type)" check_eq $P/obj-name-is-argument "yes" "$(pv obj_name_is_argument)" check_eq $P/obj-not-closed "False" "$(pv obj_closed_initial)" check_eq $P/obj-fileno-type "int" "$(pv obj_fileno_type)" check_eq $P/obj-fileno-valid "yes" "$(pv obj_fileno_nonnegative)" # THE THREE GETSUPPORT ATTRIBUTES ARE None BEFORE get_support() -- see the # header. This is the state a caller who skipped get_support() is in. check_eq $P/options-before-none "None" "$(pv options_before)" check_eq $P/firmware-version-before-none "None" "$(pv firmware_version_before)" check_eq $P/identity-before-none "None" "$(pv identity_before)" # ---------------------------------------- the eleven ioctls, with no driver --- # THEY ALL AGREE, which is the assertion that does not depend on knowing the # errno: eleven different requests against the same descriptor must be refused # for the same reason, or something is answering some of them. check_eq $P/ioctl-all-agree "yes" "$(pv ioctl_all_agree)" # AND WHAT THEY AGREE ON, recorded with a definite answer required. ENOTTY is # what the kernel's generic path returns for an ioctl a file's driver does not # implement, and it is what this kernel gives -- measured here for a regular # file, and already measured on this image for a character device (99-spitools) # and a directory (99-sdmon). Anything else is a finding about the ioctl # dispatch rather than about watchdogdev, and must not pass silently. _verdict=$(pv ioctl_verdict) case "$_verdict" in OSError/ENOTTY) bst_ok $P/ioctl-verdict ;; "") bst_notok $P/ioctl-verdict "the probe printed no ioctl_verdict line" ;; *) bst_notok $P/ioctl-verdict "the eleven WDIOC_* wrappers against a regular file gave '$_verdict', not the OSError/ENOTTY a descriptor whose driver does not implement the ioctl returns -- worth a look at the kernel's ioctl dispatch before reading anything above" ;; esac # THE EXCEPTION IS AN OSError AND NOT THE IOError THE SOURCE NAMES. The module # passes PyExc_IOError, which in Python 3 IS OSError, and CPython then maps the # errno onto a subclass -- there is none for ENOTTY, so plain OSError is the # answer. A caller catching IOError catches it; one catching a subclass by name # does not. check_eq $P/ioctl-distinct-verdicts "OSError/ENOTTY" "$(pv ioctl_distinct)" # A FAILED get_support() LEAVES ITS BUFFER BEHIND -- see the header. The # TRANSITION is the assertion; the contents are uninitialised and are asserted # nowhere, which is why this is one case and not three. check_eq $P/getsupport-leaves-buffer "yes" "$(pv getsupport_leaves_buffer)" check_eq $P/options-after-failed-getsupport-type "int" \ "$(pv options_after_failed_getsupport_type)" # ---------------------------------------------- argument checking, setters --- # "i" REJECTS A str AND A float ALIKE -- the float since 3.10, which stopped # silently truncating. Nothing here reaches an ioctl on a value it should not. check_eq $P/set-timeout-str "!TypeError" "$(pv set_timeout_str)" check_eq $P/set-timeout-float "!TypeError" "$(pv set_timeout_float)" check_eq $P/set-timeout-noargs "!TypeError" "$(pv set_timeout_noargs)" check_eq $P/set-options-str "!TypeError" "$(pv set_options_str)" # METH_NOARGS MEANS NO ARGS: keep_alive(1) is refused by the call machinery # before the method body runs. check_eq $P/keep-alive-with-arg "!TypeError" "$(pv keep_alive_with_arg)" # ==================================================================== write == # THE PATCHED METHOD. On a stock build every one of these is `!SystemError' -- # see the header. The four accepted spellings return their byte count. check_eq $P/write-bytes "4" "$(pv write_bytes)" check_eq $P/write-str "1" "$(pv write_str)" check_eq $P/write-empty "0" "$(pv write_empty)" check_eq $P/write-bytearray "2" "$(pv write_bytearray)" check_eq $P/write-memoryview "3" "$(pv write_memoryview)" check_eq $P/write-return-type "int" "$(pv write_return_type)" # AND THE ONES THAT ARE NOT BYTES-LIKE. An int is the mistake a caller makes # after reading "write a byte to the device"; it is refused by the buffer # protocol rather than by a format string, which is where the message now comes # from. check_eq $P/write-int "!TypeError" "$(pv write_int)" check_eq $P/write-none "!TypeError" "$(pv write_none)" check_eq $P/write-noargs "!TypeError" "$(pv write_noargs)" # WHAT ACTUALLY REACHED THE FILE, read back through a second descriptor -- so # this is the kernel's bytes and not the method's return value. `ping' + `V' + # `' + `ab' + `xyz' + `z', in the order the cases ran, with the three refusals # contributing nothing. check_eq $P/write-file-bytes "70696e6756616278797a7a" "$(pv write_file_bytes)" # ========================================================= close, and after == check_eq $P/closed-after-close "True" "$(pv closed_after_close)" # fileno() KEEPS ANSWERING, with -1 -- see the header. This is the case that # says a caller cannot detect a closed object by asking for its descriptor. check_eq $P/fileno-after-close "-1" "$(pv fileno_after_close)" # BOTH CLOSERS ARE IDEMPOTENT and return None rather than raising. check_eq $P/close-again "None" "$(pv close_again)" check_eq $P/magic-close-after-close "None" "$(pv magic_close_after_close)" # ...BUT THE I/O PATHS DO NOT CHECK, and hand the -1 to the kernel: what comes # back is EBADF from write(2) and ioctl(2), not a ValueError from the module the # way a closed file object would raise one. check_eq $P/write-after-close "OSError/EBADF" "$(pv write_after_close)" check_eq $P/ioctl-after-close "OSError/EBADF" "$(pv ioctl_after_close)" # `name' IS A COPY OF THE ARGUMENT, not a property of the descriptor, so it # survives close. See "what is deliberately not here" in the header for why this # case is narrower than it looks. check_eq $P/name-survives-close "yes" "$(pv name_survives_close)" # ------------------------------------------------------------- magic close --- # THE MAGIC-CLOSE SEQUENCE, measured. On a real device this is what STOPS the # timer rather than letting it expire: write `V', then close. Against a regular # file it is the same two syscalls with nothing behind them -- 0x56 is `V'. check_eq $P/magic-close-result "None" "$(pv magic_close_result)" check_eq $P/magic-close-closed "True" "$(pv magic_close_closed)" check_eq $P/magic-close-fileno "-1" "$(pv magic_close_fileno)" check_eq $P/magic-close-wrote "56" "$(pv magic_close_wrote)" # ------------------------------------------------- what the object is NOT ---- # "can be simply used as file object" is README.txt's phrase and it is generous: # there is no context manager, no iteration, no read and no flush. A caller who # writes `with watchdog(...) as w:' gets a TypeError. check_eq $P/no-context-manager "no" "$(pv has_enter)" check_eq $P/not-iterable "no" "$(pv has_iter)" check_eq $P/no-read "no" "$(pv has_read)" check_eq $P/no-flush "no" "$(pv has_flush)" # DEALLOC CLOSES THE DESCRIPTOR, which is what keeps a program that forgets to # close from holding a watchdog open until it exits. check_eq $P/dealloc-closes-fd "yes" "$(pv dealloc_closed_fd)" # --------------------------------------------------------------- filesystem -- # The probe built every fixture it used and took it away again. check_eq $P/workdir-gone "yes" "$(pv workdir_gone)" rm -rf "$W"