. "$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\s Probe \n\s\s\s\s\n\s\s\n\s\s\n\s\s\s\shello
\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\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 \ 'a\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\sraw\n\s\s\s\s
Doc ' \ "$(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\sa\s<\sb\s&\sc\s>\sd
raw
Doc \n\s\s\n\s\s\n\s\s\s\s\n\s\s\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\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\sraw\n\s\s\s\s
Doc \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\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 `&', not as `&'. check_eq $P/esc-text 'a\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\sraw\n\s\s\s\s\s\s\s\s
a\s<\sb\s&\sc\s>\sd
' "$(pv esc_text)" check_eq $P/esc-amp-only '&
' "$(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' "$(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"\n\s\s\s\s\n\s\s\n