. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is the whole cost -- now # # literally one: the probe. Every assertion below either # # reads the filesystem/dpkg with shell builtins or reads a # # key out of that single run's output; the probe itself # # parses and re-serialises a 248-byte document twice, which # # is nothing beside the interpreter load. The bound matches # # 99-python3-tomli-w's 1200 for the same reason -- the start # # dominates and the work after it is comparable. # # # # THIS BOUND IS NOT WHAT PROTECTS THE SUITE, and batch-1908 # # is how that was learned. On its first on-target run this # # file was killed with NO OUTPUT AT ALL, twice -- once in the # # sweep and once alone on a fresh guest. Not by this bound: # # by the HOST runner, which ends a guest whose process grows # # past 2048 MiB/min averaged over a 120 s window # # (brintos_local_verify), and the runner's own per-suite # # watchdog grows it at ~8 GiB/min for as long as any suite # # runs, because its `sleep 1' poll is a no-op on this runtime # # and the loop spins (brintos/hardwarejs#20). `run_one' # # replays a suite's output only after the suite ends, so a # # suite killed at 120 s reports NOTHING rather than a prefix. # # Every sibling that finished inside 110 s reported in full; # # the three that did not -- this one, jsonpointer, tomlkit -- # # reported nothing. So WALL CLOCK IS THE BUDGET: this file # # dropped from three python3 starts to one, and its key reads # # from four spawns each to none, for that reason and no # # other. Keep it that way when adding a case. # # python3-iniparse 0.5 (poky meta/recipes-devtools/python/python3-iniparse_0.5.bb, # `inherit pypi setuptools3') on the wasm32-hwjs glibc image -- THE # COMMENT-PRESERVING INI LIBRARY. Its API is ConfigParser's; what it adds is that # write-back keeps the comments, the blank lines, the indentation and the # ordering that the stdlib parser discards. That is why the tools which edit # system config files IN PLACE use it rather than configparser. # # ============================================================================ # 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). A module # carrying a compiled extension therefore could not be imported here -- not # "would be slow", could not be imported. iniparse is pure Python, which is the # entire reason it ports. # # So `module-no-so` is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump: iniparse grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import iniparse' on # somebody else's ticket three rounds later. It is anchored on the package's own # dpkg file list as well as on a find, because the two fail differently -- a find # catches a file the package does not declare, the list catches a declared file # that is not a .py. # # ============================================================================ # THE VERSION IS READ FROM THE METADATA, BECAUSE THE MODULE CARRIES NONE. # ============================================================================ # # iniparse 0.5 defines NO `__version__` -- not in __init__.py and not anywhere # else in the package; checked against the sdist this recipe builds. A suite that # asserted `iniparse.__version__' would therefore pin `' for ever and # sail straight through a version bump. The probe reads # importlib.metadata.version("iniparse") instead, which is a statement about what # the IMAGE INSTALLED, and the dist-info directory name below says the same thing # a second, independent way. # # ============================================================================ # python3-six IS A RUNTIME DEPENDENCY HERE, NOT A PACKAGING FORMALITY. # ============================================================================ # # compat.py and ini.py both `import six' at module scope and reach for # six.string_types and six.text_type on the read path, so an image that dropped # python3-six (BRINTOS-1744) would fail at `import iniparse' and every case below # would red at once. `six-import' and `pkg-depends-six' are the two cases that # would say so -- one from the interpreter's side, one from dpkg's. # # ============================================================================ # THE ONE PLACE THE TWO LIBRARIES DISAGREE, AND WHY ONLY ONE SIDE IS ASSERTED. # ============================================================================ # # Given `port = 8080 ; inline comment', iniparse's ConfigParser returns # `8080' and the stdlib's returns the whole `8080 ; inline comment' -- # configparser's inline_comment_prefixes defaults to None. Both are correct for # their own documented contract. The ticket's compatibility requirement is # therefore about the SECTION AND OPTION NAMES, compared as sets, and that is # what `sections-equal' and `options-equal' assert. The stdlib's value is emitted # by the probe and recorded here so a reader of a red can see which of the two # moved, but it is not a case: it would be pinning the STDLIB's behaviour on an # interpreter this suite does not own. # # ============================================================================ # EVERY VALUE BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # On x86-64 (CPython 3.10.12, six 1.16.0), against THIS EXACT PROBE and against # THE BYTES THIS RECIPE BUILT -- python3-iniparse_0.5-r0_wasm32.deb unpacked and # put on PYTHONPATH, not a pip copy and not the upstream git. Recorded # 2026-08-31; 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 document iniparse emits, its length, its digest, the # option names it reports and the classes it hands back depend on iniparse's code # and on the fixture, not on pointer width, byte order, the platform string, the # clock or hash randomisation. Nothing in the probe sorts by hash, formats a # float, or reads the environment. # # version=0.5 fixture_len=248 fixture_sha=17e4a6c915f41f83 # edited_len=245 edited_sha=e84474cc0d232d33 # initial_roundtrip=yes edited_matches=yes blank_line_count=3 # cp_has_readfp=yes cp_has_read_file=no cp_get_port=8080 # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - iniparse's OWN test suite (tests/test_ini.py and its neighbours). It is not # packaged -- setuptools3 installs `packages=['iniparse']' and nothing else -- # and the port closes on the API this file exercises. # - `%'-STYLE INTERPOLATION and the Interpolation* errors re-exported through # __init__. They are the stdlib's semantics re-implemented, they are exercised # by no consumer this Epic ports, and a fixture using them would make the # set comparison above a statement about interpolation rather than about # names. `api-surface' asserts the names are exported; nothing here drives # them. # - `tidy' and `change_comment_syntax'. Both are in the asserted public # surface; neither is on the ticket's path, and change_comment_syntax mutates # MODULE-LEVEL state, so driving it in the same interpreter start as the # preserving edit would make the two halves order-dependent. # - THE -doc PACKAGE. setup.py ships six files through `data_files' into # ${docdir}/iniparse-0.5, which Yocto's default PACKAGES rule puts in # python3-iniparse-doc -- not on this image. `pkg-only-python' is what would # notice them arriving in the main package instead. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-iniparse # THE CANARY, which asserts nothing about python3-iniparse. # # 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 IP=$SITE/iniparse DI=$SITE/iniparse-0.5.dist-info PKGLIST=/var/lib/dpkg/info/python3-iniparse.list PROBE=$BST_DATA/python3-iniparse-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-iniparse.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the module has to land. `import iniparse' further down is the functional # statement; these are the ones that say WHICH copy answered, so a module that # imported out of some other path is distinguishable from one that imported out # of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$IP" check $P/module-init test -f "$IP/__init__.py" # The whole of the module, which at 0.5 is five source files beside __init__. # Named individually rather than counted: a count moves on any upstream refactor, # while each of these carries a named part of the library -- ini.py the preserving # parser, compat.py the ConfigParser surface, config.py the namespace protocol, # configparser.py the stdlib re-export, utils.py `tidy'. A release that dropped # any of them is a different library. check $P/module-ini test -f "$IP/ini.py" check $P/module-compat test -f "$IP/compat.py" check $P/module-config test -f "$IP/config.py" check $P/module-configparser test -f "$IP/configparser.py" check $P/module-utils test -f "$IP/utils.py" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. See the header: on this image # a .so could not be loaded at all, so this is the case that fails a version bump # which grew one -- loudly, here, instead of at `import' on a later ticket. check_eq $P/module-no-so "" \ "$(find "$IP" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # A find sees what is on disk; the dpkg list sees what the package CLAIMS. A # compiled extension that arrived without being declared shows up in the first, # one that is declared but landed elsewhere shows up in the second. # # 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 two cases # above name the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray README -- without having to enumerate # what it might be. It is also the case that would notice setup.py's six # `data_files' landing in the MAIN package instead of in -doc. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # iniparse ships a TOP-LEVEL .dist-info, and here it is load-bearing rather than # decorative: the module carries no __version__ (see header), so this directory # is the ONLY place the installed version is written down. A package whose module # landed and whose metadata did not would import fine, serve every behavioural # case below, and report no version at all. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" # THE VERSION THE RECIPE PINS, from the metadata rather than from the module. # Together with $P/version below -- which reads importlib.metadata through the # interpreter -- this is two independent statements of the same fact, and the two # disagreeing would mean a mixed install: a dist-info from one version beside a # module from another. The directory name carries it a third time, in $DI itself. check_out $P/dist-info-version "Version: 0.5" cat "$DI/METADATA" # --------------------------------------------------------------- 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-iniparse is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take a -dev or -doc # stanza of the same family. sed -n '/^Package: python3-iniparse$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. The recipe # adds python3-core and python3-six explicitly, and six is a REAL import of this # package rather than a formality (see header) -- a Depends line that lost it # would be a defect that a fat image still passes and a lean one does not. # # ONE CASE PER DEPENDENCY, against the extracted line, rather than one case # asserting the whole list as a string. The recipe's RDEPENDS order is not dpkg's # to preserve and the separator spacing is dpkg's own, so an exact-string case # here would be asserting a FORMAT this phase cannot measure. Split, each red also # names which dependency went missing instead of printing the whole line back. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-six "python3-six" cat "$W/dep" # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # Read a single key, WITH BUILTINS ONLY. This was `grep | head | cut', which is # four spawns per key and is called forty-one times below -- and on this runtime # a spawn is the scarce resource, not a cycle: the runner's own watchdog already # forks a `sleep' per poll for the life of the suite, so every avoidable spawn is # paid on top of that. The loop reads at most fifty lines of a file already in # tmpfs and forks nothing; the `break' keeps the at-most-once contract that # `head -n 1' used to carry, and the `case' anchors the key exactly, so # `initial_len' stays distinguishable from `edited_len' and `cp_sections' from # `std_sections'. pv() { _pk=$1; _pv= while IFS= read -r _pl; do case "$_pl" in "$_pk="*) _pv=${_pl#*=}; break ;; esac done < "$OUT" printf '%s\n' "$_pv" } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was taken. # The word `unscorable' is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ---------------------------------------------------------- the interpreter --- # THE IMPORT, AND SIX'S, READ OUT OF THE PROBE'S OWN FIRST TWO STEPS. These were # two more `python3 -c' starts ahead of the probe, and they bought no fact the # probe had not already established: it imports iniparse first and six second, # and reports either failure as its own key (`import_error', `six_error') before # exiting. What the two extra starts DID buy was wall clock -- a python3 start on # this arm is tens of seconds -- and that is what killed this suite on # batch-1908: brintos_local_verify kills a guest whose host process grows past # 2048 MiB/min averaged over a 120 s window, the runner's watchdog grows it at # roughly 8 GiB/min for as long as any suite runs, and `run_one' replays a # suite's output only after it ends. So a suite that runs past ~120 s is killed # having printed NOTHING -- which is what this one did, twice, while every # sibling that finished inside 110 s reported in full. The cases stay and the # starts go (brintos/hardwarejs#20 is the platform side; the watchdog's `sleep' # is a no-op on this runtime, so its poll loop spins). # # The failure order that matters is preserved: six is iniparse's module-scope # import, so a missing python3-six presents AS a broken iniparse, and these two # cases are what tell the two apart. _imp=$(pv module_file) if [ -n "$_imp" ]; then bst_ok $P/import else _e=$(pv import_error) bst_notok $P/import "the probe could not import iniparse: ${_e:-no import_error key either -- the probe did not reach its first step}" fi _six=$(pv six_file) if [ -n "$_six" ]; then bst_ok $P/six-import else _e=$(pv six_error) bst_notok $P/six-import "the probe could not import six: ${_e:-no six_error key -- the probe stopped before its six step, so read $P/import first}" fi # THE VERSION THE RECIPE PINS, read through importlib.metadata. A mismatch here # means the recipe and what actually landed have gone out of step, and every # expectation in this file is then describing a different iniparse. check_eq $P/version "0.5" "$(pv version)" # The module answered out of the package 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 iniparse. check_eq $P/module-file "$IP/__init__.py" "$_imp" # ...and the six that answered is the image's, for the same reason. A red here # with six-import green would mean iniparse bound a different copy. check_eq $P/six-file "/usr/lib/python3.12/site-packages/six.py" "$_six" check_eq $P/six-version "1.16.0" "$(pv six_version)" # THE PUBLIC SURFACE, WHOLE. Asserted as a set rather than by presence because a # release that grew or dropped a public entry point is a different library than # the one this file describes. Sixteen names, in __all__'s own sorted order. check_eq $P/api-surface \ "BasicConfig,ConfigNamespace,ConfigParser,DEFAULTSECT,DuplicateSectionError,INIConfig,InterpolationDepthError,InterpolationMissingOptionError,InterpolationSyntaxError,MAX_INTERPOLATION_DEPTH,NoOptionError,NoSectionError,RawConfigParser,SafeConfigParser,change_comment_syntax,tidy" \ "$(pv api_surface)" # THE READER METHODS, which are a portability tripwire and not trivia. iniparse's # RawConfigParser is a STANDALONE class, so it carries its own `readfp' and no # `read_file'. The stdlib REMOVED readfp in Python 3.12 -- this image's # interpreter -- so a caller reaching for the stdlib spelling breaks HERE and not # on the x86-64 control. Both are facts about iniparse's own code. check_eq $P/cp-has-readfp "yes" "$(pv cp_has_readfp)" check_eq $P/cp-has-read-file "no" "$(pv cp_has_read_file)" # ------------------------------------------------------- the preserving edit -- # The fixture the probe drove, pinned by length and digest, so every case below # is known to be about the document this file describes rather than about an # edited probe. check_eq $P/fixture-len "248" "$(pv fixture_len)" check_eq $P/fixture-sha "17e4a6c915f41f83" "$(pv fixture_sha)" check_eq $P/parse-ok "yes" "$(pv parse_ok)" # THE UNTOUCHED ROUND TRIP, FIRST. If serialising a document nobody edited is # already lossy then every case below is measuring a second failure on top of a # first one; this is what separates them. check_eq $P/initial-roundtrip "yes" "$(pv initial_roundtrip)" check_eq $P/initial-len "248" "$(pv initial_len)" # The value read through the attribute API, WITHOUT the inline comment -- so the # reassignment below is known to be replacing a value and not a value-plus-comment. check_eq $P/read-port "8080" "$(pv read_port)" check_eq $P/read-host "example.org" "$(pv read_host)" check_eq $P/edit-ok "yes" "$(pv edit_ok)" # THE DELIVERABLE, IN ONE CASE: reassign one option, add one, delete one, and the # serialised result matches the expected document BYTE FOR BYTE -- every untouched # comment and blank line still in place. This is what the port exists for. check_eq $P/edited-matches "yes" "$(pv edited_matches)" check_eq $P/edited-len "245" "$(pv edited_len)" # The digest pins the OUTPUT independently of the probe's own EXPECTED literal: # an edit to that literal which also broke the expectation would otherwise keep # `edited-matches' green. check_eq $P/edited-sha "e84474cc0d232d33" "$(pv edited_sha)" # WHICH PART FAILED, when it does. `edited-matches' says the document changed; # these say whether it was the comments, the blank lines, the inline run or the # placement -- so a red is a bug report instead of a shrug. Redundant when # everything passes and the whole point when something does not. check_eq $P/kept-leading-hash "yes" "$(pv kept_leading_hash)" check_eq $P/kept-leading-semi "yes" "$(pv kept_leading_semi)" check_eq $P/kept-section-comment "yes" "$(pv kept_section_comment)" check_eq $P/kept-comment-after-blank "yes" "$(pv kept_comment_after_blank)" # THE STRONGEST SINGLE STATEMENT OF THE CLAIM: the inline comment AND the # five-space run in front of it survive on the line whose value changed. The # stdlib parser cannot produce this line at all. check_eq $P/kept-inline-comment "yes" "$(pv kept_inline_comment)" check_eq $P/blank-line-count "3" "$(pv blank_line_count)" # The addition landed at the end of its OWN SECTION, ahead of the blank line that # separates [server] from [client], rather than being appended to the document. check_eq $P/added-in-section "yes" "$(pv added_in_section)" check_eq $P/deleted-gone "yes" "$(pv deleted_gone)" # ...and the deletion took its line WHOLE, leaving no empty line where it stood. check_eq $P/deleted-no-hole "yes" "$(pv deleted_no_hole)" # The edits changed the PARSED document and not only its serialisation. check_eq $P/reread-port "9090" "$(pv reread_port)" check_eq $P/reread-tls "yes" "$(pv reread_tls)" # ------------------------------------------------- the ConfigParser half ------ # The ticket's other requirement: iniparse.ConfigParser and the stdlib's see the # same sections and the same option names in the same file. Compared as SETS -- # see the header for why the VALUES are deliberately not compared. check_eq $P/compat-read-ok "yes" "$(pv compat_read_ok)" check_eq $P/cp-sections "client,server" "$(pv cp_sections)" check_eq $P/sections-equal "yes" "$(pv sections_equal)" check_eq $P/cp-options-server "debug,host,port" "$(pv cp_options_server)" check_eq $P/cp-options-client "retries,timeout" "$(pv cp_options_client)" check_eq $P/options-equal "yes" "$(pv options_equal)" # NEITHER LIBRARY INVENTED A [DEFAULT]. A non-empty defaults() on either side # would add options to every section and make the set comparison above pass for # the wrong reason. check_eq $P/cp-defaults "" "$(pv cp_defaults)" check_eq $P/std-defaults "" "$(pv std_defaults)" # iniparse's OWN side of the documented divergence: the inline comment is stripped # from the value. The stdlib's side is emitted by the probe and printed in this # file's header, but is not a case -- see the header. check_eq $P/cp-get-port "8080" "$(pv cp_get_port)" check_eq $P/cp-get-retries "3" "$(pv cp_get_retries)" # The compat object exposes the underlying INIConfig. That is the documented # bridge between this library's two halves, and the reason a caller can move to # the preserving writer without changing its reader. check_eq $P/cp-data-is-iniconfig "yes" "$(pv cp_data_is_iniconfig)" # 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"