. "$BST_LIB" # BST-TIMEOUT: 1500 # THREE python3 starts, 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; the # # probe parses a 385-byte document once and then runs sixty # # compiled selectors over it, which is nothing beside the # # interpreter load. 1500 matches # # 99-python3-beautifulsoup4, which starts the interpreter # # the same three times -- the third is `imports-bs4', which # # has to be its own process to mean anything. # # 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-soupsieve 2.5 (meta-openembedded/meta-python # recipes-devtools/python/python3-soupsieve_2.5.bb, `inherit pypi # python_hatchling python_setuptools_build_meta ptest', no patches) on the # wasm32-hwjs glibc image -- THE CSS SELECTOR ENGINE. `soupsieve.compile(pattern)' # turns a selector string into a matcher; `select', `iselect', `select_one', # `match', `filter' and `closest' run it over a Beautiful Soup tree. # # ============================================================================ # THIS PORT IS ABOUT THE DECLARATION, AND THE SUITE IS ABOUT THE GRAMMAR. # ============================================================================ # # The recipe as meta-python ships it declares `RDEPENDS:${PN}-ptest' and NOTHING # for ${PN}, so the built control stanza read `Depends: python3-core' alone -- # while soupsieve's module carries an unconditional MODULE-SCOPE `import bs4' at # soupsieve/__init__.py:34 and soupsieve/css_match.py:8, neither under # `if TYPE_CHECKING:' and neither deferred into a function body. So `import # soupsieve' raised ModuleNotFoundError on this image until # python3-beautifulsoup4 landed (BRINTOS-2188), which is what parked BRINTOS-2089 # Blocked, and it would raise again on any future image that dropped bs4 while # the declaration stayed wrong. `pkg-depends-bs4' is the case that holds the # correction; `imports-bs4' is the case that says it works. # # THE PAIR IS CIRCULAR AND THAT IS A FACT ABOUT THE CODE. bs4 keeps # `python3-soupsieve' in RDEPENDS -- soupsieve is its one hard upstream # dependency and `.css'/`.select' are unusable without it -- and soupsieve # imports bs4 above. Both directions are in the shipped sources, so the honest # declaration is the circular one and the two packages are co-installed or # neither works. `pkg-depends-bs4' and 99-python3-beautifulsoup4's # `pkg-depends-soupsieve' are the two halves, one in each suite. # # WHAT THE GRAMMAR CASES ARE FOR. bs4 builds the tree and # 99-python3-beautifulsoup4 is where the tree is asserted; this suite asserts # what soupsieve does with it. The fixture is dull HTML and deliberately rich in # what a SELECTOR discriminates on: three `title' values that share prefixes and # differ in case, a multi-valued `class', a hyphenated `hreflang', five list items # so an `an+b' form has something to count, and two element types carrying the # same class so `:is()' has two arms to combine. Fifty-seven selector measurements over one # 385-byte document, one case each, so a red names the CONSTRUCT rather than the # page. # # ============================================================================ # NO COMPILED EXTENSION, WHICH IS WHY IT PORTS AT ALL. # ============================================================================ # # The 2.5 sdist holds no `.so', no `.c' and no `.pyx' -- seven pure-Python # modules and no subpackages, wheel tag py3-none-any. On this image a compiled # extension could not be imported at all (BRINTOS-1476: one static interpreter, # empty lib-dynload, no dynamic loader; BRINTOS-2046: the empty `Py*' export # surface), so `pkg-no-so' is not boilerplate -- it is a REGRESSION TRIPWIRE on a # version bump, failing at INSTALL time rather than at `import soupsieve' on # somebody else's ticket three rounds later. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED -- WITH THREE NAMED EXCEPTIONS. # ============================================================================ # # On x86-64 (CPython 3.12.3, hz1) against THIS EXACT PROBE and against THE BYTES # THIS RECIPE BUILDS -- the 2.5 sdist the recipe's SRC_URI sha256 pins # (5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690, verified), # unpacked, with no patches (the recipe carries none) and the tree put on # PYTHONPATH beside the beautifulsoup4 4.12.3 sdist that recipe pins # (74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051, verified). # Not a pip copy and not upstream git. Recorded 2026-09-03; the ticket's design # doc carries the transcript. # # What makes that control transferable is that all of it is pure Python and # content-determined: a selector's match set depends on soupsieve's code, on the # tree html.parser built and on the fixture, not on pointer width, byte order, # the platform string, the clock or hash randomisation. Nothing in the probe # sorts by hash, formats a float, or reads the environment. # # THE PACKAGING CASES ARE MEASURED SEPARATELY, AND FROM THE BUILT PACKAGE rather # than from the probe: the recipe-scope build of 2026-09-03 produced # python3-soupsieve_2.5-r0_wasm32.deb, whose control stanza reads # # Depends: python3-beautifulsoup4, python3-core, python3-datetime # # -- the bbappend's two additions beside the one the build class supplied, as # built. The `pkg-depends-*' cases, `pkg-module-count' (7 modules, no # subpackages), `pkg-no-so' (no .so anywhere in the 19 regular files) and # `meta-no-requires' (the installed METADATA carries no `Requires-Dist:' line at # all) are therefore results too, not statements of intent about a recipe. # # THE THREE THAT ARE NOT MEASURED, and are marked where they appear: # # - `dist-version'. The control ran off PYTHONPATH with no installed metadata # and reported `dist_version_error=PackageNotFoundError'. On target the # dist-info IS installed and 2.5 is what importlib.metadata must answer. # EXPECTED, NOT YET MEASURED. # - `module-answered' and `bs4-answered'. The control's paths are its unpack # directories; on target they are the site-packages paths this file computes. # Same shape of claim, same status. # # TWO MEASUREMENTS WORTH READING BEFORE A RED IS BLAMED ON THE PORT: # # - `syntax-unknown-pseudo' is NotImplementedError, NOT SelectorSyntaxError. # soupsieve parses `p:nosuchthing' successfully as a pseudo-class and then # refuses to implement it, which is a different stage and a different # exception. The ticket's acceptance criterion asks that a malformed selector # raise rather than silently match nothing, and it does -- but naming the # wrong exception in the suite would have shipped a red against a correct # library. `syntax-dangling' and `syntax-unclosed-attr' are the two that are # genuinely SelectorSyntaxError. # - `cache-ns-distinct' is `False' and that is the PASS. The key asks whether # two matchers compiled from the same pattern under DIFFERENT namespace maps # are the same object; they must not be, so `False' is what a correct cache # answers. `cache-ns-identity' is the same question with the SAME map and # answers `True'. The pair is what says the namespace map is part of the # cache key rather than ignored. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - soupsieve's OWN test suite (tests/). It needs python3-pytest and # python3-unittest-automake-output, which is what the recipe's `ptest' class # wires and this image does not run. The port closes on the API this file # exercises. # - THE TREE ITSELF -- what bs4 parsed, how attributes map, how text is # decoded. That is 99-python3-beautifulsoup4's, and asserting it twice would # make a bs4 regression red in two suites and name the wrong package in one. # - XML AND NAMESPACE MATCHING against a real namespaced document. bs4's XML # mode is lxml-only and lxml cannot exist on this image # (99-python3-beautifulsoup4/lookup-xml records it resolving to nothing), so # the namespace map is exercised here only where it is reachable: as part of # the compile cache key. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-soupsieve # THE CANARY, which asserts nothing about python3-soupsieve. # # 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' guards 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 MOD=$SITE/soupsieve/__init__.py DI=$SITE/soupsieve-2.5.dist-info BS4MOD=$SITE/bs4/__init__.py PKGLIST=/var/lib/dpkg/info/python3-soupsieve.list PROBE=$BST_DATA/python3-soupsieve-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-soupsieve.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import soupsieve' 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. # # soupsieve is a PACKAGE with no subpackages -- `soupsieve/' holding seven # modules -- so the two that carry the port's whole story are named # individually: `css_parser.py' is the grammar and `css_match.py' is the matcher, # and css_match.py is also where the second unguarded `import bs4' lives. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$SITE/soupsieve" check $P/module-file test -f "$MOD" check $P/css-parser-file test -f "$SITE/soupsieve/css_parser.py" check $P/css-match-file test -f "$SITE/soupsieve/css_match.py" check $P/css-types-file test -f "$SITE/soupsieve/css_types.py" # THE TYPE MARKER. soupsieve ships `py.typed', which is what makes its inline # annotations visible to a type checker in a consumer; a wheel that dropped it # would still import and still select, and the package would silently stop being # typed. It is also the one shipped file that is neither .py nor metadata, which # is why `pkg-only-python' below has an arm for it. check $P/py-typed-marker test -f "$SITE/soupsieve/py.typed" # THE DEPENDENCY, AS A FILE. `imports-bs4' below is the functional half; this is # the one that distinguishes "bs4 is not installed" from "bs4 is installed and # something else broke", without spending an interpreter start to find out. check $P/bs4-package-dir test -d "$SITE/bs4" check $P/bs4-module-file test -f "$BS4MOD" # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. 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' on a later # ticket. # # 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 IS PYTHON, ITS METADATA, OR THE TYPE # MARKER. The case above names the failure that is expected; this one catches the # shape nobody predicted -- a binary, a .pyd, a stray README -- without having to # enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */soupsieve/py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ...AND HOW MANY MODULES THERE ARE. The arm above admits a whole class, so this # is what keeps it honest: soupsieve ships exactly seven modules and no # subpackages, and a release that split one out reds here rather than passing # silently through the exemption written for it. check_eq $P/pkg-module-count "7" \ "$(grep -c '/soupsieve/[a-z_]*\.py$' "$W/pkgf" 2>/dev/null)" # ------------------------------------------------------ the control stanza --- # THE DECLARATION THIS PORT EXISTS TO CORRECT, read back off the package dpkg # installed rather than off the recipe. A bbappend that failed to apply, or a # feed that served an older .deb, would leave every case below green and this one # red -- which is the only way to tell the correction from the intention. dpkg -s python3-soupsieve > "$W/ctl" 2>/dev/null || : > "$W/ctl" check $P/pkg-installed test -s "$W/ctl" check_out $P/pkg-status "Status: install ok installed" cat "$W/ctl" check_out $P/pkg-version "Version: 2.5" cat "$W/ctl" # The two the bbappend adds, each asserted by name so a red says WHICH one went. check_out $P/pkg-depends-bs4 "python3-beautifulsoup4" grep '^Depends:' "$W/ctl" check_out $P/pkg-depends-datetime "python3-datetime" grep '^Depends:' "$W/ctl" # ...and the one the build class supplied all along, so the case above is known # to be an addition rather than a replacement. check_out $P/pkg-depends-core "python3-core" grep '^Depends:' "$W/ctl" # ----------------------------------------------------------- the dist-info --- # The installed metadata, which is the second, independent statement of the # version -- `dist-version' below reads it through importlib.metadata, and the # directory name carries it a third time in $DI itself. A package whose module # landed and whose metadata did not would import fine and serve every behavioural # case while reporting no installed version at all. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 2.5" cat "$DI/METADATA" # UPSTREAM DECLARES NO RUNTIME REQUIREMENT AT ALL, read off the metadata this # image installed. That is the whole reason the recipe under-declared and the # whole reason this port is a bbappend: there was nothing for meta-python to # translate. The `import bs4' in the module is a fact about the CODE that the # PACKAGING never carried, and this case is where the gap is written down -- a # release that finally declared bs4 upstream would red here, beside the bbappend # that stopped being the only place saying so. check_eq $P/meta-no-requires "" \ "$(grep '^Requires-Dist:' "$DI/METADATA" 2>/dev/null)" # ------------------------------------------------------------- the imports --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If soupsieve 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 soupsieve' # THE PAIR, IN THE ONE FORM THAT MEANS ANYTHING -- A SEPARATE INTERPRETER. # soupsieve carries an unconditional top-level `import bs4' and, until this port, # declared no RDEPENDS saying so. Asserted here as: importing soupsieve ALONE # succeeds AND drags bs4 in. Inside the probe it would prove nothing, because the # probe imports bs4 itself to build the fixture. # # 99-python3-beautifulsoup4 carries the same assertion from the other side of the # pair, deliberately: whichever of the two packages a future change breaks, one # of the two suites reds without depending on the other having run. check $P/imports-bs4 python3 -c \ 'import soupsieve, sys; raise SystemExit(0 if "bs4" in sys.modules else 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 # 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" 900 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 `version' distinguishable # from `version_info' and from `dist_version', and `nth_literal' from # `nth_literal_text'. 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 library, 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 in 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 # THE VERSION THE RECIPE PINS, three ways: the module's own `__version__', the # structured `__version_info__' it is built from, and importlib.metadata over the # installed dist-info. check_eq $P/version "2.5" "$(pv version)" check_eq $P/version-info "2.5.0.final.0.0.0" "$(pv version_info)" # EXPECTED, NOT YET MEASURED -- see the header. The control had no installed # metadata and reported PackageNotFoundError; on target the dist-info is there. check_eq $P/dist-version "2.5" "$(pv dist_version)" # The package answered out of the files this image installed, not from a stray # copy on sys.path. This is what makes every case below a statement about the # SAME soupsieve. EXPECTED, NOT YET MEASURED: the control answered from its # unpack directory. check_eq $P/module-answered "$MOD" "$(pv module_file)" # ...and so did bs4, which is the in-process half of `imports-bs4'. check_eq $P/bs4-answered "$BS4MOD" "$(pv bs4_file)" check_eq $P/bs4-version "4.12.3" "$(pv bs4_version)" # THE ADVERTISED SURFACE, whole and sorted. soupsieve declares an explicit # `__all__' of ten names, so this is a much narrower statement than a `dir()' # would be, and a release that widened it has changed its contract rather than # its formatting. check_eq $P/all-surface \ "DEBUG|SelectorSyntaxError|SoupSieve|closest|compile|filter|iselect|match|select|select_one" \ "$(pv all_surface)" # ------------------------------------------------------------- the fixture --- # The document the probe drove, pinned by length and digest, so every case below # is known to be about the document this file describes rather than about an # edited probe. check_eq $P/fixture-len "385" "$(pv fixture_len)" check_eq $P/fixture-sha "0c84be4b831d0482" "$(pv fixture_sha)" check_eq $P/parse-ok "yes" "$(pv parse_ok)" # ==================================================== THE SIMPLE SELECTORS ==== # One case per selector kind. `universal' is the count the others are read # against: a matcher that degraded to match-everything would answer 16 for all of # them, and one that degraded to match-nothing would answer 0. check_eq $P/sel-type "3" "$(pv sel_type)" check_eq $P/sel-class "3" "$(pv sel_class)" check_eq $P/sel-id "1" "$(pv sel_id)" check_eq $P/sel-universal "16" "$(pv sel_universal)" # THE COMPOUND, which is the one that says the two halves are INTERSECTED rather # than unioned: `p' is 3 and `.item' is 3, and `p.item' is 2 because the span # carries the class and the third p does not. check_eq $P/sel-type-class "2" "$(pv sel_type_class)" # A MULTI-VALUED `class' REACHES THE MATCHER AS A LIST, which is the tree # builder's rule and the thing `~=' below depends on. Stated as its length, so a # builder that handed over the string "wrap outer" would answer 11 rather than 2. check_eq $P/sel-class-is-list "2" "$(pv sel_class_len)" # ================================================== THE ATTRIBUTE OPERATORS === # Each of the six, plus presence, plus both case flags. The three `title' values # share prefixes on purpose -- "alpha beta", "alphabet", "ALPHA" -- so `^=', `$=', # `*=' and `~=' give four different answers over one attribute, and an operator # that fell through to another would collide visibly. check_eq $P/attr-present "1" "$(pv attr_present)" check_eq $P/attr-exact "1" "$(pv attr_exact)" check_eq $P/attr-includes "1" "$(pv attr_includes)" check_eq $P/attr-dash "1" "$(pv attr_dash)" check_eq $P/attr-prefix "2" "$(pv attr_prefix)" check_eq $P/attr-suffix "1" "$(pv attr_suffix)" check_eq $P/attr-substring "2" "$(pv attr_substring)" # THE `i' FLAG, asserted against its own case-sensitive twin rather than alone: # `attr-prefix' is 2 and `attr-prefix-i' is 3, and the difference IS the flag. check_eq $P/attr-prefix-i "3" "$(pv attr_prefix_i)" check_eq $P/attr-exact-i "1" "$(pv attr_exact_i)" # ...and the explicit case-SENSITIVE flag, which a matcher that ignored flags # entirely would still pass the pair above by accident. check_eq $P/attr-exact-s "1" "$(pv attr_exact_s)" # ========================================================= THE COMBINATORS ==== # All four, over the same two element types, so the answers differ by the # RELATION and not by what is being selected. `comb-child-deep' is the one that # says `>' is a CHILD combinator and not a descendant one: the three paragraphs # are inside the div, so `body > p' is 0 while `div > p' is 3. check_eq $P/comb-descendant "3" "$(pv comb_descendant)" check_eq $P/comb-child "3" "$(pv comb_child)" check_eq $P/comb-child-deep "0" "$(pv comb_child_deep)" check_eq $P/comb-adjacent "2" "$(pv comb_adjacent)" check_eq $P/comb-sibling "1" "$(pv comb_sibling)" # THE SELECTOR LIST, which is a union and not a combinator: 3 paragraphs + 1 span. check_eq $P/comb-list "4" "$(pv comb_list)" # ====================================================== :nth-child(an+b) ====== # Five list items, so odd/even/`2n+1'/`-n+2'/a literal index have distinct # answers. `nth-neg' is the form an implementation is most likely to get wrong -- # a NEGATIVE `a' counts DOWN from `b' and selects the first two, not the last two # -- and `nth-neg-text' is what pins which end it counted from: `a', the first # item, not `e'. check_eq $P/nth-odd "3" "$(pv nth_odd)" check_eq $P/nth-even "2" "$(pv nth_even)" check_eq $P/nth-2n1 "3" "$(pv nth_2n1)" check_eq $P/nth-neg "2" "$(pv nth_neg)" check_eq $P/nth-literal "1" "$(pv nth_literal)" check_eq $P/nth-last "1" "$(pv nth_last)" check_eq $P/nth-of-type "1" "$(pv nth_of_type)" check_eq $P/nth-first-child "1" "$(pv nth_first_child)" check_eq $P/nth-last-child "1" "$(pv nth_last_child)" check_eq $P/nth-only-child "1" "$(pv nth_only_child)" # WHICH element, not just how many. A matcher that counted right and indexed # wrong passes every case above and fails these two. check_eq $P/nth-literal-text "c" "$(pv nth_literal_text)" check_eq $P/nth-neg-text "a" "$(pv nth_neg_text)" # ============================================ THE LOGICAL PSEUDO-CLASSES ====== # Each on its own, then nested one inside another -- which is what the ticket # asks for and is where an implementation that expanded them textually breaks. check_eq $P/logic-not "1" "$(pv logic_not)" check_eq $P/logic-is "3" "$(pv logic_is)" check_eq $P/logic-has "1" "$(pv logic_has)" check_eq $P/logic-has-desc "1" "$(pv logic_has_desc)" check_eq $P/logic-where "3" "$(pv logic_where)" # THE NESTED FORMS. `:not(:is(...))' and `:is(...:not(...), ...)' are the two # orders, and `:has(...:not(...))' puts a negation inside a relative selector -- # three different nestings, so a red names which one. check_eq $P/logic-not-is "3" "$(pv logic_not_is)" check_eq $P/logic-is-not "3" "$(pv logic_is_not)" check_eq $P/logic-has-not "1" "$(pv logic_has_not)" check_eq $P/logic-not-has "10" "$(pv logic_not_has)" # ============================================ THE NON-SELECT ENTRY POINTS ===== # `select' is one of six documented ways in. A port that wired only `select' # would pass every case above and fail a caller who wrote `match'. Each is # asserted with its negative beside it, because a function that always answers # the same thing satisfies the positive alone. check_eq $P/api-select-one "Three" "$(pv api_select_one)" check_eq $P/api-select-one-none "None" "$(pv api_select_one_none)" check_eq $P/api-match "True" "$(pv api_match)" check_eq $P/api-match-false "False" "$(pv api_match_false)" check_eq $P/api-filter "3" "$(pv api_filter)" check_eq $P/api-closest "main" "$(pv api_closest)" check_eq $P/api-closest-none "None" "$(pv api_closest_none)" check_eq $P/api-iselect "5" "$(pv api_iselect)" # CSS IDENTIFIER ESCAPING, which is the one part of the API that is a pure string # function. The second case is the interesting one: a leading DIGIT cannot be # escaped with a backslash, so CSS spells it as a numeric escape terminated by a # space -- `\33 col', three tokens where a naive implementation writes `\3col'. check_eq $P/api-escape 'foo\.bar' "$(pv api_escape)" check_eq $P/api-escape-digit '\33 col' "$(pv api_escape_digit)" # ============================================================= THE LIMIT ===== # `select''s `limit' stops the walk early, and `0' means NO limit rather than no # results -- the pair is what says so. A matcher that ignored the argument # entirely would answer 5 to both and look correct in every count above. check_eq $P/limit-two "2" "$(pv limit_two)" check_eq $P/limit-zero "5" "$(pv limit_zero)" # ======================================================== THE SYNTAX GATE ===== # A MALFORMED SELECTOR RAISES rather than silently matching nothing, which is how # a caller tells a typo from an empty page. Asserted BY TYPE: the message is # soupsieve's to reword between releases. # # `syntax-unknown-pseudo' IS NOT SelectorSyntaxError, AND THAT IS MEASURED, NOT A # CONCESSION -- see the header. `p:nosuchthing' PARSES; soupsieve then refuses to # implement it and raises NotImplementedError. A suite that had predicted # SelectorSyntaxError here would have shipped a red against a correct library. check_eq $P/syntax-dangling "SelectorSyntaxError" "$(pv syntax_dangling)" check_eq $P/syntax-unclosed-attr "SelectorSyntaxError" "$(pv syntax_unclosed_attr)" check_eq $P/syntax-unknown-pseudo "NotImplementedError" "$(pv syntax_unknown_pseudo)" check_eq $P/syntax-is-exception "True" "$(pv syntax_error_is_exception)" # ...and a VALID selector does not raise, which is what says the three above # measured the parser rejecting something rather than the parser being broken. check_eq $P/syntax-valid "SoupSieve" "$(pv syntax_valid)" # ============================================================== THE CACHE ===== # `css_parser.py' decorates its compile step with `functools.lru_cache', so the # same pattern with the same namespace map comes back as the SAME OBJECT -- # identity, not equality. This is the one property here that is about soupsieve's # own machinery rather than about CSS, and the ticket names it. check_eq $P/cache-identity "True" "$(pv cache_identity)" # A DIFFERENT NAMESPACE MAP IS A DIFFERENT KEY. `False' IS THE PASS -- see the # header: the key asks whether the two matchers are the same object, and they # must not be. `cache-ns-identity' is the same question with the same map. The # pair is what says the namespace map is part of the cache key rather than # ignored, which is a correctness property and not an optimisation one. check_eq $P/cache-ns-distinct "False" "$(pv cache_ns_distinct)" check_eq $P/cache-ns-identity "True" "$(pv cache_ns_identity)" # COMPILING AN ALREADY-COMPILED MATCHER RETURNS IT rather than re-parsing its # pattern, which is the documented way to pass one around. check_eq $P/cache-recompile "True" "$(pv cache_recompile)" # ...and `purge()' empties the cache, so the next compile is a NEW object -- # `False' is the pass here too. This is the case that says `cache-identity' came # from the cache and not from soupsieve happening to return a singleton. check_eq $P/cache-purge "False" "$(pv cache_purge)" # The compiled object carries its pattern back, which is what a caller reads in a # traceback, and still selects and matches -- the object-oriented route to the # same engine the module-level functions above reached. check_eq $P/compiled-pattern "div > p" "$(pv compiled_pattern)" check_eq $P/compiled-select "5" "$(pv compiled_select)" check_eq $P/compiled-match "True" "$(pv compiled_match)" # 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"