. "$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 builds a handful of documents whose largest # # rendering is 2200 bytes and bisects a recursion limit over # # the same 200-node tree, 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-dominate 2.9.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-dominate_2.9.1.bb, `inherit pypi ptest # python_setuptools_build_meta', with a meta-brintos bbappend) on the wasm32-hwjs # glibc image -- HTML BUILT FROM PYTHON RATHER THAN FROM TEMPLATE TEXT. Every # element is a Python object, nesting is expressed with `with' or by adding # children, and the tree renders to a string. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY ASSERTING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), and its # exported symbol table carries no Py* API at all (BRINTOS-2046). A module # carrying a compiled extension therefore could not be imported here -- not # "would be slow", could not be imported. dominate is FIVE .py files and ships no # extension, which is the entire reason it ports, and BRINTOS-2046 does not reach # it. # # So `pkg-no-so' is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump: dominate grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import dominate' on # somebody else's ticket three rounds later. # # ============================================================================ # THE RDEPENDS THE STOCK RECIPE DOES NOT DECLARE -- AND THE CASES THAT SAY SO. # ============================================================================ # # dominate/dom_tag.py imports `asyncio' and `uuid' at MODULE SCOPE and # unconditionally, and both are their own OE sub-packages (python3-asyncio and # python3-netclient). The stock recipe declares neither, so a rootfs that took # the .deb alone would raise ModuleNotFoundError at the first `import dominate'. # The bbappend adds them; `pkg-depends-asyncio' and `pkg-depends-netclient' # assert dpkg recorded them. On THIS image the omission would have been invisible # -- both packages are here for other reasons -- which is exactly why the # assertion is on the Depends line rather than on the import. # # ============================================================================ # greenlet IS ABSENT, AND THAT IS THE PATH UNDER TEST. # ============================================================================ # # dom_tag.py takes `import greenlet' in a try/except and sets `greenlet = None' # on ImportError; the module uses it to key its per-context `with' stack. # greenlet is a C extension and is not on this image, so the FALLBACK path -- # thread id plus a contextvars async-context id -- is what every `with' block # below runs through. `greenlet-absent' pins that, so a future image that grew # greenlet is a red here rather than a silent change of which code the rest of # this file measures. # # ============================================================================ # WHAT IS ASSERTED IS THE RENDERING, AGAINST A LITERAL. # ============================================================================ # # Every rendering case compares a whole string against a literal, with newlines # spelled \n and spaces spelled \s -- the .test file compares with shell # `test x = y', so a value carrying a raw newline would be truncated at the first # one and runs of indentation would be unreadable in a red. Both escapes are # reversible and are performed in the probe. # # Attribute ORDER inside a tag is not left to chance: dominate sorts the # attribute pairs when it renders (`_render' walks `sorted(attributes)'), so # `' is a property of the library and not # of dict insertion order or of hash randomisation. # # ============================================================================ # THE RECURSION CASES ASSERT A BOUND, NOT A DEPTH. # ============================================================================ # # Rendering is recursive -- one _render plus one _render_children frame per level # -- so the depth a document renders to is a function of sys.getrecursionlimit() # and NOT of a lower bound inside dominate. The probe asserts that by BRACKETING # the interpreter's limit over one 200-node tree: it bisects the smallest limit # at which that tree still renders (403), then shows the render succeeding at 403 # and raising RecursionError at 402. # # THE DEPTH IS DELIBERATELY SMALL AND THE REASON IS THIS TARGET. Python's # RecursionError is a counter; the C stack under it is finite, and on wasm32-hwjs # it is whatever the runtime gives the module -- so a probe that drove thousands # of frames to make the same point could exhaust that stack and TRAP instead of # raising. A trap does not red a case: it ends the suite, and brintos_local_verify # quarantines a wedger and re-boots without it, costing every sibling on this # batch image its own results. 200 levels is ~400 frames, inside the default # limit of 1000 with room to spare, so the deepest stack this probe ever builds # is one an unmodified interpreter already permits. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # On x86-64 (CPython 3.12.3), against THIS EXACT PROBE and against THE BYTES THIS # RECIPE BUILDS -- the 2.9.1 sdist the recipe's SRC_URI sha256 pins # (558284687d9b8aae1904e3d6051ad132dd4a8c0cf551b37ea4e7e42a31d19dc4, verified by # sha256sum on the fetched tarball), unpacked and put on PYTHONPATH. Not a pip # copy and not upstream git. Recorded 2026-09-02; the ticket's design doc carries # the transcript. # # What makes that control transferable is that all of it is pure-Python and # content-determined: the strings dominate emits, their lengths, the attribute # order it sorts into, and the frame count its renderer uses per level depend on # dominate's code and on the literals in the probe -- not on pointer width, byte # order, the platform string, the clock or hash randomisation. Nothing in the # probe sorts by hash, formats a float, reads the environment or opens a socket. # The one number that is a fact about the INTERPRETER rather than about dominate # is `render_need' = 403: it is (frames per level x 200) plus the probe's own # call depth, so a CPython that changed its inlining would move it. Both machines # run CPython 3.12, which is why it is asserted rather than merely emitted. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - dominate's OWN test suite (tests/test_dominate.py and the seven files # beside it). The recipe installs them through `ptest', which this image does # not carry, and running them would need python3-pytest. # - THE ASYNC PATH (`dom_tag_async', the ContextVar-keyed context ids). It is # reachable -- python3-asyncio is on the image and the bbappend now declares # it -- but exercising it would make this file a statement about event-loop # scheduling rather than about the markup, and `greenlet-absent' already pins # which context-tracking branch runs. # - dominate.svg's tag table beyond its presence, and dominate.util's `include' # and `system' helpers, which read the filesystem and fork a shell. # - `unescape' and `url_escape'. They are the inverse helpers, not on the # ticket's path, and `unescape' carries a five-entity table upstream calls # unfinished. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-dominate # THE CANARY, which asserts nothing about python3-dominate. # # 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/dominate DI=$SITE/dominate-2.9.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-dominate.list PROBE=$BST_DATA/python3-dominate-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-dominate.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import dominate' 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 on sys.path is distinguishable # from one that imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A MODULE. pyproject.toml declares `packages = ["dominate"]', # so a package tree lands in site-packages -- the opposite shape from # python3-xmltodict's single file. This is the case that fails a release which # flattened to one module, loudly and here, rather than nowhere. check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/dominate.py" # THE FIVE SOURCE FILES, EACH BY NAME. `__init__' re-exports `document'; the # other four are the ones every case below runs through -- dom_tag holds the # renderer and the `with' stack, tags the HTML5 table, util the escaping and # `raw', document the doctype-plus-scaffold subclass. A wheel that dropped one # would still import and would fail somewhere far from the cause. check $P/src-init test -s "$PKGDIR/__init__.py" check $P/src-dom-tag test -s "$PKGDIR/dom_tag.py" check $P/src-tags test -s "$PKGDIR/tags.py" check $P/src-util test -s "$PKGDIR/util.py" check $P/src-document test -s "$PKGDIR/document.py" check $P/src-dom1core test -s "$PKGDIR/dom1core.py" check $P/src-svg test -s "$PKGDIR/svg.py" check $P/src-version test -s "$PKGDIR/_version.py" # The precompiled bytecode, which is what `import-quiet' below turns on. check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # NO SHIPPED SCRIPT. This package is a library and installs no executable. # Asserted so a release that grew a console_scripts entry point announces itself # here. check $P/no-cli test ! -e /usr/bin/dominate # 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 -- at install rather than 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 OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # dpkg's .list marks no directory with a trailing slash, so the entries are # sieved by asking the filesystem what each one is. _bad= while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */dominate-2.9.1.dist-info/METADATA) continue ;; */dominate-2.9.1.dist-info/RECORD) continue ;; */dominate-2.9.1.dist-info/WHEEL) continue ;; */dominate-2.9.1.dist-info/top_level.txt) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ------------------------------------------------------------ the dist-info -- # The installed distribution metadata, which is what importlib.metadata answers # `dist-version' out of in the probe. The directory name carries the version, so # a mixed install -- new sources beside an old dist-info -- shows up as the two # disagreeing rather than as neither being wrong. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # The import name, from the wheel's own record of it. `import dominate' has to # match, and a wheel that renamed the tree would change it here first. check_out $P/dist-info-top-level "dominate" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' is the # packaging-side statement of the header's claim -- no platform tag, no # extension module, nothing that could need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # ------------------------------------------------------------- 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-dominate 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 the -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-dominate$/,/^$/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: 2.9.1-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE RUNTIME DEPENDENCIES, as dpkg recorded them -- and this is the case the # bbappend exists for. The stock recipe declares python3-numbers and # python3-threading; dom_tag.py also imports `asyncio' and `uuid' at module # scope, and those live in python3-asyncio and python3-netclient. Each is # asserted by name, because a Depends line that lost one would still install on # THIS image (both are here for other reasons) and break on the first rootfs that # took the .deb alone. 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-numbers "python3-numbers" cat "$W/dep" check_out $P/pkg-depends-threading "python3-threading" cat "$W/dep" check_out $P/pkg-depends-asyncio "python3-asyncio" cat "$W/dep" check_out $P/pkg-depends-netclient "python3-netclient" cat "$W/dep" # NO -ptest PACKAGE ON THIS IMAGE. The recipe DOES `inherit ptest' and does build # one, unlike python3-aniso8601 -- this image simply does not install it, which # is why the upstream tests are out of scope in the header. The case says the # image took the runtime package alone. check_eq $P/no-ptest-package "" \ "$(sed -n '/^Package: python3-dominate-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If dominate 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 -- and, given the header, the one that would have # failed on an image without python3-asyncio or python3-netclient. check $P/import python3 -c 'import dominate' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode: the package ships precompiled .pyc files and OE writes them # with CHECKED-HASH invalidation, so each source is hashed and never recompiled. # A red here means a shipped .pyc was REJECTED -- a stale hash, a python3 minor # bump that changed the magic, or a missing __pycache__ -- and the tree is being # compiled from source on every import. # # Nothing in dominate has import-time output of its own: no logging # configuration, no warning, no print. check_eq $P/import-quiet "" \ "$(python3 -c 'import dominate' 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 # 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 # reading an empty file with no traceback to say why. 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 `nest_len' from `nest_ok_default'. 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. 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 times and independently: the package's own # `__version__' (which pyproject.toml reads dynamically to stamp the wheel), the # `version' alias `__init__' keeps beside it, and importlib.metadata over the # installed dist-info. Any two disagreeing means a mixed install. check_eq $P/version "2.9.1" "$(pv version)" check_eq $P/version-alias "2.9.1" "$(pv version_alias)" check_eq $P/dist-version "2.9.1" "$(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 dominate. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/tags-answered "$PKGDIR/tags.py" "$(pv tags_file)" # ---------------------------------------------------------------- the API ---- # `dominate' itself exposes only `document' plus the two version names and the # submodules; the tag set lives in `dominate.tags'. A release that moved either # would still import and would break every caller. check_eq $P/api-dominate "document,dom1core,dom_tag,tags,util,version" \ "$(pv dominate_surface)" check_eq $P/api-util \ "basestring,container,dom_tag,escape,include,lazy,raw,re,str_escape,system,text,unescape,unichr,url_escape,url_unescape" \ "$(pv util_surface)" # THE HTML5 TAG SET, by count and by a fixed sample. The count pins the whole # table without printing 113 names into a red; the sample names the ones this # suite and any caller actually reach. `input_' is spelled with the trailing # underscore upstream gives it, `input' being a Python builtin. check_eq $P/tag-count "113" "$(pv tag_count)" check_eq $P/tag-sample \ "a,body,br,div,head,hr,html,img,input_,li,meta,p,pre,span,title,ul" \ "$(pv tag_sample)" # THE VOID ELEMENTS, from the classes' own `is_single' -- the property the # renderer reads. Asserting it here says WHY `
' has no close tag rather than # only that it does not, and `single-div' is the other direction: a build that # marked everything single would pass the four above and fail this one. check_eq $P/single-br "True" "$(pv single_br)" check_eq $P/single-hr "True" "$(pv single_hr)" check_eq $P/single-img "True" "$(pv single_img)" check_eq $P/single-meta "True" "$(pv single_meta)" check_eq $P/single-input "True" "$(pv single_input)" check_eq $P/single-div "False" "$(pv single_div)" #
 keeps its contents verbatim -- the one tag whose children the pretty
