. "$BST_LIB" # BST-TIMEOUT: 900 # 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. # # 99-python3-pathspec carries the same 900 for the same # # shape; the one start here also hashes an 848-entry list # # and re-runs the module as __main__ in-process, which is # # arithmetic beside the start. # # 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-trove-classifiers 2024.2.23 (poky meta/recipes-devtools/python, # python3-trove-classifiers_2024.2.23.bb, `inherit pypi # python_setuptools_build_meta ptest`) -- THE CANONICAL LIST OF PyPI TROVE # CLASSIFIERS, on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THIS PACKAGE IS, AND WHY THE ASSERTIONS ARE ABOUT DATA. # ============================================================================ # # trove-classifiers ships no algorithm. It is 41 KB of Python holding one list # of 848 classifier strings, the set built from it, a two-entry deprecation map # and the union of the two -- and it is the data `hatchling` (BRINTOS-1829) # validates a project's declared classifiers against. So the port is correct # exactly when the DATA arrived intact and the four documented views of it agree # with each other. That is what the probe measures and what the cases below # assert; there is no behaviour to drive beyond a membership test. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY SAYING. # ============================================================================ # # 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 could therefore not be imported here -- not # "would be slower", could not be imported. trove-classifiers is pure Python, # which is the entire reason it ports. # # So `module-no-so` and `pkg-no-so` are not boilerplate. They are a REGRESSION # TRIPWIRE on a version bump: this release grows no extension, and the day one # adds a compiled lookup these go red at INSTALL time rather than at `import # trove_classifiers` on somebody else's ticket three rounds later. The assertion # is made from BOTH sides because the two fail differently -- a find catches a # file the package does not declare, the dpkg list catches a declared file that # is not Python -- and a third time from inside the interpreter (`module-is-py`). # # ============================================================================ # EVERY EXPECTATION BELOW WAS MEASURED, NOT PREDICTED. # ============================================================================ # # data/python3-trove-classifiers-probe.py was run on x86-64 TWICE, on 2026-08-31: # against trove_classifiers unpacked from the same trove-classifiers-2024.2.23 # .tar.gz this recipe builds, AND against the trove_classifiers extracted from # this build's own python3-trove-classifiers_2024.2.23-r0_wasm32.deb. The two # outputs agree on every key but the module's own path. The numbers are recorded # in the ticket's design doc and in test-plan section BRINTOS-1835. # # What makes the control transferable is that all of it is pure-Python and # content-determined: a string list, a set built from it, and a SHA-256 over the # joined text. Nothing here depends on pointer width, byte order or a platform # string, and nothing arch-dependent is asserted. # # THE ONE THING THE CONTROL CANNOT SETTLE is whether this arm's static python3 # starts and imports the module inside the bound above. That is what the batch # verify measures, and it is the only genuine unknown in this file. # # ============================================================================ # THREE MEASURED FACTS THAT WOULD EACH MAKE A CASE RED ON A CORRECT PACKAGE. # ============================================================================ # # 1. `sorted_classifiers` IS NOT IN `str` ORDER. The name refers to the # project's own version-aware ordering (its bin/sort.py), under which # `CUDA :: 9.2` precedes `CUDA :: 10.0`. `sorted-not-str-sorted` asserts # the "no" deliberately; asserting the "yes" the name suggests would be red # on every correct release. `all_classifiers` IS plain-sorted -- upstream # builds it with `sorted()` -- and the two are asserted separately for that # reason. # 2. THE MODULE CARRIES NO `__version__`. The version can only come from the # installed distribution's metadata, so `version` is as much a statement # about the dist-info this package shipped as about the module, and # `has-version-attr` pins the negative so a later reader does not "fix" it. # 3. `py.typed` IS A ZERO-BYTE FILE -- its existence is the PEP 561 marker and # its content is nothing. It is asserted with `test -f`; `test -s` would be # red on a correct package. The dpkg .list also names DIRECTORIES, without # a trailing slash, which is why `pkg-only-python` tests each entry for # being a regular file first. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - python3-trove-classifiers-dev. It is EMPTY on this build (measured: its # data archive carries `./` and nothing else), so the image names only the # main package and this suite says nothing about it. # - The upstream pytest suite (the recipe `inherit ptest`). ptest is not in # this image's DISTRO_FEATURES and pulling it in would drag python3-pytest # and python3-unittest-automake-output into the image for one package's # sake. The probe covers what those tests cover that matters here -- the # list is consistent with its set, its sorted view and its deprecation map. # - hatchling's use of the data. That belongs to BRINTOS-1829's own suite; the # membership test this file makes is the operation hatchling performs, not # hatchling itself. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-trove-classifiers # THE CANARY, which asserts nothing about python3-trove-classifiers. # # 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` 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. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages TC=$SITE/trove_classifiers DI=$SITE/trove_classifiers-2024.2.23.dist-info PKGLIST=/var/lib/dpkg/info/python3-trove-classifiers.list D=$BST_DATA PROBE=$D/python3-trove-classifiers-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-trove.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import trove_classifiers'` 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 "$TC" # THE DATA FILE ITSELF, and that it is not empty. 41 KB of classifier strings # live in __init__.py; a truncated install is the plausible packaging failure # here and `digest-sorted` below is what catches a truncation that kept a # plausible count. This case catches the one that kept nothing. check $P/module-init test -f "$TC/__init__.py" check $P/module-init-nonempty test -s "$TC/__init__.py" # The module's only executable surface -- `python3 -m trove_classifiers` prints # the list. Asserted present here and driven by the probe below. check $P/module-main test -f "$TC/__main__.py" # py.typed is a SHIPPED FILE, not build residue: it is what marks the package # PEP 561 typed. It is also ZERO BYTES -- measured -- so `test -s` would be red # on a correct package, and it is the one regular file under the module that is # neither .py nor metadata, which is why `pkg-only-python` below has to allow # it. Named here so that allowance is anchored on something asserted. check $P/module-py-typed test -f "$TC/py.typed" # THE DIST-INFO DIRECTORY CARRIES THE PINNED VERSION IN ITS NAME. With no # __version__ on the module (see the header), this directory is where the # version physically lives, and `version` below reads it back through # importlib.metadata -- so the two together say the metadata is both present and # readable by the interpreter, which are different failures. check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-license test -f "$DI/LICENSE" check $P/dist-info-top-level test -f "$DI/top_level.txt" # 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` later. check_eq $P/module-no-so "" \ "$(find "$TC" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # # 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. # # THE `-f` TEST IS LOAD-BEARING AND WAS MEASURED OFF THE BUILT PACKAGE. A dpkg # .list names DIRECTORIES too, and without a trailing slash, so a name-only # filter reports every directory in the package as an unexpected shape -- here # that is four entries (trove_classifiers, its __pycache__ and the dist-info # dir, plus site-packages itself) and this case would be red on a CORRECT # package. # # `py.typed` is allowed by the *.typed arm and asserted present above, so the # allowance cannot quietly cover its disappearance. The dist-info arm covers # LICENSE, METADATA, RECORD, WHEEL and top_level.txt, each of which is named # individually above for the same reason. _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 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-trove-classifiers 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-trove-classifiers` is a PREFIX of # `python3-trove-classifiers-dev`, and an unanchored match would report one # stanza as the other. sed -n '/^Package: python3-trove-classifiers$/,/^$/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 "2024.2.23-r0" cat "$W/st" # THE ONLY RUNTIME DEPENDENCY THIS PACKAGE CARRIES -- measured off the built # .deb, whose control stanza reads `Depends: python3-core` and nothing else. # Pure data over the interpreter core: no third-party module, which is why # nothing about this port waited on another member of BRINTOS-1826. check_out $P/pkg-depends-core "python3-core" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If trove_classifiers 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 trove_classifiers' check $P/import-names python3 -c 'from trove_classifiers import classifiers, sorted_classifiers, deprecated_classifiers, all_classifiers' # ------------------------------------------------------------- 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" 600 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 `count_sorted` and # `count_classifiers` 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 # corrupted data, 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 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 # A traceback the probe caught itself, surfaced as its own case rather than as # forty separate got:'' reds. check_eq $P/probe-no-fatal "" "$(pv fatal)" # THE VERSION THE RECIPE PINS. Read through importlib.metadata out of the # dist-info above, because the module carries no __version__ -- so a red here is # either a version drift or a dist-info the interpreter could not read, and # `dist-info-metadata` above is what separates the two. check_eq $P/version "2024.2.23" "$(pv version)" # The negative, pinned. See the header: asserting a `__version__` would be red # on a correct package, and this case is what stops a later reader adding one. check_eq $P/has-version-attr "no" "$(pv has_version_attr)" # 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 data # cases below statements about the SAME trove_classifiers. check_eq $P/module-file "$TC/__init__.py" "$(pv module_file)" # ...and it answered out of a .py, not an extension. The third statement of the # .so negative, this one from inside the interpreter. check_eq $P/module-is-py "yes" "$(pv module_is_py)" # ------------------------------------------------------------- the API ------- # The four documented names, one key each: a missing `classifiers` and a missing # `deprecated_classifiers` are different breakages and a single "the API is # there" case would report them identically. check_eq $P/api-sorted "yes" "$(pv api_sorted)" check_eq $P/api-classifiers "yes" "$(pv api_classifiers)" check_eq $P/api-deprecated "yes" "$(pv api_deprecated)" check_eq $P/api-all "yes" "$(pv api_all)" # THE TYPES ARE PART OF THE CONTRACT. `classifiers` being a set is what makes # the membership test hatchling performs O(1) rather than a scan of 848 # strings, and a release that turned it into a list would pass every membership # case below while changing the cost of the operation the package exists for. check_eq $P/type-sorted "list" "$(pv type_sorted)" check_eq $P/type-classifiers "set" "$(pv type_classifiers)" check_eq $P/type-deprecated "dict" "$(pv type_deprecated)" check_eq $P/type-all "list" "$(pv type_all)" # --------------------------------------------------------------- the data --- # THE COUNTS, measured on x86-64 against this build's own .deb. They are the # cheapest statement that the whole 41 KB list arrived: a truncated install # lands here first and names a number rather than a missing classifier. check_eq $P/count-sorted "848" "$(pv count_sorted)" check_eq $P/count-classifiers "848" "$(pv count_classifiers)" check_eq $P/count-deprecated "2" "$(pv count_deprecated)" check_eq $P/count-all "850" "$(pv count_all)" # Non-empty, stated on its own. Every case in this section would also fail on an # empty list, and each would name a count or a classifier rather than the cause. check_eq $P/nonempty "yes" "$(pv nonempty)" # THE SET IS THE LIST. `set(list)` silently absorbs a duplicate, so the two # cases together are what say the shipped list has none -- the equality alone # would hold with a duplicate present. check_eq $P/set-equals-list "yes" "$(pv set_equals_list)" check_eq $P/no-duplicates "yes" "$(pv no_duplicates)" # `all_classifiers` is documented as the valid list plus the deprecated keys. # Asserted as a DERIVATION rather than as a number, so a change on either side # that keeps 850 working is still caught. check_eq $P/all-is-derived "yes" "$(pv all_is_derived)" check_eq $P/all-is-sorted "yes" "$(pv all_is_sorted)" # ...and the fact that reads backwards: `sorted_classifiers` is NOT in `str` # order. See the header -- this is the project's own version-aware ordering, and # asserting the "yes" the name suggests would be red on every correct release. check_eq $P/sorted-not-str-sorted "no" "$(pv sorted_is_str_sorted)" # The two ends of the list, which pin the ordering the case above says is not # alphabetical. A reordering that preserved the count and both endpoints is what # `digest-sorted` is for. check_eq $P/first "Development Status :: 1 - Planning" "$(pv first)" check_eq $P/last "Typing :: Typed" "$(pv last)" # ONE FINGERPRINT OVER THE WHOLE SHIPPED LIST. The counts and the memberships # each cover one way the data could be wrong; this covers every other way at # once -- a reordering, a single edited entry, a truncation that kept the count. # It is content-determined and carries across arches for the same reason the # rest of this file does: pure Python text, no pointer width and no locale in it. # # This is also the case that will go red on a VERSION BUMP, correctly and on # purpose: trove-classifiers releases are exactly changes to this list, so the # digest is the port's statement of which release landed. check_eq $P/digest-sorted \ "71cbfd1eca670692e6c847686fac6edb4ba0e2230488e870066f3782e983ef96" \ "$(pv digest_sorted)" check_eq $P/digest-all \ "993f32f2ed47c43256e52b9cadb1af8f38d05b4cb2f1c9ed6d370b174533457b" \ "$(pv digest_all)" # ------------------------------------------------- the membership test ------- # THE OPERATION THIS PACKAGE EXISTS FOR, both ways round. Three classifiers that # must be on the list -- deliberately of three different shapes and from three # different sections, so a truncation that kept its first section cannot pass -- # and three that must not. check_eq $P/in-mit-license "yes" "$(pv in_MIT_License)" check_eq $P/in-python-3 "yes" "$(pv in_3)" check_eq $P/in-prod-stable "yes" "$(pv in_5_-_Production_Stable)" check_eq $P/present-all "yes" "$(pv present_all)" # THE REJECTION. `out_0` is a plausible-looking invention and is the case the # ticket names: a validator that accepted it would accept anything, and that is # precisely what hatchling uses this package to avoid. check_eq $P/rejects-invented "yes" "$(pv out_0)" # `out_1` and `out_2` are the two DEPRECATED keys, asserted ABSENT from the # valid set while remaining in `deprecated_classifiers` below. Together those # two facts are what say the two views are distinct rather than one list under # two names -- which is the whole content of "deprecated" here. check_eq $P/deprecated-not-valid-1 "yes" "$(pv out_1)" check_eq $P/deprecated-not-valid-2 "yes" "$(pv out_2)" check_eq $P/absent-all "yes" "$(pv absent_all)" # ---------------------------------------------------- the deprecation map ---- # Both keys, whole and ordered, so a map that lost one names which. check_eq $P/dep-keys \ "Natural Language :: Ukranian|Topic :: Communications :: Chat :: AOL Instant Messenger" \ "$(pv dep_keys)" # THE TWO SHAPES A DEPRECATION HAS, which is why both entries are asserted # rather than just the count: one names a replacement -- the Ukranian/Ukrainian # spelling fix -- and one names NONE, an empty list meaning "retired, with # nothing to move to". A consumer that assumed every value non-empty breaks on # the second, so the empty one is asserted as emptiness rather than skipped. check_eq $P/dep-ukrainian "Natural Language :: Ukrainian" "$(pv dep_ukranian)" check_eq $P/dep-aol-empty "yes" "$(pv dep_aol_empty)" # Every replacement a deprecated entry names is itself a VALID classifier. A # deprecation pointing at something not on the list would send a consumer from # one invalid classifier to another, and no case above would see it. check_eq $P/dep-targets-valid "yes" "$(pv dep_targets_valid)" # ------------------------------------------------------- the __main__ path --- # `python3 -m trove_classifiers` prints the list, one per line. It is the # module's only executable surface, and it is run INSIDE the probe's single # interpreter (runpy, stdout captured) because a second `python3 -m` on this arm # would cost more than everything else in this file put together. check_eq $P/main-lines "848" "$(pv main_lines)" check_eq $P/main-matches-sorted "yes" "$(pv main_matches_sorted)" # 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"