. "$BST_LIB" # BST-TIMEOUT: 2400 # SIX python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps the transcript one of those six # # left behind. # # # # WHERE THE SIX GO: the probe (ONE start for seventy-one # # measurements), a bare `import', the observer-selection # # start whose STDERR is the assertion, the threads-boundary # # witness -- separate for the reason given at that case -- # # and two watchmedo runs, which are script starts and cost # # an interpreter each. # # # # The work inside the probe is a few dozen os.stat calls # # over a six-entry tmpfs tree. Microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 2400 is 400s per start, the same per-start budget # # 99-python3-configargparse sets for its three, and roughly # # six times the ~60s a python3 start has measured at here. # # Nothing in this suite is quadratic in anything, so the # # start is the only thing the number has to cover. # # # # 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-watchdog 4.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-watchdog_4.0.0.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image -- filesystem-events monitoring # with a pluggable observer per platform. # # ============================================================================ # THE TICKET ASKED THIS SUITE TO CATCH A SILENT DEGRADATION TO POLLING. THERE # IS NONE NOW -- `Observer' IS THE INOTIFY ONE, AND IT CANNOT START. # ============================================================================ # # BRINTOS-2518 put this package in the round because it carries its own control: # it ships an InotifyObserver and a PollingObserver and picks by platform, so # asserting that `Observer()' is the inotify one is the cheapest guard against a # binding that quietly falls back to a portable path and passes anyway. # # On this image neither observer can run, and the suite says why in three # separate places rather than reporting one symptom: # # 1. ctypes WORKS, SO THE INOTIFY OBSERVER IS SELECTED. `observers/inotify_c.py' # is a ctypes binding over libc -- module-scope `import ctypes' and # `ctypes.CDLL(None)'. Until BRINTOS-2269 _ctypes was configured out, that # import raised, and the layer patch's ImportError/OSError arm fell back to # polling with a warning. Now the import succeeds, CDLL(None) exports # inotify_init, and upstream's own selection binds InotifyObserver with no # warning. The patched arm is still there; it simply no longer fires. # # 2. NO inotify SYSCALL, so #1 buys nothing. Every inotify entry point is # ENOSYS here (brintos/linux#33) -- unwired in both arch/wasm32's # sys_call_table and the hardwarejs worker, measured under BRINTOS-161 # where inotify-tools scores 7/33 against 33/33 native. InotifyEmitter's # on_thread_start() calls inotify_init() and raises OSError, and BaseThread # runs that BEFORE threading.Thread.start() -- so `observer.start()' fails # here with OSError, one step ahead of #3. # # 3. NO THREADS AT ALL. `threading.Thread.start()' RAISES here -- the guest # refuses the thread-mode clone with EAGAIN (`wasm32: REFUSING thread-mode # clone (CLONE_VM, ...) with EAGAIN') and CPython surfaces that as # `RuntimeError: can't start new thread' (brintos/linux-in-the-browser#85). # The start is what fails, one step before any body could exist # (BRINTOS-2506). EVERY observer watchdog ships is a thread -- BaseObserver # is an EventDispatcher is a BaseThread, and each watch gets an # EventEmitter thread of its own -- so the polling observer would refuse # too, with this RuntimeError. # # ============================================================================ # WHAT IS THEREFORE ASSERTED: THE MACHINERY, DRIVEN IN THE CALLING THREAD. # ============================================================================ # # A suite that scheduled a watch and waited for a callback would wait forever # and be killed at its bound with nothing to say. What the probe does instead is # drive the three calls those threads exist to make: # # * `PollingEmitter.on_thread_start()' -- the baseline snapshot; # * `PollingEmitter.queue_events(0)' -- one turn of the emitter's run loop; # * `BaseObserver.dispatch_events()' -- one turn of the observer's. # # Together they are exactly what a started observer performs between two ticks, # and they reproduce the whole scenario the ticket asks for -- created, # modified, moved, deleted, recursive versus non-recursive, a directory created # after the watch began, and the teardown -- with no thread anywhere. Same # disposition 99-python3-apscheduler took to `_process_jobs()' on this image. # # The threaded path is measured too, as a witness, in a SEPARATE python3 start. # # ============================================================================ # TWO FACTS A READER WOULD GUESS WRONG. # ============================================================================ # # 1. `watchmedo' RUNS. The entry point is declared `watchdog.watchmedo:main # [watchmedo]' -- with an extra marker -- and the PACKAGECONFIG that gates # that extra is turned off by the layer, so a reader expects the script to # refuse. It does not: setuptools' generated console script validates no # extra, and upstream's `import yaml' is LAZY, inside `load_config()' and # `tricks_generate_yaml()' alone. Its yaml subcommands refused until # BRINTOS-2472 put python3-pyyaml on the image through # packagegroup-brintos-extra; now they answer too. Both are asserted. # # 2. The parent directory's own DirModifiedEvent is NOT asserted anywhere # here. `DirectorySnapshotDiff' decides `dirs_modified' by comparing a # directory's st_mtime between two snapshots, so whether creating a file # inside one produces such an event is a property of the filesystem's # directory-mtime granularity rather than of watchdog. On the x86-64 # control's /tmp the two snapshots read the same st_mtime to the # nanosecond and no event is produced. A case either way would be # asserting something about the guest's tmpfs. P=python3-watchdog SUITE_TIMEOUT_HINT=2400 bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/watchdog MOD=$PKGDIR/__init__.py DI=$SITE/watchdog-4.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-watchdog.list PROBE=$BST_DATA/python3-watchdog-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-watchdog.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------- the artifact check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/module-file test -f "$MOD" # A PACKAGE directory, not a flat module. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/watchdog.py" ]; then echo present; else echo absent; fi)" # THE FOUR SUBPACKAGES `find_packages(SRC_DIR)' produces, each named. `import # watchdog' succeeds while any of them is missing -- __init__.py is empty -- # so a dropped one would present as a failing observer rather than a failing # import. for _f in events version watchmedo; do check $P/module-$_f test -f "$PKGDIR/$_f.py" done for _d in observers utils tricks; do check $P/subpackage-$_d test -f "$PKGDIR/$_d/__init__.py" done # THE OBSERVER MODULES SHIP WHETHER OR NOT THEY IMPORT, and the two questions # are asked in different places for that reason: this is packaging, and # `inotify-import-refused' below is the platform. The macOS/Windows/BSD ones # ship too and are never reached, sys.platform being "linux". for _m in api polling inotify inotify_c inotify_buffer kqueue fsevents winapi; do check $P/observer-module-$(echo "$_m" | tr _ -) test -f "$PKGDIR/observers/$_m.py" done check $P/typing-marker test -f "$PKGDIR/py.typed" check $P/pycache-dir test -d "$PKGDIR/__pycache__" # THE ONE EXECUTABLE. Upstream declares one console script and the layer's # PACKAGECONFIG line does not remove it -- see the header's first surprise. check $P/watchmedo-present test -f /usr/bin/watchmedo check $P/watchmedo-executable test -x /usr/bin/watchmedo # ---------------------------------------------------------------- the package # The file list is COPIED to $TMPDIR before it is read, because the `while read' # below takes it on stdin: a top-level stdin redirect on the virtiofs root makes # dup2(2) return 513 and `ash' exits, ending the suite in silence. # # BRINTOS-WORKAROUND: brintos/linux#73 remove cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION. This image's python3 is one static binary with an empty # lib-dynload and no dynamic loader, so a `.so' here could not be loaded at all. # Upstream DOES build one -- `_watchdog_fsevents' -- but only on Darwin, and # setup.py gates it on `sys.platform == "darwin"', so a `.so' appearing here # would mean the wheel was cut on a machine whose build backend ignored that. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # Everything shipped is Python source, its bytecode, the py.typed marker, # dist-info metadata or the one script. A file outside that set is a compiled # artifact or a stray. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */py.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; /usr/bin/watchmedo) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" sed -n '/^Package: python3-watchdog$/,/^$/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: 4.0.0-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 WHOLE RUNTIME CLOSURE, BY NAME. meta-python declares none of these -- the # recipe carries no RDEPENDS line at all -- so all four beyond python3-core come # from the layer's bbappend, measured by refusal on an x86-64 control. Naming # each one separately means a dropped entry names itself in the red. for _d in core ctypes logging stringold threading; do check_out $P/pkg-depends-$_d "python3-$_d" cat "$W/dep" done # AND python3-pyyaml IS NOT ON IT. The stock recipe's `PACKAGECONFIG ??= # "watchmedo"' would put it there; poky's python3-pyyaml builds the Cython # extension `_yaml', which this interpreter cannot load, so the layer sets # `PACKAGECONFIG = ""'. This case is what says that took -- nothing else on the # image would notice a fifth entry. check_eq $P/pkg-no-pyyaml-depends "" \ "$(grep '^Depends:' "$W/st" 2>/dev/null | grep 'python3-pyyaml')" # python3-pyyaml IS on the image all the same, from packagegroup-brintos-extra # (BRINTOS-2472) rather than from this package -- which is why the watchmedo # yaml subcommands below answer. check_eq $P/pkg-pyyaml-installed "present" \ "$(if grep -q '^Package: python3-pyyaml$' /var/lib/dpkg/status 2>/dev/null; \ then echo present; else echo absent; fi)" # NO -ptest PACKAGE AND NO UPSTREAM TESTS. Unlike several neighbours the recipe # does not even `inherit ptest', and upstream's `tests/' sits beside `src/' # rather than inside it, so `find_packages("src")' cannot reach it. Asserted of # THIS PACKAGE'S OWN FILE LIST, not of the site-packages `tests' path, which is # a shared namespace another distribution may occupy -- on this image # python3-reedsolo does. check_eq $P/pkg-no-ptest-status "absent" \ "$(if grep -q '^Package: python3-watchdog-ptest$' /var/lib/dpkg/status 2>/dev/null; \ then echo present; else echo absent; fi)" check_eq $P/pkg-no-tests-module "" \ "$(grep -E '/site-packages/tests(/|$)' "$W/pkgf" 2>/dev/null)" # ------------------------------------------------------------- the metadata 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.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: watchdog" cat "$DI/METADATA" check_out $P/dist-info-wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-wheel-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-wheel-generator "bdist_wheel" cat "$DI/WHEEL" check_eq $P/dist-info-top-level "watchdog" "$(cat "$DI/top_level.txt" 2>/dev/null)" check $P/dist-info-license test -f "$DI/LICENSE" check $P/dist-info-record test -f "$DI/RECORD" # PyYAML IS AN EXTRA IN THE WHEEL METADATA AND A DEPENDENCY NOWHERE. This is the # upstream half of the PACKAGECONFIG decision, and it is asserted here rather # than inferred: an unconditional `Requires-Dist: PyYAML' with no `; extra ==' on # it would mean the layer had declined a real dependency. check_out $P/dist-info-pyyaml-is-extra "Requires-Dist: PyYAML >=3.10 ; extra == 'watchmedo'" \ cat "$DI/METADATA" check_out $P/dist-info-provides-extra "Provides-Extra: watchmedo" cat "$DI/METADATA" # The console-script declaration, WITH its extra marker -- which is what makes # `watchmedo-version' below a surprise rather than a formality. check_out $P/dist-info-entry-point "watchmedo = watchdog.watchmedo:main [watchmedo]" \ cat "$DI/entry_points.txt" # ----------------------------------------------------------------- the import check $P/import python3 -c 'import watchdog, watchdog.version; watchdog.version.VERSION_STRING' # The module resolves out of site-packages rather than a stray on the path. check_out $P/import-from-site "$PKGDIR" \ python3 -c 'import watchdog, os; print(os.path.dirname(watchdog.__file__))' # ============================================================================ # THE OBSERVER SELECTION, ON STDERR. # ============================================================================ # A SEPARATE START, because the assertion IS the stderr of an import and the # probe below deliberately captures its warnings instead of printing them. # # The layer patch's `Fall back to polling.' warning fired here while _ctypes # was configured out. Since BRINTOS-2269 `import ctypes' succeeds, the patched # arm does not fire and the import is silent. The port still does NOT become a # working inotify one: brintos/linux#33 holds, see `observer-thread-start-refused'. # # BRINTOS-WORKAROUND: brintos/linux#33 revisit -- the layer patch falls back to polling only when the inotify import fails, and with ctypes present the inotify observer binds and then refuses at start(). check_eq $P/observer-selection-silent "" \ "$(python3 -c 'import watchdog.observers' 2>&1)" # ------------------------------------------------------------------ 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 # 900s for one python3 start and a few dozen os.stat calls over a six-entry # tmpfs tree. The probe creates no thread, so nothing in it can block. 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_complete)" check_eq $P/probe-no-import-error "" "$(pv import_error)" # ------------------------------------------------- identity and provenance check_eq $P/version "4.0.0" "$(pv version)" check_eq $P/version-info "4.0.0" "$(pv version_info)" check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/module-is-package "yes" "$(pv module_is_package)" check_eq $P/python-version "3.12" "$(pv python_version)" check_eq $P/sys-platform "linux" "$(pv sys_platform)" # The metadata read through importlib rather than off the filesystem. check_eq $P/dist-version "4.0.0" "$(pv dist_version)" check_eq $P/dist-name "watchdog" "$(pv dist_name)" # ============================================================================ # THE PLATFORM BOUNDARY, IN THE ORDER A READER NEEDS IT. # ============================================================================ # `ctypes-importable' is the platform fact and is taken BEFORE watchdog is # imported at all, so it is a reading about the interpreter rather than about # this package's reaction to it. It was a red witness asserting `no' until # BRINTOS-2269 built _ctypes in. check_eq $P/ctypes-importable "yes" "$(pv ctypes_importable)" check_eq $P/ctypes-no-error "" "$(pv ctypes_error)" # The package's reaction to it, one level down: the inotify observer module # itself, imported directly rather than through the selection above. check_eq $P/inotify-import-ok "" "$(pv inotify_import_error)" check_eq $P/observers-import-ok "" "$(pv observers_import_error)" check_eq $P/observer-not-warned "0" "$(pv observers_warning_n)" check_eq $P/observer-no-fallback-warning "no" "$(pv observers_warning_is_fallback)" # WHAT `Observer' ACTUALLY IS. The ticket's first assertion, answered yes -- and # it still cannot run here (header, #2 and #3). Asserted from BOTH sides, # because "is inotify" and "is not polling" are separate claims and a future # third observer would satisfy only one of them. check_eq $P/observer-is-polling "no" "$(pv observer_is_polling)" check_eq $P/observer-is-inotify "yes" "$(pv observer_is_inotify)" check_eq $P/observer-class "InotifyObserver" "$(pv observer_class)" check_eq $P/observer-module "watchdog.observers.inotify" "$(pv observer_module)" check_eq $P/observers-all "Observer" "$(pv observers_all)" # The observer modules are all PRESENT -- the packaging half of the pair. for _m in inotify inotify-c inotify-buffer polling api; do check_eq $P/probe-has-module-$_m "yes" "$(pv has_module_$(echo "$_m" | tr - _))" done # ============================================================================ # THE EVENT MODEL. Pure Python, no thread, no syscall -- and entirely portable. # ============================================================================ check_eq $P/event-type-created "created" "$(pv event_type_created)" check_eq $P/event-type-deleted "deleted" "$(pv event_type_deleted)" check_eq $P/event-type-modified "modified" "$(pv event_type_modified)" check_eq $P/event-type-moved "moved" "$(pv event_type_moved)" check_eq $P/event-type-closed "closed" "$(pv event_type_closed)" check_eq $P/event-type-opened "opened" "$(pv event_type_opened)" # THE is_directory FLAG, on both sides of the one distinction it makes -- the # ticket asks for it on every delivered event, and it is a property of the # EVENT CLASS, so it is pinned here once rather than re-asserted per delivery. check_eq $P/event-file-is-not-dir "no" "$(pv file_created_is_dir)" check_eq $P/event-dir-is-dir "yes" "$(pv dir_created_is_dir)" check_eq $P/event-carries-src "/tmp/x" "$(pv file_created_src)" check_eq $P/event-moved-carries-dest "/tmp/b" "$(pv file_moved_dest)" # THE HANDLER'S DISPATCH TABLE, exercised rather than read. A port that # dispatched every event to on_any_event and none to the specific method would # pass an import test and silently break every caller. check_eq $P/dispatch-specific-methods "created,modified,moved,deleted" "$(pv dispatch_specific)" check_eq $P/dispatch-any-called-4 "4" "$(pv dispatch_any_n)" check_eq $P/pattern-matching-filters "keep.log" "$(pv pattern_matched)" # ============================================================================ # THE SCENARIO. Driven in the calling thread; see this file's header. # ============================================================================ check_eq $P/scenario-observer-not-started "no" "$(pv observer_started)" check_eq $P/scenario-emitter-class "PollingEmitter" "$(pv emitter_class)" # The emitter IS a thread class -- it simply is never started here. Asserted so # the sync drive cannot be mistaken for a different, non-threaded code path. check_eq $P/scenario-emitter-is-thread "yes" "$(pv emitter_is_thread)" check_eq $P/scenario-observer-is-base "yes" "$(pv observer_is_base)" check_eq $P/scenario-watch-path "yes" "$(pv watch_path_is_root)" check_eq $P/scenario-watch-recursive "yes" "$(pv watch_recursive)" # The four deliveries the ticket names, each over a real tmpfs tree. check_eq $P/event-created "yes" "$(pv created_file)" check_eq $P/event-modified "yes" "$(pv modified_file)" check_eq $P/event-moved "yes" "$(pv moved_file)" check_eq $P/event-moved-dest-path "a-renamed.txt" "$(pv moved_file_dest)" check_eq $P/event-deleted "yes" "$(pv deleted_file)" # A DIRECTORY deleted, so the is-directory flag is proved on a delivered event # and not only on a constructed one. check_eq $P/event-deleted-dir "yes" "$(pv deleted_dir)" # A DIRECTORY CREATED AFTER THE WATCH BEGAN, and a file inside it -- the # ticket's own case, and the one that says the snapshot is retaken rather than # fixed at schedule time. check_eq $P/event-created-dir-late "yes" "$(pv created_dir_late)" check_eq $P/event-created-in-late-dir "yes" "$(pv created_file_in_late_dir)" # RECURSIVE VERSUS NON-RECURSIVE, differing exactly where the tree nests. Three # cases, because either half alone passes on an observer that ignores the flag # in one direction: recursive sees the nested write, non-recursive does not, and # non-recursive still sees the top-level one. check_eq $P/recursive-sees-nested "yes" "$(pv recursive_created_nested)" check_eq $P/nonrecursive-blind-to-nested "no" "$(pv nonrecursive_created_nested)" check_eq $P/nonrecursive-sees-top "yes" "$(pv nonrecursive_created_top)" check_eq $P/nonrecursive-watch-flag "no" "$(pv watch2_recursive)" # UNSCHEDULE AND STOP, on the only observer teardown available here. The # emitter's join swallows the RuntimeError a never-started thread raises, so the # call is correct on this arm; a stray emitter left behind would show up as a # non-zero count. check_eq $P/teardown-emitters-before "1" "$(pv emitters_before_unschedule)" check_eq $P/teardown-emitters-after "0" "$(pv emitters_after_unschedule)" check_eq $P/teardown-watch-gone "yes" "$(pv watch_gone_after_unschedule)" check_eq $P/teardown-observer-not-alive "no" "$(pv observer_alive_after_stop)" # THE DIFFERENCE ENGINE BARE. Every delivery above is a property of this pair, # so a red here and a red above together say WHERE the port broke. check_eq $P/snapshot-created "f.txt" "$(pv snapshot_created)" check_eq $P/snapshot-deleted "e.txt" "$(pv snapshot_deleted)" check_eq $P/snapshot-has-root "yes" "$(pv snapshot_paths_has_root)" check_eq $P/snapshot-isdir "yes" "$(pv snapshot_is_dir_root)" check_eq $P/probe-workdir-removed "yes" "$(pv workdir_removed)" # ============================================================================ # THE THREADS BOUNDARY -- A SEPARATE python3 START. # ============================================================================ # Separate for the reason 99-python3-apscheduler's is: if `observer.start()' # HANGS rather than raising, this costs one case instead of the transcript # above. The witness ends with os._exit() rather than stopping the observer, # deliberately -- `BaseObserver.stop()' joins its emitters with NO timeout, so # on any arm where a thread starts without running, that join never returns. A # suite that wedges the guest costs every member of the batch its verify, not # just this port. # # WHAT ACTUALLY HAPPENS IS A REFUSAL, AND THE THREE OUTCOMES ARE KEPT APART. # `BaseObserver.start()' starts each emitter inside a `try/except Exception: # self._remove_emitter(emitter); raise', so the RuntimeError propagates and the # observer is left with no emitter -- a clean refusal, nothing wedged, and the # dispatcher thread never reached. `OBS:REFUSED:' is that; `OBS:SILENT' # would be a start that succeeded and delivered nothing, the state #85 was # wrongly described as producing; `OBS:DELIVERED' is a working arm. Collapsing # the first two is the conflation BRINTOS-2506 had to undo in # 99-python3-stopit, so they are separate values here from the start. # # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#85 revisit -- when a started # thread runs its body, a scheduled observer delivers on this image and these # cases invert. They are the signal that says so. bst_probe $P/threads-probe-run "$W/th" 300 python3 -c ' import os, sys, threading, time done = [] t = threading.Thread(target=lambda: done.append(1)) t.daemon = True try: t.start() except Exception as e: print("THREAD:REFUSED:" + type(e).__name__) else: t.join(5) print("THREAD:RAN" if done else "THREAD:REFUSED:silent") from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler seen = [] class H(FileSystemEventHandler): def on_any_event(self, event): seen.append(event.event_type) d = os.path.join(os.environ.get("TMPDIR", "/tmp"), "bst-wd-thr.%d" % os.getpid()) os.makedirs(d, exist_ok=True) obs = Observer(timeout=1) obs.schedule(H(), d, recursive=False) try: obs.start() except Exception as e: print("OBS:REFUSED:" + type(e).__name__) else: with open(os.path.join(d, "probe.txt"), "w") as f: f.write("x") time.sleep(8) print("OBS:DELIVERED" if seen else "OBS:SILENT") sys.stdout.flush() os._exit(0) ' check_eq $P/thread-does-not-run "" \ "$(grep '^THREAD:RAN' "$W/th" 2>/dev/null)" check_out $P/thread-refused "THREAD:REFUSED" cat "$W/th" check_eq $P/observer-thread-delivers-nothing "" \ "$(grep '^OBS:DELIVERED' "$W/th" 2>/dev/null)" # The exception TYPE is pinned here, unlike the two cases above. `Observer' is # the inotify one now, and its emitter's on_thread_start() -- run before the # thread is started -- calls inotify_init(), which fails (brintos/linux#33), so # the refusal is OSError rather than #85's RuntimeError. THREAD:REFUSED above # still witnesses #85 for a plain thread. # # BRINTOS-WORKAROUND: brintos/linux#33 revisit check_out $P/observer-thread-start-refused "OBS:REFUSED:OSError" cat "$W/th" # And NOT the other failure shape -- a start that succeeds and delivers nothing. # If this ever becomes the answer, #85 changed character and this header is wrong. check_eq $P/observer-thread-not-merely-idle "" \ "$(grep '^OBS:SILENT' "$W/th" 2>/dev/null)" # ============================================================================ # watchmedo -- THE DRIVER RUNS, AND ONLY ITS yaml SUBCOMMANDS REFUSE. # ============================================================================ # The ticket asks for watchmedo to be recorded either way rather than skipped. # It runs: the generated console script validates no extra, and `import yaml' is # lazy. Two starts, one for each half of that sentence. # `$out' FOLDS STDERR IN. The layer patch's fallback warning landed there on # every watchmedo start while _ctypes was absent; since BRINTOS-2269 it must # not -- the driver binds the same inotify observer the library does. bst_run 600 watchmedo --version check_eq $P/watchmedo-version-rc "0" "$rc" case "$out" in *"4.0.0"*) bst_ok $P/watchmedo-version ;; *) bst_notok $P/watchmedo-version "want:'4.0.0' got:'$out'" ;; esac case "$out" in *"Fall back to polling."*) bst_notok $P/watchmedo-no-fallback-warning "the driver still reports the polling fallback; got:'$(_bst_1line "$out")'" ;; *) bst_ok $P/watchmedo-no-fallback-warning ;; esac # The yaml half. `tricks-generate-yaml' reaches `import yaml' inside its own # function body; with python3-pyyaml on the image (BRINTOS-2472) it emits the # tricks document instead of refusing. bst_run 600 watchmedo tricks-generate-yaml check_eq $P/watchmedo-yaml-rc "0" "$rc" case "$out" in *"python-path:"*) bst_ok $P/watchmedo-yaml-generates ;; *) bst_notok $P/watchmedo-yaml-generates "the yaml subcommand emitted no tricks document; got:'$(_bst_1line "$out")'" ;; esac rm -rf "$W"