# printer does not re-indent.
check_eq $P/pretty-pre "False" "$(pv pretty_pre)"
check_eq $P/pretty-div "True"  "$(pv pretty_div)"

# greenlet IS ABSENT AND THE FALLBACK PATH IS WHAT RUNS. See the header: this
# decides which branch of `_get_thread_context' every `with' block below takes.
check_eq $P/greenlet-absent "yes" "$(pv greenlet_absent)"

# ------------------------------------------------------ the document scaffold --
# `document()' pre-builds head and body and a title node, and `with d:' makes
# everything added inside land in the body's `main' container. The doctype is
# emitted by document._render ahead of the tag tree.
check_eq $P/doc-doctype "" "$(pv doc_doctype)"
check_eq $P/doc-title   "Probe"           "$(pv doc_title)"
check_eq $P/doc-repr    '' "$(pv doc_repr)"
# The whole scaffold, pretty-printed, against a literal -- two levels of
# indentation, the title node the constructor made, and the meta added under
# `with d.head:'.
check_eq $P/doc-render \
  '\n\n\s\s\n\s\s\s\sProbe\n\s\s\s\s\n\s\s\n\s\s\n\s\s\s\s

hello

\n\s\s\n' \ "$(pv doc_render)" # ...and compact. The doctype keeps its newline even here -- it is appended by # document._render rather than by the pretty printer, which is the one thing # `pretty=False' does not reach. check_eq $P/doc-render-compact \ '\nProbe

