. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those three. # # # # They are a bare `import', a second one whose STDERR is # # the assertion (`import-quiet'), and the probe -- which # # takes ONE start for a hundred-odd measurements, both # # backends included. # # # # The probe's own work is microseconds of pure Python plus # # two pty pairs. It waits for exactly two things, both # # bounded and both tiny: a 1-second ceiling on the # # TIOCINQ poll (fifty 0.02s sleeps, broken on the first # # non-zero) and one 0.1-second read that is SUPPOSED to # # time out empty. Nothing here waits out a serial device, # # because there is no serial device -- see the header. # # # # 1200 is 400s per start, the figure 99-python3-jdcal, # # 99-python3-parse and 99-python3-async-timeout all set for # # their three, and roughly six times the ~60s a python3 # # start has measured at here. # # # # A suite-timeout red is a batch-wide cost, so if the three # # 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-pyserial 3.5 (meta-openembedded/meta-python # recipes-devtools/python/python3-pyserial_3.5.bb, `inherit pypi setuptools3 # ptest', PyPI sdist) on the wasm32-hwjs glibc image. # NO bbappend and NO patch: the stock recipe builds and packages correctly as it # stands -- 1622 of 1622 tasks, zero ERROR lines, at recipe scope. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # `serial.Serial(port)' drives a serial line: baudrate, bytesize, parity, # stopbits and timeouts, read/write, input-buffer control, and the modem # control lines. `serial_for_url(url)' is the same class reached through a # scheme -- `loop://', `socket://', `rfc2217://', `spy://' -- each a handler in # serial/urlhandler/. `serial.tools.list_ports.comports()' enumerates ports. # # PURE PYTHON, entirely. No ext_modules, no C, no configure step -- which is # what makes it portable to an image whose python3 is ONE STATIC BINARY with an # empty lib-dynload and no dynamic loader (BRINTOS-1476). Its platform layer is # `serialposix.py' over `termios' and `fcntl.ioctl', both of which the # interpreter carries built in. # # ============================================================================ # IT SHIPS ITS OWN CONTROL, WHICH IS WHY THIS PORT IS TESTABLE WITHOUT A UART. # ============================================================================ # # `loop://' is a pure-Python loopback (urlhandler/protocol_loop.py). It touches # no device, no termios and no ioctl. Run beside the real `posix' backend over a # pty pair, it separates a LIBRARY defect from an ARM defect in one suite: # # loop:// green + pty red -> termios/ioctl on wasm32, and nowhere else. # loop:// red -> the library, whatever the pty does. # # The pty half is not a courtesy: `ioctl' is slot [16] in this arm's syscall # table, and /dev/ptmx + devpts are load-bearing here -- Option-A makes # /dev/console a symlink to /dev/pts/0, so the console this suite's own output # travels over is a pty (arch/wasm32/kernel/init.c). openpty is already proven # on this image by 15-bash, 89-screen, 89-tmux and 90-elfutils among others. # # ============================================================================ # WHAT A PTY CAN AND CANNOT VERIFY. MEASURED ON AN x86-64 CONTROL FIRST. # ============================================================================ # # A pty is not a UART, and the four line parameters do not behave alike on one. # Measured against this exact 3.5 sdist before this file was written: # # BAUDRATE STICKS -- tcsetattr's speed is stored and reads back, B19200 at # open and B115200 after a later setter. `tc-ispeed-19200', # `tc-ospeed-19200' and `tc-reconfigure-115200' are therefore REAL assertions # that the setting reached the line discipline rather than the Python object. # # BYTESIZE AND PARITY CANNOT. drivers/tty/pty.c:pty_set_termios() does # `c_cflag &= ~(CSIZE | PARENB); c_cflag |= (CS8 | CREAD);' unconditionally, # so a pty ALWAYS reports CS8 and no parity however it was asked. That is # stock Linux, not this arm. # # THE FIRST DRAFT OF THIS SUITE ASSERTED CS7/PARENB REACHED THE LINE, and the # control measured `no' for both. Had it shipped, it would have red on a # CORRECT port and sent the next reader looking for a termios bug on wasm32 # that was never there. So the port below is opened CS8/PARITY_NONE, where the # object and the line agree and the readback means something -- and the CS7/EVEN # request is measured separately, as the normalisation it is # (`pty-normalises-*'). Both halves are asserted, because one alone cannot tell # a normalising pty from a termios that does nothing at all. # # THE ORDER MATTERS TOO, and it is why the CS8 port is not merely tidier. A # tcsetattr that changes CSIZE/PARENB *without* also changing the speed is # refused EINVAL by the pty; one that changes both is accepted and normalised. # So a CS7 port cannot have `timeout' changed afterwards -- `_reconfigure_port' # re-sends CS7 with no speed change and takes EINVAL. That was measured here as # a `termios.error (22)' out of the probe's own pty block, and it is a # pyserial-on-a-pty interaction, NOT an arm defect. # # MODEM CONTROL LINES DO NOT EXIST ON A PTY. TIOCMGET/TIOCMSET return ENOTTY # (25). They are asserted to RAISE, with the errno named -- which is the # point: a stub returning a plausible `False' would pass any "reports # consistently" test and fail this one. # # ============================================================================ # THE HARDWARE CASE HAS AN ANSWER KNOWN IN ADVANCE. # ============================================================================ # # The defconfig builds in CONFIG_USB_SERIAL and CONFIG_USB_SERIAL_FTDI_SIO # (arch/wasm32/configs/wasm32_defconfig:130-131), but this project verifies on # the local Node.js runner: no browser, so no claimable WebUSB device, so no # ttyUSB node. An ABSENT /dev/ttyUSB0 is the EXPECTED RESULT TO RECORD, not a # red to explain, and `ttyusb0-exists' asserts it rather than passing over it in # silence. # # `comports-count' is 1, and the port is /dev/ttySER0. `# CONFIG_SERIAL_8250 is # not set' does remove the 8250 ttyS nodes, but comports() globs the FAMILY # ttyS* (plus ttyUSB*, ttyXRUSB*, ttyACM*, ttyAMA*, rfcomm* and ttyAP* -- # list_ports_linux.py), and the wasm32 WebSerial bridge names its port "ttySER". # /dev/ttySER0 therefore matches ttyS* and is enumerated. It is a built-in # __init driver registering WASM_SERIAL_BRIDGE_NPORTS=1 # (arch/wasm32/include/asm/serial_bridge.h:50, kernel/serial_wasm.c:472); it # comes up on every boot -- "wasm-serial: WebSerial bridge up (/dev/ttySER0 # ...)" -- and refuses open() with -ENODEV until a host port attaches, so its # NODE is unconditional even though no host port exists under the Node.js # runner. Enumerating it is correct pyserial behaviour, not a stray node. # # This corrects a phase-A prediction that read "count 0, names ": it was # derived from the defconfig alone and missed that the bridge's name falls # inside the ttyS* glob. The measured pair is the stronger assertion anyway -- # it now proves the Linux backend discovers the platform's own serial port. # `ttyusb0-exists' was the third predicted case and measured "no" as expected. # # THE ENOENT CLAIM IS ASSERTED TWICE, ON PURPOSE. `ttyusb0-*' is the # device-specific form and depends on the node being absent; `nosuchport-*' # opens a name nothing could ever create and is true on any host. The second is # what keeps the claim scorable if the image ever does grow a ttyUSB0. # # THE PROBE DOES NOT OPEN /dev/ttyUSB0 WHEN THE NODE EXISTS. On a developer's # control machine that node can be a colleague's real adapter, and opening it # would rewrite its line settings -- a side effect this suite has no business # having. It records `' instead, which reds the # assertion below rather than hiding it. # # ============================================================================ # PACKAGING: SIX PACKAGES, AND THE INTERESTING ONES ARE THE SPLITS. # ============================================================================ # # The recipe declares `PACKAGES =+ "${PN}-java ${PN}-osx ${PN}-win32 # ${PN}-tools"' and gives FILES for the first three only. Measured on the built # .debs: python3-pyserial, -dev, -dbg, -java, -osx, -win32 are produced and # -tools IS NOT -- it has no FILES, so it is empty and dropped. `no-tools-package' # asserts that, because a reader of the recipe would expect one. # # The three foreign-platform splits are what keep serialcli/serialwin32/ # serialjava OFF this image -- modules that import `ctypes.wintypes' or run on # Jython, and would be dead weight at best. `no-win32-module' and friends assert # they are gone from the runtime package, and `serial-module-is-posix' asserts # from the other side that the dispatch landed on serialposix. # # `miniterm.py' lands in -win32 too, which is an upstream oddity rather than a # choice: `FILES:${PN}-win32' globs `tools/miniterm*' and miniterm is the # cross-platform terminal tool. It is not ours to fix and nothing here depends # on it; noted so the next reader does not go looking for it in the main # package. # # `do_install:append' removes ${bindir}/pyserial-miniterm and -ports and then # rmdir's ${bindir} -- so this package installs NO command at all, while its # dist-info still declares both console_scripts. Both halves are asserted: # `no-cli-*' on the disk, `entry-points-declared' on the metadata. # # NO -ptest PACKAGE IS BUILT. The recipe does `inherit ptest' and adds a # run-ptest plus RDEPENDS on python3-pytest, but `ptest' is not in this distro's # DISTRO_FEATURES, so the split never happens -- measured on the deploy dir, # where six .debs are produced and none is -ptest. `no-ptest-package' asserts # it, so a DISTRO_FEATURES change that quietly pulled python3-pytest onto the # image would land here first. P=python3-pyserial # The canary is what tells a reader the suite STARTED, as distinct from a suite # that produced nothing because the interpreter never came up. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/serial DI=$SITE/pyserial-3.5.dist-info PKGLIST=/var/lib/dpkg/info/python3-pyserial.list PROBE=$BST_DATA/python3-pyserial-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pyserial.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- on the disk --- check $P/site-packages-dir test -d "$SITE" # A PACKAGE, not a flat module: top_level.txt is `serial' and setup.py ships # four subpackages under it. check $P/package-dir test -d "$PKGDIR" check $P/no-flat-module test ! -e "$SITE/serial.py" check $P/module-init test -s "$PKGDIR/__init__.py" check $P/module-serialutil test -s "$PKGDIR/serialutil.py" # The platform backend this image actually runs. check $P/module-serialposix test -s "$PKGDIR/serialposix.py" check $P/module-rs485 test -s "$PKGDIR/rs485.py" check $P/module-rfc2217 test -s "$PKGDIR/rfc2217.py" check $P/subpkg-tools test -d "$PKGDIR/tools" check $P/subpkg-threaded test -d "$PKGDIR/threaded" check $P/subpkg-urlhandler test -d "$PKGDIR/urlhandler" # The loopback handler -- the pure-Python control this whole suite leans on. check $P/urlhandler-loop test -s "$PKGDIR/urlhandler/protocol_loop.py" # The enumeration backend for THIS platform, and its dispatcher. check $P/tools-list-ports test -s "$PKGDIR/tools/list_ports.py" check $P/tools-list-ports-linux test -s "$PKGDIR/tools/list_ports_linux.py" check $P/tools-list-ports-posix test -s "$PKGDIR/tools/list_ports_posix.py" check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # THE FOREIGN-PLATFORM MODULES ARE SPLIT OUT and must not be here. Each is a # module that could not run on this image at all -- serialwin32 and serialcli # import ctypes.wintypes and the .NET bridge, serialjava is Jython-only, and # list_ports_osx opens IOKit. check $P/no-win32-module test ! -e "$PKGDIR/serialwin32.py" check $P/no-cli-module test ! -e "$PKGDIR/serialcli.py" check $P/no-win32-helper test ! -e "$PKGDIR/win32.py" check $P/no-java-module test ! -e "$PKGDIR/serialjava.py" check $P/no-osx-list-ports test ! -e "$PKGDIR/tools/list_ports_osx.py" check $P/no-windows-list-ports test ! -e "$PKGDIR/tools/list_ports_windows.py" # miniterm rides out with the -win32 split (see the header) -- an upstream # glob, not a decision of ours. check $P/no-miniterm-module test ! -e "$PKGDIR/tools/miniterm.py" # `do_install:append' deletes both console scripts and then rmdir's ${bindir}. # So this package installs no command, and the recipe's own /usr/bin is gone. check $P/no-cli-miniterm test ! -e /usr/bin/pyserial-miniterm check $P/no-cli-ports test ! -e /usr/bin/pyserial-ports # ------------------------------------------------------- what dpkg shipped --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, ASSERTED FROM THE PACKAGE'S OWN FILE LIST. This image's python3 # has an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a release # that grew a compiled extension could not be imported whatever it built to -- # it would land here first. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # Every packaged file is Python, a bytecode cache, or dist-info metadata. # The loop reads a copy under $TMPDIR, never the rootfs (brintos/linux#73). _bad= while IFS= read -r _p; do [ -d "$_p" ] && continue case "$_p" in *.py|*.pyc) continue ;; *.dist-info/*) continue ;; esac _bad="$_bad $_p" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # The package ships no command, so nothing of it lands under /usr/bin. check_eq $P/pkg-no-bindir "" \ "$(grep '^/usr/bin' "$W/pkgf" 2>/dev/null)" # ------------------------------------------------------------- dist-info ---- check $P/dist-info-dir test -d "$DI" check_out $P/dist-info-name "Name: pyserial" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 3.5" cat "$DI/METADATA" check_out $P/dist-info-summary "Summary: Python Serial Port Extension" \ cat "$DI/METADATA" # The wheel metadata's own statement that there is nothing arch-specific here. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "serial" cat "$DI/top_level.txt" # THE METADATA STILL DECLARES BOTH CONSOLE SCRIPTS the recipe deleted. That is # the packaging telling one story and the disk another, and it is upstream's # shape rather than a defect -- asserted so the pair above reads as deliberate. check_out $P/entry-points-declared "pyserial-miniterm = serial.tools.miniterm:main" \ cat "$DI/entry_points.txt" check_out $P/entry-points-ports "pyserial-ports = serial.tools.list_ports:main" \ cat "$DI/entry_points.txt" # --------------------------------------------------------- what dpkg says ---- dpkg -s python3-pyserial > "$W/st" 2>/dev/null || : > "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" # EVERY DECLARED RDEPENDS IS AN INTERPRETER SUBPACKAGE, named one by one. # `RDEPENDS:${PN} = "..."' in the recipe is an assignment, the shape that CAN # clobber the python3-core setuptools3-base contributes; measured on the built # .deb it does not, and `pkg-depends-core' is that measurement. for _d in fcntl io logging netclient numbers shell stringold threading; do check_out $P/pkg-depends-$_d "python3-$_d" cat "$W/dep" done check_out $P/pkg-depends-core "python3-core" cat "$W/dep" # The three foreign-platform splits must not be pulled onto the image by a # dependency the main package does not declare. check_eq $P/pkg-no-split-depends "" \ "$(grep -oE 'python3-pyserial-(java|osx|win32)' "$W/dep")" # Not installed either -- the splits exist as .debs and stay off the image. for _s in java osx win32; do check_eq $P/no-$_s-package-installed "" \ "$(dpkg -s python3-pyserial-$_s 2>/dev/null | grep '^Status: install ok installed')" done # `${PN}-tools' is DECLARED in PACKAGES and has no FILES, so it is empty and # never produced. A reader of the recipe would expect it; the build disagrees. check_eq $P/no-tools-package "" \ "$(dpkg -s python3-pyserial-tools 2>/dev/null | grep '^Status: install ok installed')" # The recipe DOES `inherit ptest', but `ptest' is not in DISTRO_FEATURES here, # so no -ptest split is produced and python3-pytest is never pulled in. check_eq $P/no-ptest-package "" \ "$(dpkg -s python3-pyserial-ptest 2>/dev/null | grep '^Status: install ok installed')" # -------------------------------------------------------------- it imports --- check $P/import python3 -c 'import serial' # STDERR IS THE ASSERTION. A DeprecationWarning or a shadowed-module warning at # import time would print here and nowhere else. check_eq $P/import-quiet "" \ "$(python3 -c 'import serial' 2>&1)" # ------------------------------------------------------------- the probe ----- check $P/probe-present test -s "$PROBE" bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if ! test -s "$OUT"; then bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" else bst_ok $P/probe-output fi check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "104" "$(pv emitted_count)" # ------------------------------------------------------------- identity ------ # The probe must have imported the INSTALLED package, not a copy beside itself. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # THE VERSION THE RECIPE PINS, read off the module rather than off the metadata. check_eq $P/version-attr "3.5" "$(pv version_attr)" check_eq $P/api-Serial "yes" "$(pv has_Serial)" check_eq $P/api-serial-for-url "yes" "$(pv has_serial_for_url)" check_eq $P/api-SerialException "yes" "$(pv has_SerialException)" check_eq $P/api-PortNotOpenError "yes" "$(pv has_PortNotOpenError)" check_eq $P/api-SerialTimeoutException "yes" "$(pv has_SerialTimeoutException)" check_eq $P/api-rs485-settings "yes" "$(pv has_rs485_settings)" # THE PLATFORM DISPATCH LANDED ON POSIX. serial/__init__.py picks the backend by # sys.platform; the disk cases above say serialwin32/serialcli/serialjava are # absent, and this says the one that IS here is the one in use. check_eq $P/serial-module-is-posix "serial.serialposix" "$(pv serial_module)" # The constants a caller writes against. check_eq $P/const-parity-none "N" "$(pv const_parity_none)" check_eq $P/const-parity-even "E" "$(pv const_parity_even)" check_eq $P/const-stopbits-one "1" "$(pv const_stopbits_one)" check_eq $P/const-eightbits "8" "$(pv const_eightbits)" check_eq $P/const-sevenbits "7" "$(pv const_sevenbits)" # The url handlers that import. `cp2110' is EXCLUDED and its absence asserted # by the value: it needs the third-party `hid' binding, which this image has no # reason to carry, and it is the one handler whose absence says nothing is wrong. check_eq $P/urlhandlers "alt,hwgrep,loop,rfc2217,socket,spy" \ "$(pv urlhandlers_importable)" # ==================================================== THE LOOPBACK ARM ======= # Pure Python. No device, no termios, no ioctl. A red in this block is a LIBRARY # defect; a red below it is not. See the header. check_eq $P/loop-open "yes" "$(pv loop_open)" check_eq $P/loop-is-open "yes" "$(pv loop_is_open)" check_eq $P/loop-name "loop://" "$(pv loop_name)" # The write returns the byte count it accepted... check_eq $P/loop-write-count "12" "$(pv loop_write_count)" # ...in_waiting reports exactly those bytes pending... check_eq $P/loop-in-waiting "12" "$(pv loop_in_waiting)" # ...and the round trip is byte-exact. check_eq $P/loop-read-back "brintos-2524" "$(pv loop_read_back)" check_eq $P/loop-drained "0" "$(pv loop_drained)" # A SHORT READ TIMES OUT AND RETURNS WHAT IT HAS. Three bytes written, five # asked for, one-second timeout: it must neither block for ever nor raise. check_eq $P/loop-short-read "abc" "$(pv loop_short_read)" check_eq $P/loop-short-read-len "3" "$(pv loop_short_read_len)" # reset_input_buffer DISCARDS the pending bytes -- the count goes to zero with # nothing having read them. check_eq $P/loop-before-reset "10" "$(pv loop_before_reset)" check_eq $P/loop-after-reset "0" "$(pv loop_after_reset)" # readline() is serialutil's own buffering, not a line discipline. check_eq $P/loop-readline "one" "$(pv loop_readline)" check_eq $P/loop-closed "yes" "$(pv loop_closed)" # WRITING TO A CLOSED PORT is the documented PortNotOpenError, not an OSError # from under the library and not a silent no-op. check_eq $P/loop-write-closed-raises "PortNotOpenError" \ "$(pv loop_write_closed_raises)" check_eq $P/loop-read-closed-raises "PortNotOpenError" \ "$(pv loop_read_closed_raises)" # The whole block ran: no `loop_fatal' line was emitted. check_eq $P/loop-no-fatal "" "$(pv loop_fatal)" # ======================================================== THE PTY ARM ======== # The real `posix' backend over termios and ioctl. THIS is the arm that measures # wasm32 rather than the library. check_eq $P/pty-allocated "yes" "$(pv pty_allocated)" check_eq $P/pty-slave-is-pts "yes" "$(pv pty_slave_is_pts)" check_eq $P/pty-open "yes" "$(pv pty_open)" check_eq $P/pty-is-open "yes" "$(pv pty_is_open)" check_eq $P/pty-name-roundtrip "yes" "$(pv pty_name_roundtrip)" # The whole block ran to the end. check_eq $P/pty-no-fatal "" "$(pv pty_fatal)" # ---- what the OBJECT thinks it was configured with ---- check_eq $P/pty-obj-baudrate "19200" "$(pv pty_obj_baudrate)" check_eq $P/pty-obj-bytesize "8" "$(pv pty_obj_bytesize)" check_eq $P/pty-obj-parity "N" "$(pv pty_obj_parity)" check_eq $P/pty-obj-stopbits "1" "$(pv pty_obj_stopbits)" check_eq $P/pty-obj-timeout "2" "$(pv pty_obj_timeout)" # ---- what the LINE DISCIPLINE thinks, read back through termios ---- # # THE ASSERTION THAT MATTERS. An arm whose tcsetattr silently did nothing would # pass every `pty-obj-*' case above and fail every one of these. check_eq $P/tc-ispeed-19200 "yes" "$(pv tc_ispeed_19200)" check_eq $P/tc-ospeed-19200 "yes" "$(pv tc_ospeed_19200)" check_eq $P/tc-csize-cs8 "yes" "$(pv tc_csize_cs8)" check_eq $P/tc-parenb-clear "yes" "$(pv tc_parenb_clear)" check_eq $P/tc-cstopb-clear "yes" "$(pv tc_cstopb_clear)" check_eq $P/tc-cread-set "yes" "$(pv tc_cread_set)" # pyserial always asks for RAW mode. These are load-bearing rather than # cosmetic: without them the round trip below would not be byte-exact. check_eq $P/tc-icanon-clear "yes" "$(pv tc_icanon_clear)" check_eq $P/tc-echo-clear "yes" "$(pv tc_echo_clear)" check_eq $P/tc-opost-clear "yes" "$(pv tc_opost_clear)" check_eq $P/tc-isig-clear "yes" "$(pv tc_isig_clear)" # A SETTING CHANGED AFTER OPEN reaches the line too -- `_reconfigure_port' runs # on the property setter, not only at open. check_eq $P/tc-reconfigure-115200 "yes" "$(pv tc_reconfigure_115200)" # ---- the round trip over a real tty ---- # Master -> pyserial-driven slave, raw mode, byte-exact. check_eq $P/pty-read "hello-pty" "$(pv pty_read)" # ...and back out through pyserial's own write(). check_eq $P/pty-master-read "from-pyserial" "$(pv pty_master_read)" # in_waiting over a real tty is a TIOCINQ ioctl, NOT a Python-side counter -- # the loopback arm cannot reach this path at all. check_eq $P/pty-in-waiting "5" "$(pv pty_in_waiting)" # reset_input_buffer over a real tty is termios.tcflush(). check_eq $P/pty-after-flush "0" "$(pv pty_after_flush)" # The timeout setter works on a CS8 port -- see the header for why a CS7 one # would take EINVAL here instead. check_eq $P/pty-timeout-setter-ok "yes" "$(pv pty_timeout_setter_ok)" # A read with nothing pending returns EMPTY at the timeout: it must not hang and # must not raise. check_eq $P/pty-timeout-read "b''" "$(pv pty_timeout_read)" check_eq $P/pty-closed "yes" "$(pv pty_closed)" check_eq $P/pty-write-closed-raises "PortNotOpenError" \ "$(pv pty_write_closed_raises)" # ---- the modem-control lines: they RAISE, and the errno is named ---- # # TIOCMGET/TIOCMSET on a pty is ENOTTY (25). Asserting the RAISE rather than a # value is the point: a stub returning a plausible `False' would satisfy any # "reports consistently" test and fail this one. for _l in cts dsr ri cd; do check_eq $P/modem-get-$_l "OSError" "$(pv modem_get_$_l)" check_eq $P/modem-get-$_l-enotty "25" "$(pv modem_get_${_l}_errno)" done check_eq $P/modem-set-rts "OSError" "$(pv modem_set_rts)" check_eq $P/modem-set-rts-enotty "25" "$(pv modem_set_rts_errno)" check_eq $P/modem-set-dtr "OSError" "$(pv modem_set_dtr)" check_eq $P/modem-set-dtr-enotty "25" "$(pv modem_set_dtr_errno)" # ---- rs485 ---- # TIOCSRS485 on a pty. A pty is not an RS485 transceiver, so this MUST raise # rather than silently accept a setting no driver will ever see. pyserial wraps # the ENOTTY in a ValueError. check_eq $P/rs485-set-raises "ValueError" "$(pv rs485_set_raises)" # AND THE ATTRIBUTE IS LEFT SET ANYWAY: serialposix assigns self._rs485_mode # BEFORE calling _set_rs485_mode, so the getter reports the object the failed # setter was handed. Recorded as the wart it is rather than asserted as # "reports consistently", which it does not -- a caller that trusts the getter # after a failed set is reading a setting no line ever received. check_eq $P/rs485-get-after-failed-set "RS485Settings" \ "$(pv rs485_get_after_failed_set)" # ---- the pty's own normalisation, measured on its own ---- # # Asked for CS7/EVEN a pty reports CS8/no-parity: drivers/tty/pty.c forces it, # on stock Linux. Kept as its own case so the `tc-*' block above stays a # statement about THIS ARM's termios rather than about the pty driver -- and so # that a termios which does nothing at all is still distinguishable from one # that normalises, which needs both halves. check_eq $P/pty7-obj-bytesize "7" "$(pv pty7_obj_bytesize)" check_eq $P/pty7-obj-parity "E" "$(pv pty7_obj_parity)" check_eq $P/pty-normalises-csize-to-cs8 "yes" "$(pv pty_normalises_csize_to_cs8)" check_eq $P/pty-normalises-parenb-off "yes" "$(pv pty_normalises_parenb_off)" # The speed still sticks, which is what says the tcsetattr was ACCEPTED and # normalised rather than refused outright. check_eq $P/pty7-speed-9600 "yes" "$(pv pty7_speed_9600)" check_eq $P/pty7-no-fatal "" "$(pv pty7_fatal)" # ==================================================== ENUMERATION ============ check_eq $P/comports-ran "yes" "$(pv comports_ran)" check_eq $P/comports-is-list "yes" "$(pv comports_is_list)" # The posix dispatcher picked the LINUX implementation, not the generic # list_ports_posix fallback that shells out to tools this image does not have. check_eq $P/comports-impl "serial.tools.list_ports_linux" "$(pv comports_impl)" # MEASURED ON THIS IMAGE. comports() globs ttyS* among its families, and the # wasm32 WebSerial bridge registers its single port under the name "ttySER" -- # so /dev/ttySER0 matches the ttyS* glob and IS a correctly enumerated port. # The bridge is a built-in __init driver with NPORTS=1 # (arch/wasm32/include/asm/serial_bridge.h:50, kernel/serial_wasm.c:472), so the # node exists on every boot whether or not a host port is attached. This pair # therefore asserts that the Linux backend discovers the platform's own serial # port: a drop to 0 means enumeration broke, not that the image got quieter. check_eq $P/comports-count "1" "$(pv comports_count)" check_eq $P/comports-names "/dev/ttySER0" "$(pv comports_names)" # ============================== THE ABSENT USB SERIAL DEVICE ================= # CONFIG_USB_SERIAL=y and CONFIG_USB_SERIAL_FTDI_SIO=y, but the local Node.js # runner has no browser and so no claimable WebUSB device. The EXPECTED result, # recorded rather than skipped in silence. check_eq $P/ttyusb0-exists "no" "$(pv ttyusb0_exists)" # It must fail as a clean SerialException carrying ENOENT, not as a traceback # from under the library. check_eq $P/ttyusb0-open-raises "SerialException" "$(pv ttyusb0_open)" check_eq $P/ttyusb0-errno-enoent "yes" "$(pv ttyusb0_errno_is_enoent)" # ============================== THE PORTS THAT ARE NOT THERE ================= # `Serial()' with no port is legal and NOT open; using it is PortNotOpenError. check_eq $P/unopened-is-open "no" "$(pv unopened_is_open)" check_eq $P/unopened-write-raises "PortNotOpenError" "$(pv unopened_write_raises)" check_eq $P/unopened-read-raises "PortNotOpenError" "$(pv unopened_read_raises)" # A name nothing could ever create. THE HOST-INDEPENDENT FORM OF THE ENOENT # CLAIM: unlike `ttyusb0-*' above it does not depend on which nodes the image # has, so it stays scorable if the image ever grows a ttyUSB0. check_eq $P/nosuchport-raises "SerialException" "$(pv nosuchport_raises)" check_eq $P/nosuchport-errno-enoent "2" "$(pv nosuchport_errno)" # An unknown URL scheme is refused rather than silently treated as a path name. check_eq $P/badurl-raises "ValueError" "$(pv badurl_raises)" # ==================================================== THE CLOSURE ============ # NOTHING OUTSIDE THE STANDARD LIBRARY IS REACHED BY THE IMPORT. Scored on the # TOP-LEVEL name and against a snapshot taken BEFORE the import: comparing # dotted names would report every `serial.tools.*' submodule as third-party, and # diffing against sys.stdlib_module_names alone reported the control host's own # sitecustomize as pyserial's dependency -- a mistake made and caught on the # x86-64 control before this file was written. check_eq $P/pulled-nonstdlib "" "$(pv pulled_nonstdlib)" # EVERY DECLARED RDEPENDS IS REACHED, named positively -- the runtime half of # the `pkg-depends-*' block above, which says only that the .deb names them. # `termios' is the one the packaging does NOT name: it is part of python3-core # here, and it is what the whole pty arm runs on. for _m in fcntl io logging numbers threading termios; do check_eq $P/reached-$_m "yes" "$(pv reached_$_m)" done rm -rf "$W"