. "$BST_LIB" # BST-TIMEOUT: 1800 # THREE python3 starts, and they are the whole cost. # # # # One is the probe, which takes a single start for 104 # # measurements. The other two are bare imports at the # # shell -- `import inotify.adapters' and `import ctypes' -- # # which cannot be folded into the probe because the point # # of them is what a CALLER on this image gets, from an # # interpreter that has not already imported anything else. # # # # The work inside each start is a few small source parses # # and a handful of imports: microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. The # # inotifywait calls in section 6 cost one exec each and # # return immediately, because the tool dies at its first # # syscall. # # # # 1800 is 600s per python3 start -- ten times the ~60s a # # python3 start has measured at on this arm, and the same # # bound 99-python3-file-magic carries for the same shape of # # work. A suite-timeout red is a batch-wide cost, so if # # this proves slower on target the RIGHT fix is this # # number, not fewer cases. # # # # 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-inotify (`inotify' 0.2.10 on PyPI; meta-openembedded/meta-python # recipes-devtools/python/python3-inotify_git.bb -- `inherit setuptools3 ptest', # a git SRCREV of 9be6a51d on github.com/dsoprea/PyInotify rather than a # tarball) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS. # ============================================================================ # # A pure-Python adapter to the kernel's inotify directory-watching interface. # Five modules, 564 lines, no C of its own: it reaches inotify_init(2), # inotify_add_watch(2) and inotify_rm_watch(2) through ctypes against the # SHARED libc, reads struct inotify_event off the returned fd, and wraps the # result in an Inotify class plus two recursive variants (InotifyTree, # InotifyTrees) whose event_gen() yields (header, type_names, path, filename). # # ============================================================================ # THE HEADLINE: THIS PORT WAITED ON THREE INDEPENDENT PLATFORM GAPS. ONE HAS # CLOSED, AND EITHER OF THE OTHER TWO STILL LEAVES IT DEAD. # ============================================================================ # # GAP 1 -- CLOSED: _ctypes IS NOW BUILT. This image's python3 used to carry # _ctypes CONFIGURED OUT (hwjs-cc#15), and `import inotify.adapters' died at # library.py line 1 with `No module named '_ctypes''. BRINTOS-2269 built # _ctypes in: `import ctypes' and `import ctypes.util' now succeed and _ctypes # is a builtin. ffi_call works; ffi closures (CFUNCTYPE callbacks) still abort # the process (linux-in-the-browser#180), and this package declares none. # # GAP 2 -- libc RESOLVES BY PATH AND THEN CANNOT BE LOADED. library.py line 8 is # # instance = ctypes.cdll.LoadLibrary(_FILEPATH) # # at MODULE SCOPE, where _FILEPATH is `ctypes.util.find_library('c')' and, when # that answers None, the literal 'libc.so.6'. The wasm32 python3 now carries its # own find_library arm, which lists /lib and /usr/lib and answers the # content-addressed /lib/libc.so. -- so the 'libc.so.6' fallback is no # longer taken. The load of that path is then refused by the host: it # instantiates a SECOND libc as a side module, which owns TLS the loader cannot # bind (brintos/hardwarejs#42; refused for TLS per hwjs-cc#106). So `import inotify.adapters' now dies at library.py # line 8 with # # OSError: /lib/libc.so.: [policy -> ELIBBAD] ... side module owns TLS ... # # Section 5's assertions about the generic find_library arms (the ldconfig stub, # no cc/gcc/ld) are still true of the rootfs, but the wasm32 arm no longer # reaches them. # # GAP 3 -- THE inotify SYSCALLS ARE ENOSYS. And this one is measured rather # than predicted, by a package already on this image: inotify-tools scores 7/33 # here against 33/33 for a native build of the same upstream commit, because # every inotify entry point returns ENOSYS (brintos/linux#33). The subsystem is # NOT missing from the kernel -- CONFIG_INOTIFY_USER is on (by Kconfig # `default y'; it is not named in wasm32_defconfig), inotify_user_setup() runs, # and /proc/sys/fs/inotify/max_user_watches reads back a real number. It is # UNREACHABLE, at two layers: arch/wasm32's sys_call_table has no row for the # inotify numbers AND hardwarejs' worker never routes them to the kernel. A # vmlinux rebuilt with the table rows wired scored the SAME 7/33, because the # worker refuses before sys_call_table is consulted. Section 6 re-asserts it # here through inotifywait, so THIS suite read alone distinguishes "blocked on # loading libc" from "inotify does not work on wasm32" -- and the answer is BOTH. # # THE THREE ARE ASSERTED SEPARATELY ON PURPOSE. The order they close in # matters, and GAP 1 closing proved it: the failure moved from library.py line 1 # to line 8 and changed its message, exactly as predicted. Closing hwjs-cc#106 # next would move it to GAP 3; closing #33 alone changes nothing a Python caller # can see at all. # # ============================================================================ # THERE IS A SOUND SUBSET HERE, AND IT IS MEASURED RATHER THAN PARSED. # ============================================================================ # # `inotify' is a PACKAGE of five modules and the gap does not fall across all # five. `inotify/__init__.py' (one line) and `inotify/constants.py' (78 lines # of integer literals plus a MASK_LOOKUP dict) import NOTHING, so both import # on this image and the probe reads their values out of a running interpreter. # That is what makes the constant assertions in section 8 statements about what # a caller would actually get, rather than about what the source says -- and it # is the ticket's "assert the event mask against the constants the kernel # headers define, not against the library's own echo": the numbers below are # from this build's own include/uapi/linux/inotify.h and # include/uapi/asm-generic/fcntl.h, and the module is compared to them. # # The other three modules have NO importable fraction and no honest bypass. # library.py's module scope IS the CDLL load, and every entry point calls.py # declares is `_LIB.' against the handle it returns; a fake handle would # produce a suite that stayed green while the port shipped dead. So they are # read and parsed, identified by sha256. # # ============================================================================ # WHAT THE LIBRARY ACTUALLY DOES WHEN A PLATFORM LETS IT -- MEASURED ON AN # x86-64 CONTROL, AND IT REFUTES FOUR OF THE TICKET'S OWN EXPECTATIONS. # ============================================================================ # # The byte-identical package was driven on x86-64 against the real kernel. None # of this is assertable here; it is recorded because this suite is where the # next reader will look, and because four of the five behaviours the ticket # predicted are not what the library does. # # THE EVENT STREAM CARRIES IN_OPEN. Creating, writing, renaming and deleting # one file under a watched directory yields, in order: IN_CREATE, IN_OPEN, # IN_MODIFY, IN_CLOSE_WRITE, IN_MOVED_FROM, IN_MOVED_TO, IN_DELETE -- seven # events, not the six the ticket lists. The MOVED_FROM/MOVED_TO pair carries # one matching non-zero cookie and every other event's cookie is 0, which is # the half the ticket had right. # # THE EXCEPTION FOR A MISSING PATH CARRIES errno=0, NOT ENOENT. add_watch on # a path that does not exist raises `InotifyError: Call failed (should not be # -1): (-1) ERRNO=(0)'. InotifyError.__init__ reads ctypes.get_errno(), but # library.py builds its handle with `ctypes.cdll.LoadLibrary(...)' and NOT # with use_errno=True, so ctypes never copies the thread's errno and # get_errno() answers 0 for every failure the package can raise. That is an # upstream defect, measured on x86-64, and nothing to do with this arm. The # probe asserts both halves of it from the source (calls_uses_get_errno, # calls_use_errno_requested) so it cannot be lost. # # IN_IGNORED IS NOT DELIVERED WHEN A WATCH IS REMOVED. remove_watch() deletes # the wd from the reverse map BEFORE calling inotify_rm_watch, and # _handle_inotify_event drops any event whose wd it cannot resolve -- so the # IN_IGNORED the kernel queues is discarded inside the adapter. It IS # delivered when the watched DIRECTORY is deleted, preceded by # IN_DELETE_SELF, because that watch is still in the map. # # THE RECURSIVE WATCH HAS AN UPSTREAM RACE, which its own docstring admits. # InotifyTree adds a watch on a new subdirectory only when the generator # PROCESSES that subdirectory's IN_CREATE, so a tree built faster than the # caller drains events loses the deeper levels. Measured: creating a/, then # a/b/, then a/b/deep.txt and only then draining yielded exactly one event -- # IN_CREATE|IN_ISDIR for `a'. # # ============================================================================ # THE RED WITNESSES. # ============================================================================ # # `pkg-import-refused', `probe-import-library', `probe-import-calls', # `probe-import-adapters', `no-libc-so-6' and `inotify-init-enosys' assert the # FAILURE, verbatim. `ctypes-refused' and the probe's ctypes cases were GAP 1's # witnesses; they went red when _ctypes was built and now assert the import # succeeding. They go red on the day # the platform gap behind each one closes. That inversion is deliberate: an # exclusion nobody re-tests outlives its cause, and the day any of the three # gaps closes is the day this file, the roster entry and the test plan should # all be re-read together. # # The messages are asserted IN FULL rather than by exit status: an `import # inotify.adapters' failing for some other reason -- the package not installed, # a broken stdlib -- would also be non-zero, and would read as this documented # boundary while being an unrelated defect. P=python3-inotify bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/inotify DI=$SITE/inotify-0.2.10.dist-info PROBE=$BST_DATA/python3-inotify-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-py-inotify.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --- 1. what shipped -------------------------------------------------------- # IT IS A PACKAGE DIRECTORY, NOT A SINGLE MODULE -- setup.py uses # find_packages(exclude=['tests']). Asserted both ways round, because a release # that flattened it would still satisfy every source-level case in the probe. check $P/site-packages-dir test -d "$SITE" check $P/pkg-dir test -d "$PKG" check $P/no-flat-module test ! -e "$SITE/inotify.py" for _m in __init__ library calls constants adapters test_support; do check $P/module-$_m test -s "$PKG/$_m.py" done # THE BYTE-EXACT IDENTITY OF WHAT LANDED. Each sha256 was taken off the built # .deb and verified byte-identical to the PyInotify tree at the SRCREV the # recipe pins (9be6a51d). The recipe's one patch, new-test-inotify.patch, # touches tests/test_inotify.py ONLY -- and nothing under tests/ is packaged # here, because this build produces no -ptest package -- so the installed # package is upstream unmodified. check_eq $P/sha256-init \ "258c1bf82fbcb38c709d4a76cb7fc7a7b71a049c9c885261ea5289b3f59c7a13" \ "$(sha256sum "$PKG/__init__.py" 2>/dev/null | cut -d' ' -f1)" check_eq $P/sha256-library \ "2dd2e183dd0f68910b9389013a8f4dc23acc439957986a0ad2300501967dfb70" \ "$(sha256sum "$PKG/library.py" 2>/dev/null | cut -d' ' -f1)" check_eq $P/sha256-calls \ "fd6f0a8547c0b41ca5f7e2d03ff8cfd1e9aa162ae88e344b0f3b37f0e1262470" \ "$(sha256sum "$PKG/calls.py" 2>/dev/null | cut -d' ' -f1)" check_eq $P/sha256-constants \ "918196ad7d08ac089935fb5108874a6175830aefe85b634b0cd1f2305935a645" \ "$(sha256sum "$PKG/constants.py" 2>/dev/null | cut -d' ' -f1)" check_eq $P/sha256-adapters \ "fe49e9d711c05ece34a1d5b881c943b27811dd272a90e713e582e2ecc9a0a881" \ "$(sha256sum "$PKG/adapters.py" 2>/dev/null | cut -d' ' -f1)" # THE UPSTREAM TEST TREE IS NOT HERE, and its helper IS. `inherit ptest' would # put tests/ in a -ptest package; this build produces none, so tests/ is absent # while test_support.py -- which lives inside the package directory rather than # under tests/ -- ships as an ordinary module. Both halves asserted, because a # reader looking for the upstream suite on this image needs to know it is not # here and needs not to conclude the package is truncated. check $P/no-tests-dir test ! -d "$PKG/tests" check $P/test-support-here test -s "$PKG/test_support.py" check $P/pycache-dir test -d "$PKG/__pycache__" for _m in __init__ library calls constants adapters; do check $P/pycache-$_m test -s "$PKG/__pycache__/$_m.cpython-312.pyc" done # --- 2. the dist-info ------------------------------------------------------- # THE VERSION IN THE DIRECTORY NAME IS THE ONE inotify/__init__.py DECLARES, # and neither is the recipe's: the recipe is python3-inotify_git.bb pinning an # SRCREV, so the .deb's Version is `git-r0' while the distribution's is 0.2.10. # A reader auditing "which version is on the image" will meet both numbers. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: inotify" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 0.2.10" cat "$DI/METADATA" check_out $P/dist-info-summary \ "An adapter to Linux kernel support for inotify directory-watching." \ cat "$DI/METADATA" check_eq $P/dist-info-top-level "inotify" "$(cat "$DI/top_level.txt" 2>/dev/null)" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check $P/dist-info-license test -s "$DI/LICENSE" check $P/dist-info-record test -s "$DI/RECORD" # The package data setup.py declares, which is where the long description and # the (empty) install_requires come from. An empty requirements.txt is the fact # that makes this package's dependency set purely interpreter subpackages. check $P/resources-readme test -s "$PKG/resources/README.rst" check $P/resources-requirements test -f "$PKG/resources/requirements.txt" check_eq $P/resources-requirements-empty "" \ "$(cat "$PKG/resources/requirements.txt" 2>/dev/null)" # --- 3. dpkg status and the dependency set ---------------------------------- sed -n '/^Package: python3-inotify$/,/^$/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: git-r0" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" # THE THREE DEPENDENCIES, EACH ASSERTED SEPARATELY, measured off the built .deb: # Depends: python3-core, python3-ctypes, python3-logging # # ALL THREE ARE INTERPRETER SUBPACKAGES -- this port pulls in no library and no # other package at all, which is what a pure-Python ctypes binding to libc # looks like. # # python3-ctypes packages the pure-Python half of ctypes; the C half, _ctypes, # is a builtin of the interpreter itself, so `import ctypes' works. # # python3-select IS NOT AMONG THEM AND SHOULD BE. adapters.py builds a # select.epoll() in Inotify.__init__, so the package needs `select' at run time # and upstream's RDEPENDS does not name it. It happens to be satisfied here -- # section 7 asserts `select' imports -- because other packages on this image # pull python3-selectors in; on a minimal install it would not be. That is an # upstream declaration defect rather than a platform one, and we file nothing # for it: docs/conventions/no-public-upstream-reports. for _d in python3-core python3-ctypes python3-logging; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-ctypes, python3-logging" \ "$(cat "$W/dep")" # --- 4. the import, from a shell ------------------------------------------- # RED WITNESS for GAP 2 (brintos/hardwarejs#42; refused for TLS per hwjs-cc#106). The import a caller writes now # gets past `import ctypes' and dies loading /lib/libc.so. at # library.py line 8. Only the traceback's last line is asserted, and only its # stable parts: the digest changes with every libc build. _pi=$(python3 -c 'import inotify.adapters' 2>&1 | tail -1) case "$_pi" in "OSError: /lib/libc.so."*": [policy -> E"*"side module owns TLS"*) bst_ok $P/pkg-import-refused ;; *) bst_notok $P/pkg-import-refused "want the hardwarejs#42 libc load refusal, got: $(_bst_1line "$_pi")" ;; esac # GAP 1, CLOSED: `import ctypes' is silent. Kept as a case so a python3 that # loses _ctypes again goes red here rather than only in the symptom above. check_eq $P/ctypes-refused "" \ "$(python3 -c 'import ctypes' 2>&1 | tail -1)" # --- 5. GAP 2: nothing on this rootfs answers to `libc.so.6' ---------------- # The second, independent blocker, asserted against the rootfs because it is a # question about FILES rather than about the interpreter. library.py asks # find_library('c') first and falls back to the literal 'libc.so.6'. # # THIS IMAGE DOES HAVE A SHARED libc, staged as /lib/libc.so., the name # brintos-wasm-shlibs gives it. The wasm32 find_library arm now answers that # path, and loading it is what hwjs-cc#106 refuses (section 4). The assertions # below about the generic arms still hold of the rootfs; the wasm32 arm no # longer consults them. libc_so=$(ls /lib/libc.so.* 2>/dev/null | head -n 1) if [ -n "$libc_so" ]; then bst_ok $P/shared-libc-present else bst_notok $P/shared-libc-present "no /lib/libc.so.* on this rootfs — GAP 2's shape has changed and this section needs re-reading" fi # RED WITNESS: a libc.so.6 appearing here means the SONAME the fallback asks # for is answerable, and this port should be re-measured. check_eq $P/no-libc-so-6 "" "$(ls -d /lib/libc.so.6 /usr/lib/libc.so.6 2>/dev/null)" # ...and find_library('c') still has nothing to answer WITH. That is a claim # about the THREE ARMS of ctypes.util.find_library, and it used to be asserted # here as "none of gcc, objdump, ldconfig is on this image". THAT PREMISE WAS # WRONG, and this suite is what caught it: batch-2514's verify found objdump and # ldconfig both present. The conclusion holds for a DIFFERENT REASON PER ARM, so # each arm is asserted on its own terms rather than through a tool's absence -- # the tools being a proxy for the arms was the whole of the mistake. # # ARM 1 -- `_findSoname_ldconfig'. It execs the HARDCODED /sbin/ldconfig, which # IS here, and matches `libc\.[^\s]+ (libc6' against its `-p' output. It matches # nothing because /sbin/ldconfig ON THIS IMAGE IS A STUB -- seventeen bytes, # `#!/bin/sh' and `exit 0' -- shipped by the `ldconfig' package to satisfy # postinsts. It prints NOTHING and exits 0, so the arm gets an empty string to # search and answers None. # # There IS also an /etc/ld.so.cache here, and it is a well-formed # glibc-ld.so.cache1.1 carrying ZERO entries -- brintos-wasm-shlibs stages # /lib/libc.so. without ever running a real ldconfig over it. That is a # true fact and NOT the operative one: the stub never opens the cache, so the # cache being populated later would still change nothing until a real ldconfig # is what sits at this path. It is recorded here so a later reader does not # "fix" the cache and expect this port to move. # # The stub's SILENCE is the load-bearing assertion, so it is made directly # rather than inferred from the file's size or its being a script. check_rc $P/ldconfig-present 0 test -x /sbin/ldconfig check_eq $P/ldconfig-p-silent "" "$(/sbin/ldconfig -p 2>/dev/null)" check_rc $P/ldconfig-p-rc0 0 /sbin/ldconfig -p check_eq $P/ldconfig-no-libc "" \ "$(/sbin/ldconfig -p 2>/dev/null | grep -E '[[:space:]]libc\.[^[:space:]]+[[:space:]]+\(libc6' || true)" # ARM 2 -- `_findLib_gcc' shells out to $CC, then `cc', then `gcc'. # ARM 3 -- `_findLib_ld' runs `ld -t -lc'. # RED WITNESSES: none of the three is on this image, so neither arm can run at # all. Any of them arriving means find_library may resolve and GAP 2 needs # re-measuring. for _t in cc gcc ld; do if command -v "$_t" >/dev/null 2>&1; then bst_notok $P/no-$_t "$_t is on this image — find_library('c') may now resolve, and GAP 2 needs re-measuring" else bst_ok $P/no-$_t fi done # objdump IS on this image and does NOT change the answer -- recorded as a FACT, # deliberately not as a red witness. Only `_get_soname' uses it, and _get_soname # is reached only on a file ARM 2 or ARM 3 already found; with both of those # dead there is nothing for it to read a SONAME off. Asserting its absence was # the original error, and asserting its presence as a blocker would repeat it in # mirror image. check_rc $P/objdump-present 0 command -v objdump # --- 6. GAP 3: the inotify syscalls are ENOSYS ------------------------------ # The third blocker, asserted through a package that is already here and that # does NOT go through Python at all -- so this section is a statement about the # kernel interface rather than about the binding. inotify-tools' inotifywait # calls inotify_init1(2) before it looks at its arguments. # # Without this, a reader could not tell "blocked on loading libc" from "inotify # does not work on wasm32". Both are true, and the libc one is merely the first # to fire. if command -v inotifywait >/dev/null 2>&1; then bst_ok $P/inotifywait-present D=$W/watched mkdir -p "$D" 2>/dev/null inotifywait -t 1 "$D" > "$W/iw.out" 2> "$W/iw.err" # RED WITNESS: this goes red the day brintos/linux#33 lands, which is exactly # when every functional case this port would have had becomes writable. check_out $P/inotify-init-enosys \ "Couldn't initialize inotify: Function not implemented" cat "$W/iw.err" else bst_skip $P/inotifywait-present "inotify-tools not installed on this image" bst_skip $P/inotify-init-enosys "no inotifywait to probe the syscall with" fi # ...while THE KERNEL HALF IS BUILT. inotify_user_setup() registers # max_user_watches, so reading a number back is a direct probe of the subsystem # rather than an inference from a tool failure -- and it is what says #33 is a # wiring gap rather than a missing CONFIG_INOTIFY_USER. # # Assert on the READ, not on `test -r`: access(2) answers from the inode alone # and has been seen to succeed on this image for a file whose read then fails, # so a `test -r' case flaps between green and red across runs. Same reading # 99-inotify-tools takes. _mw=$(cat /proc/sys/fs/inotify/max_user_watches 2>&1) case "$_mw" in ''|*[!0-9]*) bst_notok $P/kernel-inotify-built "want a number from /proc/sys/fs/inotify/max_user_watches, got: $(_bst_1line "$_mw")" ;; *) bst_ok $P/kernel-inotify-built ;; esac # --- 7. 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 bst_probe $P/probe-run "$W/probe" 900 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 died before its first line; every CASE below is unscorable" fi check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "104" "$(pv emitted_count)" # The probe read the SAME copy this suite has been asserting against. Without # this case every measurement below is a statement about whatever was first on # sys.path. check_eq $P/probe-site-dir "$SITE" "$(pv site_dir)" check_eq $P/probe-pkg-dir "yes" "$(pv pkg_dir_found)" for _m in __init__ library calls constants adapters; do check_eq $P/probe-sha256-$_m \ "$(sha256sum "$PKG/$_m.py" 2>/dev/null | cut -d' ' -f1)" "$(pv sha256_$_m)" done check_eq $P/probe-bytes-adapters "13198" "$(pv bytes_adapters)" check_eq $P/probe-lines-adapters "395" "$(pv lines_adapters)" check_eq $P/probe-bytes-library "173" "$(pv bytes_library)" check_eq $P/probe-lines-library "8" "$(pv lines_library)" # --------------------------------------- the boundary, from inside python ---- # RED WITNESSES for GAP 2 (brintos/hardwarejs#42; refused for TLS per hwjs-cc#106). The three modules that do not # import, each named, because the chain dies in library.py and a reader who has # only the first name would look for the defect in adapters.py. for _m in library calls adapters; do _v=$(pv import_$_m) case "$_v" in "OSError: /lib/libc.so."*": [policy -> E"*"side module owns TLS"*) bst_ok $P/probe-import-$_m ;; *) bst_notok $P/probe-import-$_m "want the hardwarejs#42 libc load refusal, got: $(_bst_1line "$_v")" ;; esac done # GAP 1, CLOSED: ctypes and ctypes.util import, and _ctypes is a builtin of the # one static binary. check_eq $P/probe-ctypes-import "imported" "$(pv ctypes_import)" check_eq $P/probe-ctypes-util "imported" "$(pv ctypes_util_import)" check_eq $P/probe-ctypes-not-builtin "present" "$(pv ctypes_builtin)" # ...and the pure-Python ctypes package is what python3-ctypes ships. See # section 3. check_eq $P/probe-ctypes-pkg-present "yes" "$(pv ctypes_pkg_present)" # ------------------------------------------------------ the sound subset ---- # THESE TWO IMPORT, and that is this port's one genuinely working fraction. # `import inotify' and its version are the ticket's "the module reports the # version the recipe pins" -- which for a git recipe is the distribution's # 0.2.10 rather than the .deb's git-r0. Both numbers are asserted, in different # places, on purpose (section 2). check_eq $P/probe-inotify-import "imported" "$(pv inotify_import)" check_eq $P/probe-inotify-version "0.2.10" "$(pv inotify_version)" check_eq $P/probe-constants-import "imported" "$(pv constants_import)" # EVERY OTHER MODULE THE PACKAGE IMPORTS IS PRESENT. A red here would mean the # image is missing more than ctypes and that this suite is understating the gap. for _m in logging select os struct collections time errno; do check_eq $P/stdlib-$_m "ok" "$(pv stdlib_$_m)" done # `select.epoll' specifically: Inotify.__init__ registers the inotify fd on an # epoll immediately after inotify_init(), so epoll is a SECOND kernel interface # this port needs. It is present in the interpreter; whether it works on this # guest is a question this suite cannot reach past GAP 2, and one #33 closing # would put next in line. check_eq $P/select-has-epoll "yes" "$(pv select_has_epoll)" # struct inotify_event's header, computed from the format string adapters.py # declares. 16 bytes on every ILP32 and LP64 Linux, and it is what # _handle_inotify_event slices the read(2) buffer by -- so a disagreement here # would break event parsing even with the syscalls wired. It agrees with the # kernel's own struct: __s32 wd + __u32 mask + __u32 cookie + __u32 len. check_eq $P/header-struct-format "iIII" "$(pv header_struct_format)" check_eq $P/header-struct-length "16" "$(pv header_struct_length)" # ------------------------- THE EVENT MASK, AGAINST THE KERNEL'S OWN NUMBERS -- # Every literal below is from THIS BUILD's include/uapi/linux/inotify.h, and # IN_CLOEXEC/IN_NONBLOCK from include/uapi/asm-generic/fcntl.h (wasm32 adds no # arch override, so O_CLOEXEC is 02000000 and O_NONBLOCK 00004000). The values # on the right are what the module DEFINES, read out of a running interpreter. check_eq $P/const-in-access "1" "$(pv const_IN_ACCESS)" check_eq $P/const-in-modify "2" "$(pv const_IN_MODIFY)" check_eq $P/const-in-attrib "4" "$(pv const_IN_ATTRIB)" check_eq $P/const-in-close-write "8" "$(pv const_IN_CLOSE_WRITE)" check_eq $P/const-in-close-nowrite "16" "$(pv const_IN_CLOSE_NOWRITE)" check_eq $P/const-in-open "32" "$(pv const_IN_OPEN)" check_eq $P/const-in-moved-from "64" "$(pv const_IN_MOVED_FROM)" check_eq $P/const-in-moved-to "128" "$(pv const_IN_MOVED_TO)" check_eq $P/const-in-create "256" "$(pv const_IN_CREATE)" check_eq $P/const-in-delete "512" "$(pv const_IN_DELETE)" check_eq $P/const-in-delete-self "1024" "$(pv const_IN_DELETE_SELF)" check_eq $P/const-in-move-self "2048" "$(pv const_IN_MOVE_SELF)" check_eq $P/const-in-unmount "8192" "$(pv const_IN_UNMOUNT)" check_eq $P/const-in-q-overflow "16384" "$(pv const_IN_Q_OVERFLOW)" check_eq $P/const-in-ignored "32768" "$(pv const_IN_IGNORED)" check_eq $P/const-in-onlydir "16777216" "$(pv const_IN_ONLYDIR)" check_eq $P/const-in-dont-follow "33554432" "$(pv const_IN_DONT_FOLLOW)" check_eq $P/const-in-mask-add "536870912" "$(pv const_IN_MASK_ADD)" check_eq $P/const-in-isdir "1073741824" "$(pv const_IN_ISDIR)" check_eq $P/const-in-oneshot "2147483648" "$(pv const_IN_ONESHOT)" check_eq $P/const-in-cloexec "524288" "$(pv const_IN_CLOEXEC)" check_eq $P/const-in-nonblock "2048" "$(pv const_IN_NONBLOCK)" check_eq $P/const-in-close "24" "$(pv const_IN_CLOSE)" check_eq $P/const-in-move "192" "$(pv const_IN_MOVE)" check_eq $P/const-in-all-events "4095" "$(pv const_IN_ALL_EVENTS)" # The composites, recomputed by the probe rather than restated: a release that # changed one member and not the union would pass every literal above. check_eq $P/const-close-composite "yes" "$(pv const_close_composite)" check_eq $P/const-move-composite "yes" "$(pv const_move_composite)" check_eq $P/const-all-events-composite "yes" "$(pv const_all_events_composite)" # MASK_LOOKUP is the bit->name table _get_event_names walks to turn a raw mask # into the strings a caller matches on. A name missing from it is an event the # adapter would ASSERT on rather than report -- _get_event_names ends in # `assert event_type == 0'. # # IT HAS 21 ENTRIES FOR 22 NAMES, AND THAT IS AN UPSTREAM COLLISION rather than # a packaging accident: IN_NONBLOCK is O_NONBLOCK, 00004000 = 2048, and # IN_MOVE_SELF is 0x800 = 2048. Both are keys in the same dict literal, the # later wins, and the table therefore resolves 2048 to 'IN_MOVE_SELF'. The two # belong to different argument positions -- IN_NONBLOCK is an inotify_init1(2) # flag and IN_MOVE_SELF an event bit -- so nothing misbehaves in practice, but # the table cannot name the flag. MEASURED on x86-64 against the byte-identical # module; `mask_lookup_missing' reports exactly IN_NONBLOCK and nothing else, # which is what makes this a known collision rather than a table with a hole in # it. We file nothing upstream: docs/conventions/no-public-upstream-reports. check_eq $P/mask-lookup-len "21" "$(pv mask_lookup_len)" check_eq $P/mask-lookup-missing "IN_NONBLOCK" "$(pv mask_lookup_missing)" # ------------------------------------------------ the shipped API, by source -- # What landed really is PyInotify 0.2.10's API and not some other package named # inotify. These are parsed, not executed -- see the header for why there is no # alternative for these three modules on this image. for _m in __init__ library calls constants adapters test_support; do check_eq $P/probe-ast-parse-$_m "ok" "$(pv ast_parse_$_m)" done # library.py: eight lines, and the module the whole chain dies in. check_eq $P/ast-library-imports "ctypes,ctypes.util" "$(pv library_imports)" check_eq $P/ast-library-ctypes-line "1" "$(pv library_ctypes_line)" check_eq $P/ast-library-find-library-line "4" "$(pv library_find_library_line)" check_eq $P/ast-library-find-library-arg "c" "$(pv library_find_library_arg)" # THE LOAD AT MODULE SCOPE, at line 8 -- the single fact that says this package # has no lazily-reachable fraction behind GAP 1. It is not a lazy loader and # there is no import-time guard around it. check_eq $P/ast-library-load-module-scope "yes" "$(pv library_loadlibrary_module_scope)" check_eq $P/ast-library-load-line "8" "$(pv library_loadlibrary_line)" # ...and the SONAME the fallback asks for, which is the name section 5 asserts # nothing on this rootfs answers to. check_eq $P/ast-library-fallback-soname "libc.so.6" "$(pv library_fallback_soname)" # calls.py: the libc entry points the package binds. THERE ARE THREE, AND # inotify_init1 IS NOT AMONG THEM -- this package binds the older zero-argument # inotify_init(2). On a generic-syscall-ABI arch like wasm32 the kernel offers # only inotify_init1, and glibc's inotify_init() is the wrapper that calls it # with flags=0; so the syscall #33 has to wire is inotify_init1 either way. check_eq $P/ast-calls-libc-symbols \ "inotify_add_watch,inotify_init,inotify_rm_watch" "$(pv calls_libc_symbols)" check_eq $P/ast-calls-libc-symbol-count "3" "$(pv calls_libc_symbol_count)" check_eq $P/ast-calls-imports "ctypes,inotify.library,logging" "$(pv calls_imports)" check_eq $P/ast-calls-classes "InotifyError" "$(pv calls_classes)" check_eq $P/ast-calls-funcs \ "_check_nonnegative,_check_nonzero,_check_zero" "$(pv calls_funcs)" # THE errno DEFECT, from the source, both halves. InotifyError reads # ctypes.get_errno(); library.py never asks for use_errno=True; therefore every # InotifyError this package can raise carries ERRNO=(0). Measured on x86-64 -- # see the header -- and this is what pins it so the finding cannot be lost. check_eq $P/ast-calls-uses-get-errno "yes" "$(pv calls_uses_get_errno)" check_eq $P/ast-calls-use-errno-requested "no" "$(pv calls_use_errno_requested)" # adapters.py: the API a caller reaches for. check_eq $P/ast-adapters-imports \ "collections,errno,inotify.calls,inotify.constants,logging,os,select,struct,time" \ "$(pv adapters_imports)" check_eq $P/ast-adapters-calls-line "11" "$(pv adapters_calls_line)" check_eq $P/ast-adapters-classes \ "EventTimeoutException,Inotify,InotifyTree,InotifyTrees,TerminalEventException,_BaseTree" \ "$(pv adapters_classes)" # NO MODULE-LEVEL FUNCTIONS AT ALL -- everything is a method. A caller reaching # for a convenience function will not find one. check_eq $P/ast-adapters-funcs "" "$(pv adapters_funcs)" check_eq $P/ast-adapters-inotify-methods \ "__del__,__get_block_duration,__init__,_get_event_names,_handle_inotify_event,add_watch,event_gen,last_success_return,remove_watch,remove_watch_with_id" \ "$(pv adapters_inotify_methods)" check_eq $P/ast-adapters-basetree-methods \ "__init__,event_gen,inotify" "$(pv adapters_basetree_methods)" check_eq $P/ast-adapters-tree-methods \ "__init__,__load_tree" "$(pv adapters_tree_methods)" check_eq $P/ast-adapters-trees-methods \ "__init__,__load_trees" "$(pv adapters_trees_methods)" # The namedtuple one event header is unpacked into, IN FIELD ORDER -- a caller # reading header.cookie to correlate a MOVED_FROM/MOVED_TO pair depends on it. check_eq $P/ast-adapters-event-fields "wd,mask,cookie,len" \ "$(pv adapters_event_fields)" check_eq $P/ast-adapters-header-format "iIII" "$(pv adapters_header_format)" check_eq $P/ast-adapters-block-duration "1" "$(pv adapters_block_duration)" # The two events that are raised as TerminalEventException rather than yielded. check_eq $P/ast-adapters-terminal-events "IN_Q_OVERFLOW,IN_UNMOUNT" \ "$(pv adapters_terminal_events)" rm -rf "$W"