. "$BST_LIB" # BST-TIMEOUT: 2400 # SIX python3 starts, and they are the whole cost. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps the probe's output. # # # # They are a bare `import', a second whose STDERR is the # # assertion (`import-quiet'), and the probe -- which takes # # ONE start for itself and spawns FOUR children, because # # this package's public entry points are implemented as # # subprocesses (see THE SUBPROCESS MODEL below). # # # # THE MEASUREMENTS ARE NOT THE COST. Measured on the build # # host against this build's own payload under CPython # # 3.12.3: 0.35 s wall for the whole probe including all # # four children. Every case here is a dictionary lookup or # # a regex over a short string. # # # # 2400 is 400 s per start, the same budget # # 99-python3-mpmath sets for its three, and roughly six # # times the ~60 s a python3 start has measured at on this # # arm. The margin is for the starts, not for the work. # # # # 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-py-cpuinfo 9.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-py-cpuinfo_9.0.0.bb, `inherit ptest pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image, plus this layer's # two carried patches. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # CPU identification in pure Python. It reads /proc/cpuinfo, sysctl, lscpu, the # Windows registry and the CPUID instruction where each exists, merges whatever # answered into one dictionary, and reports it -- from the API # (`get_cpu_info()', `get_cpu_info_json()') and from a console script # (/usr/bin/cpuinfo). # # ============================================================================ # THIS PORT NEEDED TWO PATCHES, AND WITHOUT EITHER ONE THE PACKAGE IS DEAD. # ============================================================================ # # BOTH failures are at `import cpuinfo', and neither is visible from a build: # `bitbake python3-py-cpuinfo' succeeds unpatched -- 1622 tasks, all of them -- # because a pure-Python recipe's build is a byte-compile and an install, and # neither imports the module. So no build gate anywhere would have caught this, # and `import-works' below is the case that matters most in this file. # # 1. THE ARCH CHECK RUNS AT IMPORT TIME. The bottom of cpuinfo/cpuinfo.py is # # if __name__ == '__main__': main() # else: g_trace = ...; _check_arch() # # so every ordinary importer takes the `else' arm and runs _check_arch(), # which RAISES for any arch _parse_arch() does not recognise. Upstream has # cases for X86, ARM, PPC, SPARC, S390X, MIPS and RISCV -- and none for # wasm32. 0001 adds wasm32 -> (WASM_32, 32). # # 2. ctypes IS IMPORTED AT MODULE SCOPE (line 34), so the import also needs # the interpreter's built-in _ctypes. # # Measured on an x86-64 control with platform.machine() forced to wasm32 -- # unpatched, the arch Exception; with 0001, imports and reports 9.0.0. # # ============================================================================ # THE VALUES ARE THE MACHINE'S, SO THIS SUITE ASSERTS SHAPE, NOT NUMBERS. # ============================================================================ # # What CPU this guest reports is not something this port gets to fix, and it is # not stable enough to pin even if it were. Two examples from this image alone: # `nproc' says 2 while sysconf(_SC_NPROCESSORS_ONLN) says 1 (55-pbzip2 measured # both), so `count' has no single right answer; and `hz_actual' is a live clock # reading that differs between two runs a second apart, which is why the # round-trip cases below compare a STABLE SUBSET rather than whole dictionaries. # # So what is asserted is TYPES, PRESENCE, and INTERNAL CONSISTENCY -- `bits' # against sys.maxsize, `arch' against platform.machine() -- plus, for the fields # that depend on a probe this machine lacks, whether the library DECLINED or # FABRICATED. That last group is the point of the ticket: a wasm32 guest has no # CPUID and no lscpu, and a library that answered anyway would be reporting # somebody else's CPU. # # ============================================================================ # THE SUBPROCESS MODEL, WHICH IS WHY THIS SUITE COSTS SIX STARTS. # ============================================================================ # # get_cpu_info_json() does NOT compute in-process. It runs # [sys.executable, cpuinfo.py, '--json'] as a CHILD and parses its stdout, # returning "{}" if the child exits non-zero; get_cpu_info() is json.loads() of # that. So the headline call is also a test of whether this guest can spawn an # interpreter at all, and `get-info-raised'/`info-nonempty' are where a guest # that cannot would show up. # # It is also why the /proc/cpuinfo error path is asked IN-PROCESS, against # _get_cpu_info_internal(): a DataSource override in the probe would not reach # a child. Measured on the control -- the child returned all twenty fields # regardless of what the parent had patched. # # ============================================================================ # WHAT WAS PREDICTED HERE, AND WHAT THE FIRST ON-TARGET RUN MEASURED. # ============================================================================ # # NOTHING BELOW IS PREDICTED ANY MORE. This port's first on-target run was the # batch-2436 verify (2026-09-09), which scored 110 cases on this arm; the three # groups that were open when the suite was written are now measured, and are # recorded here because two of them did not land where they were predicted to. # # * whether the guest can spawn a child interpreter at all (`get-info-raised', # `info-nonempty', the `cli-*' group) -- IT CAN: all green. The public entry # points are subprocesses and the failure would have been quiet, so this was # the group most likely to take the port down. It did not. # * what /proc/cpuinfo this runtime synthesises, which decides whether the # `declined-*' group is `' or carries a value -- PREDICTED WRONG, # and in the platform's favour. The wasm32 kernel answers `vendor_id' and # `model name' itself, so two of the eight carry this machine's own # identity. They are now asserted by value; the long note at that group has # the detail. # * `maxsize-bits'/`bits-agree-maxsize' -- 32 and `yes' as asserted. The # CONTROL reported 64 and `no', because it is a 64-bit host wearing a forced # wasm32 arch string; the suite asserted the target's form over the # control's, and the target produced it. P=python3-py-cpuinfo bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/cpuinfo DI=$SITE/py_cpuinfo-9.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-py-cpuinfo.list PROBE=$BST_DATA/python3-py-cpuinfo-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-py-cpuinfo.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ what shipped --- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/cpuinfo.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/cpuinfo-py test -s "$PKGDIR/cpuinfo.py" # __main__.py IS WHAT `python3 -m cpuinfo' RUNS, and the probe's four children go # through it, so its presence is a precondition of half this file. check $P/main-py test -s "$PKGDIR/__main__.py" check $P/pycache test -s "$PKGDIR/__pycache__/cpuinfo.cpython-312.pyc" # THE console_script THE RECIPE INSTALLS. setuptools3 turns the entry_points # declaration into this file; it is the CLI the ticket asks about. check $P/cli-installed test -x /usr/bin/cpuinfo # THREE MODULES AND NOTHING ELSE. py-cpuinfo is one package of three files, so a # release that grew a subpackage is visible here rather than somewhere below. check_eq $P/module-file-count "3" \ "$(ls "$PKGDIR"/*.py 2>/dev/null | wc -l | tr -d ' ')" # UPSTREAM'S TESTS ARE **NOT** INSTALLED, and this differs from the other # 99-python3-* ports on purpose: py-cpuinfo's tests/ tree sits at the SDIST root # and setup.py's packages=['cpuinfo'] does not carry it, so nothing lands in # site-packages. (mpmath ships its tests INSIDE the package and does.) check $P/no-tests-dir test ! -d "$PKGDIR/tests" check $P/no-test-suite test ! -e "$SITE/test_suite.py" # NO PEP 561 MARKER, which is what lets `pkg-only-python' below allow nothing # but .py, .pyc and dist-info. check $P/no-py-typed test ! -e "$PKGDIR/py.typed" 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 SHIPPED FILE IS PYTHON, its bytecode, its dist-info or the console # script. A compiled extension could not be imported on this arm at all # (BRINTOS-1476), so this is the packaging-side statement of what # `impure-modules' says from inside the interpreter. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; /usr/bin/cpuinfo) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE dist-info DIRECTORY IS py_cpuinfo-, NOT py-cpuinfo-. The distribution name # is `py-cpuinfo' and the import name is `cpuinfo'; the wheel normalises the # first to an underscore. Pinned because getting it wrong is how a packaging # case passes by finding nothing. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: py-cpuinfo" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 9.0.0" cat "$DI/METADATA" check_out $P/dist-info-summary "Get CPU info with pure Python" cat "$DI/METADATA" check_out $P/dist-info-top-level "cpuinfo" cat "$DI/top_level.txt" # THE ENTRY POINT DECLARATION behind /usr/bin/cpuinfo. check_out $P/dist-info-entry-point "cpuinfo = cpuinfo:main" cat "$DI/entry_points.txt" # THE WHEEL WAS BUILT BY setuptools' bdist_wheel -- what `setuptools3' reduces to # here -- and it is a PURE-PYTHON wheel, the packaging-side form of the claim # `impure-modules' makes from inside the interpreter. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # ------------------------------------------------------------- the package --- sed -n '/^Package: python3-py-cpuinfo$/,/^$/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: 9.0.0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" # THE Depends LINE BY NAME. Every one is a stdlib split of this image's own # interpreter, so nothing about this port waited on another. check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-json "python3-json" cat "$W/dep" check_out $P/pkg-depends-multiprocessing "python3-multiprocessing" cat "$W/dep" check_out $P/pkg-depends-pickle "python3-pickle" cat "$W/dep" # python3-ctypes IS DECLARED, and the declaration is left exactly as # meta-python wrote it. _ctypes is built into the static interpreter rather than # shipped as an extension, so nothing matches under lib-dynload. check_out $P/pkg-depends-ctypes "python3-ctypes" cat "$W/dep" check_eq $P/no-ctypes-extension "" \ "$(find /usr/lib/python3*/lib-dynload -name '_ctypes*' 2>/dev/null)" # NO -ptest PACKAGE, AND IT IS NEVER BUILT RATHER THAN MERELY WITHHELD. The # recipe DOES `inherit ptest', but `ptest' is not in this distro's # DISTRO_FEATURES ("ipv4 x11 wayland") and ptest.bbclass gates the package on # it. Measured at build time: the runtime pkgdata lists -dbg, -dev, -doc, # -locale, -src and -staticdev and no -ptest, and three .debs were produced. # This case is the on-image half of that, and the statement that nothing here # runs upstream's tests. check_eq $P/pkg-no-ptest "" \ "$(sed -n '/^Package: python3-py-cpuinfo-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------------- it imports --- # THE CASE THE PATCH BUYS. Unpatched this is where the port dies, and every # case below it is unreachable. check $P/import python3 -c 'import cpuinfo' # ...AND SAYS NOTHING WHILE DOING IT. Anything printed at import would land on # every caller's stderr at every start. check_eq $P/import-quiet "" \ "$(python3 -c 'import cpuinfo' 2>&1)" if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 1800 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } 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 check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "70" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- check_eq $P/import-works "yes" "$(pv import_works)" # THE MODULE THE PROBE MEASURED IS THE PACKAGED ONE. The probe sits in # ${libdir}/brintos-tests/data and the interpreter puts a script's own directory # first on sys.path, so this would catch a probe that shadowed its subject. check_eq $P/module-file "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "9.0.0" "$(pv version_attr)" check_eq $P/version-tuple "9,0,0" "$(pv version_tuple)" # PURE PYTHON FROM INSIDE THE INTERPRETER: not one imported cpuinfo module has a # file that is anything but a .py. check_eq $P/impure-modules "" "$(pv impure_modules)" # --------------------------------------------------------------- the arch --- # THE FACT 0001 ADDED, read back from the library's own parser. `wasm32' is not # an assumption made here: 90-gnu-config pins `uname -m' to it and # 99-python3-packaging pins platform_machine to it, both independently. check_eq $P/platform-machine "wasm32" "$(pv platform_machine)" check_eq $P/datasource-arch-raw "wasm32" "$(pv datasource_arch_raw)" check_eq $P/parsed-arch "WASM_32" "$(pv parsed_arch)" check_eq $P/parsed-bits "32" "$(pv parsed_bits)" # _check_arch() IS THE IMPORT-TIME GATE. Asserting it separately says the import # survived for the right reason rather than by luck. check_eq $P/check-arch-quiet ": None" "$(pv check_arch_raises)" # THE INTERPRETER'S OWN WIDTH. PREDICTED, NOT MEASURED: 32 on a 32-bit build. # The control says 64 and `no' below, because the control is a 64-bit host # wearing a forced wasm32 arch string -- that is an artifact of the control, not # a claim about this arm. This is the case that catches a width assumption. check_eq $P/maxsize-bits "32" "$(pv maxsize_bits)" check_eq $P/bits-agree-maxsize "yes" "$(pv bits_agree_maxsize)" # ------------------------------------------------------------ CPUID declines --- # NO CPUID ON A wasm32 GUEST, and upstream's own guard is what declines: it # returns {} for any arch outside X86 before it constructs anything. So the # right answer is an EMPTY DICT reached deliberately -- not an exception, and # not a register read that invented a value. check_eq $P/cpuid-empty "yes" "$(pv cpuid_is_empty)" check_eq $P/cpuid-result "{}" "$(pv cpuid_result)" # -------------------------------------------------------- get_cpu_info() --- # THE HEADLINE CALL, AND A TEST OF THE SPAWN. PREDICTED: it starts a child # interpreter, so a guest that cannot spawn one shows up in these two cases # rather than anywhere below. check_eq $P/get-info-raised "" "$(pv get_info_raised)" check_eq $P/info-type "dict" "$(pv info_type)" check_eq $P/info-nonempty "yes" "$(pv info_nonempty)" # THE FIELDS THE DOCUMENTATION GUARANTEES ARE ALWAYS PRESENT, each asserted for # PRESENCE and for TYPE. Their VALUES are the machine's and are not pinned. for f in python_version cpuinfo_version cpuinfo_version_string arch bits count arch_string_raw; do check_eq $P/field-$(echo "$f" | tr '_' '-')-present "yes" "$(pv "field_${f}_present")" done check_eq $P/field-python-version-type "str" "$(pv field_python_version_type)" check_eq $P/field-cpuinfo-version-type "list" "$(pv field_cpuinfo_version_type)" check_eq $P/field-cpuinfo-version-string-type "str" "$(pv field_cpuinfo_version_string_type)" check_eq $P/field-arch-type "str" "$(pv field_arch_type)" check_eq $P/field-bits-type "int" "$(pv field_bits_type)" check_eq $P/field-count-type "int" "$(pv field_count_type)" check_eq $P/field-arch-string-raw-type "str" "$(pv field_arch_string_raw_type)" # THE REPORTED arch/bits AGREE WITH THE LIBRARY'S OWN PARSE of this machine -- # the assertion that the dictionary is about the CPU the module thinks it is on. check_eq $P/info-arch "WASM_32" "$(pv info_arch)" check_eq $P/info-bits "32" "$(pv info_bits)" check_eq $P/info-arch-string-raw "wasm32" "$(pv info_arch_string_raw)" check_eq $P/info-arch-agrees "yes" "$(pv info_arch_agrees)" check_eq $P/info-bits-agrees "yes" "$(pv info_bits_agrees)" # THE VERSION THE RECIPE PINS, read out of the dictionary rather than the module. check_eq $P/info-version-string "9.0.0" "$(pv info_version_string)" # ------------------------------------------------ declined, not fabricated --- # THE POINT OF THE TICKET, and the group where the phase-A PREDICTION was wrong # in the platform's favour -- recorded here rather than quietly rewritten. # # What was predicted: that every field below depends on a probe this machine # lacks -- CPUID, lscpu, sysctl, dmesg, the Windows registry -- and so must come # back ABSENT or EMPTY, any value being the library reporting somebody else's # CPU. The CONTROL fills all of them from the build host's own Ryzen, which is # the failure mode this group exists to catch. # # What the batch-2436 verify MEASURED: six of the eight are absent or empty as # predicted, and two are not, because this kernel answers them ITSELF. # arch/wasm32/kernel/setup.c:wasm32_show_cpuinfo() synthesises /proc/cpuinfo # from five constants -- `processor', `vendor_id: brintos', # `model name: wasm32-hwjs virtual CPU', an EMPTY `flags' and `bogomips' -- and # py-cpuinfo reads that file, mapping `model name' to brand_raw and `vendor_id' # to vendor_id_raw. So those two carry THIS machine's own truthful identity, not # another CPU's, and the kernel's own comment says as much: "Nothing is invented # and nothing is faked". An empty `flags' is why declined-flags stays green. # # So the two are asserted BY VALUE below rather than dropped. That keeps what # the ticket is actually for -- a fabricated x86 brand is still red, and now red # twice over, here and at `fabricated-vendor' -- while no longer calling the # platform's honest self-description a fault. for f in hardware_raw model family stepping flags l2_cache_size; do _v="$(pv "probe_${f}")" case "$_v" in ''|'') bst_ok "$P/declined-$(echo "$f" | tr '_' '-')" ;; '') bst_notok "$P/declined-$(echo "$f" | tr '_' '-')" \ "the probe emitted no probe_${f} line at all -- unscorable, not a pass" ;; *) bst_notok "$P/declined-$(echo "$f" | tr '_' '-')" \ "this machine has no probe for ${f}, so a value here is somebody else's CPU: $_v" ;; esac done # THE TWO THE KERNEL ANSWERS, pinned to the exact constants it emits. Equality # rather than "non-empty is fine": the whole value of this pair is that it goes # red the moment brand_raw stops being this machine and starts being the host's. check_eq $P/declined-brand-raw "'wasm32-hwjs virtual CPU'" "$(pv probe_brand_raw)" check_eq $P/declined-vendor-id-raw "'brintos'" "$(pv probe_vendor_id_raw)" # AN x86 VENDOR STRING WOULD BE SOMEBODY ELSE'S CPU, named explicitly because a # fabricated value is far likelier to be a plausible x86 brand than a random one. check_eq $P/fabricated-vendor "" "$(pv fabricated_vendor)" # ------------------------------------------------------ the JSON round-trip --- check_eq $P/json-is-str "str" "$(pv json_is_str)" check_eq $P/json-parses "dict" "$(pv json_parses)" # THE STABLE SUBSET, NOT THE WHOLE DICTIONARY. Each entry point spawns its own # child and a child MEASURES THE CLOCK: measured on the control, the two # dictionaries agreed on every key and differed on hz_actual and hz_advertised, # [4104226000, 0] against [4104247000, 0]. Whole-dict equality would be # asserting the CPU frequency did not change, which says nothing about the port. check_eq $P/json-matches-api "yes" "$(pv json_matches_api)" # --------------------------------------------------------------- the CLI --- # THE console_script RUN WITH --json, exactly as the ticket names it: asserted to # exit 0, to parse, and to AGREE with the API on the stable subset. check_eq $P/cli-json-rc "0" "$(pv cli_json_rc)" check_eq $P/cli-json-parses "dict" "$(pv cli_json_parses)" check_eq $P/cli-json-arch "WASM_32" "$(pv cli_json_arch)" check_eq $P/cli-json-bits "32" "$(pv cli_json_bits)" check_eq $P/cli-json-matches-api "yes" "$(pv cli_json_matches_api)" check_eq $P/cli-version-rc "0" "$(pv cli_version_rc)" check_eq $P/cli-version-out "9.0.0" "$(pv cli_version_out)" # REPORTED, DELIBERATELY NOT SCORED: whole-dict equality between the two spawns, # which is `no' whenever the clock moved between them. It is echoed so a reader # can see which of the two happened without a red that means nothing. echo "python3-py-cpuinfo: cli_json_full_match=$(pv cli_json_full_match) (clock-dependent, not scored)" # ----------------------------------------------------------- the error paths --- # NO HELPER BINARY RESOLVES. Emptying PATH defeats every probe that shells out to # lscpu, sysctl, dmesg or `cat' at once -- the closest a running image gets to # the machine this library was not written for. It must still return a # DICTIONARY and exit 0. Measured on the control as exactly the seven base # fields with brand_raw ABSENT rather than guessed, which is the behaviour the # ticket asks for. check_eq $P/nopath-rc "0" "$(pv nopath_rc)" check_eq $P/nopath-parses "yes" "$(pv nopath_parses)" check_eq $P/nopath-type "dict" "$(pv nopath_type)" # THE IDENTITY FIELDS SURVIVE IT, because arch and bits are computed from # platform.machine() rather than read from any helper. check_eq $P/nopath-has-arch "yes" "$(pv nopath_has_arch)" check_eq $P/nopath-brand "''" "$(pv nopath_brand)" # /proc/cpuinfo UNREADABLE, asked IN-PROCESS against _get_cpu_info_internal(). # It has to be in-process: the public entry points spawn a child, and a # DataSource override in the probe would not reach one -- measured on the # control, where the child returned all twenty fields regardless. check_eq $P/noproc-raised "" "$(pv noproc_raised)" check_eq $P/noproc-type "dict" "$(pv noproc_type)" check_eq $P/noproc-has-arch "yes" "$(pv noproc_has_arch)" check_eq $P/noproc-has-bits "yes" "$(pv noproc_has_bits)" rm -rf "$W" exit 0