. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output, so # # the bound is one interpreter load plus a dump and a parse # # of a 392-byte document. It matches 99-python3-flit-core's # # 1200 rather than undercutting it: the work after the start # # is strictly cheaper there than here, and the start is what # # 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-tomli-w 1.0.0 (meta-openembedded meta-python, # python3-tomli-w_1.0.0.bb, `inherit pypi python_flit_core ptest`) -- THE # WRITE-ONLY TOML SERIALISER, on the wasm32-hwjs glibc image. It is the # counterpart to python3-tomli (BRINTOS-1838), which reads; this one writes. # # ============================================================================ # 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). A module that # carried a compiled extension therefore could not be imported here -- not # "would be slow", could not be imported. tomli_w 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: tomli_w grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import tomli_w` 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 READER IS THE STDLIB'S, ON PURPOSE. THIS SUITE DOES NOT WAIT ON tomli. # ============================================================================ # # A write-only library can only be checked by reading back what it wrote, and the # obvious reader -- python3-tomli, the read-only counterpart -- is a SEPARATE # PORT in this same Epic (BRINTOS-1838). Depending on it would make this suite go # red whenever that ticket slipped, on a package that was working perfectly. # # data/python3-tomli-w-probe.py therefore reads back with `tomllib`, which is in # the interpreter's OWN stdlib split (poky python3-manifest.json: tomllib # rdepends `core` alone) and reaches this image through python3-modules, the # package the image's `python3` entry resolves to. Note that this is NOT via # python3-tomli-w's own RDEPENDS, which names python3-datetime, python3-numbers # and python3-stringold and no reader at all -- so `reader-present` below is a # real case and not a tautology. It is also the right reader on the merits: # CPython vendored tomllib FROM tomli, so it is the same parser upstream tomli-w # tests against. # # ============================================================================ # THE ROUND TRIP IS DRIVEN END TO END, AND THE EXPECTATIONS ARE MEASURED. # ============================================================================ # # The probe dumps ONE fixed dict covering every TOML type this port claims to # serialise -- strings (including escapes), integers, floats, booleans, offset # and local datetimes, dates, times, arrays including a nested one, and a nested # table -- to a scratch file with tomli_w.dump, reads the file back with # tomllib.load, and compares against the original object. # # EVERY VALUE ASSERTED BELOW WAS MEASURED, on x86-64, against THIS EXACT PROBE # and against THE BYTES THIS RECIPE BUILT -- python3-tomli-w_1.0.0-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). They are # not predictions and they are not read off the source. # # What makes the control transferable is that all of it is pure-Python and # content-determined: the document tomli_w emits, its length, the table headers # it chooses and the types tomllib hands back depend on tomli_w's code and on the # fixture, not on pointer width, byte order or the platform string. The two # floats are binary fractions (3.5, -0.25), so `float -> decimal text -> float` # is lossless and rt_float is an equality rather than a tolerance. # # version=1.0.0 api_surface=dump,dumps dumps_len=392 # roundtrip=yes mismatch_keys= key_count=16 # file_size=392 file_matches_dumps=yes dt_offset_utcoffset=zero # type_date=date type_time=time type_dt_offset=datetime # typeerror_object=TypeError typeerror_set=TypeError # # THE LENGTH IS PINNED, NOT BOUNDED, and that is the one choice here worth # defending. 99-python3-flit-core bounds its artifact's size because a WHEEL is a # zip and zip framing is legitimately allowed to shift between producers. This # artifact is plain UTF-8 text with no framing of any kind, emitted by pure # Python from a fixed dict, so 392 is reproducible by construction and a pinned # value turns any formatting drift in a version bump into a red here rather than # into a silent change nobody sees. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THE ptest PACKAGE. The recipe inherits ptest, but `ptest` is not in this # distro's DISTRO_FEATURES, so no python3-tomli-w-ptest is produced (verified # against the built deploy/deb: main, -dev and -dbg and nothing else). # `pkg-no-ptest` below asserts it is not installed anyway, because that is the # case that catches a DISTRO_FEATURES change -- the ptest package RDEPENDS on # python3-pytest AND python3-tomli, and would drag both onto this image as a # side effect of a distro-level edit nobody connected to this port. # - READING TOML. That is python3-tomli's port (BRINTOS-1838) and belongs to # its suite. tomllib is used here as an instrument, and `reader-present` is # the only case that is about it. # - tomli_w's multiline-string and inline-table formatting options. 1.0.0's # public surface is `dump` and `dumps` with no formatting keywords # (api_surface is asserted whole), so there is nothing to drive. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-tomli-w # THE CANARY, which asserts nothing about python3-tomli-w. # # 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 TW=$SITE/tomli_w DI=$SITE/tomli_w-1.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-tomli-w.list D=$BST_DATA PROBE=$D/python3-tomli-w-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-tomliw.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import tomli_w'` 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 "$TW" check $P/module-init test -f "$TW/__init__.py" # The whole of the module, which at 1.0.0 is two source files. Named individually # rather than counted: a count moves on any upstream refactor, while __init__ is # the public surface and _writer is where the serialiser actually lives, so a # release that dropped either is a different library. check $P/module-writer test -f "$TW/_writer.py" # The PEP 561 marker. tomli_w ships it, and it is the one non-.py, non-metadata # regular file in the package -- so it is named here rather than discovered as a # surprise by pkg-only-python below. check $P/module-py-typed test -f "$TW/py.typed" # 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 "$TW" -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 UNDER site-packages 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 shared archive -- # without having to enumerate what it might be. # # 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. The # allowed classes were checked against the built .deb: __init__.py, _writer.py, # py.typed, two __pycache__ entries and the four dist-info files, and nothing # else. py.typed is allowed by its extension rather than by name, matching # 99-python3-flit-core, so a second PEP 561 marker would not be a red on its own. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ---------------------------------------------------------- the dist-info ---- # tomli_w ships a TOP-LEVEL .dist-info -- like python3-pyproject-hooks and unlike # python3-flit-core, which ships none. All four files are asserted because they # are what makes the installed module discoverable by importlib.metadata; a # package whose module landed but whose metadata did not would import fine here # and be invisible to any dependency resolver on the image. 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" check $P/dist-info-license test -f "$DI/LICENSE" # THE VERSION THE RECIPE PINS, from the metadata rather than from the module. # Together with $P/version below -- which reads tomli_w.__version__ -- 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: 1.0.0" 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-tomli-w 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. The # anchor is exact: `python3-tomli-w` is a PREFIX of `python3-tomli-w-ptest`, and # an unanchored match would report one stanza as the other. It is also worth # noting that `python3-tomli` is a prefix of THIS name, so the anchoring matters # in both directions on an image carrying BRINTOS-1838's port too. sed -n '/^Package: python3-tomli-w$/,/^$/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-datetime, python3-numbers and python3-stringold to the python3-core # every python package gets; the fixture exercises datetimes and numbers directly, # so a Depends line that lost one would be a real defect that the round trip might # still pass on a fat image and fail on a lean one. # # 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 -- it would be a # prediction dressed as a measurement, and the first version of this file made # exactly that mistake. 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-datetime "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" check_out $P/pkg-depends-stringold "python3-stringold" cat "$W/dep" # ${PN}-ptest IS NOT INSTALLED. See the header: it is not built at all today, # because ptest is not in this distro's DISTRO_FEATURES -- so this case is a # tripwire on a DISTRO_FEATURES change rather than on anything this port does. # It matters because that package RDEPENDS on python3-pytest and python3-tomli, # and would pull both onto the image with nothing else here noticing. sed -n '/^Package: python3-tomli-w-ptest$/,/^$/p' /var/lib/dpkg/status > "$W/stp" 2>/dev/null check $P/pkg-no-ptest test ! -s "$W/stp" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If tomli_w 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 tomli_w' # THE READER, asserted separately and before the probe. tomllib reaches this image # through python3-modules and NOT through this package's RDEPENDS (see header), so # a lean image that dropped the stdlib split would take every round-trip case red # at once -- this is the case that would say why. check $P/reader-present python3 -c 'import tomllib' # ------------------------------------------------------------- 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" "$W" 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` and # `dumps_len` distinguishable, and `rt_date` from `rt_datetime_local`. 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 serialiser, 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 VERSION THE RECIPE PINS, read from the module. 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 tomli_w. check_eq $P/version "1.0.0" "$(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 tomli_w. check_eq $P/module-file "$TW/__init__.py" "$(pv module_file)" # THE PUBLIC SURFACE, WHOLE. tomli_w 1.0.0 documents exactly two names. Asserted # as a set rather than by presence because a release that GREW a public entry # point would be a different library than the one this file describes -- and # because the absence of any formatting keyword is what makes the pinned # dumps_len below a fair assertion. check_eq $P/api-surface "dump,dumps" "$(pv api_surface)" check_eq $P/api-dump "yes" "$(pv api_dump)" check_eq $P/api-dumps "yes" "$(pv api_dumps)" # The probe's reader resolved, and resolved to the stdlib one this suite intends. # A red here with reader-present green would mean tomllib imported from somewhere # unexpected, which would make the round trip a statement about that copy. check_eq $P/probe-reader "tomllib" "$(pv reader)" # ------------------------------------------------------ the serialiser wrote -- # dumps() returned, and returned the exact document measured against the built # package. See the header for why this length is pinned rather than bounded. check_eq $P/dumps-ok "yes" "$(pv dumps_ok)" check_eq $P/dumps-len "392" "$(pv dumps_len)" # The nested mapping became a TABLE HEADER rather than an inline dict. Both are # valid TOML and both round trip, so no equality case below can tell them apart: # this is the only statement that tomli_w laid the document out the documented # way, and the nested form says it recursed rather than flattening. check_eq $P/toml-table-header "yes" "$(pv toml_has_table_header)" check_eq $P/toml-nested-header "yes" "$(pv toml_has_nested_header)" # ------------------------------------------------------------ dump() to a file # THE TICKET'S OWN PATH. dump() writes to a binary handle; these three cases say # it returned, that a file is actually there, and that it is the same document # dumps() produced -- which is what lets the pinned length above be a statement # about the file the read-back parses. check_eq $P/dump-ok "yes" "$(pv dump_ok)" check_eq $P/file-exists "yes" "$(pv file_exists)" check_eq $P/file-size "392" "$(pv file_size)" check_eq $P/file-matches-dumps "yes" "$(pv file_matches_dumps)" # --------------------------------------------------------------- the read-back check_eq $P/parse-ok "yes" "$(pv parse_ok)" check_eq $P/key-count "16" "$(pv key_count)" # THE DELIVERABLE, IN ONE CASE: what came back equals what went in. Python's == # on dicts is recursive and type-aware, so this covers nesting, key ordering and # the datetime classes at once. check_eq $P/roundtrip "yes" "$(pv roundtrip)" # ...and WHICH keys failed, when it does. Empty on a clean trip. This is the case # that turns a red `roundtrip` into a bug report instead of a shrug. check_eq $P/roundtrip-mismatches "" "$(pv mismatch_keys)" # The same trip, split by TYPE FAMILY. Redundant when everything passes and the # whole point when something does not: `roundtrip` says the document changed, # these say whether it was the floats, the datetimes or the nested table. check_eq $P/rt-string "yes" "$(pv rt_string)" check_eq $P/rt-int "yes" "$(pv rt_int)" check_eq $P/rt-float "yes" "$(pv rt_float)" check_eq $P/rt-bool "yes" "$(pv rt_bool)" check_eq $P/rt-datetime-offset "yes" "$(pv rt_datetime_offset)" check_eq $P/rt-datetime-local "yes" "$(pv rt_datetime_local)" check_eq $P/rt-date "yes" "$(pv rt_date)" check_eq $P/rt-time "yes" "$(pv rt_time)" check_eq $P/rt-array "yes" "$(pv rt_array)" check_eq $P/rt-table "yes" "$(pv rt_table)" # THE TYPES CAME BACK AS TYPES. The equality above would also be satisfied by a # writer that quoted a date and a parser that read the quotes back -- no, it # would not: a str never equals a datetime.date. What these catch is the # NARROWER confusion the equality genuinely cannot see, `True == 1`, and they # name the class for the others so a red says what was handed back instead. check_eq $P/type-datetime-offset "datetime" "$(pv type_dt_offset)" check_eq $P/type-datetime-local "datetime" "$(pv type_dt_local)" check_eq $P/type-date "date" "$(pv type_date)" check_eq $P/type-time "time" "$(pv type_time)" check_eq $P/type-bool "bool" "$(pv type_bool)" check_eq $P/type-float "float" "$(pv type_float)" # The UTC offset survived as an offset rather than being silently dropped to a # naive datetime. rt-datetime-offset would already be red if it had, but this # says WHICH way -- an offset that came back shifted rather than absent. check_eq $P/dt-offset-kept "zero" "$(pv dt_offset_utcoffset)" # ------------------------------------------------------ the documented refusal # The ticket's other requirement: a mapping carrying a value TOML cannot express # raises TypeError. Two different unsupported values, because a library that # special-cased one would still be wrong for the other. The exception TYPE is the # assertion -- the probe catches TypeError specifically and reports anything else # by its own class name, so a KeyError here reds with "KeyError" rather than # passing as "it raised something". check_eq $P/typeerror-object "TypeError" "$(pv typeerror_object)" check_eq $P/typeerror-set "TypeError" "$(pv typeerror_set)" # The message is the documented one rather than an incidental failure that # happened to be a TypeError -- an internal TypeError from a refactor would raise # the right class for entirely the wrong reason. check_eq $P/typeerror-object-message "yes" "$(pv typeerror_object_mentions_toml)" check_eq $P/typeerror-set-message "yes" "$(pv typeerror_set_mentions_toml)" # 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"