. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is very nearly the whole cost. # # Every assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output; the # # probe itself writes a handful of small text files and does # # no compression, no compilation and no network. The bound # # matches 99-python3-flit-core's rather than being tightened, # # because the dominant term in both is the interpreter start # # and that is what has not been measured on this arm yet. # # 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-editables 0.5 (poky meta/recipes-devtools/python, # python3-editables_0.5.bb, `inherit pypi python_setuptools_build_meta`) -- THE # LIBRARY THAT GENERATES THE IMPORT WRAPPERS BEHIND AN EDITABLE INSTALL, on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THIS PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # editables does NOT install anything and does NOT build a wheel. A build backend # implementing PEP 660's `build_editable` hook hands it a project directory, says # which parts to expose, and gets back a list of (filename, content) PAIRS to put # in the wheel it is building. Everything it returns is text. # # There are three mechanisms and they are genuinely different, so each is driven # separately below rather than being treated as one feature: # # add_to_path() -- a directory written into the .pth file. Needs NO # runtime support at all; site.py just adds it. # add_to_subpackage() -- a generated __init__.py whose __path__ is rewritten to # the source directory, so `some.package.mod` resolves # into a tree that was never copied. # map() -- a name-to-file redirect, which is the one that needs # the runtime half: a generated `_editable_impl_.py` # installs editables.redirector.RedirectingFinder on # sys.meta_path, and the .pth's FIRST line imports it. # # The last is why `dependencies()` matters and is asserted in both directions: a # wheel built with map() must declare a runtime requirement on this package, and # one built with add_to_path() alone must not -- a version that returned the # dependency unconditionally would drag editables onto every target that ever # installed an editable wheel. # # ============================================================================ # 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 that # carried a compiled extension therefore could not be imported here -- not "would # be slow", could not be imported. editables is pure Python (two source files and # a py.typed marker), which is the entire reason it ports. # # So `module-no-so` is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump: editables grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import editables` on # somebody else's ticket. 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 LIBRARY IS DRIVEN END TO END, AND THE EXPECTATIONS ARE MEASURED. # ============================================================================ # # data/python3-editables-probe.py builds a fixture source tree in a scratch # directory, drives EditableProject over all three mechanisms, writes the files it # generates into a SECOND scratch directory, and makes that directory a site dir # with site.addsitedir() -- which is exactly what installing the generated wheel # would do, .pth processing and all. It then imports the fixture module and asks # where it came from. One key=value per line; the suite asserts on that output. # # EVERY VALUE ASSERTED BELOW WAS MEASURED, on x86-64, against THIS EXACT PROBE and # editables 0.5 unpacked from the same editables-0.5.tar.gz this image's recipe # builds (sha256 309627d9b5c4adc0e668d8c6fa7bac1ba7c8c5d415c2d27f60f081f8e80d1de2, # recorded 2026-08-31; the full control is in the ticket's design doc). 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 # normalised project name, the generated filenames, the .pth's first line, the # bootstrap's four lines and the resolved __file__ of a redirected import depend # on editables' own code and on the fixture, not on pointer width, byte order or # the platform string. Nothing arch-dependent is asserted anywhere in this file. # # version=0.5 project_name=bst_fixture_proj # all_names=EditableProject,__version__ # normalize_mixed=bst_fixture_proj normalize_dots=a_b_c_d # project_bootstrap=_editable_impl_bst_fixture_proj # files_count=3 deps=editables deps_path_only=(empty) # pth_first_line=import _editable_impl_bst_fixture_proj # mod_file_is_source=yes sub_file_is_source=yes # # THE CASE WORTH READING TWICE IS `mod-file-is-source`. An editable install that # silently COPIED the sources would satisfy every other case in this file -- # the module imports, the mark is right, the finder is installed -- and would have # defeated the entire purpose of the library. That case is the one that says the # import resolved back into the original tree. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - Building an actual wheel around the generated files. That is the backend's # job, not this library's -- upstream's own docs say editables "does not build # wheel files directly". flit_core's wheel path is BRINTOS-1828's suite and # hatchling's is BRINTOS-1829's. # - PEP 660 `build_editable` end to end through a backend. Nothing on this image # performs an editable install of a real project; the port's deliverable is # the library, and hatchling is what consumes it. # - upstream's tests/ corpus. It is not in the package (the sdist carries it, # the wheel does not), and this port closes on this suite instead. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-editables # THE CANARY, which asserts nothing about python3-editables. # # 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 ED=$SITE/editables PKGLIST=/var/lib/dpkg/info/python3-editables.list D=$BST_DATA PROBE=$D/python3-editables-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-editables.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import editables'` 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 "$ED" check $P/module-init test -f "$ED/__init__.py" # The runtime half, named on its own. Every generated bootstrap file imports it, # so a package that shipped __init__.py without redirector.py would pass the # import case and fail only at the end-to-end redirect, forty cases later. check $P/module-redirector test -f "$ED/redirector.py" # PEP 561's marker. Not decoration: it is the one non-Python regular file the # package ships under site-packages, and `pkg-only-python` below allows it BY # NAME, so a second such file is still a red. check $P/module-py-typed test -f "$ED/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 "$ED" -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. `py.typed` is allowed # by name rather than by a wildcard, for the reason `module-py-typed` gives. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */editables/py.typed) 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-editables 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. sed -n '/^Package: python3-editables$/,/^$/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 editables 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. The redirector is imported separately because it # is a separate module and nothing in __init__.py pulls it in -- only a generated # bootstrap file does, at editable-install time. check $P/import python3 -c 'import editables' check $P/import-redirector python3 -c 'from editables.redirector import RedirectingFinder' # ------------------------------------------------------------- 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 make `version` and # `redirector_file` distinguishable from the keys they are prefixes of. 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 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 probe's own import, reported from inside the interpreter. Distinguishable # from $P/import above: that one says the shell could start python3 and import, # this one says the SAME interpreter run that took every measurement below did. check_eq $P/probe-import "yes" "$(pv import_ok)" # 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 editables. 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 editables -- for both of its files. check_eq $P/module-file "$ED/__init__.py" "$(pv module_file)" check_eq $P/redirector-file "$ED/redirector.py" "$(pv redirector_file)" # ---------------------------------------------------------- the public API --- # __all__ is the module's own statement of its surface, asserted as an ordered # join so an addition and a removal read differently. It is DELIBERATELY short: # is_valid, normalize and EditableException are public in practice and absent from # __all__, which is upstream's shape and not a defect -- they are checked # individually below rather than being expected here. check_eq $P/api-all "EditableProject,__version__" "$(pv all_names)" check_eq $P/api-project "yes" "$(pv has_project)" check_eq $P/api-exception "yes" "$(pv has_exception)" check_eq $P/api-exception-derives "yes" "$(pv exception_is_exception)" check_eq $P/api-is-valid "yes" "$(pv has_is_valid)" check_eq $P/api-normalize "yes" "$(pv has_normalize)" check_eq $P/api-redirector-find-spec "yes" "$(pv redirector_is_metapathfinder)" check_eq $P/api-redirector-install "yes" "$(pv redirector_has_install)" check_eq $P/api-redirector-map-module "yes" "$(pv redirector_has_map_module)" # --------------------------------------------------------- the name rules ---- # normalize() and is_valid() are PEP 503 / PEP 426 in a few lines each, and they # decide the .pth filename and the bootstrap module name that everything below # depends on -- which is why they are asserted here rather than left to upstream's # own tests. `a.b--c__d` collapses every run of . - _ to ONE underscore, which is # the rule a naive implementation gets wrong by leaving `a_b__c__d`. check_eq $P/normalize-mixed "bst_fixture_proj" "$(pv normalize_mixed)" check_eq $P/normalize-runs "a_b_c_d" "$(pv normalize_dots)" check_eq $P/is-valid-plain "yes" "$(pv is_valid_plain)" check_eq $P/is-valid-rejects-space "no" "$(pv is_valid_bang)" check_eq $P/is-valid-rejects-leading-dash "no" "$(pv is_valid_leading_dash)" # ------------------------------------------------------------- the project --- # The probe could write its fixture tree. Separated from everything after it so a # read-only or full tmpfs is not reported as a library failure. check_eq $P/fixture "yes" "$(pv fixture_ok)" check_eq $P/project-name "bst_fixture_proj" "$(pv project_name)" # The bootstrap module's NAME is part of the on-disk contract: the .pth imports it # by this exact name, so a change here breaks every generated wheel silently. check_eq $P/project-bootstrap "_editable_impl_bst_fixture_proj" "$(pv project_bootstrap)" check_eq $P/project-dir "yes" "$(pv project_dir_is_src)" # The one error on this surface that is NOT EditableException. Asserted by TYPE # rather than by "it raised": a backend catching EditableException would let this # one through, and that is upstream's intent. check_eq $P/bad-name-raises "ValueError" "$(pv bad_name_raises)" # ------------------------------------------------- what to expose, and how ---- check_eq $P/map-ok "yes" "$(pv map_ok)" # map() resolved a DIRECTORY to its __init__.py. This is the one place the API # rewrites what it was given, and a version that stopped doing it would generate a # redirect pointing at a directory, which spec_from_file_location cannot load. check_eq $P/map-pkg-to-init "yes" "$(pv redirect_pkg_is_init)" check_eq $P/map-module-file "yes" "$(pv redirect_mod_is_file)" check_eq $P/map-count "2" "$(pv redirect_count)" # The two documented refusals, by exception type. A dotted name cannot be mapped # because the finder returns None for dotted names, so accepting one would write a # redirect into the wheel that silently never fires; a target that is not a # package or module is refused rather than deferred to an ImportError on target. check_eq $P/map-dotted-refused "EditableException" "$(pv map_dotted_raises)" check_eq $P/map-missing-refused "EditableException" "$(pv map_missing_raises)" # dependencies() is metadata the backend must copy into the wheel. Both directions # are asserted: map() needs this package at RUNTIME, add_to_path() alone does not. check_eq $P/deps-with-map "editables" "$(pv deps)" check_eq $P/deps-path-only "(empty)" "$(pv deps_path_only)" # ---------------------------------------------------------- the files() set --- # The generated names, as a set. Asserted whole rather than counted: a missing # bootstrap and a stray extra file are different defects that a count reports # identically. The count is asserted too, because the two fail differently when a # name changes rather than disappears. check_eq $P/files-count "3" "$(pv files_count)" check_eq $P/files-names \ "_editable_impl_bst_fixture_proj.py,bst_fixture_proj.pth,bstns/child/__init__.py" \ "$(pv files_names)" # THE .pth's FIRST LINE is what makes an editable install work at interpreter # start: site.py executes a line beginning `import `, and that is what installs the # finder. Asserted as the first line specifically -- a bootstrap import that had # moved below the path entries would still be executed, but only after those # entries had been added, and the ordering is the contract. check_eq $P/pth-first-line "import _editable_impl_bst_fixture_proj" "$(pv pth_first_line)" check_eq $P/pth-line-count "2" "$(pv pth_line_count)" check_eq $P/pth-carries-path-entry "yes" "$(pv pth_has_extra)" # The bootstrap module: import the finder, install it, and register one redirect # per map(). Four lines for two redirects, so the count is a statement that # nothing extra was generated. check_eq $P/boot-line-count "4" "$(pv boot_line_count)" check_eq $P/boot-imports-redirector "yes" "$(pv boot_imports_redirector)" check_eq $P/boot-installs-finder "yes" "$(pv boot_installs)" check_eq $P/boot-maps-module "yes" "$(pv boot_maps_module)" # The subpackage shim is an __init__.py that rewrites __path__ -- the whole of the # add_to_subpackage mechanism, and the only generated file that is a package # rather than a support module. check_eq $P/subpkg-file "bstns/child/__init__.py" "$(pv subpkg_name)" check_eq $P/subpkg-rewrites-path "yes" "$(pv subpkg_sets_path)" # ------------------------------------------- the editable install, end to end -- # THE DELIVERABLE. The generated files were written into a second directory and # that directory was made a site dir with site.addsitedir() -- which is exactly # what installing the generated wheel would do, .pth processing and all. Nothing # reaches inside the library to install the finder by hand. check_eq $P/gen-written "yes" "$(pv gen_written)" check_eq $P/site-module "yes" "$(pv site_import)" check_eq $P/addsitedir "yes" "$(pv addsitedir_ok)" # The .pth's path entry reached sys.path -- the no-runtime-support half of the # library, which needs no finder at all. check_eq $P/syspath-carries-entry "yes" "$(pv syspath_has_extra)" # ...and the finder the bootstrap installed is on meta_path, carrying both maps. check_eq $P/finder-installed "yes" "$(pv finder_installed)" check_eq $P/finder-redirect-count "2" "$(pv finder_redirect_count)" # THE CASES THE TICKET IS ABOUT. Each mechanism imports its fixture, and each # asserts the import resolved back INTO THE ORIGINAL SOURCE TREE rather than into # the generated directory. An editable install that silently copied the sources # would satisfy the `-import` and `-mark` cases and fail exactly these. check_eq $P/map-import "yes" "$(pv mod_import)" check_eq $P/map-import-from-source "yes" "$(pv mod_file_is_source)" check_eq $P/map-import-ran-fixture "brintos-editables-1833" "$(pv mod_mark)" check_eq $P/map-package-import "yes" "$(pv pkg_import)" check_eq $P/map-package-from-source "yes" "$(pv pkg_file_is_source)" check_eq $P/map-package-ran-fixture "brintos-editables-1833" "$(pv pkg_mark)" check_eq $P/subpkg-import "yes" "$(pv sub_import)" check_eq $P/subpkg-from-source "yes" "$(pv sub_file_is_source)" check_eq $P/subpkg-ran-fixture "brintos-editables-1833" "$(pv sub_mark)" check_eq $P/path-entry-import "yes" "$(pv extra_import)" check_eq $P/path-entry-from-source "yes" "$(pv extra_file_is_source)" check_eq $P/path-entry-ran-fixture "brintos-editables-1833" "$(pv extra_mark)" # THE FINDER DECLINES WHAT IT MUST DECLINE, which is what keeps a redirect for # `bstfixture` from shadowing some other project's `pkg.bstfixture`. A finder that # answered too eagerly would pass every case above and break unrelated imports on # any target that had ever installed an editable wheel. check_eq $P/finder-declines-dotted "yes" "$(pv finder_declines_dotted)" check_eq $P/finder-declines-submodule-search "yes" "$(pv finder_declines_pathed)" check_eq $P/finder-declines-unmapped "yes" "$(pv finder_declines_unknown)" # install() is idempotent -- site.py runs a bootstrap once per site dir, and a # finder appended twice would double every failed import's meta_path walk. check_eq $P/finder-install-idempotent "yes" "$(pv install_idempotent)" # 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"