hello

' \ "$(pv doc_render_compact)" # `doctype=None' starts the rendering at `'. check_eq $P/doc-nodoctype '' "$(pv doc_nodoctype_head)" # The header/main/footer containers add NO level of their own, which is why # `doc-render' above shows no wrapper element around the

. check_eq $P/container-render '

x

y

' "$(pv container_render)" # -------------------------------------------------- the literal document ------ # THE TICKET'S CENTRAL CASE: one document built with the context-manager form, # its rendering compared against a literal. Everything in it is deliberate -- a # void element rendered without a close tag, `cls' renamed to `class', `data_role' # to `data-role', `fr' to `for', a boolean attribute expanded to its own name and # a False one dropped, text escaping of < & > and of a quote inside an attribute, # and `raw' passing markup through untouched. check_eq $P/lit-render \ '\n\s\s\n\s\s\s\sDoc\n\s\s\n\s\s\n\s\s\s\s\n\s\s\s\s\s\sa\s<\sb\s&\sc\s>\sd


\n\s\s\s\s\s\s\n\s\s\s\s\s\s\n\s\s\s\s\s\sNameraw\n\s\s\s\s\n\s\s\n' \ "$(pv lit_render)" # THE SAME TREE, COMPACT -- the pretty printer removed and nothing else. A build # whose indentation had drifted would red on `lit-render' alone; one whose MARKUP # had drifted reds on both, which is what distinguishes the two failures. check_eq $P/lit-render-compact \ 'Doca\s<\sb\s&\sc\s>\sd


