. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those three. # # # # They are a bare `import', a second one whose STDERR is the # # assertion (`import-quiet'), and the probe -- which takes # # ONE start for seventy-six measurements. The work inside the # # probe is struct packing and fewer than a hundred coroutine # # steps over pipes, microseconds beside an interpreter start # # on this arm, which is tens of seconds before the first line # # of the program runs. # # # # The exception is the `loop_*' group, which runs a genuine # # event loop; its timers total under a second of real time. # # # # 1200 is 400s per start, the same budget 99-python3-dateutil # # and 99-python3-asyncio-throttle set for their three. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that 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-asyncinotify 4.0.6 (meta-openembedded/meta-python # recipes-devtools/python/python3-asyncinotify_4.0.6.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # THE PACKAGE, AND THE THREE THINGS IT IS. # ============================================================================ # # asyncinotify is 651 + 43 lines of pure Python in two modules, and it is three # separable pieces stacked on each other: # # 1. A ctypes BINDING. `_ffi.py' resolves libc through # `ctypes.util.find_library("c")' at import time and declares four # prototypes on it -- inotify_init, inotify_init1, inotify_add_watch, # inotify_rm_watch -- each routed through a `check_return' that turns -1 # into OSError(errno). # # 2. An event-stream PARSER. `Inotify._get' reads `struct inotify_event' # records off the descriptor and builds Event objects: variable-length # names, NUL truncation inside a padded field, cookie pairing for the # move pair, watch lookup by wd, and the IGNORED/ONESHOT ownership # transfer from Inotify to Event. # # 3. A READINESS path. `_get_events_future' registers the descriptor with # `loop.add_reader()' and awaits a future the loop resolves, which is what # makes `async for event in inotify' work. # # ONLY THE FIRST OF THE THREE NEEDS THE SYSCALL, and that is the whole reason # this port ships rather than being parked. See the next section. # # ============================================================================ # TWO PLATFORM GAPS, BOTH OPEN, AND WHICH ONE ACTUALLY FIRES. # ============================================================================ # # INOTIFY IS ENOSYS ON THIS ARM -- brintos/linux#33, open. The subsystem is # compiled into vmlinux (CONFIG_INOTIFY_USER takes its upstream `default y' and # nothing in wasm32_defconfig turns it off) and its sysctls register at boot, # but no member of the family has an entry in # arch/wasm32/kernel/syscall_table.c and the hardwarejs worker does not answer # for it either. So there is no way to obtain a watch descriptor on this image. # # EPOLL IS ENOSYS TOO -- brintos/linux#53, open, and INDEPENDENT of the first. # `Inotify.__init__' calls `select.epoll()' unconditionally, one line after the # inotify call, so the object is unconstructible for two separate reasons. # # THE SUITE MEASURES BOTH AND REPORTS WHICH ONE FIRED. `__init__' runs # `inotify_init1' BEFORE `select.epoll()', so the errno the constructor reports # is inotify's and epoll is never reached -- `ctor-stage' asserts exactly that. # Naming the wrong one of two ENOSYS gaps is the easiest mistake available here, # and it is the one that would send the batch's other fs-notification members # after the wrong issue. If #33 closes while #53 stays open, `ctor-stage' flips # to `epoll' and nothing else in this file moves. # # WHAT SAVES THE OTHER TWO THIRDS. The parser reads through # `os.read(self.fd, ...)' and the readiness path calls `loop.add_reader(fd)'; # neither asks what KIND of descriptor it has. So both are driven over a PIPE # carrying `struct inotify_event' bytes the probe lays out by hand -- which # makes the expected values exact rather than whatever a kernel happened to # emit, and makes them reachable at all on an arm that will not open an inotify # fd. Every case that relies on the substitution says so in its own name # (`pipe-'/`loop-' groups below), so nothing here reads as a claim about the # kernel's inotify. # # AND THE READINESS HALF IS THE ONE THE TICKET ASKED FOR. BRINTOS-2517 calls out # that "a descriptor this arm hands out can be registered for readability and # actually wakes the loop" has never been asserted on target from Python. That # question is answerable over a pipe, and `loop-async-for', `loop-interleave', # `loop-quiet-timeout' and `loop-cancel-clean' answer it. What is NOT answered # is whether the kernel's inotify would deliver the right events; that is # attributed to #33 and is not dressed up as something this suite measured. # # THE LOOP RUNS AT ALL because `selectors._can_use' probes each backend by # CONSTRUCTING it inside try/except OSError rather than trusting the build-time # macro, so the epoll that #53 denies is refused at runtime and the selector # falls through to poll (`[7] = w_sys_poll') or select (`[23] = w_sys_select'). # `selector-not-epoll' asserts the fallback HAPPENED; the suite deliberately # does NOT pin WHICH fallback, because poll and select are both correct answers # and an exact value would red on a change that harms nothing. The same # reasoning, and the same measurement, as 99-python3-async-timeout. # # ============================================================================ # ONE BEHAVIOUR THE TICKET ASKED FOR THAT THE PACKAGE DOES NOT HAVE. # ============================================================================ # # THERE IS NO `__version__'. The ticket asks the module to report "the version # the recipe pins". flit takes the version from pyproject.toml and # asyncinotify/__init__.py never defines the attribute, so the only answer is # the installed distribution's metadata. `version-attr-absent' asserts the # absence and `dist-version' reads the real one, so a release that ADDS # `__version__' is a red here rather than a surprise to a caller who copied # this suite. # # ============================================================================ # ONE UPSTREAM WART, VISIBLE ONLY ON AN ARM THAT REFUSES INOTIFY. # ============================================================================ # # `Inotify.__init__' assigns `self._fd' only AFTER `inotify_init1' returns, so # where that call raises, the half-built object reaches `__del__' with the slot # unset and `close()' raises AttributeError inside the destructor. CPython # routes that to sys.unraisablehook and prints a traceback. It is upstream's, # it is reachable only where #33 is open, and we file nothing against # third-party public repositories -- so `ctor-del-unraisable' records it here # and a release that fixes the ordering becomes a visible change rather than a # silence. # # ============================================================================ # THE CONTROL. # ============================================================================ # # Every expected value below was read off an x86-64 CPython 3.12.14 -- the # build's own python3-native, the SAME minor version the target ships -- # running THIS BUILD'S wasm32 .deb contents on PYTHONPATH. Measured three ways: # stock (EpollSelector, working inotify), with `select.epoll' AND the four # inotify entry points forced to raise ENOSYS (PollSelector -- this arm), and # with poll knocked out on top (SelectSelector). Every value was IDENTICAL # across all three except the selector's name and the gap group itself. That is # why the expected values here can be read off a control at all: the package's # behaviour does not depend on the backend, only on there being one. # # The packaging values were read off the built .deb itself # (python3-asyncinotify_4.0.6-r0_wasm32.deb), not off the recipe. # # NOTHING HERE ASSERTS A DURATION OR AN IntFlag repr(). Durations measure the # host's load; IntFlag's repr and str changed in 3.11 and its member ordering # is not a stable contract. Names, integer values and orderings are what get # asserted. P=python3-asyncinotify # A suite that emits nothing is reported as no-cases with no cause. This lands # one line before anything can go wrong, so the file is never silent. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/asyncinotify DI=$SITE/asyncinotify-4.0.6.dist-info PKGLIST=/var/lib/dpkg/info/python3-asyncinotify.list PROBE=$BST_DATA/python3-asyncinotify-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-asyncinotify.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --- the files the packaging promises --------------------------------------- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/asyncinotify.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/ffi-file test -s "$PKGDIR/_ffi.py" # py.typed is the PEP 561 marker and is legitimately EMPTY -- `test -s' would # fail on it, which is why it is the one file here asserted with `test -f'. check $P/py-typed test -f "$PKGDIR/py.typed" check $P/py-typed-empty test ! -s "$PKGDIR/py.typed" check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-ffi test -s "$PKGDIR/__pycache__/_ffi.cpython-312.pyc" # Two modules, no more: a release that split the parser out would land here. check_eq $P/module-count "2" \ "$(find "$PKGDIR" -maxdepth 1 -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" # A library, not a tool: no console script, and flit emits no entry_points.txt. check $P/no-cli test ! -e /usr/bin/asyncinotify check $P/no-entry-points test ! -f "$DI/entry_points.txt" # --- the package as dpkg sees it -------------------------------------------- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Pure Python: nothing compiled ships, on this arm or any other. A .so here # would mean the recipe grew a native extension nobody asked for. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" check_eq $P/tree-no-so "" \ "$(find "$PKGDIR" -name '*.so*' 2>/dev/null)" # Every shipped file is a .py, a .pyc, the py.typed marker, or dist-info. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */py.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # The recipe splits a -dev package that is EMPTY -- one directory, no file -- # so only the main package is on the image and naming -dev would add a package # with nothing in it. Measured on the built .deb. check_eq $P/no-dev-installed "" \ "$(sed -n '/^Package: python3-asyncinotify-dev$/p' /var/lib/dpkg/status 2>/dev/null)" # --- dist-info: flit's, not setuptools' ------------------------------------- # The recipe inherits python_setuptools_build_meta, but `python3 -m build # --no-isolation' reads the backend out of pyproject.toml, which names # flit_core. So flit built this wheel and the dist-info is flit's shape: a # LICENSE, no top_level.txt and no entry_points.txt. `dist-info-generator' # pins that, because a silent switch back to setuptools would change the # installed file set. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 4.0.6" cat "$DI/METADATA" check_out $P/dist-info-name "Name: asyncinotify" cat "$DI/METADATA" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-generator "Generator: flit" cat "$DI/WHEEL" check $P/dist-info-license test -s "$DI/LICENSE" check $P/dist-info-record test -s "$DI/RECORD" check $P/no-top-level test ! -f "$DI/top_level.txt" # --- the .deb installed on the running target ------------------------------- sed -n '/^Package: python3-asyncinotify$/,/^$/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" # All four RDEPENDS are interpreter subpackages shipped by the python3 recipe # itself, so nothing about this port waited on another. Read off the built # .deb's control stanza: `Depends: python3-asyncio, python3-core, # python3-ctypes, python3-io'. 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-asyncio "python3-asyncio" cat "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-ctypes "python3-ctypes" cat "$W/dep" check_out $P/pkg-depends-io "python3-io" cat "$W/dep" # --- import: the package that installs must also load ----------------------- check $P/import python3 -c 'import asyncinotify' check $P/import-inotify python3 -c 'from asyncinotify import Inotify' check $P/import-names python3 -c 'from asyncinotify import Inotify, Watch, Event, Mask, InitFlags' check $P/import-ffi python3 -c 'import asyncinotify._ffi' # THE IMPORT MUST BE SILENT. _ffi.py loads libc through ctypes at import time, # which is the one thing in this package that can warn or fail on a target # image, so stderr is the assertion rather than an afterthought. check_eq $P/import-quiet "" \ "$(python3 -c 'import asyncinotify' 2>&1)" check $P/import-asyncio python3 -c 'import asyncio' check $P/import-ctypes python3 -c 'import ctypes, ctypes.util' # --- 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 # probe_done is the last line the probe prints. Its absence means the file # stopped early -- which on this arm would mean the loop group hung, since # everything before it is flushed line by line. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "76" "$(pv emitted_count)" # --- identity --------------------------------------------------------------- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/ffi-module "asyncinotify._ffi" "$(pv ffi_module)" # The public surface, exactly. asyncinotify does not define __all__, so `dir()' # reports its imports alongside its own five classes -- that is the real # surface a `from asyncinotify import *' would take, and pinning it makes a # release that adds or drops a name a red rather than a surprise. check_eq $P/public-names \ "BytesIO,Callable,Dict,Event,Future,Generator,InitFlags,Inotify,IntFlag,List,Mask,Optional,Path,ReferenceType,TYPE_CHECKING,Union,Watch,cast,contextmanager,get_running_loop,os,select,weakref" \ "$(pv public_names)" # The ticket's "module reports the version the recipe pins", answered the only # way this package can answer it. See the header. check_eq $P/version-attr-absent "yes" "$(pv version_attr_absent)" check_eq $P/dist-version "4.0.6" "$(pv dist_version)" # --- the kernel struct ABI -------------------------------------------------- # `struct inotify_event' is int + three uint32 and carries no pointer, so it is # 16 bytes on LP64 and on this arm's ILP32 alike. A divergence here would # mis-parse every event rather than fail, which is why it is asserted field by # field rather than by total size alone. check_eq $P/abi-struct-size "16" "$(pv event_struct_size)" check_eq $P/abi-offsets "wd@0,mask@4,cookie@8,len@12" "$(pv event_offsets)" check_eq $P/abi-sizes "wd:4,mask:4,cookie:4,len:4" "$(pv event_field_sizes)" check_eq $P/abi-name-max "255" "$(pv name_max)" check_eq $P/abi-read-chunk "2720" "$(pv read_chunk)" # --- the ctypes binding ----------------------------------------------------- # find_library('c') shells out to ldconfig/gcc/objdump and may legitimately # answer None on a target image carrying none of them; CDLL(None) then opens # the main program's handle, which resolves libc because the interpreter is # dynamically linked against it. So the NAME is reported and not pinned, while # what actually matters -- that the handle loaded and all four entry points # resolved through it -- is asserted. check $P/libc-name-reported test -n "$(pv libc_name)" check_eq $P/libc-loaded "yes" "$(pv libc_loaded)" check_eq $P/sym-inotify-init "yes" "$(pv sym_inotify_init)" check_eq $P/sym-inotify-init1 "yes" "$(pv sym_inotify_init1)" check_eq $P/sym-inotify-add-watch "yes" "$(pv sym_inotify_add_watch)" check_eq $P/sym-inotify-rm-watch "yes" "$(pv sym_inotify_rm_watch)" # check_return is the errno helper all four go through: -1 becomes OSError # carrying ctypes' errno, anything else passes through untouched. check_eq $P/check-return-passthrough "7" "$(pv check_return_passthrough)" check_eq $P/check-return-raises "OSError:2" "$(pv check_return_raises)" # --- the two platform gaps, MEASURED ---------------------------------------- # Both arms are written. Today each is `ok' against an open issue; when the # issue closes the case becomes a `skip' that says so, and the ctor cases below # switch arms with it. That is what keeps this suite from silently continuing # to assert a gap that has been fixed. epoll_u=$(pv epoll_usable); epoll_e=$(pv epoll_errno) check_eq $P/epoll-attr-present "yes" "$(pv epoll_attr_present)" if [ "$epoll_u" = "no" ]; then if [ "$epoll_e" = "38" ]; then bst_ok $P/epoll-enosys else bst_notok $P/epoll-enosys "select.epoll() failed with errno=$epoll_e, not ENOSYS -- a new failure mode, not brintos/linux#53" fi elif [ "$epoll_u" = "yes" ]; then bst_skip $P/epoll-enosys "select.epoll() constructed -- brintos/linux#53 has closed; re-read this suite's header, the ctor cases below now take their other arm" else bst_notok $P/epoll-enosys "select.epoll is $epoll_u -- the interpreter shipped without epoll support, which is neither #53 nor the expected shape" fi # The fallback is what lets the loop group run at all. WHICH fallback is the # interpreter's business -- poll and select are both correct -- so only the # negative is asserted and the name is reported for the record. if [ "$epoll_u" = "yes" ]; then bst_skip $P/selector-not-epoll "epoll is usable again, so the selector is entitled to pick it -- brintos/linux#53 has closed" else check_eq $P/selector-not-epoll "no" "$(pv selector_is_epoll)" fi check $P/selector-reported test -n "$(pv selector_class)" ino_rc=$(pv inotify_init1); ino_e=$(pv inotify_init1_errno) if [ "$ino_rc" = "OSError:38" ]; then bst_ok $P/inotify-enosys elif [ "$ino_rc" = "fd" ]; then bst_skip $P/inotify-enosys "inotify_init1 returned a descriptor -- brintos/linux#33 has closed; the kernel round trip this suite attributes away is now reachable and should be added" else bst_notok $P/inotify-enosys "inotify_init1 answered '$ino_rc' (errno=$ino_e) -- neither a descriptor nor #33's ENOSYS" fi # --- the documented failure of the constructor ------------------------------ ctor=$(pv inotify_ctor); stage=$(pv inotify_ctor_stage) if [ "$ctor" = "OSError:38" ]; then bst_ok $P/ctor-enosys elif [ "$ctor" = "constructed" ]; then bst_skip $P/ctor-enosys "Inotify() constructed -- both brintos/linux#33 and #53 have closed" else bst_notok $P/ctor-enosys "Inotify() raised '$ctor', not the OSError(ENOSYS) #33 predicts" fi # WHICH gap fired. This is the case that keeps the batch's fs-notification # members pointed at the right issue; see the header. case "$stage" in inotify_init1) bst_ok $P/ctor-stage ;; epoll) bst_notok $P/ctor-stage "the constructor now dies at select.epoll(), not inotify_init1 -- brintos/linux#33 has closed while #53 has not; this suite's attribution needs re-reading" ;; constructed) bst_skip $P/ctor-stage "Inotify() constructed -- neither gap fired" ;; *) bst_notok $P/ctor-stage "constructor died at an unrecognised stage: '$stage'" ;; esac # The upstream destructor wart, reachable only while #33 is open. See header. del_u=$(pv ctor_del_unraisable) case "$stage:$del_u" in inotify_init1:AttributeError) bst_ok $P/ctor-del-unraisable ;; inotify_init1:*) bst_notok $P/ctor-del-unraisable "expected AttributeError from Inotify.__del__ on the failed-ctor path, got '$del_u'" ;; *) bst_skip $P/ctor-del-unraisable "the constructor did not fail at inotify_init1, so the half-built __del__ path is not reachable" ;; esac # --- the inotify UAPI bitmask ----------------------------------------------- # Mask is the kernel's bitmask transcribed into an IntFlag. A transcription # error would mis-classify events rather than fail, so all twenty-two members # are pinned by value in one case, and the two composites are pinned as unions # rather than as literals so a change to either half is caught. check_eq $P/mask-values \ "ACCESS:0x1,MODIFY:0x2,ATTRIB:0x4,CLOSE_WRITE:0x8,CLOSE_NOWRITE:0x10,OPEN:0x20,MOVED_FROM:0x40,MOVED_TO:0x80,CREATE:0x100,DELETE:0x200,DELETE_SELF:0x400,MOVE_SELF:0x800,UNMOUNT:0x2000,Q_OVERFLOW:0x4000,IGNORED:0x8000,ONLYDIR:0x1000000,DONT_FOLLOW:0x2000000,EXCL_UNLINK:0x4000000,MASK_CREATE:0x10000000,MASK_ADD:0x20000000,ISDIR:0x40000000,ONESHOT:0x80000000" \ "$(pv mask_values)" check_eq $P/mask-close "0x18" "$(pv mask_close)" check_eq $P/mask-move "0xc0" "$(pv mask_move)" check_eq $P/mask-close-union "yes" "$(pv mask_close_is_union)" check_eq $P/mask-move-union "yes" "$(pv mask_move_is_union)" # InitFlags takes its two values from os.O_*, so this also asserts that the # interpreter's O_CLOEXEC/O_NONBLOCK are the Linux ones on this arm. check_eq $P/initflags "CLOEXEC:0x80000,NONBLOCK:0x800" "$(pv initflags)" check_eq $P/initflags-match-os "yes" "$(pv initflags_match_os)" # --- the parser, over a pipe carrying bytes the probe laid out -------------- # The ticket's ordered event set: create, modify, close-write, the # moved-from/moved-to pair sharing one cookie, and delete -- each naming the # file. On a working kernel this is what a watch on a directory would emit for # `touch a; echo >a; mv a b; rm b'. Here the byte stream itself is the input, # which is the half of that sentence this arm can still answer. See the header. check_eq $P/pipe-parse-count "6" "$(pv parse_count)" check_eq $P/pipe-parse-masks "CREATE,MODIFY,CLOSE_WRITE,MOVED_FROM,MOVED_TO,DELETE" \ "$(pv parse_masks)" check_eq $P/pipe-parse-names "a.txt,a.txt,a.txt,a.txt,b.txt,b.txt" "$(pv parse_names)" check_eq $P/pipe-parse-paths \ "/watched/a.txt,/watched/a.txt,/watched/a.txt,/watched/a.txt,/watched/b.txt,/watched/b.txt" \ "$(pv parse_paths)" check_eq $P/pipe-parse-wds "1,1,1,1,1,1" "$(pv parse_wds)" # The cookie is the only thing tying the move pair together, and it is the one # field that means anything on exactly two of the six events. check_eq $P/pipe-cookies "0,0,0,42,42,0" "$(pv parse_cookies)" check_eq $P/pipe-move-paired "yes" "$(pv move_cookie_paired)" check_eq $P/pipe-nonmove-zero "yes" "$(pv nonmove_cookies_zero)" # `in' on an Event tests its mask and raises TypeError on anything that is not # a Mask -- the documented behaviour of Event.__contains__. check_eq $P/event-contains "yes" "$(pv event_contains)" check_eq $P/event-not-contains "yes" "$(pv event_not_contains)" check_eq $P/event-contains-type "TypeError" "$(pv event_contains_typeerror)" # Name handling is the parser's fiddliest corner: len 0 means the event is about # the watch itself (name None, path the watch path), a padded field is truncated # at the first NUL, and a NAME_MAX name survives whole. check_eq $P/pipe-name-count "3" "$(pv name_count)" check_eq $P/pipe-name-absent "None" "$(pv name_absent)" check_eq $P/pipe-name-truncated "short" "$(pv name_truncated)" check_eq $P/pipe-name-max-len "255" "$(pv name_max_len)" check_eq $P/pipe-name-absent-path "/w" "$(pv name_absent_path)" # The two ownership transfers. rm_watch() deliberately does NOT drop the watch; # the IGNORED event does, and it hands the caller a live reference rather than a # weakref so the watch cannot vanish underneath the event that reports it. # ONESHOT does the same on its single event. An ordinary event holds a weakref # and leaves the Inotify owning the watch. check_eq $P/pipe-ignored-transfer "1,live,dropped" "$(pv ignored_transfer)" check_eq $P/pipe-oneshot-transfer "live,dropped" "$(pv oneshot_transfer)" check_eq $P/pipe-weakref-watch "same,retained" "$(pv weakref_watch)" # Q_OVERFLOW carries wd -1, which matches no watch: Event.watch is None and # Event.path cannot be constructed, exactly as the docstrings promise. check_eq $P/pipe-overflow "1,None,None" "$(pv overflow_event)" # A directory event is CREATE|ISDIR, a composite the parser must keep whole. check_eq $P/pipe-isdir "yes,yes,0x40000100" "$(pv isdir_composite)" # An event for a wd nobody is watching yields watch None, not a KeyError. check_eq $P/pipe-unknown-wd "None,orphan" "$(pv unknown_wd)" # One read yields every record; get() returns one and banks the other five. check_eq $P/pipe-batched-read "CREATE,5" "$(pv batched_read)" # --- the object surface that needs no descriptor ---------------------------- # The constructor's defaults are assertable off its signature even where the # constructor itself cannot run -- which is how "the inotify descriptor is # non-blocking" gets answered on an arm that will not hand one out. The flags # the constructor WOULD pass are CLOEXEC|NONBLOCK; the descriptor the loop # actually gets is asserted non-blocking separately, below. check_eq $P/ctor-default-flags "0x80800" "$(pv ctor_default_flags)" check_eq $P/ctor-flags-nonblock "yes" "$(pv ctor_default_flags_nonblock)" check_eq $P/ctor-default-cache-size "10" "$(pv ctor_default_cache_size)" check_eq $P/ctor-default-sync-timeout "None" "$(pv ctor_default_sync_timeout)" check_eq $P/closed-fd "ValueError: Can not work with closed inotify" "$(pv closed_fd)" check_eq $P/watch-api "/a/b,/moved,11,0x300,live" "$(pv watch_api)" check_eq $P/watch-weakref "yes" "$(pv watch_weakref)" check_eq $P/api-present "" "$(pv api_present)" # --- the readiness path: what the ticket actually asked for ----------------- # `async for' over an Inotify, driven over a pipe. This is the assertion that a # descriptor this arm hands out can be registered with the event loop and # actually wakes it. See the header for why a pipe is the honest instrument # here and what it does not claim. check_eq $P/loop-async-for \ "CREATE:a.txt,MODIFY:a.txt,CLOSE_WRITE:a.txt,MOVED_FROM:a.txt,MOVED_TO:b.txt,DELETE:b.txt" \ "$(pv loop_async_for)" # THE LOOP MUST MAKE PROGRESS WHILE THE WATCH IS IDLE. A second coroutine ticks # on a timer and its three ticks must all land before the event. A loop blocked # inside the read returns the wrong ORDER rather than merely running slowly, # which is the point: a loop that is stuck looks exactly like a directory in # which nothing happened. The value is an ordering, not a duration. check_eq $P/loop-interleave "t0,t1,t2,e:late" "$(pv loop_interleave)" # A quiet watch must make wait_for raise at its deadline rather than hang. check_eq $P/loop-quiet-timeout "TimeoutError" "$(pv loop_quiet_timeout)" # A coroutine awaiting a watch cancels cleanly, the reader # `_get_events_future' registered is removed in its finally, and no task is # left pending. A leaked reader would leave the loop watching a descriptor # forever, which is the failure this case exists for. check_eq $P/loop-cancel-clean "CancelledError,removed,0" "$(pv loop_cancel_clean)" check_eq $P/loop-reader-removed "removed" "$(pv loop_reader_removed)" # A blocking descriptor in an event loop is the defect this group exists to # catch, so it is asserted on the descriptor the loop was actually given. check_eq $P/loop-fd-nonblocking "yes" "$(pv loop_fd_nonblocking)" check $P/loop-selector-reported test -n "$(pv loop_selector)" rm -rf "$W"