. "$BST_LIB" # BST-TIMEOUT: 1200 # 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; the # # probe itself parses two documents totalling 447 bytes and # # evaluates 20 expressions over them, which is nothing beside # # the interpreter load. It matches 99-python3-xmltodict's and # # 99-python3-iniparse's 1200 for the same reason -- the start # # dominates and the work after it is comparable. # # 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-elementpath 4.4.0 (meta-openembedded # meta-python/recipes-devtools/python/python3-elementpath_4.4.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image -- XPATH SELECTORS # OVER ElementTree TREES. `select(root, expr)' evaluates an XPath 1.0 or 2.0 # expression against a stdlib ElementTree tree and returns the selected nodes or # the computed value. # # ============================================================================ # WHAT THIS PORT ACTUALLY ADDS, WHICH IS NOT "XPATH" IN GENERAL. # ============================================================================ # # ElementTree ALREADY has `findall', and it already takes something that looks # like XPath -- so a reader could reasonably conclude this package duplicates the # stdlib. It does not. `findall' implements a RESTRICTED SUBSET with NO AXIS # SPECIFIERS AT ALL: no `following-sibling::', no `ancestor::', no `attribute::', # no XPath function library, and no XPath 2.0. Those are exactly what this # package adds, and they are exactly what the axis and function cases below # assert. A case that only checked `//title' would pass against the stdlib alone # and would therefore be evidence for nothing. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". # ============================================================================ # # 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 not be imported here -- not "would be # slow", could not be imported. # # elementpath ships NO C AND NO EXTENSION: the sdist contains not one .c, .pyx or # .so, and setup.py declares no ext_modules. It also sits on nothing that would # need one -- unlike xmltodict, whose whole parser is the BUILTIN pyexpat, this # package's tree access is `xml.etree.ElementTree' (pure Python at the API level) # and its own Pratt parser. So `pkg-no-so' below is a REGRESSION TRIPWIRE on a # version bump rather than a statement about today: the day a release grows a # compiled speedup, this case reds at INSTALL time instead of at `import # elementpath' on somebody else's ticket three rounds later. # # ============================================================================ # IT IS A PACKAGE, NOT A MODULE -- A DIRECTORY WITH SIX SUBPACKAGES. # ============================================================================ # # setup.py declares `find_packages(include=['elementpath', 'elementpath.*'])', so # what lands in site-packages is the DIRECTORY `elementpath/' carrying # datatypes/, regex/, validators/, xpath1/, xpath2/, xpath30/ and xpath31/. This # is the opposite shape from 99-python3-xmltodict's single .py file, and the # suite asserts it as such: a release that flattened, or a packaging change that # dropped a subpackage, would leave `import elementpath' working and the 2.0 # cases failing with an import error from inside the package. # # NOTE `xpath30/' AND `xpath31/' SHIP TOO. The recipe's DESCRIPTION says "XPath # 1.0 and 2.0" and this ticket ports it for those, but the package's own # description is "XPath 1.0/2.0/3.0/3.1". The 3.0/3.1 parsers are therefore on # the image whether or not anything asks for them; they are NOT exercised here -- # see WHAT IS NOT COVERED below -- and `pkg-subpackages' states their presence so # that a future ticket adding 3.x cases starts from a recorded fact. # # ============================================================================ # package_data IS PART OF THE DELIVERABLE, AND IT IS THE HALF THAT GOES MISSING. # ============================================================================ # # setup.py ships two .xsd files (`validators/analyze-string.xsd', # `validators/schema-for-json.xsd') and a `py.typed' marker through # `package_data'. Data files are the thing a wheel build drops silently: every # .py can land correctly while the .xsd files do not, and NOTHING in the # behavioural cases below would notice, because the XPath 2.0 expressions this # port is about never read them. `pkg-xsd-*' asserts them directly for that # reason -- it is the one part of the payload the behaviour cannot speak for. # # ============================================================================ # NODE SETS ARE ASSERTED AS TEXT OR TAGS, NEVER AS A repr. # ============================================================================ # # `select()' returns live ElementTree Element objects, and their repr carries a # MEMORY ADDRESS -- `' -- which differs on every run # and every build. A case asserting a repr would be pinning the allocator. The # probe reduces each node to the one string that states what was selected (its # text, or its tag) and preserves ORDER, because document order is precisely what # the axis cases are about. # # ============================================================================ # THE TWO VERSIONS ARE SEPARATED, NOT ASSUMED TO AGREE. # ============================================================================ # # `(1, 2, 3)' is a SEQUENCE expression. XPath 2.0 has a sequence type and a comma # operator; XPath 1.0 has neither. So one string is a valid expression under one # parser and a SYNTAX ERROR under the other, and the suite asserts BOTH # directions: `xp2-sequence' that 2.0 returns the sequence, `xp1-sequence-raised' # and `xp1-sequence-code' that 1.0 refuses it with the standard's own XPST0003. # Asserting only the 2.0 side would pass just as well if XPath1Parser were an # alias for XPath2Parser, which is the failure mode that matters here -- this # package's claim is that it ships TWO parsers. # # ============================================================================ # NAMESPACES: THE CALLER'S PREFIX MAP, NOT THE DOCUMENT'S PREFIXES. # ============================================================================ # # An XPath prefix is bound by the EXPRESSION's namespace context, not by the # document -- a document declaring `xmlns:dc' does not make `dc:' usable in a # selector. The namespaced fixture therefore binds the prefix `c' to the # document's DEFAULT namespace, deliberately choosing a prefix the document does # not use, so a pass cannot come from the document's own declarations leaking # into the expression. `ns-unbound-raised' is the negative half: without the map # the same expression must fail with XPST0081, and a build in which the # declarations DID leak would pass `ns-prefixed' and red there -- which is the # only way to tell the two apart. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED -- AND HERE IS THE ONE GAP. # ============================================================================ # # On x86-64 (CPython 3.10.12), against THIS EXACT PROBE, three times: # (a) the 4.4.0 sdist the recipe's SRC_URI sha256 pins, unpacked and put on # PYTHONPATH -- not a pip copy and not upstream git; # (b) THE BYTES THIS RECIPE BUILDS -- python3-elementpath_4.4.0-r0_wasm32.deb, # extracted, its site-packages on PYTHONPATH; # (c) the same .deb bytes with lxml BLOCKED from importing, which is the code # path this image takes. # (a) and (b) agreed on every key. (b) and (c) agreed on every key except # `lxml_present' itself. Recorded 2026-08-31; the ticket's design doc carries the # transcript. # # THE GAP, STATED PLAINLY: the control interpreter is CPython 3.10 and this image # ships CPython 3.12. Everything asserted below is content-determined -- the node # sets the axes select, the values the functions compute, the error codes the # parsers raise -- and none of it reads the clock, the environment, hash # randomisation, pointer width or byte order. But the 3.12 run has NOT been # performed anywhere: the batch verify is the first. A red confined to # `api-surface' or to an error CODE would most likely be that gap rather than the # port, and the design doc records this so a later reader is not left guessing. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - XPath 3.0 and 3.1. Their parsers ship (see above) but this ticket is # scoped to 1.0 and 2.0, which is what the recipe's own DESCRIPTION claims; # asserting 3.x here would be scope this port has not justified. # - THE lxml TREE INTERFACE. elementpath supports it and this image ships no # lxml, so the stdlib path is the only one that can run. `no-lxml' asserts # that ABSENCE rather than leaving it implied -- an image that later grew # lxml would be exercising different code behind identical assertions. # - SCHEMA-AWARE evaluation (`AbstractSchemaProxy', the validators/ .xsd # files). It needs xmlschema, which is not on this image. The .xsd files are # asserted as PAYLOAD above, not as behaviour. # - elementpath's OWN test suite (tests/ in the sdist). The recipe does not # inherit ptest and does not install them, and running them would need # pytest, which this image does not carry. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-elementpath # THE CANARY, which asserts nothing about python3-elementpath. # # 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 PKGDIR=$SITE/elementpath DI=$SITE/elementpath-4.4.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-elementpath.list PROBE=$BST_DATA/python3-elementpath-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-elementpath.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import elementpath' further down is the # functional statement; these are the ones that say WHICH copy answered, so a # package that imported out of some other path is distinguishable from one that # imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/package-init test -f "$PKGDIR/__init__.py" # ...AND IT IS A DIRECTORY, NOT A SINGLE MODULE. The inverse of # 99-python3-xmltodict's `no-module-dir': a release that flattened to # elementpath.py would still import and would still pass many cases below, while # the shape this image ships had silently changed. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/elementpath.py" ]; then echo present; else echo absent; fi)" # THE SUBPACKAGES, each named. `import elementpath' succeeds while a subpackage # is missing -- the 2.0 parser lives in xpath2/ and is imported lazily enough # that a dropped directory presents as a failing EXPRESSION rather than as a # failing import. One case each, so a red names which one went. for _sub in datatypes regex validators xpath1 xpath2 xpath30 xpath31; do check $P/pkg-subpackage-$_sub test -f "$PKGDIR/$_sub/__init__.py" done # THE package_data FILES. The half of the payload no behavioural case can speak # for: every .py can land correctly while these do not, and nothing this port # does would notice. See the header. check $P/pkg-py-typed test -f "$PKGDIR/py.typed" check $P/pkg-xsd-analyze-string test -f "$PKGDIR/validators/analyze-string.xsd" check $P/pkg-xsd-schema-for-json test -f "$PKGDIR/validators/schema-for-json.xsd" # 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' later. # # 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 DECLARED # package_data. 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 ;; *.xsd|*/py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata, which is the second, independent statement of the # version -- `$P/dist-version' below reads it through importlib.metadata, and the # directory name carries it a third time in $DI itself. A package whose modules # 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: 4.4.0" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what a # `import elementpath' has to match, and a wheel that renamed the package would # change it here first. check_out $P/dist-info-top-level "elementpath" cat "$DI/top_level.txt" # --------------------------------------------------------------- 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-elementpath 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, and # the anchor is exact -- an unanchored match would also take a -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-elementpath$/,/^$/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" # THE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. All five # are sub-packages of the ported python3 (BRINTOS-1476) and all five are real, so # this port waits on nothing: python3-xml carries ElementTree itself, # python3-datetime and python3-numbers back the XPath 2.0 atomic types, and # python3-stringold is what the string function group needs. # # ONE CASE PER DEPENDENCY, against the extracted line, rather than one case # asserting the whole list as a string. The recipe's RDEPENDS order is not dpkg's # to preserve and the separator spacing is dpkg's own, so an exact-string case # here would be asserting a FORMAT this phase cannot measure. Split, each red also # names which dependency went missing instead of printing the whole line back. 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-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-xml "python3-xml" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-stringold "python3-stringold" cat "$W/dep" # ---------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If elementpath 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 elementpath' # ...and ElementTree separately, because elementpath is USELESS without the tree # it drives and a missing python3-xml presents AS a broken elementpath. This case # is what tells the two apart. check $P/etree-import python3 -c 'import xml.etree.ElementTree' # ...and the two parser classes by name, which is the import-level statement that # this package ships BOTH versions. The behavioural halves are asserted further # down; this one separates "the 2.0 parser is absent" from "the 2.0 parser is # present and wrong". check $P/parsers-import python3 -c 'from elementpath import XPath1Parser, XPath2Parser' # ---------------------------------------------------------------- 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 `dist_version', and `ns_prefixed' from `ns_prefixed_error'. 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, twice and independently: the package's own # `__version__' (which matches setup.py's hard-coded 4.4.0) and # importlib.metadata over the installed dist-info. The two disagreeing means a # mixed install -- a dist-info from one version beside a package from another. check_eq $P/version "4.4.0" "$(pv version)" check_eq $P/dist-version "4.4.0" "$(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 the payload cases above and the behaviour # cases below statements about the SAME elementpath. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # THE TREE BINDING, from the interpreter's own answer: the stdlib ElementTree, # which is what python3-xml provides and what every case below drives. check_eq $P/etree-module "xml.etree.ElementTree" "$(pv etree_module)" # NO lxml ON THIS IMAGE, asserted rather than implied. elementpath supports both # tree interfaces and picks by the type it is handed; with lxml absent the stdlib # path is the ONLY one that can run, which is what makes the control measurement # (c) in the header the right code path. An image that later grew lxml would be # exercising different code behind identical assertions, and this case is what # would say so. check_eq $P/no-lxml "no" "$(pv lxml_present)" # THE PUBLIC SURFACE, WHOLE. elementpath DOES declare `__all__', so this is the # package's own statement of what it exports rather than dir()'s report of what # happens to be bound -- and it is therefore a real API assertion: a release that # withdrew `select', `iter_select' or either parser class would red here. check_eq $P/api-surface \ "AbstractSchemaProxy,AttributeNode,CommentNode,DocumentNode,ElementNode,ElementPathError,ElementPathKeyError,ElementPathLocaleError,ElementPathNameError,ElementPathOverflowError,ElementPathRuntimeError,ElementPathSyntaxError,ElementPathTypeError,ElementPathValueError,ElementPathZeroDivisionError,LazyElementNode,MissingContextError,NamespaceNode,ProcessingInstructionNode,RegexError,SchemaElementNode,Selector,TextNode,XPath1Parser,XPath2Parser,XPathContext,XPathFunction,XPathNode,XPathSchemaContext,XPathToken,build_lxml_node_tree,build_node_tree,build_schema_node_tree,datatypes,etree,get_node_tree,iter_select,protocols,select,translate_pattern" \ "$(pv api_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 "227" "$(pv fixture_len)" check_eq $P/fixture-sha "00bee1bfd8d08b32" "$(pv fixture_sha)" # ---------------------------------------------------------------- the paths -- # An ABSOLUTE path naming every step, and a DESCENDANT path naming none. They # select DIFFERENT node sets, so a parser that collapsed one into the other could # not pass both. check_eq $P/abs-path "First|Second" "$(pv abs_path)" check_eq $P/desc-path "10|25" "$(pv desc_path)" # ---------------------------------------------------------------- the axes --- # THE DELIVERABLE. These three are what `ElementTree.findall' cannot express at # all -- its path grammar has no axis specifiers -- so they are what this port # adds to the image rather than what it duplicates. # # `following-sibling' from the first book returns TWO nodes of DIFFERENT tags, so # a result of the right length but the wrong membership still reds. check_eq $P/axis-following-sibling "book|note" "$(pv following_sibling)" # `ancestor' walks UP from the second book's title, which is the direction no # ElementTree traversal has: the stdlib tree holds no parent pointers, so this # case is also the statement that elementpath builds its own node tree. check_eq $P/axis-ancestor "catalog|book" "$(pv ancestor)" # `attribute' yields STRINGS rather than elements -- a different result TYPE, not # just a different node set. check_eq $P/axis-attribute "b1|b2" "$(pv attr_axis)" # ---------------------------------------------------------- the predicates --- # Positional filters by document position, attribute by value. Each selects the # ONE book the other does not, so a predicate silently ignored -- returning both # books -- reds on both. check_eq $P/pred-positional "Second" "$(pv pos_pred)" check_eq $P/pred-attribute "First" "$(pv attr_pred)" # ------------------------------------------------- one function per group ---- # The XPath 1.0 function library, the other half of what ElementTree leaves out. # Each returns a value the others could not produce: a LENGTH, a SUM that is # neither a count nor a position, and booleans over a node set that exists beside # one that does not. check_eq $P/fn-string-length "5" "$(pv fn_string_length)" check_eq $P/fn-concat "a-b" "$(pv fn_concat)" # 35.0, not 35: XPath's numeric type is a DOUBLE, so `sum' over two integers # returns a float and Python prints it with the point. The `.0' is the assertion # that the XPath type system is in play rather than Python's own integer # arithmetic. check_eq $P/fn-sum "35.0" "$(pv fn_sum)" check_eq $P/fn-count "2" "$(pv fn_count)" check_eq $P/fn-boolean "True" "$(pv fn_boolean)" check_eq $P/fn-not "True" "$(pv fn_not)" # ------------------------------------------------------- 1.0 AND 2.0, APART -- # The sequence expression, asserted from both sides. See the header: asserting # only the 2.0 half would pass just as well if XPath1Parser were an alias for # XPath2Parser. check_eq $P/xp2-sequence "1|2|3" "$(pv xp2_sequence)" check_eq $P/xp1-sequence-raised "yes" "$(pv xp1_sequence_raised)" # The exception TYPE is elementpath's own, and the CODE is the standard's. The # message around it carries a line and column and is upstream's to reword, so the # code is what is asserted and the prose is not. check_eq $P/xp1-sequence-type "ElementPathSyntaxError" "$(pv xp1_sequence_error_type)" check_eq $P/xp1-sequence-code "XPST0003" "$(pv xp1_sequence_error_code)" # ---------------------------------------------------------- the namespaces --- # A prefix the CALLER binds, over a document whose own prefixes differ -- `c' is # bound to the default namespace the document never names with a prefix. A pass # here therefore cannot come from the document's declarations leaking into the # expression context. check_eq $P/ns-prefixed "Ada" "$(pv ns_prefixed)" check_eq $P/ns-default "Widget" "$(pv ns_default)" # ...and the negative half, which is what makes the two above meaningful: without # the map the same expression MUST fail, with the standard's own code. check_eq $P/ns-unbound-raised "yes" "$(pv ns_unbound_raised)" check_eq $P/ns-unbound-code "XPST0081" "$(pv ns_unbound_error_code)" # 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 probe_complete)" rm -rf "$W"