Nameraw' \ "$(pv lit_render_compact)" # XHTML: the SAME void elements close themselves, and nothing else moves. This is # the case that says `is_single' above is what the renderer consults. check_eq $P/lit-render-xhtml \ '\n\s\s\n\s\s\s\sDoc\n\s\s\n\s\s\n\s\s\s\s\n\s\s\s\s\s\sa\s<\sb\s&\sc\s>\sd

\n\s\s\s\s\s\s\n\s\s\s\s\s\s\n\s\s\s\s\s\sNameraw\n\s\s\s\s\n\s\s\n' \ "$(pv lit_render_xhtml)" # A four-space indent, so the indent string is known to be a parameter rather # than a constant -- every level widens and nothing else changes. check_eq $P/lit-render-indent4 \ '\n\s\s\s\s\n\s\s\s\s\s\s\s\sDoc\n\s\s\s\s\n\s\s\s\s\n\s\s\s\s\s\s\s\s\n\s\s\s\s\s\s\s\s\s\s\s\sa\s<\sb\s&\sc\s>\sd


\n\s\s\s\s\s\s\s\s\s\s\s\s\n\s\s\s\s\s\s\s\s\s\s\s\s\n\s\s\s\s\s\s\s\s\s\s\s\sNameraw\n\s\s\s\s\s\s\s\s\n\s\s\s\s\n' \ "$(pv lit_render_indent4)" # ------------------------------------------------- attributes, one at a time -- # `lit-render' above states all of these at once; these say WHICH mapping broke # when it does. Four spellings reach `class' and four reach `for' -- the # workaround for Python's reserved words -- and the suite asserts the ones a # caller is likely to type. check_eq $P/attr-cls '' "$(pv attr_cls)" check_eq $P/attr-classname '' "$(pv attr_classname)" check_eq $P/attr-class-name '' "$(pv attr_class_name)" check_eq $P/attr-klass '' "$(pv attr_klass)" check_eq $P/attr-fr '' "$(pv attr_fr)" check_eq $P/attr-html-for '' "$(pv attr_html_for)" # data_* and aria_* have EVERY underscore turned into a dash and are lowercased # -- not just the prefix, which is why the probe passes a two-underscore name. check_eq $P/attr-data '' "$(pv attr_data)" check_eq $P/attr-aria '' "$(pv attr_aria)" # A LEADING UNDERSCORE IS STRIPPED. That is how a reserved word is spelled # positionally, and it is a different rule from the dash rewrite above. check_eq $P/attr-underscore '' "$(pv attr_underscore)" # `http_equiv' is named explicitly in clean_attribute's dash set, and `xlink_' # takes the COLON rewrite rather than the dash one -- three separate branches, # each with its own case. The attribute order here is dominate's own sort, not # the order they were passed. check_eq $P/attr-http-equiv '' \ "$(pv attr_http_equiv)" check_eq $P/attr-xlink '' "$(pv attr_xlink)" # BOOLEAN ATTRIBUTES: True becomes the attribute's own name; False is dropped by # the renderer entirely, leaving a bare tag. Both halves, because a build that # rendered `checked="False"' would pass the first. check_eq $P/attr-bool-true '' "$(pv attr_bool_true)" check_eq $P/attr-bool-false '' "$(pv attr_bool_false)" # ...and a plain string value is untouched by either rule, which is what says the # two above are rules and not a coincidence of spelling. check_eq $P/attr-str '' "$(pv attr_str)" # ------------------------------------------------------------- the escaping -- # The four characters escape() rewrites. `&' MUST be rewritten first -- escaping # `<' before it would double-escape the ampersand that rewrite introduces -- and # `esc-amp-only' is the case that would catch the wrong order: a literal `&' # in the source must come out as `&amp;', not as `&'. check_eq $P/esc-text '

