. "$BST_LIB" # BST-TIMEOUT: 1800 # FIVE python3 starts, and they are the whole cost. # # # # The five: a bare `import zope.interface'; a second whose # # STDERR is the assertion; a third with PURE_PYTHON=0, which # # must REFUSE; and the probe, run TWICE -- once with the # # environment as the image leaves it, once with # # PURE_PYTHON=1 forced. Everything else reads the # # filesystem or dpkg with shell builtins, or greps a probe # # transcript. # # # # The work inside a start is a few hundred attribute # # lookups and isinstance() calls over interfaces of six # # names -- 0.02s on the x86-64 control. Microseconds beside # # an interpreter start on this arm, which is tens of # # seconds before the first line of the program runs. # # # # 1800 is 360s per start, against the ~60s a python3 start # # has measured at here. The start dominates and nothing in # # this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-zopeinterface 6.3 (meta-openembedded/meta-python # recipes-devtools/python/python3-zopeinterface_6.3.bb, `inherit pypi # setuptools3', PyPI sdist of `zope.interface') on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # It is the interface-declaration layer the whole Zope stack is built on: # # class IShape(Interface): # sides = Attribute("how many sides") # def area(): ... # # @implementer(IShape) # class Square: ... # # and then three questions about that declaration -- does this OBJECT provide # the interface (`providedBy'), does this CLASS implement it (`implementedBy'), # and does the implementation actually SATISFY it (`verifyObject' / # `verifyClass', which raise rather than return false). The declarations are # structural claims a class makes about itself; the verify half is the only part # that inspects the implementation, and the two are routinely confused. # # ============================================================================ # IT IS NOT PURELY INTERPRETED, AND THAT IS THE SUBSTANCE OF THIS PORT. # ============================================================================ # # Nearly everything else in this round is pure Python. This package is not: it # ships `_zope_interface_coptimizations.c', a C accelerator carrying the # base types (`SpecificationBase', `InterfaceBase', `LookupBase', # `VerifyingBase', `ClassProvidesBase', `ObjectSpecificationDescriptor') and the # `providedBy' / `implementedBy' functions, BESIDE a complete pure-Python # implementation of the same. `_compat.py`'s `_use_c_impl' decorator picks # between them at import: # # catch = () if _c_optimizations_required() else (ImportError,) # try: from zope.interface import _zope_interface_coptimizations # except catch: return False # -> the Python object is used # # with `PURE_PYTHON' selecting the policy: unset means "use C if it imports", # any value but "0" means "never", and exactly "0" means "REQUIRE it" -- and # there the `catch' tuple is empty, so the ImportError propagates and the whole # package fails to import. # # THE ACCELERATOR IS NOT BUILT HERE, AND THAT IS THE WHOLE COMPILE-SIDE PORT. # `python3-zopeinterface_%.bbappend' exports PURE_PYTHON=1 for class-target and # carries a one-hunk patch teaching `setup.py' to read it, so `ext_modules' is # empty and no shared object is produced. `native' and `nativesdk' keep the # accelerator, where it links against a CPython that exports its API. # # IT IS DECLINED BECAUSE IT WOULD BUILD AND COULD NOT LOAD. The stock recipe -- # measured, not assumed -- packages # `_zope_interface_coptimizations.cpython-312.so', 105808 bytes, a genuine wasm # side module (`file': `WebAssembly (wasm) binary module version 0x1') with a # `dylink.0' section, this arm's `hwjs.abi' section and NO `NEEDED' entries. Of # its 76 wasm imports, 46 are `env.Py*' -- `PyArg_ParseTuple', `PyType_Ready', # `PyModule_Create2', `_Py_NoneStruct' and the rest -- and this build's # python3-core 3.12.13-r0 exports 35 symbols, not one of them `Py*'. Its own # `_sysconfigdata__linux_.py' has HAVE_DYNAMIC_LOADING 1 and `EXT_SUFFIX' # `.cpython-312.so' -- exactly the name that would be installed -- so importlib # would NOT skip the file: it would find it, call dlopen, and fail on unbound # imports. That is BRINTOS-2046, measured on python3-bitarray (BRINTOS-2032) and # python3-crc32c (BRINTOS-2033). # # THIS FOLLOWS python3-cbor2 AND python3-crcmod (BRINTOS-2039, BRINTOS-2038), # which declined their accelerators on the same reasoning, and this project # already reads a package that installs a .so it cannot load as a defect rather # than a partial success (BRINTOS-1476, the python3-pyparsing suite's # `pkg-no-so'). python3-crc32c is the exception that proves it: crc32c IS its # extension -- its sdist contains no .py at all -- so declining would have # shipped nothing. This package has a second half, so it lands on it # DELIBERATELY rather than by tripping over a failed dlopen on the guest. # # ============================================================================ # SO THE SUITE MEASURES WHICH IMPLEMENTATION ANSWERED, NOT ONLY THAT IT DID. # ============================================================================ # # The two implementations are meant to be indistinguishable, which is precisely # why a degradation either way would otherwise be invisible: every # behavioural case below passes on either. The `impl-*' cases read # `InterfaceBase.__module__' and its six siblings directly, so the answer is a # recorded fact rather than an inference -- and `impl-selected' is the single # derived verdict, so there is one case to read rather than seven to agree on. # # THE `copt-*' AND `impl-*' CASES ARE ABOUT OUR OWN BUILD, NOT ABOUT # BRINTOS-2046. They assert that the accelerator is absent and that the Python # objects answered, which is what the bbappend was written to produce. They red # if the extension is reinstated -- deliberately, because that is a decision # somebody would be making, and re-measuring this package against the C path is # what it calls for. Nothing in this file is waiting on the dlopen gap to close. # # ============================================================================ # THE PROBE RUNS TWICE, AND THE COMPARISON IS ITSELF A CASE. # ============================================================================ # # `pure-python-identical' diffs the two transcripts with the three keys that are # ABOUT the environment removed. Both runs take the Python path -- one because # there is no C module to find, one because the policy forbids looking -- so they # agree, and the case says the two routes to the fallback reach the same # implementation. It is also what would catch a build that reinstated the # extension for one of the two runs and not the other. # # ============================================================================ # `zope' HAS NO `__init__.py', DELIBERATELY, AND IS IMPORTABLE TWICE OVER. # ============================================================================ # # `zope' is a namespace package. The build ships no `zope/__init__.py' and # installs `zope.interface-6.3-py3.12-nspkg.pth' instead, which `site' executes # at interpreter start to synthesise the parent module. PEP 420's implicit # namespace packages would make `import zope.interface' work even if that .pth # were never read, so the two mechanisms overlap -- and a case asserting the # absence of `__init__.py' is what keeps the shipped shape from drifting into # the one where only one of them is left. # # ============================================================================ # WHAT IS NOT INSTALLED, STATED RATHER THAN LEFT TO CHANCE. # ============================================================================ # # The recipe splits four packages. The image installs ONE of them. `-dev' still # holds `_zope_interface_coptimizations.c' -- the SOURCE ships either way; it is # the compiled object that does not -- `-test' holds upstream's own test tree # (`zope/interface/tests' and `zope/interface/common/tests', which RDEPEND on # python3-unittest and python3-doctest), and `-dbg' is empty. The `no-*' cases # below assert their absence, so that a case like `common-dir' cannot pass # because some other package happened to install the same path. P=python3-zopeinterface # THE CANARY, which asserts nothing about python3-zopeinterface. # # 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 PKGDIR=$SITE/zope/interface DI=$SITE/zope.interface-6.3.dist-info PTH=$SITE/zope.interface-6.3-py3.12-nspkg.pth SO=$PKGDIR/_zope_interface_coptimizations.cpython-312.so PKGLIST=/var/lib/dpkg/info/python3-zopeinterface.list PROBE=$BST_DATA/python3-zopeinterface-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-zopeinterface.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import zope.interface' further down is the # functional statement; these say WHICH copy answered, so a package importing # out of some other path on sys.path is distinguishable from one importing out # of what this image installed. check $P/site-packages-dir test -d "$SITE" check $P/namespace-dir test -d "$SITE/zope" check $P/package-dir test -d "$PKGDIR" # THE NAMESPACE PACKAGE HAS NO `__init__.py'. See the header: the .pth is what # upstream ships for it, and PEP 420 covers the case where the .pth is not read. # Both are asserted, because losing either one silently is the drift this pair # catches. check $P/no-namespace-init test ! -f "$SITE/zope/__init__.py" check $P/nspkg-pth test -s "$PTH" check_out $P/nspkg-pth-names-zope "'zope'" cat "$PTH" # The thirteen modules that are the library, each by name. A release that # merged or dropped one would change the import surface without any behavioural # case below noticing. for _m in __init__ _compat _flatten adapter advice declarations document \ exceptions interface interfaces registry ro verify; do check $P/module-$_m test -s "$PKGDIR/$_m.py" done # The sizes are pinned rather than floored: a truncated or partially written # payload passes `test -s' and fails only here. Byte for byte as the sdist ships # them -- there is no patch of ours, so the installed file IS upstream's. check_eq $P/init-size 3460 "$(wc -c < "$PKGDIR/__init__.py" 2>/dev/null | tr -d ' ')" check_eq $P/compat-size 4369 "$(wc -c < "$PKGDIR/_compat.py" 2>/dev/null | tr -d ' ')" check_eq $P/interface-size 39260 "$(wc -c < "$PKGDIR/interface.py" 2>/dev/null | tr -d ' ')" check_eq $P/declarations-size 43007 "$(wc -c < "$PKGDIR/declarations.py" 2>/dev/null | tr -d ' ')" check_eq $P/adapter-size 36218 "$(wc -c < "$PKGDIR/adapter.py" 2>/dev/null | tr -d ' ')" check_eq $P/verify-size 7226 "$(wc -c < "$PKGDIR/verify.py" 2>/dev/null | tr -d ' ')" check_eq $P/exceptions-size 8636 "$(wc -c < "$PKGDIR/exceptions.py" 2>/dev/null | tr -d ' ')" # `zope.interface.common' -- the interfaces declared for the builtin types and # the ABCs. It is a subpackage, not a module, and its `idatetime' is the reason # the recipe RDEPENDS on python3-datetime. check $P/common-dir test -d "$PKGDIR/common" for _m in __init__ builtins collections idatetime interfaces io mapping \ numbers sequence; do check $P/common-$_m test -s "$PKGDIR/common/$_m.py" done # THE C ACCELERATOR IS NOT HERE. Asserted by its exact installed name, and then # by a glob, because the two fail differently: the first catches the bbappend # being dropped, the second catches a build that produced the object under a # name this file does not spell out. check $P/no-extension test ! -e "$SO" check_eq $P/no-extension-glob "" \ "$(ls "$PKGDIR"/_zope_interface_coptimizations*.so 2>/dev/null)" # The precompiled bytecode, which `import-quiet' below turns on. Asserted as # present and non-empty rather than pinned, since a .pyc header carries # build-time state the source does not. for _m in __init__ _compat declarations interface verify adapter; do check $P/pycache-$_m test -s "$PKGDIR/__pycache__/$_m.cpython-312.pyc" done check $P/pycache-common-idatetime \ test -s "$PKGDIR/common/__pycache__/idatetime.cpython-312.pyc" # WHAT THE IMAGE DID NOT INSTALL. See the header: three of the recipe's four # packages are absent, and saying so is what keeps `common-dir' above from # passing for the wrong reason. check $P/no-dev-c-source test ! -f "$PKGDIR/_zope_interface_coptimizations.c" check $P/no-test-tree test ! -d "$PKGDIR/tests" check $P/no-common-tests test ! -d "$PKGDIR/common/tests" # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-metadata test -s "$DI/METADATA" check $P/dist-record test -s "$DI/RECORD" check $P/dist-licence test -s "$DI/LICENSE.txt" check_out $P/dist-metadata-name "Name: zope.interface" cat "$DI/METADATA" check_out $P/dist-metadata-version "Version: 6.3" cat "$DI/METADATA" check_out $P/dist-top-level "zope" cat "$DI/top_level.txt" check_out $P/dist-namespace-packages "zope" cat "$DI/namespace_packages.txt" # 58 entries: every file the wheel carried, including the ones dpkg later split # into -dev and -test, and RECORD's own self-reference. It is 59 with the # extension built, and the one line of difference is the .so. check_eq $P/record-entries 58 "$(grep -c . "$DI/RECORD" 2>/dev/null)" # THE WHEEL METADATA MOVED WITH THE DECISION, and that is the half a # do_install-time deletion of the .so would have got wrong. With the extension # the same build gives `Root-Is-Purelib: false' and # `Tag: cp312-cp312-linux_x86_64' -- the BUILD HOST's tag on a wasm32 target, # because bdist_wheel names the platform it ran on. Declining it at # `ext_modules' instead gives a wheel that is honestly pure and portable, and a # RECORD that does not name a file which is not there. All three are asserted. check_out $P/wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/wheel-tag-any "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/record-no-so "" "$(grep -i '\.so' "$DI/RECORD" 2>/dev/null)" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files; this is the one that says # python3-zopeinterface is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. sed -n '/^Package: python3-zopeinterface$/,/^$/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 "Version: 6.3-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE DEPENDENCIES, as dpkg recorded them, and there are exactly two -- both # sub-packages of this image's own python3 (BRINTOS-1476), which is why this # port waited on no other package. Each is asserted by name, because "the # Depends line is non-empty" would pass on the wrong pair. check_out $P/pkg-depends-core "python3-core" grep '^Depends:' "$W/st" check_out $P/pkg-depends-datetime "python3-datetime" grep '^Depends:' "$W/st" # THE FILE COUNTS, which are what catch a payload that changed shape without # changing any name this file spells out. Measured in the .deb this build # produced: 22 .py, one .pyc for each of them, the six dist-info entries and the # namespace .pth -- 51 regular files in all, and no shared object. # # Counted by testing each entry with `test -f' rather than by counting lines, # because dpkg records the directories it created in the same list and their # number is a fact about what else the image installed, not about this package. # # BRINTOS-WORKAROUND: brintos/linux#73 remove -- read `$PKGLIST' directly when # the redirect stops failing. 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. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" _py=0; _pyc=0; _so=0; _reg=0 while IFS= read -r _f; do [ -f "$_f" ] || continue _reg=$((_reg + 1)) case "$_f" in *.py) _py=$((_py + 1)) ;; *.pyc) _pyc=$((_pyc + 1)) ;; *.so) _so=$((_so + 1)) ;; esac done < "$W/pkgf" check_eq $P/pkg-py-count 22 "$_py" check_eq $P/pkg-pyc-count 22 "$_pyc" # NO SHARED OBJECT ANYWHERE THE PACKAGE PUT ANYTHING -- the file-list statement # of the bbappend, and the one that catches a version bump which grew a second # extension this file does not name. check_eq $P/pkg-so-count 0 "$_so" check_eq $P/pkg-file-count 51 "$_reg" # NOTHING IN ${bindir}. zope.interface declares no `scripts' and no # `console_scripts': it is a library with no CLI, so there is no executable and # no shebang to assert. Stated as a fact about the package rather than left to # be inferred from an absent case. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # ...and the three subpackages the image did NOT install are unknown to dpkg # too, not merely absent from disk. for _s in dev test dbg; do check $P/no-subpkg-$_s \ sh -c "! grep -q '^Package: python3-zopeinterface-$_s\$' /var/lib/dpkg/status" done # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If zope.interface 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 -- and, given the header, the cheapest # statement that the C accelerator's refusal is being CAUGHT rather than # propagated. check $P/import python3 -c 'import zope.interface' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. Two facts at once. A module # compiled from SOURCE at import time would emit whatever diagnostics 3.12 # raises over this code, and the package ships precompiled .pyc precisely so the # compiler never runs. And the dlopen that fails underneath must fail QUIETLY -- # `_use_c_impl' swallows the ImportError, but the hwjs loader prints its own # refusal to the console, and a package that emitted that on every import would # be unusable in a program that parses its own stderr. check_eq $P/import-quiet "" "$(python3 -c 'import zope.interface' 2>&1)" # PURE_PYTHON=0 IS THE THIRD POLICY AND IT MUST REFUSE. With the C # implementation REQUIRED, `_c_optimizations_available' catches nothing and the # ImportError propagates out of `import zope.interface' itself. This is the case # that proves the accelerator is genuinely UNAVAILABLE on this arm rather than # merely not preferred -- without it, every `impl-selected=python' below would # also be satisfied by a package that simply never looked. # # PINNED TO BRINTOS-2046: when the dlopen gap closes this import succeeds, both # cases red, and that is the signal to re-measure. _pp0=$(PURE_PYTHON=0 python3 -c 'import zope.interface' 2>&1); _pp0rc=$? check_eq $P/pure-python-0-refused 1 "$_pp0rc" case "$_pp0" in *ImportError*) bst_ok $P/pure-python-0-importerror ;; *) bst_notok $P/pure-python-0-importerror \ "want an ImportError from the required C path, got: $(printf '%s' "$_pp0" | tr '\n' ' ')" ;; esac # ---------------------------------------------------------------- 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 `^' and `=' anchors keep `marked_after' # distinguishable from `marked_after_something'. 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 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 # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart, and `emitted-count' is # what catches a key skipped in the middle. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "82" "$(pv emitted_count)" check_eq $P/probe-quiet "" "$(cat "$OUT.err" 2>/dev/null)" # The module answered out of the file this image installed, not from a stray # copy on sys.path -- which is what makes the payload cases above and the # behavioural cases below statements about the SAME zope.interface. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/env-unset "" "$(pv env_pure_python)" check_eq $P/dist-name "zope.interface" "$(pv dist_name)" check_eq $P/dist-version "6.3" "$(pv dist_version)" # ============================================================================ # WHICH IMPLEMENTATION ANSWERED -- see the header. About OUR build, not 2046. # ============================================================================ # The accelerator's own import, attempted directly rather than through # `_use_c_impl', so the failure is observable instead of swallowed. check_eq $P/copt-import "failed" "$(pv copt_import)" # THE LOAD-BEARING FACT OF THE WHOLE PORT. `_use_c_impl' catches ImportError and # NOTHING ELSE, so a dlopen refusal arriving as any other exception would take # the entire package down instead of falling back. This case is why the port # ships. check_eq $P/copt-error-is-importerror "yes" "$(pv copt_import_error_is_importerror)" # The refusal has a REASON attached. The text is the hwjs loader's and is not # pinned -- it names whichever `env.Py*' import was looked up first, which is a # fact about link order rather than about this package -- but an empty reason # would mean the module vanished rather than failed to bind. if [ -n "$(pv copt_import_error)" ]; then bst_ok $P/copt-error-nonempty else bst_notok $P/copt-error-nonempty "the accelerator import failed with no reason attached" fi # THE INTERPRETER'S EXTENSION SUFFIXES -- THE FACT THE bbappend RESTS ON, and # the one case in this file that is about the platform rather than about this # package. Read on target rather than quoted from the build tree, where the same # values come out of `_sysconfigdata__linux_.py' (EXT_SUFFIX `.cpython-312.so', # HAVE_DYNAMIC_LOADING 1). It says that a # `_zope_interface_coptimizations.cpython-312.so', HAD ONE BEEN SHIPPED, would be # a candidate under its full name -- so importlib would reach the dlopen and # BRINTOS-2046 would refuse it, rather than the finder skipping the file # harmlessly. An interpreter with a different EXT_SUFFIX would make this port's # central decision wrong, and nothing else here would notice. check_eq $P/ext-suffixes ".cpython-312.so,.abi3.so,.so" "$(pv ext_suffixes)" # The exception TYPE. `ModuleNotFoundError' is what a submodule import finds when # there is no file at all -- as against the `ImportError: .so: [policy -> # ELIBBAD] hwjs-dlopen: unresolved import env.Py...' that BRINTOS-2033 measured # from a .so which WAS shipped. This case is what tells the two apart, so a build # that quietly started shipping the object again is visible here and not only in # the file-count cases. check_eq $P/copt-error-type "ModuleNotFoundError" "$(pv copt_import_error_type)" # No `__file__' was reached, because no module object was created. This is what # separates "the import failed" from "the import succeeded and the module is # somehow empty". check_eq $P/copt-no-file "-" "$(pv copt_file)" # The policy the environment produced, before any of it is acted on: with # PURE_PYTHON unset the C path is ATTEMPTED and not ignored. Together with # `copt-import' above this says the fallback was reached by failure, not by # policy -- the two are indistinguishable from `impl-selected' alone. check_eq $P/copt-should-attempt "yes" "$(pv copt_should_attempt)" check_eq $P/copt-ignored "no" "$(pv copt_ignored)" check_eq $P/copt-required "no" "$(pv copt_required)" # The seven base objects, each read directly. `zope.interface.interface' and its # siblings are the Python modules; `zope.interface._zope_interface_coptimizations' # would be the C one. check_eq $P/impl-interface-base "type/zope.interface.interface" "$(pv impl_interface_base)" check_eq $P/impl-specification-base "type/zope.interface.interface" "$(pv impl_specification_base)" check_eq $P/impl-lookup-base "type/zope.interface.adapter" "$(pv impl_lookup_base)" check_eq $P/impl-verifying-base "type/zope.interface.adapter" "$(pv impl_verifying_base)" check_eq $P/impl-class-provides-base "type/zope.interface.declarations" "$(pv impl_class_provides_base)" # The two module-level FUNCTIONS, where the C path would give a # `builtin_function_or_method' instead. check_eq $P/impl-implementedby "function/zope.interface.declarations" "$(pv impl_implementedby)" check_eq $P/impl-providedby "function/zope.interface.declarations" "$(pv impl_providedby)" # `adapter_hooks' is a plain list on both paths -- the C module exports one too. # Asserted so that the hook cases below rest on a stated type. check_eq $P/impl-adapter-hooks "list" "$(pv impl_adapter_hooks)" # THE ONE DERIVED VERDICT. One case to read rather than seven to agree on. check_eq $P/impl-selected "python" "$(pv impl_selected)" # The fallback names `_use_c_impl' keeps WHICHEVER path won. These say the # Python object is reachable by name, not that it was chosen -- `impl-selected' # is what says that -- so they stay green when BRINTOS-2046 closes. check_eq $P/fallback-kept-py "yes" "$(pv fallback_kept_py)" check_eq $P/fallback-kept-fallback "yes" "$(pv fallback_kept_fallback)" # `Interface.__class__' is InterfaceClass, which 6.3 does NOT accelerate -- it is # a Python class deriving from whichever `InterfaceBase' won. Recorded as the # constant it is, so a release that moves it to C is visible here. check_eq $P/interface-metaclass "InterfaceClass" "$(pv interface_metaclass)" check_eq $P/interface-metaclass-module "zope.interface.interface" "$(pv interface_metaclass_module)" # ============================================================================ # THE DECLARATION -- everything from here down is implementation-independent. # ============================================================================ check_eq $P/iface-name "IShape" "$(pv iface_name)" check_eq $P/iface-module "__main__" "$(pv iface_module)" check_eq $P/iface-doc "A shape the probe declares." "$(pv iface_doc)" # `names()' is the interface's OWN names; `names(all=True)' walks the bases. The # pair is what says inheritance reached the declaration and not only the class. check_eq $P/iface-names "area,colour,scale,sides" "$(pv iface_names)" check_eq $P/iface-names-all "area,colour,depth,scale,sides,volume" "$(pv iface_names_all)" # An `Attribute' and a `def' in an interface body become different objects, and # only the second carries a signature. Stated, because `verify-wrong-sig' below # is only meaningful if `scale' really is a Method. check_eq $P/iface-attr-type "Attribute" "$(pv iface_attr_is_attribute)" check_eq $P/iface-method-type "Method" "$(pv iface_method_is_method)" check_eq $P/iface-method-sig "factor" "$(pv iface_method_sig)" check_eq $P/iface-contains-own "yes" "$(pv iface_contains_sides)" check_eq $P/iface-contains-derived "no" "$(pv iface_contains_volume)" # ---------------------------------------------------------- the inheritance -- # `extends' is STRICT and `isOrExtends' is not -- the whole of the difference # between them, and the pair a caller gets wrong. Measured in both directions, # so an implementation that answered True to everything cannot pass. check_eq $P/extends-base "yes" "$(pv extends_shape)" check_eq $P/extends-self "no" "$(pv extends_self)" check_eq $P/is-or-extends-self "yes" "$(pv isOrExtends_self)" check_eq $P/is-or-extends-base "yes" "$(pv isOrExtends_shape)" check_eq $P/extends-not-upward "no" "$(pv shape_extends_solid)" check_eq $P/iro "ISolid,IShape,Interface" "$(pv iro)" check_eq $P/bases "IShape" "$(pv bases)" # ---------------------------------------------------------- the conformance -- # `providedBy' asks about the OBJECT, `implementedBy' about the CLASS, and the # third case is the one that separates them: the class does not PROVIDE the # interface its instances do. check_eq $P/provided-by-instance "yes" "$(pv providedBy_instance)" check_eq $P/implemented-by-class "yes" "$(pv implementedBy_class)" check_eq $P/provided-by-class "no" "$(pv providedBy_class)" check_eq $P/provided-by-lists "IShape" "$(pv providedBy_decl_lists)" check_eq $P/implemented-by-lists "IShape" "$(pv implementedBy_decl_lists)" # The DERIVED interface is not provided by a class declaring only the base. check_eq $P/not-provided-derived "no" "$(pv provided_solid)" # ------------------------------------------------------- verifyObject/Class -- # These RAISE rather than return false, which is why every case here reads an # exception NAME. A `verify' that returned a falsy value instead would show up # as `false' rather than passing. check_eq $P/verify-object-conforming "ok" "$(pv verifyObject_conforming)" check_eq $P/verify-class-conforming "ok" "$(pv verifyClass_conforming)" # A declared attribute the implementation never sets. check_eq $P/verify-missing-attribute "BrokenImplementation" "$(pv verify_missing_attr)" # A declared method whose signature does not match. check_eq $P/verify-wrong-signature "BrokenMethodImplementation" "$(pv verify_wrong_sig)" # A class that never declared the interface at all -- a THIRD failure, and the # one that says `verifyObject' checks the declaration before the shape. check_eq $P/verify-undeclared "DoesNotImplement" "$(pv verify_undeclared)" # All three are `Invalid' subclasses, which is what lets a caller catch one # exception instead of three. check_eq $P/broken-implementation-is-invalid "yes" "$(pv verify_missing_attr_is_invalid)" check_eq $P/broken-method-is-invalid "yes" "$(pv verify_wrong_sig_is_invalid)" check_eq $P/does-not-implement-is-invalid "yes" "$(pv verify_undeclared_is_invalid)" # THE DIFFERENCE BETWEEN THE TWO VERIFIERS, measured rather than assumed: # `verifyClass' has no instance, so it cannot see a missing instance attribute # and PASSES the class `verifyObject' rejects. It still catches the signature. check_eq $P/verify-class-misses-attribute "ok" "$(pv verifyClass_missing_attr)" check_eq $P/verify-class-catches-signature "BrokenMethodImplementation" "$(pv verifyClass_wrong_sig)" # ------------------------------------------- declaration on the INSTANCE ---- check_eq $P/marked-before "no" "$(pv marked_before)" check_eq $P/marked-after "yes" "$(pv marked_after)" check_eq $P/marked-keeps-class-declaration "yes" "$(pv marked_keeps_class_decl)" # The marker did NOT reach the class, and so did not reach a sibling instance. # Two cases, because an implementation that wrote through to the class would # pass the first of them by accident. check_eq $P/marked-not-on-class "no" "$(pv marked_not_on_class)" check_eq $P/marked-not-on-sibling "no" "$(pv marked_not_on_sibling)" check_eq $P/directly-provided "IMarked" "$(pv directly_provided)" # `directlyProvides' REPLACES where `alsoProvides' adds -- the distinction a # caller gets wrong, and three cases because the replacement has to leave the # CLASS declaration alone while dropping the instance one. check_eq $P/directly-provides-replaces "IOther" "$(pv directly_provides_replaces)" check_eq $P/directly-provides-drops-marker "no" "$(pv directly_provides_drops_marker)" check_eq $P/directly-provides-keeps-class "yes" "$(pv directly_provides_keeps_class)" # ------------------------------------------------------------ adapter hooks -- # Adaptation BEFORE the hook is registered, so the case below cannot pass by # some pre-existing hook answering. `IJSON(sq, None)' is the two-argument form # that returns the default rather than raising. check_eq $P/adapt-before-hook "None" "$(pv adapt_before_hook)" check_eq $P/adapter-hooks-len "1" "$(pv adapter_hooks_len)" # The hook ran, and the object it returned is the adapter -- asserted by TYPE # and then by a value only that adapter could produce, so a hook that returned # the wrong object cannot pass by being truthy. check_eq $P/adapt-via-hook "JSONForSquare" "$(pv adapt_via_hook)" check_eq $P/adapt-via-hook-value "square:4" "$(pv adapt_via_hook_value)" # A miss goes to the default with the two-argument form and RAISES without it. check_eq $P/adapt-miss-default "None" "$(pv adapt_miss)" check_eq $P/adapt-miss-raises "TypeError" "$(pv adapt_miss_raises)" # The hook was removed again, so this suite leaves no global state behind for # whatever runs next in the same interpreter. Nothing does today; it is asserted # so that it stays true. check_eq $P/adapter-hooks-len-after "0" "$(pv adapter_hooks_len_after)" # --------------------------------------------------------- adapter registry -- # `AdapterRegistry' is the other half of `zope.interface' -- the lookup the # hooks above are usually wired to. Three lookups that must find the # registration and one that must not. check_eq $P/registry-lookup "JSONForSquare" "$(pv registry_lookup)" check_eq $P/registry-lookup1 "JSONForSquare" "$(pv registry_lookup1)" check_eq $P/registry-query-adapter "JSONForSquare" "$(pv registry_queryAdapter)" check_eq $P/registry-miss "None" "$(pv registry_miss)" # ================================================================ PURE_PYTHON # THE SAME PROBE AGAIN, with the pure-Python implementation demanded by policy # rather than reached by failure. See the header: on this arm both runs take the # same path, and this run is what says the two are the same path. bst_probe $P/probe-pure-run "$W/pure" 600 env PURE_PYTHON=1 python3 "$PROBE" POUT="$W/pure" ppv() { grep "^$1=" "$POUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$POUT" ]; then bst_ok $P/probe-pure-output else bst_notok $P/probe-pure-output "the PURE_PYTHON=1 probe emitted nothing; every pure-* CASE below is unscorable" fi check_eq $P/probe-pure-complete "yes" "$(ppv probe_done)" check_eq $P/probe-pure-emitted-count "82" "$(ppv emitted_count)" # The three keys that are ABOUT the environment, and are therefore the three # that are ALLOWED to differ. Asserted individually so that the whole-transcript # comparison below can be a plain equality with nothing to explain. check_eq $P/pure-env "1" "$(ppv env_pure_python)" check_eq $P/pure-should-attempt "no" "$(ppv copt_should_attempt)" check_eq $P/pure-ignored "yes" "$(ppv copt_ignored)" # ...and the accelerator is still absent, which is what says the two runs agree # because they reach the same code and not because PURE_PYTHON hid a difference. check_eq $P/pure-copt-import "failed" "$(ppv copt_import)" check_eq $P/pure-impl-selected "python" "$(ppv impl_selected)" # THE COMPARISON IS THE CASE. Everything except those three keys must be # identical, key for key and in order. A single behavioural difference between # the fallback-selected and policy-selected paths lands here as one red naming # both sides, rather than as a scatter of cases nobody can line up. # # It is also what would catch a build that reinstated the extension for one of # the two runs and not the other: the default run would take the C path, the # transcripts would stop agreeing, and this one red would say so with both sides # quoted. _drop='^\(env_pure_python\|copt_should_attempt\|copt_ignored\)=' check_eq $P/pure-python-identical \ "$(grep -v "$_drop" "$OUT" 2>/dev/null)" \ "$(grep -v "$_drop" "$POUT" 2>/dev/null)" rm -rf "$W"