. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is nearly the whole cost. # # Everything else reads the filesystem and dpkg with shell # # builtins or greps the single probe run's output. The # # probe's own work is 14 sha256 digests over 14 small files # # -- the largest is a 15 kB stylesheet template and the # # whole payload is under 30 kB -- plus one configparser # # parse, which is nothing beside the interpreter load. It # # matches 99-python3-jinja2's and 99-python3-mako's 1200 for # # the same reason: the start dominates. # # 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-alabaster 0.7.16 (poky/meta recipes-devtools/python/ # python3-alabaster_0.7.16.bb, `inherit python_flit_core pypi', WITH a # meta-brintos bbappend) on the wasm32-hwjs glibc image -- THE DEFAULT SPHINX # HTML THEME. It is what a Sphinx project renders as before it chooses a theme # of its own. # # ============================================================================ # WHY THIS IS ASSERTED AS PACKAGING, AND WHY THAT IS NOT A WEAKER TEST. # ============================================================================ # # A theme is DATA. The package ships 17 files and only two of them execute: # __init__.py, whose three functions are all called by Sphinx and none of them # by anything else, and support.py, which defines a Pygments style class. # Everything else is five Jinja templates, a stylesheet template, a stylesheet # and a theme.conf. This image does not install Sphinx, so there is no rendering # to exercise and no output to compare. # # What CAN be asserted -- and asserted far more strongly than by a `test -s' per # file -- is that the content of every installed file is the content upstream # shipped. The wheel carries its own manifest of sha256 digests for exactly that # purpose: alabaster-0.7.16.dist-info/RECORD gives a digest and a byte count for # all 14 payload files. So `record-*' below is the PACKAGE's own statement about # itself, re-measured on target -- not an expectation this suite invented, and # not one that a version bump silently invalidates. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASE IS 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. alabaster is two .py files and a pile # of data, which is the entire reason it ports, and BRINTOS-2046 does not reach # it. The wheel says so itself: `Root-Is-Purelib: true', tag `py3-none-any', # generator `flit 3.9.0'. # # So `pkg-no-so' is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump, and it goes red at INSTALL time rather than at `import alabaster' on # somebody else's ticket three rounds later. # # ============================================================================ # THE ONE THING THE STOCK RECIPE GETS WRONG, AND THE CASE THAT PINS IT. # ============================================================================ # # python_flit_core's base contributes python3-core and nothing else, so the # stock .deb reads `Depends: python3-core'. For `import alabaster' that is # exactly right and no more -- __init__.py imports `os' and nothing else. # # But alabaster/support.py does `from pygments.style import Style' and `from # pygments.token import (...)' at MODULE scope, and theme.conf line 5 reads # `pygments_style = alabaster.support.Alabaster', which is the theme's DEFAULT. # So every Sphinx build that selects this theme and does not override that # setting imports a module the stock package will not let you import. Upstream # declares no dependencies at all (alabaster's pyproject.toml has no # `dependencies' key), which is defensible for a PyPI theme whose only consumer # is Sphinx and not for an OE package installed on its own. # python3-alabaster_%.bbappend adds `RDEPENDS:${PN} += "python3-pygments"'; # `pkg-depends-pygments' is what says the bbappend is in the image, and the # `support-*' cases are what say the dependency it declares is sufficient. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # The expectations come from the tree unpacked from the .deb this recipe built # (python3-alabaster_0.7.16-r0_wasm32.deb), read on the x86-64 control with the # probe below run against it verbatim. Nothing here is arm-dependent: the wheel # is `py3-none-any', the digests are the wheel's own, and the two .py files are # the same bytes on both machines. The paths are the only substitution, and they # are built from $SITE rather than written out. # # The runner's read drops the first line a suite emits, per-suite and # deterministic -- not a defect in the host tool (BRINTOS-1149, filed 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 # stands in that position so the eaten line costs nothing. It sits ahead of the # `require' guard deliberately, because a `require' that skips would emit its # skip as the first line and be eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. P=python3-alabaster bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/alabaster DI=$SITE/alabaster-0.7.16.dist-info PKGLIST=/var/lib/dpkg/info/python3-alabaster.list PROBE=$BST_DATA/python3-alabaster-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-alabaster.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. The probe's `module-answered' further down is # what says the interpreter agreed; these say what the filesystem holds, 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. A theme MUST be a directory -- Sphinx looks inside # it for theme.conf -- so a single alabaster.py would be a release that could not # work as a theme at all, caught here rather than nowhere. check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/alabaster.py" # THE TWO SOURCE FILES, EACH BY NAME. __init__.py carries setup(), which is what # Sphinx calls to register the theme, and support.py carries the Pygments style # theme.conf names. They are the only two files here that execute. check $P/src-init test -s "$PKGDIR/__init__.py" check $P/src-support test -s "$PKGDIR/support.py" # THE THEME ITSELF. theme.conf is the file that makes this directory a theme; # the five templates are the pages and partials Sphinx renders through it; the # two static files are the stylesheet template and the empty-by-design override # hook. Each is named rather than counted, because a wheel that dropped one # would still import and would fail at render time in somebody else's build. check $P/theme-conf test -s "$PKGDIR/theme.conf" check $P/tpl-layout test -s "$PKGDIR/layout.html" check $P/tpl-about test -s "$PKGDIR/about.html" check $P/tpl-navigation test -s "$PKGDIR/navigation.html" check $P/tpl-relations test -s "$PKGDIR/relations.html" check $P/tpl-donate test -s "$PKGDIR/donate.html" check $P/static-dir test -d "$PKGDIR/static" # alabaster.css_t, NOT alabaster.css. The `_t' suffix is Sphinx's marker for a # file it renders through Jinja with theme.conf's [options] as the context, # writing alabaster.css into the output. theme.conf's `stylesheet' key therefore # names a file that does not exist in the package, which is correct and is the # single most confusing thing about a Sphinx theme's layout. The probe's # `stylesheet-templated' states the relationship rather than leaving it to this # comment. check $P/static-css-template test -s "$PKGDIR/static/alabaster.css_t" check $P/no-rendered-css test ! -f "$PKGDIR/static/alabaster.css" # custom.css is 42 bytes of comment -- the documented hook a user overrides. It # is asserted with `test -f', not `test -s': it is meant to be nearly empty and # a release that emptied it completely would be within its rights. check $P/static-custom-css test -f "$PKGDIR/static/custom.css" # The precompiled bytecode for BOTH modules, which is what `import-quiet' below # turns on. support.py gets a .pyc even though nothing imports it at build time: # OE byte-compiles the tree with compileall, which does not import. check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-support test -s "$PKGDIR/__pycache__/support.cpython-312.pyc" # NO COMPILED EXTENSION. See the header -- this is the regression tripwire, not # boilerplate. # # 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)" # NOT ONE FILE IN THIS PACKAGE IS EXECUTABLE, AND NONE IS UNDER /usr/bin. A # theme installs no program. The case above names the failure that is expected; # this one catches the shape nobody predicted. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(bin|sbin)/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON, A TEMPLATE, A STYLESHEET OR # ITS METADATA -- without having to enumerate what an unexpected one 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|*.html|*.css|*.css_t) continue ;; */alabaster/theme.conf) continue ;; */alabaster-0.7.16.dist-info/METADATA) continue ;; */alabaster-0.7.16.dist-info/RECORD) continue ;; */alabaster-0.7.16.dist-info/WHEEL) continue ;; */alabaster-0.7.16.dist-info/LICENSE.rst) continue ;; */alabaster-0.7.16.dist-info/entry_points.txt) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------ the dist-info -- # The installed distribution metadata. It is not decoration here: the entry point # Sphinx discovers the theme through lives in it, and importlib.metadata answers # `dist-version' out of it. The directory name carries the version, so a mixed # install -- new files 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" check $P/dist-info-wheel test -s "$DI/WHEEL" check $P/dist-info-license test -s "$DI/LICENSE.rst" check $P/dist-info-entry-points-file test -s "$DI/entry_points.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" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-generator "Generator: flit 3.9.0" cat "$DI/WHEEL" # alabaster ships NO top_level.txt -- flit does not write one -- so the import # name is taken from METADATA's Name instead. Lowercase `alabaster', unlike # Jinja2's capitalised Name in the same round. check_out $P/dist-info-name "Name: alabaster" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 0.7.16" cat "$DI/METADATA" # THE ENTRY POINT, READ AS TEXT. The probe reads the same fact through # importlib.metadata; this reads the file. Both are here because they fail # differently: a corrupt file reds here, and a metadata cache that cannot see a # perfectly good file reds there. check_out $P/entry-point-group "[sphinx.html_themes]" cat "$DI/entry_points.txt" check_out $P/entry-point-line "alabaster=alabaster" cat "$DI/entry_points.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-alabaster 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-alabaster$/,/^$/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: 0.7.16-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE RUNTIME DEPENDENCIES, as dpkg recorded them, and this is where the port's # one substantive change is visible. python3-core is what python_flit_core's # base contributes; python3-pygments is the bbappend's, and without it the .deb # ships alabaster/support.py -- the module theme.conf's `pygments_style' names -- # on a rootfs that cannot import it. See the header. 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-pygments "python3-pygments" cat "$W/dep" # THE IMAGE TOOK THE RUNTIME PACKAGE ALONE. The recipe BBCLASSEXTENDs to native # and nativesdk and OE splits a -dev and a -dbg from every recipe; none of the # three belongs on a target rootfs, and a -dbg installed here would double the # package's footprint for a theme that has nothing to debug. check_eq $P/no-dev-package "" \ "$(sed -n '/^Package: python3-alabaster-dev$/p' /var/lib/dpkg/status 2>/dev/null)" check_eq $P/no-dbg-package "" \ "$(sed -n '/^Package: python3-alabaster-dbg$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If alabaster 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 alabaster' # AND pygments SEPARATELY, because it is the one dependency that is a real # package rather than a python3 split, and it is the one the bbappend exists for. # A red here and a red on `support-import' together mean the dependency is # missing from the rootfs; a red on `support-import' alone points at # alabaster/support.py itself. check $P/import-pygments python3 -c 'import pygments' # ...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. # # alabaster has no import-time output of its own: no logging configuration, no # warning, no print. check_eq $P/import-quiet "" \ "$(python3 -c 'import alabaster' 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 `version_info' and `dist_version'. 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 package, 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__', its `__version_info__' tuple (which update_context puts in every # rendered page's context, so a disagreement between the two is a visible defect # in the output), and importlib.metadata over the installed dist-info. Two of # these disagreeing means a mixed install. check_eq $P/version "0.7.16" "$(pv version)" check_eq $P/version-info "0.7.16" "$(pv version_info)" check_eq $P/dist-version "0.7.16" "$(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 metadata # cases below statements about the SAME alabaster. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/theme-dir "$PKGDIR" "$(pv theme_dir)" # Everything alabaster loaded is .py or .pyc -- the interpreter's own view of the # header's claim, complementing `pkg-no-so', which is the packaging view. check_eq $P/all-pure-python "yes" "$(pv alabaster_all_py)" # ------------------------------------------------------------------ the API --- # THE PUBLIC SURFACE, sorted and read off the module -- alabaster declares no # __all__. `os' is in it, and that is not a mistake in this expectation: # __init__.py does a plain `import os' at module scope, so the name is a module # attribute like any other. It is stated rather than filtered out, because a # release that switched to `from os import path' would change the surface and # this is the case that would say so. check_eq $P/api-surface "get_path,os,setup,update_context" "$(pv api_surface)" # The three functions, without the imported module. setup() is what Sphinx calls # to register the theme, update_context() is connected to html-page-context and # runs on every page, get_path() is the shortcut for wiring the theme by path. check_eq $P/api-callables "get_path,setup,update_context" "$(pv api_callables)" # WHAT get_path() ACTUALLY RETURNS, WHICH IS NOT THE THEME DIRECTORY. It returns # the package's PARENT -- site-packages -- because that is html_theme_path # semantics: Sphinx is handed a search path and looks in it for a subdirectory # named after the theme. A reader who assumes it points at theme.conf gets a # wrong answer that looks plausible, so both halves are asserted: the value, and # the relationship that makes it correct. check_eq $P/get-path "$SITE" "$(pv get_path)" check_eq $P/get-path-is-parent "yes" "$(pv get_path_is_parent)" # ...and the theme is reachable BOTH ways: theme.conf sits in the package # directory, and it is found under /alabaster/theme.conf, which is # the path Sphinx would actually walk. check_eq $P/theme-conf-in-package "yes" "$(pv theme_conf_under_theme_dir)" check_eq $P/theme-conf-via-getpath "yes" "$(pv theme_conf_reachable_from_get_path)" # ------------------------------------------------------- the entry point --- # HOW SPHINX FINDS THIS THEME AT ALL. It does not look in site-packages for a # directory; it enumerates the `sphinx.html_themes' entry-point group over the # installed distributions. An installed tree whose dist-info lost this group is a # theme Sphinx cannot see, and every file case above would still pass. Exactly # one entry point, named for the theme and pointing at the package. check_eq $P/ep-count "1" "$(pv ep_count)" check_eq $P/ep-name "alabaster" "$(pv ep_name)" check_eq $P/ep-value "alabaster" "$(pv ep_value)" # AND IT LOADS, to this same module -- which is what ties the metadata statement # to the package the payload cases measured. importlib.metadata resolving a name # is a weaker claim than importing what it resolves to. check_eq $P/ep-loads "$PKGDIR/__init__.py" "$(pv ep_loads_to_module_file)" # --------------------------------------------------------- the theme.conf --- # theme.conf is a config file Sphinx PARSES, so it is asserted by parsing rather # than by grepping: a file that no longer parses is a theme that fails in # somebody's doc build, and a grep would not notice. `inherit' and `stylesheet' # are the two keys the loader requires. check_eq $P/tc-parsed "True" "$(pv tc_parsed)" check_eq $P/tc-sections "theme,options" "$(pv tc_sections)" check_eq $P/tc-inherit "basic" "$(pv tc_inherit)" check_eq $P/tc-stylesheet "alabaster.css" "$(pv tc_stylesheet)" # The theme's DEFAULT Pygments style, and the reason this package needs # python3-pygments at all. See the header. check_eq $P/tc-pygments-style "alabaster.support.Alabaster" "$(pv tc_pygments_style)" # The [options] block is the theme's configuration surface -- every key here is # a setting a user may set in html_theme_options and a name alabaster.css_t # interpolates. A count rather than a list: 112 keys named one by one would be # 112 cases asserting the same fact, and the checksum pass below already pins # theme.conf's exact bytes. check_eq $P/tc-options-count "112" "$(pv tc_options_count)" # The sidebar templates, spaces escaped \s by the probe. Four of the five are in # this package; searchbox.html is NOT, and its absence is correct -- it comes # from the `basic' theme this one inherits, which ships with Sphinx. Split into # two cases so the inherited one is recorded as inherited rather than found # missing by a later reader counting files. check_eq $P/tc-sidebars \ "about.html,\snavigation.html,\srelations.html,\ssearchbox.html,\sdonate.html" \ "$(pv tc_sidebars)" check_eq $P/tc-sidebars-shipped \ "about.html,donate.html,navigation.html,relations.html" \ "$(pv sidebar_templates_shipped)" check_eq $P/tc-sidebars-inherited "searchbox.html" "$(pv sidebar_templates_inherited)" # theme.conf's `stylesheet' names alabaster.css and the package ships # static/alabaster.css_t. This case states that the second IS the first -- # Sphinx renders the `_t' through Jinja with [options] as the context -- so the # `no-rendered-css' case above reads as the design rather than as a missing file. check_eq $P/stylesheet-templated "yes" "$(pv stylesheet_ships_as_template)" # ------------------------------------------------- the manifest checksums --- # THE CENTRE OF THIS SUITE. Every payload file re-hashed on target and scored # against the wheel's own RECORD -- 14 rows, each a sha256 and a byte count. # RECORD's own row carries no digest (a file cannot hash itself) and is skipped # by construction rather than by name, which is why `record-rows' is 14 and not # 15. # # These four cases together say something no per-file `test -s' can: not that the # files are present, but that they are the RIGHT BYTES. A truncated template, a # stylesheet mangled by a line-ending conversion, a file that landed from a # different version -- each is a red here and is invisible to everything else in # this suite. check_eq $P/record-rows "14" "$(pv record_rows)" check_eq $P/record-all-found "14" "$(pv record_checked)" check_eq $P/record-none-missing "" "$(pv record_missing)" check_eq $P/record-digests-match "" "$(pv record_bad_digest)" check_eq $P/record-sizes-match "" "$(pv record_bad_size)" # ------------------------------------------------------ the second module --- # alabaster.support IS THE HALF THAT NEEDS python3-pygments. These are the cases # that say the bbappend's dependency is not merely declared but sufficient: the # module imports, the class theme.conf names exists, it derives from Pygments' # own Style, and it carries the full token map a style is. check_eq $P/support-import "$PKGDIR/support.py" "$(pv support_file)" check_eq $P/support-style-class "Alabaster" "$(pv support_style_class)" check_eq $P/support-style-base "Style" "$(pv support_style_base)" check_eq $P/support-style-bg "#f8f8f8" "$(pv support_style_bg)" check_eq $P/support-style-tokens "80" "$(pv support_style_token_count)" # The name theme.conf points at resolves to the class support.py defines -- the # two halves of the pygments story agreeing with each other. A theme.conf that # named a class the module does not have would pass every case above. check_eq $P/pygments-style-resolves "yes" "$(pv pygments_style_resolves)" rm -rf "$W"