. "$BST_LIB" # BST-TIMEOUT: 900 # ONE python3 start, and that is the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output. # # 99-sip-module carries 600 for one start and 99-python3 # # 1800 for three; this sits just above the former because # # the one start here also compiles five patterns and walks # # a ten-path corpus, which is arithmetic beside the start. # # 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-pathspec 0.12.1 (poky meta/recipes-devtools/python, # python3-pathspec_0.12.1.bb, `inherit pypi setuptools3`) -- GITIGNORE-STYLE # PATTERN MATCHING OVER FILE PATHS, on the wasm32-hwjs glibc image. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY SAYING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module # carrying a compiled extension could therefore not be imported here -- not # "would be slower", could not be imported. pathspec is pure Python, which is # the entire reason it ports. # # So `module-no-so` and `pkg-no-so` are not boilerplate. They are a REGRESSION # TRIPWIRE on a version bump: pathspec grows no extension today, and the day a # release adds a compiled matcher these go red at INSTALL time rather than at # `import pathspec` on somebody else's ticket three rounds later. The assertion # is made from BOTH sides because the two fail differently -- a find catches a # file the package does not declare, the dpkg list catches a declared file that # is not Python. # # ============================================================================ # THE MATCHER IS DRIVEN, AND EVERY EXPECTATION BELOW WAS MEASURED. # ============================================================================ # # data/python3-pathspec-probe.py compiles a FIXED five-pattern gitwildmatch set # against a FIXED ten-path corpus and prints one key=value per line. The suite # asserts on that output. # # EVERY VALUE ASSERTED BELOW WAS MEASURED, on x86-64, against THIS EXACT PROBE # and pathspec 0.12.1 unpacked from the same pathspec-0.12.1.tar.gz this image's # recipe builds (recorded 2026-08-31, and kept in the ticket's design doc and # test-plan section BRINTOS-1834). They are not predictions and they are not # read off the source. What makes the control transferable is that all of it is # pure-Python and content-determined: a gitwildmatch pattern compiles to a # Python regex and the verdict depends on the pattern and the path string, not # on pointer width, byte order or the platform string. Nothing arch-dependent # is asserted here at all. # # THE FIVE PATTERNS ARE THE FIVE BEHAVIOURS THE TICKET CLOSES ON, and they are # deliberately interdependent -- the negation only means something because # `*.log` matched first, and the anchor only means something because the # unanchored patterns above it DO match at depth: # # *.log a plain glob app.log yes, logs/deep/app.log yes # build/ directory-only build/out.o yes, src/build/nested.o yes # docs/**/*.tmp a `**` pattern docs/a/b/c.tmp yes, docs/c.tmp yes # !keep.log a negation keep.log NO -- re-included # /top.txt root-anchored top.txt yes, sub/top.txt NO # # `docs/c.tmp` is the case worth reading twice: `**/` matches ZERO directories # as well as many, so the `**` pattern must match at depth 1. A matcher that # required at least one intervening directory would leave every other case here # green. `sub/top.txt` is its mirror -- the one path that must NOT match, and # the only thing separating an anchor from an ordinary glob. `notes.md` matches # nothing and is the control that says the matcher is not matching everything. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - The `gitignore` walk over a real directory tree. pathspec can match # against the filesystem, but this image's root is virtiofs and a walk would # be measuring the filesystem's latency rather than the matcher; the # corpus is a fixed list of strings for that reason. # - Pattern syntaxes other than gitwildmatch. It is the only one this round's # consumer (hatchling) uses, and the only one the recipe's dependents need. # - python3-pathspec-dev. It is empty on this build (measured: its data # archive carries `./` and nothing else) and the image does not name it. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-pathspec # THE CANARY, which asserts nothing about python3-pathspec. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require` guard deliberately, # because a `require` that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PS=$SITE/pathspec DI=$SITE/pathspec-0.12.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-pathspec.list D=$BST_DATA PROBE=$D/python3-pathspec-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pathspec.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import pathspec'` further down is # the functional statement; these are the ones that say WHICH copy answered, so # a module that imported out of some other path is distinguishable from one # that imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$PS" check $P/module-init test -f "$PS/__init__.py" # The source files the matcher is actually made of, named individually rather # than counted: a count moves on any upstream refactor, while each of these is # a documented part of pathspec's surface. `patterns/gitwildmatch.py` is the # one this port closes on -- it is the pattern class every case below compiles. check $P/module-pathspec test -f "$PS/pathspec.py" check $P/module-pattern test -f "$PS/pattern.py" check $P/module-util test -f "$PS/util.py" check $P/module-gitignore test -f "$PS/gitignore.py" check $P/module-gitwildmatch test -f "$PS/patterns/gitwildmatch.py" # py.typed is a SHIPPED FILE, not an artefact of the build: it is what marks # pathspec PEP 561 typed. It is also the one regular file in the package that is # neither .py nor metadata, so `pkg-only-python` below has to allow it -- named # here so that allowance is anchored on something asserted rather than assumed. check $P/module-py-typed test -f "$PS/py.typed" # THE DIST-INFO DIRECTORY CARRIES THE PINNED VERSION IN ITS NAME. This is a # second, independent statement of the version -- from the installed layout # rather than from the module's __version__ -- and the two disagreeing would # mean a mixed install that no single case could otherwise see. check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-metadata test -f "$DI/METADATA" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. See the header: on this # image a .so could not be loaded at all, so this is the case that fails a # version bump which grew one -- loudly, here, instead of at `import` later. check_eq $P/module-no-so "" \ "$(find "$PS" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # THE `-f` TEST IS LOAD-BEARING AND WAS MEASURED OFF THE BUILT PACKAGE. A dpkg # .list names DIRECTORIES too, and without a trailing slash, so a name-only # filter reports every directory in the package as an unexpected shape -- here # that is five entries (pathspec, pathspec/patterns, both __pycache__ dirs and # the dist-info dir) and this case would be red on a CORRECT package. # # `py.typed` is allowed by the *.typed arm, and is asserted present above so the # allowance cannot quietly cover its disappearance. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-pathspec is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list. The # anchor is exact: `python3-pathspec` is a PREFIX of `python3-pathspec-dev`, and # an unanchored match would report one stanza as the other. sed -n '/^Package: python3-pathspec$/,/^$/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 "0.12.1-r0" cat "$W/st" # THE RUNTIME DEPENDENCY THE RECIPE DECLARES. `RDEPENDS:${PN} += "python3-profile"` # is the only one pathspec carries that is not the interpreter core, and it is a # stdlib split of python3 itself -- so this case says the split was resolved and # installed rather than silently dropped, which is what would make `import # pathspec` fail on a path no case here walks. check_out $P/pkg-depends-profile "python3-profile" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If pathspec cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import pathspec' check $P/import-gitwildmatch python3 -c 'from pathspec.patterns.gitwildmatch import GitWildMatchPattern' # ------------------------------------------------------------- the probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `matched` and # `matched_count` distinguishable. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken matcher, when what actually happened is that no measurement was # taken. The word `unscorable` is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. 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 # A traceback the probe caught itself, surfaced as its own case rather than as # ten separate got:'' reds. check_eq $P/probe-no-fatal "" "$(pv fatal)" # THE VERSION THE RECIPE PINS, read from the module rather than from dpkg. A # mismatch here means the recipe and what actually landed have gone out of step, # and every expectation in this file is then describing a different pathspec. check_eq $P/version "0.12.1" "$(pv version)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME pathspec. check_eq $P/module-file "$PS/__init__.py" "$(pv module_file)" # ...and it answered out of a .py, not an extension. The third statement of the # .so negative, this one from inside the interpreter. check_eq $P/module-is-py "yes" "$(pv module_is_py)" # ------------------------------------------------------------- the API ------- # The public surface, named one key at a time: `PathSpec` missing and # `match_file` missing are different breakages and a single "the API is there" # case would report them identically. check_eq $P/api-pathspec "yes" "$(pv api_pathspec)" check_eq $P/api-gitignorespec "yes" "$(pv api_gitignorespec)" check_eq $P/api-from-lines "yes" "$(pv api_from_lines)" check_eq $P/api-match-file "yes" "$(pv api_match_file)" check_eq $P/api-match-files "yes" "$(pv api_match_files)" # The gitwildmatch pattern class is registered under the name the probe compiles # with. A missing registration fails `from_lines` with a lookup error rather # than a match difference, which is why it is asserted apart from the matches. check_eq $P/api-gitwildmatch "yes" "$(pv api_gitwildmatch)" check_eq $P/gitwildmatch-name "GitWildMatchPattern" "$(pv gitwildmatch_name)" # All five patterns compiled. A pattern that failed to compile is DROPPED by # from_lines rather than raising, so without this case a four-pattern spec would # show up only as whichever match it silently changed. check_eq $P/pattern-count "5" "$(pv pattern_count)" # ------------------------------------------------ the five matcher behaviours -- # See the header for the pattern set. Each case below is one path, so a single # wrong verdict names the path it is about instead of collapsing into a count. # A PLAIN GLOB, unanchored -- so it matches at the root and at depth. check_eq $P/match-glob-root "yes" "$(pv m_app_log)" check_eq $P/match-glob-depth "yes" "$(pv m_logs_deep_app_log)" # A NEGATION RE-INCLUDES a path the glob above excluded. This is the case that # says pattern ORDER is honoured: with the negation dropped, `keep.log` matches # `*.log` and this goes red while nothing else moves. check_eq $P/match-negation "no" "$(pv m_keep_log)" # A DIRECTORY-ONLY pattern, at the root and at depth. check_eq $P/match-dir-root "yes" "$(pv m_build_out_o)" check_eq $P/match-dir-depth "yes" "$(pv m_src_build_nested_o)" # A `**` PATTERN. The second of these is the one worth reading twice: `**/` # matches ZERO directories as well as many, so `docs/c.tmp` must match. A # matcher that required an intervening directory would leave every other case # in this file green. check_eq $P/match-globstar-deep "yes" "$(pv m_docs_a_b_c_tmp)" check_eq $P/match-globstar-zero "yes" "$(pv m_docs_c_tmp)" # AN ANCHORED PATTERN, and its mirror. `/top.txt` matches at the root and MUST # NOT match at depth -- that non-match is the only thing separating an anchor # from an ordinary glob, and it is the assertion the ticket names. check_eq $P/match-anchored-root "yes" "$(pv m_top_txt)" check_eq $P/match-anchored-depth "no" "$(pv m_sub_top_txt)" # THE CONTROL. Nothing in the pattern set names notes.md, so a matcher that had # degenerated into matching everything is caught here and nowhere else. check_eq $P/match-unmatched "no" "$(pv m_notes_md)" # The same ten verdicts as an ordered set. Asserted whole as well as case by # case because a WHOLESALE difference -- an inverted matcher, an empty spec -- # reads as one red here instead of ten above. check_eq $P/matched \ "app.log,logs/deep/app.log,build/out.o,src/build/nested.o,docs/a/b/c.tmp,docs/c.tmp,top.txt" \ "$(pv matched)" check_eq $P/matched-count "7" "$(pv matched_count)" # ------------------------------------------------------- the batch API ------- # match_files() is a DIFFERENT code path in pathspec -- a generator over the # whole corpus rather than a call per path -- and the two disagreeing would mean # the port is only half working. check_eq $P/batch-matched \ "app.log,build/out.o,docs/a/b/c.tmp,docs/c.tmp,logs/deep/app.log,src/build/nested.o,top.txt" \ "$(pv batch_matched)" check_eq $P/batch-agrees "yes" "$(pv batch_agrees)" # GitIgnoreSpec applies gitignore's own last-match-wins ordering rather than # PathSpec's. Over this corpus the two agree, and asserting that they agree is # what says the negation was honoured under both readings -- which is the # reading hatchling actually takes. check_eq $P/gitignore-matched \ "app.log,logs/deep/app.log,build/out.o,src/build/nested.o,docs/a/b/c.tmp,docs/c.tmp,top.txt" \ "$(pv gitignore_matched)" check_eq $P/gitignore-agrees "yes" "$(pv gitignore_agrees)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"