. "$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 104 sha256 digests over the wheel's # # RECORD -- the whole payload is 96 message catalogues # # averaging 1 kB, one 10 kB .py and two templates under # # 1 kB -- plus one build of a small docutils tree and two # # template renders into a tmpdir. That is nothing beside the # # interpreter load. It matches 99-python3-sphinxcontrib- # # jquery'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-sphinxcontrib-qthelp 1.0.7 (poky/meta recipes-devtools/python/ # python3-sphinxcontrib-qthelp_1.0.7.bb, `inherit pypi python_flit_core', NO # meta-brintos bbappend) on the wasm32-hwjs glibc image -- THE SPHINX BUILDER # THAT EMITS QT HELP INPUT. It writes the .qhp project file and the .qhcp # collection file that `qhelpgenerator' compiles into the .qch Qt Assistant # reads. One of the five builders split out of Sphinx core into their own # distributions; four of its siblings -- applehelp, devhelp, htmlhelp, # serializinghtml -- are sibling tickets in this same Epic. # # ============================================================================ # THE ONE FACT THAT SHAPES THIS WHOLE SUITE: THE MODULE CANNOT BE IMPORTED HERE. # ============================================================================ # # sphinxcontrib/qthelp/__init__.py imports NINE names out of `sphinx' at MODULE # scope -- addnodes, Sphinx, StandaloneHTMLBuilder, IndexEntries, # get_translation, logging, NodeMatcher, canon_path/make_filename and # SphinxRenderer. This image does not install Sphinx: python3-sphinx exists in # poky (recipes-devtools/python/python3-sphinx_7.2.6.bb) and is in neither # core-image-brintos-emcraft nor any port ticket. So on this rootfs # # python3 -c 'import sphinxcontrib.qthelp' # # raises ModuleNotFoundError: No module named 'sphinx', and `import-needs-sphinx' # below asserts exactly that. THAT CASE IS GREEN ON PURPOSE. It is not a defect # being tolerated and it is not a platform gap: it is the measured, expected # state of an image that installs a Sphinx builder without installing Sphinx, # and it is the case that will go RED -- and so demand that this decision be # revisited -- on the day python3-sphinx lands. # # What is asserted instead is the package's CONTRACT WITH SPHINX and, more # substantially than for an extension that only registers assets, ITS OWN # ARITHMETIC. A builder is not a thin registration shim: this one walks a # doctree into a , folds an index into elements, normalises a Qt # namespace under rules the format imposes, selects the files that belong in the # project, and renders two documents. Every one of those is a pure function of # inputs this suite supplies, and every one of them is measured below against a # fixture, without Sphinx and without a documentation build. # # ============================================================================ # WHAT IS *NOT* STUBBED, AND WHY THAT MATTERS HERE MORE THAN FOR jquery. # ============================================================================ # # python3-docutils IS on this image -- 0.20.1, with its own suite # (99-python3-docutils) -- and the node classes qthelp's tree walk is written # against are the REAL ones: bullet_list, list_item, reference, and the # document those hang off. `compact_paragraph' is the single node type Sphinx # adds, and the probe defines it exactly as Sphinx does, as a nodes.paragraph # subclass. So write_toc()'s recursion and isdocnode()'s four-part shape test run # over a real docutils tree; only the names that live in Sphinx itself are # supplied. `docutils-version' below pins the docutils the fixture ran against, # because the tree walk is the one part of this suite whose behaviour a docutils # bump could move. # # The one piece of stub that does real work is the template renderer. render_file # goes through sphinx.util.template.SphinxRenderer, which is Jinja2, and neither # Sphinx nor Jinja2 is on this image; the probe therefore carries a small # renderer covering exactly the constructs the two shipped templates use. That # renderer is the probe's, not the package's, so the XML is NOT the primary # evidence: `qhp-ctx-*' and `qhcp-ctx-*' assert the context dict the PACKAGE # hands the template engine, which is pure package output and independent of any # renderer, and the rendered documents are asserted beside them to pin the # shipped templates' own text. A red in a context case means the package moved; # a red in an XML case alone means a template did. # # ============================================================================ # WHY THERE IS NO bbappend -- AND WHY, UNLIKE jquery, THERE IS NOTHING TO RECORD. # ============================================================================ # # The built .deb reads `Depends: python3-core'; python_flit_core's base # contributes that and nothing else. For sphinxcontrib-jquery (BRINTOS-2106) that # was a real disagreement with upstream, whose METADATA carries a bare # `Requires-Dist: Sphinx>=1.8'. HERE IT IS NOT. This package's pyproject.toml # declares `dependencies = []' and puts Sphinx in an OPTIONAL extra # (`standalone'), so every Requires-Dist in its METADATA is guarded by an # `extra ==' marker and the package declares NO unconditional runtime # dependency at all. `dist-requires-hard' and `dist-info-no-bare-requires' below # assert that emptiness directly, which is what makes `Depends: python3-core' a # faithful translation rather than a dropped dependency. # # That does not make the module importable -- it still imports sphinx at module # scope, and upstream's own metadata is simply optimistic about who is doing the # importing. It does mean there is no packaging defect to fix and no shortfall to # record. `pkg-depends-no-sphinx' is kept anyway, as the tripwire: it goes red # together with `import-needs-sphinx' the day python3-sphinx is ported, which is # exactly when the question of installing a builder without its application # should be reopened. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL. # ============================================================================ # # 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. This package is one 10 kB .py file, two # Jinja2 templates and 96 message-catalogue files, 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 somebody else's import # three rounds later. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # The expectations come from the tree unpacked from the .deb this recipe built # (python3-sphinxcontrib-qthelp_1.0.7-r0_wasm32.deb), read on the x86-64 control # with the probe below run against it verbatim, on CPython 3.14.7 with docutils # 0.20.1 -- the docutils the image ships. Nothing here is arm-dependent: the # wheel is `py3-none-any', the digests are the wheel's own, and the single .py # file is the same bytes on both machines. The interpreter differs from the # image's 3.12 and nothing asserted below is version-sensitive: the fixtures are # string and node arithmetic, and `version-info' reads the package's own tuple. # 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-sphinxcontrib-qthelp bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages NSDIR=$SITE/sphinxcontrib PKGDIR=$NSDIR/qthelp DI=$SITE/sphinxcontrib_qthelp-1.0.7.dist-info PKGLIST=/var/lib/dpkg/info/python3-sphinxcontrib-qthelp.list PROBE=$BST_DATA/python3-sphinxcontrib-qthelp-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-scqthelp.$$ 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" # THE NAMESPACE DIRECTORY, AND THE FILE THAT MUST NOT BE IN IT. `sphinxcontrib' # is a PEP 420 implicit namespace package: it is a directory with NO # __init__.py, which is how the whole sphinxcontrib-* family -- jquery, jsmath, # serializinghtml, applehelp, devhelp, htmlhelp and this one, every one of them a # sibling ticket in this Epic -- shares one top-level name without any single # distribution owning it. A release that started shipping sphinxcontrib/ # __init__.py would shadow all of them, and would pass every other case here. check $P/namespace-dir test -d "$NSDIR" check $P/namespace-no-init test ! -f "$NSDIR/__init__.py" check $P/package-dir test -d "$PKGDIR" # THE ONE SOURCE FILE. Everything that executes in this package is in it: # QtHelpBuilder, the two module-level helpers, and setup(). check $P/src-init test -s "$PKGDIR/__init__.py" # THE TWO TEMPLATES. These are DATA, not code, and they are the package's actual # output format: project.qhp is the QtHelpProject document Qt's help generator # consumes and project.qhcp is the collection project that drives it. A port that # shipped the .py and lost the templates would import cleanly under Sphinx and # fail at the end of a documentation build, which is the most expensive place to # find out. check $P/templates-dir test -d "$PKGDIR/templates" check $P/tpl-qhp test -s "$PKGDIR/templates/project.qhp" check $P/tpl-qhcp test -s "$PKGDIR/templates/project.qhcp" # THE MESSAGE CATALOGUES. setup() calls add_message_catalog() against # $PKGDIR/locales, so the whole tree is a runtime dependency of the builder's # user-facing strings rather than documentation. The count is asserted in the # probe; here the directory and one representative pair, so a tree that vanished # entirely reads differently from one that lost a language. check $P/locales-dir test -d "$PKGDIR/locales" check $P/locale-de-mo test -s "$PKGDIR/locales/de/LC_MESSAGES/sphinxcontrib.qthelp.mo" check $P/locale-de-po test -s "$PKGDIR/locales/de/LC_MESSAGES/sphinxcontrib.qthelp.po" # The template catalogue and the Transifex config ship too -- upstream's own # translation workflow, carried into the wheel rather than left in the sdist. # They are inert at runtime; they are asserted because `pkg-only-expected' has to # name them and a reader should be able to see why. check $P/locale-pot test -s "$PKGDIR/locales/sphinxcontrib.qthelp.pot" check $P/locale-tx test -s "$PKGDIR/locales/.tx/config" # The precompiled bytecode. OE byte-compiles the tree with compileall, which # does NOT import -- which matters here more than usual, since importing this # module needs a Sphinx that is not on the image. So the .pyc exists despite the # module being unimportable, and that is correct rather than surprising. check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.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 # Sphinx builder installs no program -- the program that consumes its output is # Qt's qhelpgenerator, which is not part of this distribution and is not on this # image. 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 CATALOGUE 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|*.mo|*.po|*.pot) continue ;; */templates/project.qhp|*/templates/project.qhcp) continue ;; */locales/.tx/config) continue ;; */sphinxcontrib_qthelp-1.0.7.dist-info/LICENSE) continue ;; */sphinxcontrib_qthelp-1.0.7.dist-info/METADATA) continue ;; */sphinxcontrib_qthelp-1.0.7.dist-info/RECORD) continue ;; */sphinxcontrib_qthelp-1.0.7.dist-info/WHEEL) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------ the dist-info -- # The installed distribution metadata. 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-license test -s "$DI/LICENSE" 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" # NO entry_points.txt, AND ITS ABSENCE IS THE DESIGN. A Sphinx THEME is # discovered through the `sphinx.html_themes' entry-point group -- python3- # alabaster ships one for exactly that. A BUILDER is not discovered at all: the # user names the extension in conf.py's `extensions' list, Sphinx imports it, and # setup() calls add_builder(). So there is nothing to declare, and a file here # would mean the package had changed shape. check $P/no-entry-points test ! -f "$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. Unlike jquery's, # this wheel carries the py3 tag ALONE: the package requires >=3.9 and says so. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-no-py2 "" \ "$(grep -F 'Tag: py2-none-any' "$DI/WHEEL" 2>/dev/null)" check_out $P/dist-info-generator "Generator: flit 3.9.0" cat "$DI/WHEEL" # The distribution name is HYPHENATED and the import name is DOTTED -- # `sphinxcontrib-qthelp' installs `sphinxcontrib.qthelp'. flit writes no # top_level.txt, so METADATA's Name is the only packaging-side statement of the # name, and it is not the name anyone imports. check_out $P/dist-info-name "Name: sphinxcontrib-qthelp" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 1.0.7" cat "$DI/METADATA" check_out $P/dist-info-python "Requires-Python: >=3.9" cat "$DI/METADATA" # SPHINX IS AN OPTIONAL EXTRA, NOT A DEPENDENCY -- the fact that makes this port # different from sphinxcontrib-jquery's and the reason no bbappend is warranted. # See the header. check_out $P/dist-info-standalone-extra \ 'Requires-Dist: Sphinx>=5 ; extra == "standalone"' cat "$DI/METADATA" # ...AND NOT ONE Requires-Dist IS UNCONDITIONAL. Every one carries an `extra ==' # marker, so the package declares no runtime dependency whatever, which is what # `Depends: python3-core' faithfully translates. The probe reads the same fact # through importlib.metadata; both are here because they fail differently -- a # corrupt file reds here, a metadata cache that cannot see a good file reds there. check_eq $P/dist-info-no-bare-requires "" \ "$(grep '^Requires-Dist:' "$DI/METADATA" 2>/dev/null | grep -v 'extra ==')" # ------------------------------------------------------------- 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-sphinxcontrib-qthelp 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-sphinxcontrib-qthelp$/,/^$/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: 1.0.7-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE RUNTIME DEPENDENCIES, as dpkg recorded them. python3-core is what # python_flit_core's base contributes and it is the whole of the line -- and, # unlike jquery's, it is not a shortfall: upstream declares none. 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" # THE TRIPWIRE. Asserting the ABSENCE of a python3-sphinx dependency dates the # decision described in the header, and makes the day Sphinx is ported the day # this case goes red together with `import-needs-sphinx'. check_eq $P/pkg-depends-no-sphinx "" \ "$(grep -E 'python3-sphinx([ ,]|$)' "$W/dep" 2>/dev/null)" # 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. check_eq $P/no-dev-package "" \ "$(sed -n '/^Package: python3-sphinxcontrib-qthelp-dev$/p' /var/lib/dpkg/status 2>/dev/null)" check_eq $P/no-dbg-package "" \ "$(sed -n '/^Package: python3-sphinxcontrib-qthelp-dbg$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT FAILS, AND THAT IS THE MEASURED STATE OF THIS IMAGE, NOT A DEFECT. # See the header. Nine names imported from `sphinx' at module scope, no # python3-sphinx in the rootfs, so the interpreter raises ModuleNotFoundError and # exits 1. This case is what pins the fact; the probe's `bare-import' names the # missing module, so a DIFFERENT import failure -- a genuinely broken package -- # is distinguishable from this one rather than hiding behind the same red. check_rc $P/import-needs-sphinx 1 python3 -c 'import sphinxcontrib.qthelp' # ...AND SPHINX ITSELF IS WHAT IS ABSENT, asserted separately so the cause is # stated rather than inferred. A red HERE and a pass above would mean Sphinx had # arrived, which is the event that reopens the decision in the header. check_rc $P/sphinx-absent 1 python3 -c 'import sphinx' # DOCUTILS, ON THE OTHER HAND, IS PRESENT -- which is what lets the probe run the # builder's tree walk over real nodes instead of stubs. A red here explains, in # one line, why most of the probe's cases below would also be red. check $P/docutils-present python3 -c 'import docutils' # THE NAMESPACE PACKAGE ITSELF IMPORTS, THOUGH -- it is a directory with no code # in it, so nothing it does can fail. This is what separates "the submodule needs # Sphinx" from "sphinxcontrib is not installed at all", which would look the same # from `import-needs-sphinx'. check $P/import-namespace python3 -c 'import sphinxcontrib' # ---------------------------------------------------------------- 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 BARE IMPORT, MEASURED INSIDE THE PROBE TOO, and naming the module that is # missing. `check_rc' above says the import exits 1; this says WHY, and it is the # case that would distinguish a package broken some other way -- a SyntaxError, a # missing file -- from the absent Sphinx this image is expected to have. check_eq $P/bare-import "ModuleNotFoundError:sphinx" "$(pv bare_import)" # THE DOCUTILS THE FIXTURE RAN AGAINST. The tree walk below is the one part of # this suite a docutils bump could move -- Node.traverse() is deprecated in # favour of findall() from 0.18 on, and this package still calls traverse(). # Pinning the version makes a future red here the explanation for a red there. check_eq $P/docutils-version "0.20.1" "$(pv docutils_version)" # THE VERSION THE RECIPE PINS, three times and independently: the package's own # `__version__', its `__version_info__' tuple, and importlib.metadata over the # installed dist-info. Two of these disagreeing means a mixed install. check_eq $P/version "1.0.7" "$(pv version)" check_eq $P/version-info "1.0.7" "$(pv version_info)" check_eq $P/dist-version "1.0.7" "$(pv dist_version)" check_eq $P/dist-name "sphinxcontrib-qthelp" "$(pv dist_name)" # 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 package. `package_dir' is the module's # own computed constant -- what render_file() and add_message_catalog() resolve # their paths against -- so it is asserted rather than inferred from __file__. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/package-dir-const "$PKGDIR" "$(pv package_dir)" # Everything it 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 all_pure_python)" # ------------------------------------------------------ the metadata plane --- # READ THROUGH importlib.metadata rather than grepped, so what is asserted is # what the interpreter resolves out of the installed dist-info. check_eq $P/dist-requires \ 'flake8 ; extra == "lint",mypy ; extra == "lint",docutils-stubs ; extra == "lint",Sphinx>=5 ; extra == "standalone",pytest ; extra == "test"' \ "$(pv dist_requires)" # THE EMPTINESS THAT IS THE POINT: not one of them is unconditional. See the # header -- this is why `Depends: python3-core' is correct and no bbappend is # needed, and it is the single line that separates this port from jquery's. check_eq $P/dist-requires-hard "" "$(pv dist_requires_hard)" check_eq $P/dist-extras "lint,standalone,test" "$(pv dist_extras)" check_eq $P/dist-requires-python ">=3.9" "$(pv dist_requires_python)" # AND DECLARES NO ENTRY POINTS. See `no-entry-points' above: a builder is # registered from setup(), never discovered. check_eq $P/ep-count "0" "$(pv ep_count)" # ------------------------------------------------- the namespace package --- # THE PEP 420 FACTS, from the interpreter rather than the filesystem. # `sphinxcontrib' has __file__ None and a _NamespacePath for __path__ -- that is # what an implicit namespace package IS, and it is the mechanism every sibling # sphinxcontrib-* port in this Epic depends on. `namespace-no-init' above says no # __init__.py is on disk; these say the interpreter agreed and built a namespace # rather than a regular package. check_eq $P/ns-file "None" "$(pv ns_file)" check_eq $P/ns-path-type "_NamespacePath" "$(pv ns_path_type)" check_eq $P/ns-path "$NSDIR" "$(pv ns_path)" # ------------------------------------------------------------------ the API -- # THE PUBLIC SURFACE, sorted and read off the module -- it declares no __all__. # The imported names are in it and that is not a mistake in this expectation: # `from sphinx import addnodes', `from docutils import nodes' and the rest bind # module attributes like any other, so the surface is a faithful statement of # what this module imports. It is also the plainest possible statement of why the # bare import fails: `IndexEntries', `NodeMatcher', `SphinxRenderer', # `StandaloneHTMLBuilder', `addnodes', `canon_path', `get_translation', # `make_filename' and `Sphinx' are all Sphinx's. check_eq $P/api-surface \ "Any,IndexEntries,Iterable,Node,NodeMatcher,QtHelpBuilder,Sphinx,SphinxRenderer,StandaloneHTMLBuilder,addnodes,annotations,canon_path,cast,get_translation,html,logger,logging,make_filename,nodes,os,package_dir,path,posixpath,re,render_file,section_template,setup" \ "$(pv api_surface)" # The two things this module defines that are callable -- render_file() and # setup() -- among the callables it imported. QtHelpBuilder is a class and so is # callable too; that is why it is here. check_eq $P/api-callables \ "Any,IndexEntries,Iterable,Node,NodeMatcher,QtHelpBuilder,Sphinx,SphinxRenderer,StandaloneHTMLBuilder,canon_path,cast,get_translation,make_filename,render_file,setup" \ "$(pv api_callables)" # --------------------------------------------------------- the builder class -- # THE BUILDER'S IDENTITY AND ITS FIVE DELIBERATE DEVIATIONS FROM HTML. `qthelp' # is the name a user writes in `sphinx-build -b qthelp'. The flags are not # decoration: Qt Assistant renders the pages inside its own chrome and runs its # own index, so the builder turns off the reST source copy, the permalink # anchors, the sidebar (`embedded'), the download role and the search index. # Every one of those being wrong produces a .qch that works and is subtly bloated # or broken, which is a bad failure to find by hand. check_eq $P/builder-name "qthelp" "$(pv builder_name)" check_eq $P/builder-base "StandaloneHTMLBuilder" "$(pv builder_base)" check_eq $P/builder-copysource "False" "$(pv builder_copysource)" check_eq $P/builder-permalinks "False" "$(pv builder_add_permalinks)" check_eq $P/builder-embedded "True" "$(pv builder_embedded)" check_eq $P/builder-download-off "False" "$(pv builder_download_support)" check_eq $P/builder-search-off "False" "$(pv builder_search)" check_eq $P/builder-image-types \ "image/svg+xml,image/png,image/gif,image/jpeg" "$(pv builder_image_types)" # The epilog Sphinx prints when the build ends -- the instruction that tells a # user what to do with the .qhcp this builder just wrote. It is the package's # only user-facing string outside the catalogues. check_eq $P/builder-epilog \ 'You\scan\snow\srun\s"qcollectiongenerator"\swith\sthe\s.qhcp\sproject\sfile\sin\s%(outdir)s,\slike\sthis:' \ "$(pv builder_epilog_head)" # ------------------------------------------------------------------ setup() -- # WHAT THE EXTENSION REGISTERS, against a stub application that records the calls # and does nothing else. This is the whole of an extension's contract with Sphinx # and none of it needs Sphinx to be installed. check_eq $P/setup-keys "parallel_read_safe,parallel_write_safe,version" "$(pv setup_keys)" check_eq $P/setup-version "1.0.7" "$(pv setup_version)" check_eq $P/setup-parallel-read "True" "$(pv setup_parallel_read)" check_eq $P/setup-parallel-write "True" "$(pv setup_parallel_write)" # THE MINIMUM SPHINX IT DEMANDS, and the extension it pulls in first -- # QtHelpBuilder subclasses StandaloneHTMLBuilder, so the HTML builder must be set # up before this one registers. check_eq $P/setup-require-sphinx "5.0" "$(pv setup_require_sphinx)" check_eq $P/setup-extension "sphinx.builders.html" "$(pv setup_extensions)" check_eq $P/setup-builders "QtHelpBuilder" "$(pv setup_builders)" check_eq $P/setup-builder-is-qthelp "True" "$(pv setup_builder_is_qthelp)" # THE FOUR CONFIG VALUES A conf.py MAY SET, with the rebuild class and the type # constraint each is declared with. All four rebuild `html'; only # `qthelp_namespace' constrains its type, and that it constrains it to str is the # one thing standing between a conf.py that sets a list and the re.sub() in # build_qhp() raising deep inside a documentation build. check_eq $P/setup-config-names \ "qthelp_basename,qthelp_namespace,qthelp_theme,qthelp_theme_options" \ "$(pv setup_config_names)" check_eq $P/setup-config-rebuild "html,html,html,html" "$(pv setup_config_rebuild)" check_eq $P/setup-config-types \ "qthelp_basename:,qthelp_namespace:str,qthelp_theme:,qthelp_theme_options:" \ "$(pv setup_config_types)" # THE DEFAULTS, and the one that is a CALLABLE rather than a constant. Sphinx # resolves a callable default by calling it with the config object, which is how # the basename tracks `project' without setup() reading a config that does not # exist yet. `qthelp_theme' is `nonav' -- the sidebar-less theme that goes with # `embedded' above. check_eq $P/setup-config-defaults \ "qthelp_basename:callable;qthelp_namespace:None;qthelp_theme:'nonav';qthelp_theme_options:{}" \ "$(pv setup_config_defaults)" # ...AND THE CALLABLE RESOLVED, over a config carrying `project'. The filename is # the project name with everything outside [A-Za-z0-9_-] removed, which is # Sphinx's make_filename(); the stub reproduces it, so what this case measures is # that the default DELEGATES to it with the project rather than what the # algorithm is. check_eq $P/setup-basename-default "MyProject20" "$(pv basename_default_of_project)" # THE CATALOGUE REGISTRATION, by name and by directory. The name is the module's # own __name__, which is the .mo basename shipped in every language directory, so # the two agreeing is what makes 96 catalogue files reachable at all. check_eq $P/setup-catalog-name "sphinxcontrib.qthelp" "$(pv setup_catalog_name)" check_eq $P/setup-catalog-dir "$PKGDIR/locales" "$(pv setup_catalog_dir)" check_eq $P/setup-catalog-is-locales "True" "$(pv setup_catalog_is_locales)" # --------------------------------------------------------- the table of contents # THE TREE WALK, over a REAL docutils document. The fixture carries both shapes # write_toc() distinguishes and the discrimination the matcher must make: # # * a `document' list_item -- compact_paragraph>reference followed by a # bullet_list -- which isdocnode() accepts and which becomes an OPEN #
with its children nested inside and a closing tag; # * bare references, which become the self-closing
form; # * a second compact_paragraph carrying toctree=False, which the matcher must # NOT pick up. `toc-matched' and `toc-decoy-absent' are the two halves of # that: one toctree found, and the decoy's title nowhere in the output. # # The titles carry `&', `<', `"' and a non-ASCII character on purpose, because # THE TWO BRANCHES ESCAPE DIFFERENTLY: both run html.escape and turn `"' into # ", but only the self-closing branch then re-encodes the whole item with # xmlcharrefreplace. That is why `Section été' appears below as # `Section été' while the enclosing document title's `&' and `<' are # entity-escaped without any character reference. A port that had collapsed the # two branches would pass a single-shape fixture and fail here. check_eq $P/toc-matched "1" "$(pv toc_matched)" check_eq $P/toc-decoy-absent "True" "$(pv toc_decoy_absent)" check_eq $P/toc-sections \ '\s\s\s\s\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\s\s\s\s\s\s\s\s\n\s\s\s\s\s\s\s\s\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\s\s\s\s
\n\s\s\s\s\s\s\s\s\s\s\s\s\s\s\s\s' \ "$(pv toc_sections)" # ------------------------------------------------------------- the keywords -- # THE INDEX FOLD, over a fixture index carrying one of each branch of # build_keywords()/keyword_item(): a title matching the `name (class in module)' # pattern, which gets an `id' attribute built from the module and the shortened # title; a title that does not, which gets none; an entry with TWO refs, which is # emitted twice; and a subitem, which is the recursion one level down. The `id' # is what Qt Assistant resolves a `qthelp://' link against, so a keyword that # lost it still appears in the index and stops being addressable. check_eq $P/keyword-id-form \ '' \ "$(pv keyword_id_form)" check_eq $P/keyword-plain-form \ '' \ "$(pv keyword_plain_form)" # BOTH ATTRIBUTES ESCAPED WITH quote=True, name and ref alike -- a ref carrying a # bare `&' is not well-formed XML and Qt's generator rejects the whole project # file rather than the one keyword. check_eq $P/keyword-escaped \ '' \ "$(pv keyword_escaped)" check_eq $P/keywords \ '\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\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\s\n\s\s\s\s\s\s\s\s\s\s\s\s' \ "$(pv keywords)" # ------------------------------------------------------------ the namespace -- # THE FORMAT'S OWN RULE, which the module states in a comment and implements in # three lines: a QtHelp namespace may carry no character outside [a-zA-Z0-9.-], # no run of successive dots, no leading or trailing dot, and is lowercased. Qt # silently mis-registers a collection whose namespace breaks those, so this is # the arithmetic most worth pinning in the whole package. # # default -- `org.sphinx..', lowercased # explicit -- taken as given when already legal # explicit, illegal -- `Org..Example_Demo!! 2.1..' loses the underscore, the # bang, the space; the doubled dots collapse; the # trailing dot goes; the whole thing lowercases # odd version -- `1.0-rc1+x' keeps the hyphen (it is legal) and loses # the plus check_eq $P/nspace-default "org.sphinx.demoproject.2.1" "$(pv nspace_default)" check_eq $P/nspace-explicit "org.example.demo" "$(pv nspace_explicit)" check_eq $P/nspace-stripped "org.exampledemo2.1" "$(pv nspace_stripped)" check_eq $P/nspace-odd-version "org.sphinx.demo.1.0-rc1x" "$(pv nspace_default_odd_version)" # --------------------------------------------------------- the project files -- # WHICH FILES GO INTO , over a fixture output tree carrying one of every # decision get_project_files() makes: every .html anywhere is in; everything under # _static/ and _images/ is in EXCEPT .js; nothing else is. The .js exclusion is # the surprising one and it is deliberate upstream -- Qt Assistant does not run # scripts, so shipping them into the .qch only inflates it. check_eq $P/project-files \ "_images/plot.png,_static/frame.html,_static/style.css,index.html,sub/page.html" \ "$(pv project_files)" # ----------------------------------------------------- the emitted documents -- # ONE build_qhp() over the fixture, writing both documents into a scratch # directory. `render-files' is the order they are rendered, which is the order # the two log lines are emitted and the order a reader of a build log sees. check_eq $P/build-qhp-ran "yes" "$(pv build_qhp_ran)" check_eq $P/render-files "project.qhp,project.qhcp" "$(pv render_files)" check_eq $P/qhp-written "True" "$(pv qhp_written)" check_eq $P/qhcp-written "True" "$(pv qhcp_written)" # THE CONTEXT THE PACKAGE HANDS THE TEMPLATE ENGINE. This is the primary evidence # for the writer -- pure package output, computed without any renderer -- and it # is what the header means by not resting the case on the probe's Jinja stand-in. # Nine keys for the project file, four for the collection. check_eq $P/qhp-ctx-keys \ "files,keywords,master_doc,namespace,outname,project,sections,title,version" \ "$(pv qhp_ctx_keys)" check_eq $P/qhp-ctx-scalars \ 'outname=DemoProject;title=Demo\sProject\s&\s"Docs";version=2.1;project=Demo\sProject;namespace=org.sphinx.demoproject.2.1;master_doc=index' \ "$(pv qhp_ctx_scalars)" # THE FILE LIST IS PASSED UNESCAPED AND IN WALK ORDER; the template escapes it. check_eq $P/qhp-ctx-files "index.html,_static/style.css" "$(pv qhp_ctx_files)" check_eq $P/qhcp-ctx-keys "homepage,outname,startpage,title" "$(pv qhcp_ctx_keys)" # THE TWO qthelp:// URLs, which are the collection's whole purpose: they are how # Qt Assistant addresses a page inside the compiled .qch, and they are built from # the NORMALISED namespace and the fixed `doc' virtual folder. A namespace that # normalised differently would silently produce URLs that resolve to nothing. check_eq $P/qhcp-ctx-scalars \ 'outname=DemoProject;title=Demo;homepage=qthelp://org.sphinx.demoproject.2.1/doc/index.html;startpage=qthelp://org.sphinx.demoproject.2.1/doc/index.html' \ "$(pv qhcp_ctx_scalars)" # THE DOCUMENTS THEMSELVES. See the header: the renderer is the probe's, so these # pin the SHIPPED TEMPLATES' text against a known context rather than standing in # for the cases above. The collection project is small enough to assert whole; the # project file is asserted by its head, its block, the virtual folder the # format fixes at `doc', its line count and its digest. check_eq $P/qhcp-xml \ '\n\n\s\s\s\s\n\s\s\s\s\s\s\s\sDemo\n\s\s\s\s\s\s\s\sqthelp://org.sphinx.demoproject.2.1/doc/index.html\n\s\s\s\s\s\s\s\sqthelp://org.sphinx.demoproject.2.1/doc/index.html\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\s\s\s\s\s\s\s\s\s\s\s\sDemoProject.qhp\n\s\s\s\s\s\s\s\s\s\s\s\s\s\s\s\sDemoProject.qch\n\s\s\s\s\s\s\s\s\s\s\s\s\n\s\s\s\s\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\sDemoProject.qch\n\s\s\s\s\s\s\s\s\n\s\s\s\s\n\n' \ "$(pv qhcp_xml)" check_eq $P/qhp-head \ '\n\n\s\s\s\sorg.sphinx.demoproject.2.1\n\s\s\s\sdoc\n\s\s\s\s\n\s\s\s\s\s\s\s\sDemoProject\n\s\s\s\s\s\s\s\s2.1\n\s\s\s\s' \ "$(pv qhp_head)" check_eq $P/qhp-files-block \ '\s\s\s\s\s\s\s\s\n\s\s\s\s\s\s\s\s\s\s\s\sindex.html\n\s\s\s\s\s\s\s\s\s\s\s\s_static/style.css\n\s\s\s\s\s\s\s\s' \ "$(pv qhp_files_block)" # The virtual folder is a LITERAL in the template, not a config value. Qt resolves # every qthelp:// URL under it, so the URLs above and this line have to agree. check_eq $P/qhp-virtualfolder "True" "$(pv qhp_virtualfolder)" check_eq $P/qhp-lines "35" "$(pv qhp_lines)" check_eq $P/qhp-sha256 \ "3967390e3d3729f6dacd7e750172c2b3c23b88863e28d9c5958ed9a57a3fc0cc" \ "$(pv qhp_sha256)" # THE EPILOG handle_finish() LEAVES BEHIND, which is what Sphinx prints at the end # of a qthelp build. The basename is substituted in and the outdir placeholder # deliberately is NOT -- Sphinx fills that one itself when it prints the line. check_eq $P/finish-epilog \ '$\sqcollectiongenerator\s%(outdir)s/DemoProject.qhcp' \ "$(pv finish_epilog)" # ------------------------------------------------- the manifest checksums --- # EVERY PAYLOAD FILE RE-HASHED ON TARGET AND SCORED AGAINST THE WHEEL'S OWN # RECORD -- 104 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 104 and not 105. # # This is the strongest single assertion in the suite: it says that every byte # this package ships -- the source file, both templates, 96 catalogue files and # the metadata -- is the byte upstream shipped, end to end through what OE # packaged, dpkg installed and virtiofs served. The __pycache__ .pyc is correctly # absent from RECORD (OE creates it at build time; the wheel never carried it), # which is why the row count is 104 while the package installs 106 files: the # 104 digested rows, RECORD itself, and the .pyc. check_eq $P/record-audit "ok" "$(pv record_audit)" check_eq $P/record-rows "104" "$(pv record_rows)" check_eq $P/record-all-found "104" "$(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 CATALOGUES, counted from the interpreter. 48 languages, each carrying one # .mo and one .po, and every .mo named sphinxcontrib.qthelp.mo -- which is the # name setup()'s add_message_catalog() call resolves. A language whose .mo went # missing would lose its translations silently, since gettext falls back to the # untranslated string. check_eq $P/locale-langs "48" "$(pv locale_langs)" check_eq $P/locale-mo "48" "$(pv locale_mo)" check_eq $P/locale-po "48" "$(pv locale_po)" check_eq $P/locale-mo-name "sphinxcontrib.qthelp.mo" "$(pv locale_mo_named)" rm -rf "$W"