. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps the output of one of those two. # # # # The two are: a bare `import spidev' at the shell -- the # # platform gate, which cannot be folded into the probe # # because the point of it is what a CALLER on this image # # gets -- and the probe, which takes ONE start for a hundred # # and twenty measurements. # # # # There is no third. The distribution declares no # # console_scripts and installs no executable, so there is # # nothing to reach through a shebang; `no-bin-script' # # asserts that absence rather than leaving it unstated. # # # # The work inside the probe is attribute lookups, a handful # # of open(2)s against a path that does not exist, and # # ioctl(2)s on fd -1 -- microseconds beside an interpreter # # start on this arm, which is tens of seconds before the # # first line of the program runs. Nothing here sleeps and # # nothing retries. The largest allocation is a 4097-element # # Python list, built twice. # # # # 1200 is 600s per start, matching 99-python3-crc32c's # # suite budget and ten times the ~60s a python3 start has # # measured at on this arm. # # # # A suite-timeout red is a batch-wide cost, so if the two # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # 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-spidev 3.6 (meta-openembedded/meta-python # recipes-devtools/python/python3-spidev_3.6.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # The Python bindings for the Linux `spidev' character device. A `SpiDev' object # wraps open(2) on /dev/spidev.; its nine attributes are the # SPI_IOC_{RD,WR}_* ioctls for clock mode, bit order, word width and maximum # clock; and its transfer methods are read(2), write(2) and the full-duplex # SPI_IOC_MESSAGE(n). # # IT IS A C EXTENSION AND NOTHING ELSE. The sdist is one file -- # spidev_module.c, 37 KB -- and setup.py declares # `ext_modules=[Extension("spidev", ["spidev_module.c"])]'. There is no .py # anywhere in the distribution. So the port either loads a side module or it # delivers nothing, and there is no pure-Python half to fall back to: the same # shape as 99-python3-crc32c, and the opposite of python3-protobuf and # python3-cbor2, each of which documents a pure-Python codec. # # ============================================================================ # THE PLATFORM GATE, AND WHY IT IS EXPECTED TO FAIL. # ============================================================================ # # THIS IS A PREDICTION MADE AT RECIPE SCOPE AND NOT A MEASUREMENT. It is written # out because the suite's shape depends on it; the on-target answer belongs in # the test plan and the design doc, and where the two disagree the MEASUREMENT # is what is recorded there. # # The extension builds and ships. Measured at recipe scope: `bitbake # python3-spidev' attempted 1622 tasks, all succeeded, zero ERROR lines, and the # .deb carries /usr/lib/python3.12/site-packages/spidev.cpython-312.so, 105671 # bytes, a genuine wasm side module opening with a `dylink.0' section -- and one # carrying NO NEEDED entries, so it depends on no other shared library. # # It is not expected to LOAD. Of its 95 wasm imports, 27 are CPython API # functions in `env' -- PyType_Ready, PyModule_Create2, PyUnicode_FromString, # PyModule_GetDict, PyDict_SetItemString, PyModule_AddObject and the rest -- and # /usr/bin/python3.12 on this image is one static binary that exports 35 symbols, # the whole hwjs/wasm32 runtime ABI, of which NOT ONE is `Py*'. Py_ENABLE_SHARED # is 0, so there is no libpython3.12.so to bind against instead. That is # BRINTOS-2046, first measured on python3-bitarray (BRINTOS-2032) and since # confirmed on python3-psutil, python3-schedutils, python3-crc32c, python3-lz4 # and python3-xxhash. The expected failure: # # HWJS-5 dlopen service failed (policy -> ELIBBAD -80): hwjs-dlopen: # unresolved import env.PyType_Ready (function) in # "/usr/lib/python3.12/site-packages/spidev.cpython-312.so" # # `env.PyType_Ready' is named because it is the FIRST CPython import in the # module's import section, and it is also the first call PyInit_spidev makes. It # is a prediction about which name the loader prints, not about whether it # prints one. # # SO THE SUITE IS BUILT TO BE INFORMATIVE EITHER WAY, which is 99-python3-crc32c's # shape adopted for the same reason. The packaging half runs with no interpreter # at all and measures this port whatever the loader does. The behavioural half is # gated on one case: if the import fails, `import' and `import-ok' are the two # reds that carry the verdict and every behavioural case is SKIPPED with the load # error attached. A hundred reds all naming one platform fact would bury it # rather than report it. If it succeeds, every behavioural case runs and this # port is complete with no attribution needed at all. # # ============================================================================ # THE ABSENCE OF THE CONTROLLER IS THE SECOND AXIS, AND IT IS AN ASSERTION # RATHER THAN AN OBSTACLE. # ============================================================================ # # There is no SPI controller behind this machine and no /dev/spidev* node. # Everything spidev does in that state is documented and typed, so the whole of # the behavioural half is assertable without hardware: # # THE OPEN FAILS ENOENT, AS FileNotFoundError. SpiDev_open wraps its failed # open(2) with PyErr_SetFromErrno(PyExc_IOError). PyExc_IOError IS # PyExc_OSError in Python 3, and OSError.__new__ maps ENOENT to the # FileNotFoundError subclass -- which is why the type asserted below is that # subclass and not a bare OSError. Both entry points reach the same function # and both are asserted. # # VALIDATION HAPPENS BEFORE ANY ioctl. mode > 3, bits_per_word outside 8..32, # a non-integer, an empty transfer list and a list longer than SPIDEV_MAXPATH # are each refused in the setter or the argument parser. A TypeError from one # of those cases is therefore also evidence that no ioctl was attempted. # # A VALID VALUE REACHES ioctl(-1, ...) AND FAILS EBADF. That is as close as # this machine can come to exercising the SPI_IOC_* request encodings: the # request is formed and handed to the kernel, which rejects the DESCRIPTOR # before it looks at the request. Nine attribute setters and six transfer # methods are asserted to fail that way and no other. # # THE REQUEST ENCODING ITSELF WAS SETTLED AT RECIPE SCOPE, ON THE ARTIFACT, # because nothing at runtime can reach it. `struct spi_ioc_transfer' is 32 bytes # with identical field offsets on 32- and 64-bit userland -- the uapi header says # so in a comment and it was confirmed by compiling against this image's own # linux-brintos-headers both ways -- so SPI_IOC_MESSAGE(1) is 0x40206b00 on # wasm32 exactly as it is on x86-64. All seven SPI_IOC_* constants were then # found as i32.const immediates in the shipped wasm module at those values. That # measurement is in the design doc; it cannot be a case here. # # ============================================================================ # WHERE THE EXPECTED VALUES COME FROM. # ============================================================================ # # EVERY behavioural expectation below is the OUTPUT of an x86-64 control at the # same version -- spidev 3.6 built from the same sdist against CPython 3.12, # with this probe run against it -- rather than a reading of the source. Three # control results are worth naming because they are not what the source reads # like: # # * `mode = 256' IS ACCEPTED. The value is read into a uint8_t BEFORE the # `> 3' test, so 256 arrives as 0 and passes validation. Measured OSError # EBADF -- it reached the ioctl -- not the TypeError the range check # suggests. Upstream 3.6's behaviour, recorded not repaired. # # * THE CONSTRUCTOR'S KEYWORDS DO NOT WORK, EITHER SPELLING. SpiDev_init # declares {"bus", "client"} and then hands its own args and kwds to # SpiDev_open, which declares {"bus", "device"}. So `SpiDev(bus=0, client=0)' # dies in open() on an unexpected `client', and `SpiDev(bus=0, device=0)' # dies in __init__ on an unexpected `device'. Both measured TypeError. Only # the positional form reaches the device, and `open()' -- whose keywords # really are {"bus","device"} -- is measured FileNotFoundError beside them, # which is what makes the pair a statement about the constructor rather than # about keyword arguments in general. # # * `The mode attribute must be an integerbetween 0 and 3.' -- upstream # concatenates two string literals with no separator. Asserted verbatim, # typo included: an expectation that quietly corrected it would go red on # the day the port is working. # # The control could NOT measure two of them: `dist-version' and `version-agrees' # read the dist-info directory, which a bare `gcc -shared' control does not # produce. Both come from the .deb this port ships and are read off it directly. P=python3-spidev # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages SO=$SITE/spidev.cpython-312.so DI=$SITE/spidev-3.6.dist-info PKGLIST=/var/lib/dpkg/info/python3-spidev.list PROBE=$BST_DATA/python3-spidev-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-spidev.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the module misbehaves -- which # matters more than usual for this port, since a missing .so and an unloadable # one both end at the same ImportError. check $P/site-packages-dir test -d "$SITE" check $P/extension-present test -f "$SO" check $P/extension-nonempty test -s "$SO" # IT IS AN EXTENSION AND NOTHING ELSE. The sdist has no .py, so a `spidev.py' or # a `spidev/' package directory here would mean a release that grew a # pure-Python half -- which would change what this port is and wants noticing. check $P/no-py-module test ! -f "$SITE/spidev.py" check $P/no-py-package test ! -d "$SITE/spidev" # THE SONAME CARRIES NO PLATFORM TRIPLE. Upstream names an extension # `spidev.cpython-312-.so'; poky's python3 reports an empty EXT_SUFFIX # triple on this arm, so the file is plain `spidev.cpython-312.so'. The import # resolves through importlib.machinery.EXTENSION_SUFFIXES, so the two must agree # -- and if a future poky restored the triple, this case reds before the import # does and says why. check_eq $P/extension-name "spidev.cpython-312.so" \ "$(cd "$SITE" 2>/dev/null && echo spidev.cpython-312*.so)" # IT IS A WASM SIDE MODULE, asserted from its own first bytes rather than from # the build log. `\0asm' then the `dylink.0' custom section is what the hwjs # loader requires; a file lacking it would be exactly as unloadable as a missing # one, and would fail at the import with nothing to say why. check_out $P/extension-is-wasm "asm" dd if="$SO" bs=1 count=4 skip=1 2>/dev/null check_out $P/extension-is-dylink "dylink.0" dd if="$SO" bs=1 count=8 skip=11 2>/dev/null # NO EXECUTABLE. setup.py declares no console_scripts, so nothing lands in # /usr/bin. Asserted so that a future release which started shipping one is # noticed rather than acquiring an untested surface in silence. check $P/no-bin-script test ! -e /usr/bin/spidev # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: spidev" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 3.6" cat "$DI/METADATA" check_out $P/dist-info-top-level "spidev" cat "$DI/top_level.txt" check $P/dist-info-license test -s "$DI/LICENSE" check $P/dist-info-record test -s "$DI/RECORD" # NOT PURELIB, stated by the packaging itself. `Root-Is-Purelib: false' is the # wheel's own claim that it contains something architecture-specific -- the # opposite of what every pure-Python python3-* port here asserts, and the # one-line summary of why this port is different. check_out $P/dist-info-not-purelib "Root-Is-Purelib: false" cat "$DI/WHEEL" # THE WHEEL TAG IS WRONG, AND IT IS UPSTREAM SETUPTOOLS RATHER THAN THIS PORT. # bdist_wheel computes the tag from the BUILD HOST and the cross build never # tells it otherwise, so the wheel says `cp312-cp312-linux_x86_64' for a wasm32 # module. Nothing reads it at runtime -- the .deb is what installs the file and # dpkg has the right architecture -- so this is pinned rather than repaired, the # same call 99-python3-crc32c made for the identical line. check_out $P/dist-info-tag-is-host "Tag: cp312-cp312-linux_x86_64" cat "$DI/WHEEL" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # EXACTLY ONE SHARED OBJECT SHIPS. Everything else in the package is dist-info # metadata. A second .so would mean the build produced something this port did # not expect and did not measure. check_eq $P/pkg-one-so "1" "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null)" # Every payload path is under site-packages and is the .so or a dist-info file. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.so) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-spidev$/,/^$/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 DEPENDENCY SET IS THE PORT'S CLAIM THAT IT WAITS ON NOTHING. The recipe # carries no DEPENDS and RDEPENDS only python3-core, and the wasm shlibs pass # derived nothing further -- the extension's dylink.0 declares no NEEDED entry, # so there is no library dependency to derive. python3-core is therefore the # whole of it, and it is asserted by name: a control stanza that had lost it # would surface as an ImportError that looked like this port's fault. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-python3-core "python3-core" cat "$W/dep" # The package's architecture is this arm's, which is the half the wrong wheel tag # above might otherwise be read as contradicting. check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # ------------------------------------------------- the absence of the device -- # ASSERTED, not assumed. Every ENOENT the probe measures below is attributed to # this absence, and a machine that had grown a /dev/spidev0.0 would make those # expectations wrong while every one of them still "passed" for the wrong reason. check $P/no-spidev-node test ! -e /dev/spidev0.0 # ------------------------------------------------- the import, from a shell -- # THE PLATFORM GATE, and the first of two python3 starts. This is what a caller # on this image actually gets. See the header: it is EXPECTED to fail on # BRINTOS-2046, and the day it stops failing is the day this whole suite turns # green with no change to it. check $P/import python3 -c 'import spidev' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so a hung probe is reported as a probe overrun with the # last line it printed, rather than as a suite timeout that says nothing about # where it stopped. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the module works" -- `import-ok' below is what carries that. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' because # a key is emitted once and a duplicate would otherwise concatenate; `cut -f2-' # because a value may legitimately contain `='. 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 # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" # The probe found the copy this suite has been asserting against, and agrees with # it about the device node. Both are emitted BEFORE the import is attempted, so # they are scorable whichever way the gate goes. check_eq $P/probe-site-dir "$SITE" "$(pv site_dir)" check_eq $P/probe-so-found "yes" "$(pv so_found)" check_eq $P/probe-devnode-absent "yes" "$(pv devnode_absent)" # ------------------------------------------------------- the gate's verdict --- # ONE case for the platform fact, carrying the exception if there is one. _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # The extension did not load. SKIP -- not red -- every behavioural case, each # with the reason attached: they are all one fact, and a hundred reds would # bury it rather than report it. `import' and `import-ok' above are the two # reds that carry the verdict, and the design doc attributes them. _why="spidev.cpython-312.so did not load: $(pv import_error)" for _c in probe-count module-answered is-extension version-attr dist-version \ version-agrees has-SpiDev type-name module-publics \ attr-mode attr-cshigh attr-threewire attr-lsbfirst attr-loop \ attr-no-cs attr-bits-per-word attr-max-speed-hz attr-read0 \ meth-open meth-close meth-fileno meth-readbytes meth-writebytes \ meth-writebytes2 meth-xfer meth-xfer2 meth-xfer3 meth-enter \ meth-exit \ ctor-noargs unopened-usable fileno-unopened \ get-mode get-bits-per-word get-max-speed-hz get-cshigh \ get-threewire get-lsbfirst get-loop get-no-cs get-read0 \ set-mode-4 set-mode-str del-mode set-bits-7 set-bits-33 \ set-bits-str set-speed-str set-cshigh-str set-lsbfirst-str \ set-threewire-str set-loop-str set-no-cs-str set-read0-str \ set-mode-256-type set-mode-256-errno \ set-mode-0-type set-mode-0-errno set-mode-3-type set-mode-3-errno \ set-bits-8-type set-bits-8-errno set-speed-type set-speed-errno \ set-cshigh-type set-cshigh-errno set-lsbfirst-type \ set-lsbfirst-errno set-threewire-type set-threewire-errno \ set-loop-type set-loop-errno set-no-cs-type set-no-cs-errno \ set-read0-true get-read0-after \ writebytes-empty writebytes-toolong writebytes-nonint \ xfer-empty xfer-toolong xfer2-empty xfer2-toolong \ writebytes-4096-type writebytes-4096-errno \ readbytes-type readbytes-errno writebytes-type writebytes-errno \ writebytes2-type writebytes2-errno xfer-type xfer-errno \ xfer2-type xfer2-errno xfer3-type xfer3-errno \ close-unopened fileno-after-close \ ctor-00-type ctor-00-errno ctor-00-str open-00-type open-00-errno \ open-kwargs-type ctor-kwargs-type ctor-kw-device-type \ open-badtype open-noargs open-high-errno \ ctx-fileno ctx-open-missing-type fd-count-unchanged; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi check_eq $P/probe-count "120" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this every measurement below would be a statement # about whatever was first on sys.path. check_eq $P/module-answered "$SO" "$(pv module_file)" check_eq $P/is-extension "yes" "$(pv is_extension)" # The three version statements that must agree: the module's own __version__, # which PyInit_spidev sets from the _VERSION_ macro in spidev_module.c, and the # dist-info the recipe's PV produced. setup.py DERIVES the distribution version # by grepping that same macro out of the C file, so a disagreement here would # mean the recipe and the source had come apart. check_eq $P/version-attr "3.6" "$(pv version_attr)" check_eq $P/dist-version "3.6" "$(pv dist_version)" check_eq $P/version-agrees "yes" "$(pv version_agrees)" check_eq $P/has-SpiDev "yes" "$(pv has_SpiDev)" check_eq $P/type-name "SpiDev" "$(pv spidev_type_name)" # The module exposes the type and __version__ and NOTHING else -- there are no # module-level functions, and SpiDev_module_methods[] is empty. A release that # grew one would show here rather than acquiring an untested surface in silence. check_eq $P/module-publics "SpiDev" "$(pv module_publics)" # ------------------------------------------------------- the documented API -- # Every getset in SpiDev_getset[] and every entry in SpiDev_methods[], by name. # `read0' is in the module and is measured here although this ticket's own # description does not name it: an attribute nobody names is one nobody notices # going missing. for _a in mode cshigh threewire lsbfirst loop no_cs bits_per_word max_speed_hz \ read0; do check_eq $P/attr-$(echo "$_a" | tr _ -) "yes" "$(pv has_attr_$_a)" done for _m in open close fileno readbytes writebytes writebytes2 xfer xfer2 xfer3; do check_eq $P/meth-$_m "yes" "$(pv has_meth_$_m)" done check_eq $P/meth-enter "yes" "$(pv has_meth___enter__)" check_eq $P/meth-exit "yes" "$(pv has_meth___exit__)" # ------------------------------------------------------------ construction -- # SpiDev() WITH NO ARGUMENTS OPENS NOTHING and must not raise: SpiDev_init only # calls SpiDev_open when `bus' >= 0, and tp_new leaves fd at -1. Every case after # this one is about that object, so a red here explains all of them at once. check_eq $P/ctor-noargs "no-raise" "$(pv ctor_noargs)" check_eq $P/unopened-usable "yes" "$(pv unopened_usable)" check_eq $P/fileno-unopened "-1" "$(pv fileno_unopened)" # --------------------------------------------- getters on an unopened device -- # Each reads the object's own cached state rather than the hardware, so each has # a documented value with no controller present. mode, bits_per_word and # max_speed_hz are zeroed by tp_new; the six booleans are bits OF that zero mode, # which is why they are False rather than absent. check_eq $P/get-mode "0" "$(pv get_mode)" check_eq $P/get-bits-per-word "0" "$(pv get_bits_per_word)" check_eq $P/get-max-speed-hz "0" "$(pv get_max_speed_hz)" for _a in cshigh threewire lsbfirst loop no_cs read0; do check_eq $P/get-$(echo "$_a" | tr _ -) "False" "$(pv get_$_a)" done # ----------------------------------------- validation, BEFORE any ioctl(2) --- # The device is unopened throughout, so a TypeError from one of these cases is # ALSO evidence that no ioctl was attempted on the -1 descriptor: the EBADF block # below is what those cases would have produced had they got that far. # # The typo in `integerbetween' is upstream's -- two adjacent string literals with # no separator -- and is asserted verbatim. An expectation that quietly corrected # it would go red on the day the port starts working. check_eq $P/set-mode-4 \ "TypeError: The mode attribute must be an integerbetween 0 and 3." \ "$(pv set_mode_4)" check_eq $P/set-mode-str \ "TypeError: The mode attribute must be an integer" "$(pv set_mode_str)" check_eq $P/del-mode \ "TypeError: Cannot delete attribute" "$(pv del_mode)" check_eq $P/set-bits-7 \ "TypeError: invalid bits_per_word (8 to 32)" "$(pv set_bits_7)" check_eq $P/set-bits-33 \ "TypeError: invalid bits_per_word (8 to 32)" "$(pv set_bits_33)" check_eq $P/set-bits-str \ "TypeError: The bits_per_word attribute must be an integer" "$(pv set_bits_str)" check_eq $P/set-speed-str \ "TypeError: The max_speed_hz attribute must be an integer" "$(pv set_speed_str)" check_eq $P/set-cshigh-str \ "TypeError: The cshigh attribute must be boolean" "$(pv set_cshigh_str)" check_eq $P/set-lsbfirst-str \ "TypeError: The lsbfirst attribute must be boolean" "$(pv set_lsbfirst_str)" # THE MESSAGE NAMES `3wire', NOT `threewire'. The attribute is `threewire' and # its setter's message is not; upstream's, measured, and asserted as it is. check_eq $P/set-threewire-str \ "TypeError: The 3wire attribute must be boolean" "$(pv set_threewire_str)" check_eq $P/set-loop-str \ "TypeError: The loop attribute must be boolean" "$(pv set_loop_str)" check_eq $P/set-no-cs-str \ "TypeError: The no_cs attribute must be boolean" "$(pv set_no_cs_str)" check_eq $P/set-read0-str \ "TypeError: The read0 attribute must be boolean" "$(pv set_read0_str)" # UPSTREAM'S TRUNCATION, measured on the control rather than reasoned about. # `mode' is read into a uint8_t BEFORE the `> 3' test, so 256 arrives as 0 and is # ACCEPTED -- it reaches the ioctl, which is what the EBADF says. Recorded as # behaviour of spidev 3.6; the range check the ticket asks about is real, and # this is the one input that walks past it. check_eq $P/set-mode-256-type "OSError" "$(pv set_mode_256_type)" check_eq $P/set-mode-256-errno "9" "$(pv set_mode_256_errno)" # ------------------------------- past validation, into ioctl(-1, SPI_IOC_*) --- # A VALID value on an unopened device reaches the ioctl and fails EBADF. This is # the closest this machine can come to exercising the SPI_IOC_* request # encodings: the request is formed and handed to the kernel, which rejects the # DESCRIPTOR before it looks at the request. Nine setters, each asserted to fail # that way and no other -- a setter that wrote through the null handle instead, # which is the failure the ticket asks about, would return None here. for _n in set-mode-0 set-mode-3 set-bits-8 set-speed set-cshigh set-lsbfirst \ set-threewire set-loop set-no-cs; do _k=$(echo "$_n" | tr - _) check_eq $P/$_n-type "OSError" "$(pv ${_k}_type)" check_eq $P/$_n-errno "9" "$(pv ${_k}_errno)" done # `read0' IS THE CONTROL FOR THE NINE ABOVE. It is the one attribute that is pure # object state with no ioctl behind it, so it succeeds where they fail -- which # is what places their failure at the descriptor rather than in the setter # machinery they all share. check_eq $P/set-read0-true "no-raise" "$(pv set_read0_true)" check_eq $P/get-read0-after "True" "$(pv get_read0_after)" # --------------------------------------------- transfers on an unopened dev -- # THE ARGUMENT CHECKS FIRST. Empty and over-long lists are refused before the # descriptor is touched, so they are assertable exactly. check_eq $P/writebytes-empty \ "TypeError: Empty argument list." "$(pv writebytes_empty)" check_eq $P/writebytes-toolong \ "OverflowError: Argument list size exceeds 4096 bytes." "$(pv writebytes_toolong)" check_eq $P/writebytes-nonint "TypeError" "$(pv writebytes_nonint)" check_eq $P/xfer-empty \ "TypeError: Empty argument list." "$(pv xfer_empty)" check_eq $P/xfer-toolong \ "OverflowError: Argument list size exceeds 4096 bytes." "$(pv xfer_toolong)" check_eq $P/xfer2-empty \ "TypeError: Empty argument list." "$(pv xfer2_empty)" check_eq $P/xfer2-toolong \ "OverflowError: Argument list size exceeds 4096 bytes." "$(pv xfer2_toolong)" # 4096 IS SPIDEV_MAXPATH EXACTLY AND IS ACCEPTED -- the test is `> 4096' -- so it # goes on to the descriptor and fails EBADF there. The 4096/4097 pair is what # pins the boundary; either case alone would only show that some large list is # refused and some smaller one is not. check_eq $P/writebytes-4096-type "OSError" "$(pv writebytes_4096_type)" check_eq $P/writebytes-4096-errno "9" "$(pv writebytes_4096_errno)" # THEN THE DESCRIPTOR. Each of these forms its buffer, reaches read(2), write(2) # or the SPI_IOC_MESSAGE ioctl, and fails EBADF. for _n in readbytes writebytes writebytes2 xfer xfer2 xfer3; do check_eq $P/$_n-type "OSError" "$(pv ${_n}_type)" check_eq $P/$_n-errno "9" "$(pv ${_n}_errno)" done # close() on an unopened device is a documented no-op -- SpiDev_close guards on # fd != -1 -- and leaves fileno at -1. check_eq $P/close-unopened "no-raise" "$(pv close_unopened)" check_eq $P/fileno-after-close "-1" "$(pv fileno_after_close)" # --------------------------------------- opening a device that does not exist -- # THE TICKET'S HEADLINE ASSERTION. Both entry points reach the same SpiDev_open, # whose open(2) fails ENOENT and is wrapped by # PyErr_SetFromErrno(PyExc_IOError). PyExc_IOError IS PyExc_OSError in Python 3 # and OSError.__new__ maps ENOENT to FileNotFoundError, which is why the type is # that subclass and not a bare OSError. The errno is asserted separately from the # type, because a wrapper that lost it would still raise something OSError-ish. check_eq $P/ctor-00-type "FileNotFoundError" "$(pv ctor_00_type)" check_eq $P/ctor-00-errno "2" "$(pv ctor_00_errno)" check_eq $P/ctor-00-str \ "FileNotFoundError: [Errno 2] No such file or directory" "$(pv ctor_00_str)" check_eq $P/open-00-type "FileNotFoundError" "$(pv open_00_type)" check_eq $P/open-00-errno "2" "$(pv open_00_errno)" # open()'s OWN keywords are {"bus","device"} and they work. check_eq $P/open-kwargs-type "FileNotFoundError" "$(pv open_kwargs_type)" # THE CONSTRUCTOR'S KEYWORDS DO NOT WORK, EITHER SPELLING, and the pair above is # what makes this a statement about the constructor rather than about keyword # arguments. SpiDev_init declares {"bus","client"} and then hands its own args # AND kwds to SpiDev_open, which declares {"bus","device"}: so `client' dies in # open() and `device' dies in __init__. Only the positional form reaches the # device. Upstream 3.6's, measured on the control, recorded not repaired. check_eq $P/ctor-kwargs-type "TypeError" "$(pv ctor_kwargs_type)" check_eq $P/ctor-kw-device-type "TypeError" "$(pv ctor_kw_device_type)" check_eq $P/open-badtype "TypeError" "$(pv open_badtype)" check_eq $P/open-noargs "TypeError" "$(pv open_noargs)" # A bus/device pair no controller could have is still ENOENT rather than a crash # -- the path is built with snprintf into a 4096-byte buffer and the OverflowError # arm is unreachable for two-digit numbers. check_eq $P/open-high-errno "2" "$(pv open_high_errno)" # ------------------------------------------------------ the context manager -- # __enter__/__exit__ exist, so `with SpiDev() as d:' is the documented shape. # Entered on an unopened device it must not raise and must yield the same object; # the open inside it still fails ENOENT, and __exit__ still closes. check_eq $P/ctx-fileno "-1" "$(pv ctx_fileno)" check_eq $P/ctx-open-missing-type "FileNotFoundError" "$(pv ctx_open_missing_type)" # ------------------------------------------------------ no descriptor leaked -- # /proc/self/fd before the first constructor case and after the last. Every case # above either failed before open(2) or wrapped the failure and closed, so the # count must be identical. A constructor that raised while still holding its # descriptor would pass every other case in this file. check_eq $P/fd-count-unchanged "$(pv fd_count_start)" "$(pv fd_count_end)" rm -rf "$W"