a\s<\sb\s&\sc\s>\sd

' "$(pv esc_text)" check_eq $P/esc-amp-only '

&amp;

' "$(pv esc_amp_only)" # A QUOTE INSIDE AN ATTRIBUTE VALUE becomes ", which is what keeps the # attribute from ending early. This is the `quote=True' branch of escape(). check_eq $P/esc-attr-quote '' \ "$(pv esc_attr_quote)" # A SINGLE QUOTE IS NOT ESCAPED -- escape() rewrites the double quote only. # Asserted so a release that started escaping it is a red here rather than a # surprise in somebody's rendered output. check_eq $P/esc-apostrophe "

it's

" "$(pv esc_apostrophe)" # `raw' BYPASSES ALL OF IT. The same string that escapes above reaches the tree # untouched -- the documented, deliberately unsafe hatch, and the mechanism is # `text(x, escape=False)' rather than a separate code path, which the three cases # after it state directly. check_eq $P/raw-text '

a\s<\sb\s&\sc\s>\sd

' "$(pv raw_text)" check_eq $P/raw-is-text "True" "$(pv raw_is_text)" check_eq $P/text-escape-flag "True" "$(pv text_escape_flag)" check_eq $P/raw-escape-flag "False" "$(pv raw_escape_flag)" # ------------------------------------------------ the container's own model -- # `__len__' counts CHILDREN -- not attributes, and not the tag itself. check_eq $P/len-children "2" "$(pv len_children)" check_eq $P/len-empty "0" "$(pv len_empty)" # ...but an empty tag is still TRUTHY. `__bool__' is pinned to True precisely so # `if tag:' does not silently become a child count, and this is the case that # says so. check_eq $P/bool-empty "True" "$(pv bool_empty)" # `__getitem__' is overloaded by KEY TYPE: an int indexes children, a string # indexes attributes, and anything else is a TypeError. All three, plus the two # misses, because each raises a DIFFERENT exception and a caller catches them # separately. check_eq $P/get-child0 '

