. "$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 parse, a dump and # # a re-parse of a 50-line document. It matches # # 99-python3-tomli's 1200 rather than 99-sip-module's 600 # # because the start dominates either way, and the round trip # # itself is microseconds. 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-toml 0.10.2 (poky meta/recipes-devtools/python, python3-toml_0.10.2.bb, # `inherit pypi setuptools3`) -- THE ORIGINAL PYTHON TOML READER AND WRITER, at # TOML 0.5, on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT DISTINGUISHES THIS PACKAGE FROM THE THREE BESIDE IT ON THIS IMAGE. # ============================================================================ # # This image now carries four TOML packages, and they are not interchangeable: # # python3-toml (this one) reads AND writes, at TOML 0.5 -- uiri/toml # python3-tomli (BRINTOS-1838) reads only, at TOML 1.0.0 # python3-tomli-w (BRINTOS-1839) writes only, at TOML 1.0.0 # python3-tomlkit (BRINTOS-1910) reads and writes, style-preserving # # So the spec level is asserted directly rather than inferred from the version: # toml exposes it as `toml._spec_`, and the `spec` case below reads it. A port # that silently became a 1.0.0 parser would change what every consumer of this # package sees, and no version-number case would notice. # # THE NAME COLLISION IS LIVE AND IT IS THE WORST OF THE FOUR. `python3-toml` is a # strict PREFIX of `python3-tomli`, `python3-tomli-w` AND `python3-tomlkit`, all # three of which are on this same image -- and `toml` is a prefix of the `tomli` # and `tomlkit` DIRECTORIES under site-packages. Every dpkg lookup below anchors # the name at BOTH ends, and every path case names a file that only this package # ships. An unanchored match here would report one of three other packages as # this one and be green while this port was entirely absent. # # ============================================================================ # 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. toml 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: toml grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import toml` on # somebody else's ticket three rounds later. The case 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. # # ============================================================================ # WHAT THE PARSE AND ROUND-TRIP CASES ASSERT, AND WHY NOT IN SHELL. # ============================================================================ # # data/python3-toml-probe.py carries the TOML 0.5 specification's own example # document and, BESIDE IT, the Python dict that document denotes. It loads the # fixture from a file, compares the result to that literal field by field, then # dumps the parsed object back to TOML, re-parses THAT, and compares again. # Shell can neither build the expectation nor compare it, so the probe does the # comparison and this suite asserts on the verdicts -- one per field, so a red # names the field that mis-parsed rather than reporting the whole document as # unequal. # # THE ROUND TRIP IS COMPARED AS A DOCUMENT, NOT AS TEXT. `toml.dumps` output is # not canonical -- key order, quoting style and array layout are the encoder's # choice -- so a golden-file comparison would be a test of the encoder's # formatting rather than of its correctness, and would red on any upstream # cosmetic change. What is asserted is that the re-parse equals the first parse # AND equals the literal; the second half is the one that matters, because the # first would still hold if the reader and the writer were wrong in matching # ways. # # EVERY VALUE ASSERTED BELOW WAS MEASURED, against THIS EXACT PROBE and toml # 0.10.2 -- the version this image's recipe pins -- on x86-64 CPython 3.12.13, # the same 3.12 series this image's interpreter is (recorded 2026-08-31, and kept # in the ticket's design doc). They are not predictions and they are not read off # the source. The same run on CPython 3.10.12 produced BYTE-IDENTICAL output, # which is the control that says nothing asserted here is interpreter-version # dependent. What makes it transferable to this arm is that all of it is pure # Python and content-determined: a TOML parse maps text to # str/int/bool/datetime/list/dict, and none of that depends on pointer width, # byte order or the platform string. Nothing arch-dependent is asserted anywhere # in this file. # # version=0.10.2 spec=0.5.0 doc_equal=yes load_matches_loads=yes # field_*=yes (all fifteen) products_len=3 products_middle_empty=yes # roundtrip_equal=yes roundtrip_equal_expect=yes # dob_iso=1979-05-27T07:32:00-08:00 dob_utcoffset_seconds=-28800.0 # len_ml_basic=30 len_ml_continuation=44 len_ml_escapes=56 # dup_key_raises=yes leading_zero_raises=yes (both TomlDecodeError) # # THE TWO CONSTRUCTS WORTH READING TWICE. # # THE OFFSET DATE-TIME keeps its -08:00. TOML makes the offset part of the # value, so a parser that normalised to UTC would still compare equal on the # instant -- which is why the offset is asserted separately, as a number. # # THE LINE-ENDING BACKSLASH in a multi-line basic string swallows the newline # AND the leading whitespace of the next line, so three indented source lines # denote one unbroken 44-character sentence. It is the construct in the whole # fixture most likely to be got wrong, and it fails by producing a string that # merely looks plausible. Its length is asserted beside its value, because two # multi-line strings differing by one swallowed newline are indistinguishable # in a shell case's got/want line. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - TOML 1.0.0. This library implements 0.5 and says so; the 1.0.0 parser on # this image is python3-tomli, a separate port with its own suite. # - STYLE-PRESERVING ROUND TRIPS (comments and layout surviving a read/write # cycle). toml does not offer that and does not claim to -- python3-tomlkit # does, and it is a separate port in this same round. # - THE FULL TOML COMPLIANCE CORPUS. That is toml-test, thousands of documents, # and it is upstream's job -- running it here would cost minutes of guest time # to re-answer a question upstream CI answers per release. This suite asserts # the specification's own example document plus the two error paths the ticket # names, which is what says the port LANDED, not that toml is correct. # - `tomllib`. This interpreter is 3.12 and DOES carry tomllib in its stdlib; # that is not what this ticket ports, and asserting the two agree would be a # statement about the interpreter rather than about this package. It would # also be wrong to expect agreement: tomllib is 1.0.0 and this is 0.5. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-toml # THE CANARY, which asserts nothing about python3-toml. # # 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 TM=$SITE/toml PKGLIST=/var/lib/dpkg/info/python3-toml.list D=$BST_DATA PROBE=$D/python3-toml-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-toml.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import toml'` 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 "$TM" check $P/module-init test -f "$TM/__init__.py" # The four source files toml is made of, named individually rather than counted: # a count moves on any upstream refactor, while this split -- reader, writer, the # ordered-dict variant and the fixed-offset tzinfo -- is the one toml has shipped # since 0.10 and the one its public surface is assembled from in __init__.py. # # `tz.py` is the load-bearing one for this port: it is the fixed-offset tzinfo # the offset date-time case below depends on, so an install missing it would red # the dob cases with no clue as to why. check $P/module-decoder test -f "$TM/decoder.py" check $P/module-encoder test -f "$TM/encoder.py" check $P/module-ordered test -f "$TM/ordered.py" check $P/module-tz test -f "$TM/tz.py" # NOT tomli, NOT tomlkit. Three other TOML packages sit beside this one under the # same site-packages, and `toml` is a PREFIX of two of their directory names, so # this case says the directory above is this package's own and not a prefix match # on a sibling's. It is cheap and it is the difference between testing this port # and testing somebody else's. check_eq $P/module-dir-exact "$TM" "$(cd "$TM" 2>/dev/null && pwd)" # 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 "$TM" -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. # # `.pyi` is allowed because toml SHIPS TYPE STUBS beside its sources -- it # predates py.typed and declares its types that way instead, which is why there # is no py.typed case in this file where 99-python3-tomli has one. Measured # against the built .deb; see the design doc for the file list. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.pyi|*.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */*.egg-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # --------------------------------------------------------- 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-toml 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 at BOTH ends: `python3-toml` is a strict PREFIX of # `python3-tomli`, `python3-tomli-w` and `python3-tomlkit`, all three of which are # on THIS image, and an unanchored match would report any of them as this one. sed -n '/^Package: python3-toml$/,/^$/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 interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If toml 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 toml' # ------------------------------------------------------------- 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 `dob_utcoffset` and # `dob_utcoffset_seconds` distinguishable. 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 parser, 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 rather than from dpkg. 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 toml. check_eq $P/version "0.10.2" "$(pv version)" # THE SPECIFICATION LEVEL, which is what distinguishes this package from the # three TOML siblings on this image. See the header. check_eq $P/spec "0.5.0" "$(pv spec)" # The module answered out of the package this image installed, not from a stray # copy on sys.path -- and, given the prefix collision, not out of tomli or # tomlkit. This is what makes the payload cases above and the behaviour cases # below statements about the SAME toml. check_eq $P/module-file "$TM/__init__.py" "$(pv module_file)" # toml's documented public surface, in full, and in BOTH directions: this package # is a reader and a writer, which is what makes the round trip below possible at # all. A library missing TomlDecodeError is not merely incomplete -- the two error # paths at the end have nothing to assert against. check_eq $P/api-loads "yes" "$(pv has_loads)" check_eq $P/api-load "yes" "$(pv has_load)" check_eq $P/api-dumps "yes" "$(pv has_dumps)" check_eq $P/api-dump "yes" "$(pv has_dump)" check_eq $P/api-decode-error "yes" "$(pv has_TomlDecodeError)" check_eq $P/api-decoder "yes" "$(pv has_TomlDecoder)" check_eq $P/api-encoder "yes" "$(pv has_TomlEncoder)" check_eq $P/api-decode-error-is-exception "yes" "$(pv decode_error_is_exception)" # The probe could write its fixture. Separated from the parse so a read-only or # full tmpfs is not reported as a parser failure. check_eq $P/fixture "yes" "$(pv fixture_ok)" # ------------------------------------------------------------- the parse ----- # THE DELIVERABLE, first half. load() read the document FROM A FILE -- the call # every consumer actually makes -- and loads() read the same text from a string. # Split so that "load and loads disagree" is distinguishable from "both are wrong # the same way", and so a red in load alone reads as an I/O or encoding fault # rather than a parser fault. check_eq $P/load "yes" "$(pv load_ok)" check_eq $P/loads "yes" "$(pv loads_ok)" check_eq $P/load-matches-loads "yes" "$(pv load_matches_loads)" # What was parsed EQUALS the dict the fixture denotes -- the whole document, # compared as one structure. The per-field verdicts below say WHICH field # disagreed; this one is the statement that nothing else did. check_eq $P/doc-equal "yes" "$(pv doc_equal)" # The key SET, asserted separately. doc-equal already covers it, but when # doc-equal reds this is the case that says whether the damage was a missing or # extra top-level table rather than a wrong value under a right key. check_eq $P/top-level-keys \ "clients,database,owner,products,servers,strings,title" \ "$(pv top_level_keys)" # ---------------------------------------------------- field by field --------- # The ticket's requirement, one case per field, so a red names the field. The # pytype_* case beside each verdict is what names the wrong type when the verdict # reds, so the pair reads as "wrong, and here is what it became" -- `1 == True` # and `1.0 == 1` in Python, so a value-only comparison would pass a bool parsed # as an int. check_eq $P/field-title "yes" "$(pv field_title)" check_eq $P/pytype-title "str" "$(pv pytype_title)" check_eq $P/field-owner-name "yes" "$(pv field_owner_name)" check_eq $P/pytype-owner-name "str" "$(pv pytype_owner_name)" check_eq $P/field-owner-dob "yes" "$(pv field_owner_dob)" check_eq $P/pytype-owner-dob "datetime" "$(pv pytype_owner_dob)" check_eq $P/field-database-server "yes" "$(pv field_database_server)" check_eq $P/field-database-ports "yes" "$(pv field_database_ports)" check_eq $P/pytype-database-ports "list" "$(pv pytype_database_ports)" check_eq $P/field-database-max "yes" "$(pv field_database_max)" check_eq $P/pytype-database-max "int" "$(pv pytype_database_max)" check_eq $P/field-database-enabled "yes" "$(pv field_database_enabled)" check_eq $P/pytype-database-enabled "bool" "$(pv pytype_database_enabled)" # THE NESTED TABLES the ticket names: [servers.alpha] and [servers.beta] are # sub-tables of [servers], and a parser that flattened them to keys literally # named "servers.alpha" would still produce a document. check_eq $P/field-servers-alpha "yes" "$(pv field_servers_alpha)" check_eq $P/pytype-servers-alpha "dict" "$(pv pytype_servers_alpha)" check_eq $P/field-servers-beta "yes" "$(pv field_servers_beta)" check_eq $P/servers-subtables "alpha,beta" "$(pv servers_subtables)" # The array of arrays, whose two members have different element types -- allowed, # and part of the specification's own example -- and the line-broken array beside # it, which is the same value written across four source lines. check_eq $P/field-clients-data "yes" "$(pv field_clients_data)" check_eq $P/pytype-clients-data "list" "$(pv pytype_clients_data)" check_eq $P/field-clients-hosts "yes" "$(pv field_clients_hosts)" # THE ARRAY OF TABLES the ticket names. [[products]] appears three times and the # MIDDLE ONE IS EMPTY, which is the specification's own example and the case a # parser is most likely to drop. The length and the emptiness are asserted # separately from the value, because a parser that dropped the empty table would # parse the two populated entries perfectly and give a length of 2. check_eq $P/field-products "yes" "$(pv field_products)" check_eq $P/pytype-products "list" "$(pv pytype_products)" check_eq $P/products-len "3" "$(pv products_len)" check_eq $P/products-middle-empty "yes" "$(pv products_middle_empty)" # THE OFFSET DATE-TIME the ticket names, stated as values as well as a verdict. # TOML makes the offset part of the value: a parser that normalised -08:00 to UTC # would compare equal on the instant and be wrong about the document. The offset # is asserted as a NUMBER because `str(timedelta(hours=-8))` is # "-1 day, 16:00:00", which is correct and unreadable. check_eq $P/dob-iso "1979-05-27T07:32:00-08:00" "$(pv dob_iso)" check_eq $P/dob-utcoffset-seconds "-28800.0" "$(pv dob_utcoffset_seconds)" # THE MULTI-LINE BASIC STRINGS the ticket names, and the escapes inside them. # Each verdict is a comparison against a literal in the probe; the length beside # it is what makes a red readable, since two multi-line strings that differ by # one swallowed newline are indistinguishable in a got/want line. # # ml_basic the newline after the opening delimiter is trimmed, the one # inside is kept -> 30 chars # ml_continuation a line-ending backslash swallows the newline AND the leading # whitespace of the next line, so three indented source lines # denote one unbroken sentence -> 44 chars # ml_escapes an escaped quote, a tab, and a \u code point outside ASCII, # inside a string that also carries a real newline -> 56 chars check_eq $P/field-ml-basic "yes" "$(pv field_ml_basic)" check_eq $P/len-ml-basic "30" "$(pv len_ml_basic)" check_eq $P/field-ml-continuation "yes" "$(pv field_ml_continuation)" check_eq $P/len-ml-continuation "44" "$(pv len_ml_continuation)" check_eq $P/field-ml-escapes "yes" "$(pv field_ml_escapes)" check_eq $P/len-ml-escapes "56" "$(pv len_ml_escapes)" # ------------------------------------------------------------ the round trip - # THE DELIVERABLE, second half, and the only thing here that exercises the WRITER. # dumps() the parsed object, parse THAT, and assert the two parses are equal. # Compared as a document rather than as text: the emitted text is not canonical, # so a golden-file comparison would test the encoder's formatting instead of its # correctness. check_eq $P/dumps "yes" "$(pv dumps_ok)" check_eq $P/dumps-nonempty "yes" "$(pv dumps_nonempty)" check_eq $P/roundtrip-parse "yes" "$(pv roundtrip_parse_ok)" check_eq $P/roundtrip-equal "yes" "$(pv roundtrip_equal)" # The stronger of the two: `reparsed == parsed` would still hold if the reader # and the writer were wrong in matching ways. This one compares what came back # against the literal the fixture denotes. check_eq $P/roundtrip-equal-expect "yes" "$(pv roundtrip_equal_expect)" check_eq $P/roundtrip-keys \ "clients,database,owner,products,servers,strings,title" \ "$(pv roundtrip_keys)" # dump() to a real file -- the writer's other entry point, and the one that # touches the filesystem. Separated so a read-only or full tmpfs is not reported # as an encoder failure. check_eq $P/dump "yes" "$(pv dump_ok)" check_eq $P/dump-matches-dumps "yes" "$(pv dump_matches_dumps)" # ---------------------------------------------------- the malformed fixtures -- # THE TICKET'S OTHER REQUIREMENT, and the two documents are wrong in two # different ways. A DUPLICATE KEY is structural: TOML makes redefinition an # error, and a parser that merely overwrote would pass every case above. A BARE # INTEGER WITH A LEADING ZERO is lexical: TOML forbids it precisely so that `01` # can never be read as octal, and a parser that accepted it would silently change # the value of a real config file. # # Each pair splits "did not raise" from "raised the wrong class", which a single # boolean reports identically. check_eq $P/dup-key-raises "yes" "$(pv dup_key_raises)" check_eq $P/dup-key-exc "TomlDecodeError" "$(pv dup_key_exc)" # The diagnostic is not empty. A parser that raised the right class with no # message is useless to the consumer that has to report WHERE the document is # wrong. check_eq $P/dup-key-msg "yes" "$(pv dup_key_msg_nonempty)" check_eq $P/leading-zero-raises "yes" "$(pv leading_zero_raises)" check_eq $P/leading-zero-exc "TomlDecodeError" "$(pv leading_zero_exc)" check_eq $P/leading-zero-msg "yes" "$(pv leading_zero_msg_nonempty)" # 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"