one

' "$(pv get_child0)" check_eq $P/get-attr "c" "$(pv get_attr)" check_eq $P/get-child-oob \ 'ERR:IndexError:Child with index "9" does not exist.' "$(pv get_child_oob)" check_eq $P/get-attr-missing \ 'ERR:AttributeError:Attribute "nope" does not exist.' "$(pv get_attr_missing)" check_eq $P/get-badkey \ 'ERR:TypeError:Only integer and string types are valid for accessing child tags and attributes, respectively.' \ "$(pv get_badkey)" # `__getattr__ = __getitem__', so an attribute is also readable as a Python # attribute -- under its CLEANED name. That aliasing has a sharp edge worth # pinning: EVERY attribute miss is routed into the attribute dict, so `tag.tagname' # raises AttributeError on a perfectly good tag rather than answering `p'. check_eq $P/get-attr-dotted "x" "$(pv get_attr_dotted)" check_eq $P/get-tagname-shadowed \ 'ERR:AttributeError:Attribute "tagname" does not exist.' "$(pv iter_tagname_attr)" # `__iter__' walks the children in order. check_eq $P/iter-children "p,p" "$(pv iter_children)" # ------------------------------------------------------------- the recursion -- # See the header for why this is a bracket over one 200-node tree rather than a # deep nest. `nest-ok-default' is the ticket's claim -- the tree renders at the # interpreter's untouched limit; the two beside it are what make it a statement # about the INTERPRETER'S bound: the same tree renders at 403 and raises # RecursionError at 402. # # `recursion_limit_default' is emitted by the probe and NOT asserted here: it is # a fact about the interpreter rather than about dominate, and 99-python3 owns # it. It is in the capture for whoever reads a red on the three cases below. check_eq $P/nest-ok-default "True" "$(pv nest_ok_default)" check_eq $P/nest-len "2200" "$(pv nest_len)" check_eq $P/render-need "403" "$(pv render_need)" check_eq $P/nest-at-need "ok" "$(pv nest_at_need)" check_eq $P/nest-below-need "RecursionError" "$(pv nest_below_need)" # A three-level nest rendered whole, so the pretty printer's indentation is # pinned by a value a reader can check by eye against the two big literals above. check_eq $P/nest-small \ '
\n\s\s
\n\s\s\s\s
\n\s\s
\n
' "$(pv nest_small)" # 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"