. "$BST_LIB" # BST-TIMEOUT: 600 # the interpreter is one large static image and this # # suite starts it once; on a loaded guest a single start # # has been minutes, and the runner's 60s default would # # report the port as broken when what ran out was the # # clock. Same trade, and the same reason, as # # 99-python3-certifi.test. # # system-config-keyboard 1.4.0 (meta-openembedded/meta-oe # recipes-support/system-config-keyboard, `inherit python3-dir gettext', with a # meta-brintos bbappend carrying one patch) on wasm32-hwjs -- the Fedora # keyboard-layout table and the Python module that wraps it. # # WHAT THIS PORT DELIVERS IS THE LIBRARY AND ITS DATA, not the configurator the # package is named after. The distinction is the whole shape of this suite and # it is worth stating before the cases rather than leaving a reader to infer it # from what is missing. # # THE DEFECT THIS PORT EXISTS TO REMOVE. The stock recipe installs lib/*.py # into ${libdir}/python3.12/site-packages/system_config_keyboard/ -- a PYTHON 3 # directory -- and two of the four modules that land there are Python 2 source: # keyboard_models.py:271 `print map' and keyboard.py:103 `raise KeyError, ...'. # Both are SyntaxErrors raised at COMPILE time, so the package's entire # importable deliverable was inert as shipped: not degraded, unreachable. The # bbappend's patch is the mechanical 2->3 change. Every import-side case below # is therefore a regression test on that patch -- measured against the # unpatched sources, the probe dies at its first import and every one of those # cases reds together. # # THE APPLICATION HALF IS OUT OF SCOPE, DELIBERATELY, AND IS PINNED AS SUCH. # keyboard_cli.py and keyboard_gui.py are Python 2 too, and /usr/sbin/system- # config-keyboard execs /usr/bin/python -- a Python 2 interpreter path that # does not exist here. They are NOT patched, because keyboard_cli's constructor # builds keyboard.Keyboard(), which opens dbus.SystemBus() and a # localed.LocaledWrapper: this image carries neither dbus nor systemd, so the # application cannot run under ANY interpreter. Porting it would produce code # that is still unreachable while making the package look as though it shipped # a working configurator. The cases at the end assert those facts AS FACTS -- # they pass on today's package -- so a version bump that repaired them upstream # flips them and prompts a re-look, rather than passing in silence. # # WHAT `read' AND `write' ACTUALLY TALK TO, because the name misleads. There is # no /etc/sysconfig/keyboard path in 1.4.0 and no file-backed configuration # reader at all: Keyboard.read() calls localed.get_keyboard_info() and # Keyboard.write() calls SetX11Keyboard/SetVConsoleKeyboard, both over the # system D-Bus to systemd-localed. The `instPath' argument both take is ignored # except for one os.unlink of a stale kmap. So there is no fixture file a suite # could point them at; the honest subject is the table, and that is what is # measured. # # NO CONSOLE, NO DISPLAY, NO DEVICE, NO NETWORK. Nothing here loads a keymap, # opens a tty or reaches a peer -- the table is offline data and the assertions # are a compile and a dict lookup, so this port closes offline and the suite # cannot go red on something else's outage. require python3 system-config-keyboard/present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/system_config_keyboard SHARE=/usr/share/system-config-keyboard SCRATCH=${TMPDIR:-/tmp}/bst-system-config-keyboard.$$ mkdir -p "$SCRATCH" 2>/dev/null # --------------------------------------------------------- the deliverable --- # Asserted as files first, so a red separates "not installed" from "installed # and does not work". All four modules get their own case: the two the patch # touches are the port's subject, and __init__.py is what makes the directory a # package at all -- without it the relative imports the patch introduces have # nothing to resolve against. check system-config-keyboard/site-packages-dir test -d "$SITE" check system-config-keyboard/package-dir test -d "$PKG" check system-config-keyboard/init test -f "$PKG/__init__.py" check system-config-keyboard/keyboard-module test -s "$PKG/keyboard.py" check system-config-keyboard/models-module test -s "$PKG/keyboard_models.py" check system-config-keyboard/localed-module test -s "$PKG/localed.py" # The rest of the package's payload. These are what the recipe's widened # FILES:${PN} sweeps in, and each is a separate install rule upstream, so each # can go missing on its own. check system-config-keyboard/sbin-wrapper test -x /usr/sbin/system-config-keyboard check system-config-keyboard/bin-wrapper test -x /usr/bin/system-config-keyboard check system-config-keyboard/desktop-file test -s /usr/share/applications/system-config-keyboard.desktop check system-config-keyboard/polkit-policy test -s /usr/share/polkit-1/actions/org.fedoraproject.config.keyboard.policy check system-config-keyboard/icon test -s /usr/share/icons/hicolor/48x48/apps/system-config-keyboard.png check system-config-keyboard/pixmap test -s "$SHARE/pixmaps/system-config-keyboard.png" # THE SYMLINK UPSTREAM'S Makefile CREATES BY HAND. `ln -sf' in an install rule # is work outside any packaging convention and is exactly what a cross build # can drop while every file list still looks complete -- the same class of # failure 99-sound-theme-freedesktop.test pins its eight aliases for. Asserted # three ways: that it is a link and not a copy, that it RESOLVES to the file # upstream declared, and that what it resolves to is readable through the link. # # THE TARGET IS ASSERTED RESOLVED (readlink -f), NOT AS THE RAW STRING, AND # THAT IS THE MEASURED CORRECTION OF A PREDICTION. Upstream's Makefile writes # an ABSOLUTE link and the .deb ships it verbatim -- checked, this build's # system-config-keyboard_1.4.0-r0_wasm32.deb carries # /usr/share/firstboot/modules/keyboard.py -> /usr/share/system-config- # keyboard/keyboard_gui.py. The MACHINE does not, and cannot: scripts/stage- # machine.sh step 1 walks the unpacked rootfs and rewrites EVERY absolute # symlink to a relative one ("ABSOLUTE symlinks trap the browser FUSE # bridge/kernel -- relativize all"), so on target the link reads # ../../system-config-keyboard/keyboard_gui.py. A raw-string compare against # the absolute spelling therefore asserts something the platform guarantees is # false on every staged machine, for every package, and it red-ed here on a # link that was correct and resolving. Comparing the RESOLVED path is the # property the packaging can actually lose, and it is invariant across the # relativizer. check system-config-keyboard/firstboot-is-link test -L /usr/share/firstboot/modules/keyboard.py check_eq system-config-keyboard/firstboot-target "$SHARE/keyboard_gui.py" \ "$(readlink -f /usr/share/firstboot/modules/keyboard.py)" # The relativized spelling itself, pinned as the measured fact it is: this goes # red if stage-machine.sh's relativizer changes shape or stops running, which # is a platform-wide change worth a re-look rather than a silent pass. check_eq system-config-keyboard/firstboot-relativized "../../system-config-keyboard/keyboard_gui.py" \ "$(readlink /usr/share/firstboot/modules/keyboard.py)" check system-config-keyboard/firstboot-resolves test -s /usr/share/firstboot/modules/keyboard.py # THE NO-.so ASSERTION. This image's python3 is one static binary with an empty # lib-dynload and this arm has no dynamic loader at all, so a version bump that # grew a compiled extension would install cleanly and die at import on target. # system-config-keyboard is pure Python and must stay so. The find would pass # VACUOUSLY against a directory that is not there, which is why the package-dir # case above precedes it. check_eq system-config-keyboard/no-so "" \ "$(find "$PKG" -name '*.so' 2>/dev/null)" # ----------------------------------------------------------- the probe run --- # Forked, so a failure to stage the exec costs this file its cases rather than # the run its terminator. One interpreter start-up answers every question # below. PROBE_OUT=$SCRATCH/probe.out ( python3 "$BST_DATA/system-config-keyboard-probe.py" > "$PROBE_OUT" 2>&1 ) # field : the value the probe printed, empty if it never got there. # Empty reds every expectation below, which is the intent -- a probe that died # half-way must not leave the cases it never reached looking untested. field() { sed -n "s/^$1 //p" "$PROBE_OUT" | head -1; } # The probe ran to the end. Every case after this reads a field; this one is # what tells a reader WHY a dozen went red at once. Against the UNPATCHED # package this is the first thing to fail, because the probe cannot import # keyboard_models at all. check_eq system-config-keyboard/probe-complete "yes" "$(field probe-complete)" # The module resolves to the package the recipe installed, from the # interpreter's own side. An import satisfied from some stray path would meet # every case below while the port had installed nothing. check_eq system-config-keyboard/import-path "$PKG/keyboard_models.py" \ "$(field module-file)" # ------------------------------------------------- the patch, as a negative --- # THE TWO CASES THIS WHOLE PORT TURNS ON. Both modules must COMPILE under # Python 3 -- which is a different and stronger question than whether they # import, because keyboard.py's imports reach dbus and this image has none. # Compiling is the half the patch fixed and the half that is fully testable # here; a SyntaxError is the defect returning, an ImportError is only the # absent dbus and would not show up in these cases at all. # # localed.py is asserted beside it because it was ALREADY clean and the patch # does not touch it -- the claim in the bbappend is that two of four modules # were broken, and a suite that only checked the two would not be able to tell # that from all four. check_eq system-config-keyboard/keyboard-py-compiles "yes" "$(field keyboard-compiles)" check_eq system-config-keyboard/localed-py-compiles "yes" "$(field localed-compiles)" # ------------------------------------------------------------- the table --- # THE ROW COUNT. 70 entries, measured on the shipped .deb rather than reasoned # from the source. This is the case that catches a table silently truncated by # a bad patch rebase -- every spot check below would still pass on a table that # had lost half its rows. check_eq system-config-keyboard/table-entries "70" "$(field entries)" # THREE PINNED ROWS, chosen to be different SHAPES rather than three of a kind: # # us the module's own hard-coded default (Keyboard.__init__ calls set("us")), # and the only row whose X model is not plain pc105. # de the simplest row there is -- one layout, no variant, no options. # ru the row that shows why the table exists: the console keymap `ru' maps to # a TWO-GROUP X layout `ru,us' plus a group-switch option, so console name # and X layout are emphatically not the same string. A port that collapsed # the row to its first field would pass `de' and fail here. # # The fields are name|xlayout|xmodel|variant|options, the order the module # returns them in. check_eq system-config-keyboard/row-us "U.S. English|us|pc105+inet||" "$(field row-us)" check_eq system-config-keyboard/row-de "German|de|pc105||" "$(field row-de)" check_eq system-config-keyboard/row-ru "Russian|ru,us|pc105||grp:shifts_toggle,grp_led:scroll" \ "$(field row-ru)" # The table's endpoints in sorted order -- a cheap whole-table assertion that a # spot check cannot make: it pins both ends at once, so a row added or removed # at either edge is caught even if the count case were somehow satisfied. check_eq system-config-keyboard/sorted-first "ar-azerty" "$(field sorted-first)" check_eq system-config-keyboard/sorted-last "us-acentos" "$(field sorted-last)" # STRUCTURAL INVARIANTS OVER EVERY ROW, not just the three pinned ones. Each is # a count of violations, so a red says how many rows are wrong rather than only # that one is. # # Five fields wide: every consumer unpacks these rows as a 5-tuple # (`(name, layout, model, variant, options) = kbd' appears three times in # keyboard.py), so a short row is a ValueError at the caller, not here. check_eq system-config-keyboard/rows-5-wide "0" "$(field rows-not-5-wide)" # No name still carries the `keyboard|' translation context. modelDict is a # property that maps every name through gkn(); a leaked prefix would be # USER-VISIBLE TEXT in a keyboard picker, which is the kind of defect that # ships because nothing crashes. check_eq system-config-keyboard/names-stripped "0" "$(field names-with-context-prefix)" # Every row names an X layout. A row whose layout were empty would set an empty # LAYOUT and configure nothing while reporting success. check_eq system-config-keyboard/xlayouts-nonempty "0" "$(field rows-with-empty-xlayout)" # gkn() itself, both branches: it strips a context prefix when there is a bar, # and passes the string through untouched when there is not. The second is the # one a naive `split("|")[1]' implementation would get wrong, and no row in the # table exercises it. check_eq system-config-keyboard/gkn-strips "U.S. English" "$(field gkn-strips)" check_eq system-config-keyboard/gkn-passthrough "no-bar-here" "$(field gkn-passes-through)" # ------------------------------------------------- the module's entry point --- # get_supported_models() is the only callable the module offers and it is the # one thing besides syntax that the patch changed: `keys()' is a VIEW in Python # 3 and has no .sort(), so the Python 2 spelling would raise AttributeError at # runtime even after the print was fixed. Asserted three ways -- it prints every # key, in sorted order, and the set it prints is exactly the table's own keys -- # because a function that printed a sorted SUBSET would satisfy the first two. check_eq system-config-keyboard/supported-count "70" "$(field supported-count)" check_eq system-config-keyboard/supported-sorted "yes" "$(field supported-sorted)" check_eq system-config-keyboard/supported-complete "yes" "$(field supported-matches-table)" # --------------------------------------- the application half, pinned as-is --- # NOT FAILURES. Each case below asserts what upstream ACTUALLY SHIPS, and each # passes today. They are here so the scope boundary is measured rather than # described in a comment nobody re-reads: if a future version bump ports the # application to Python 3, these flip red and say so, which is the moment to # revisit the bbappend's decision not to patch it. # # The /usr/sbin wrapper names /usr/bin/python -- the Python 2 interpreter path. check_out system-config-keyboard/sbin-names-python2-path "/usr/bin/python" \ cat /usr/sbin/system-config-keyboard # ...and that interpreter is absent here, so the wrapper cannot run. Asserted # as the absence it is, rather than by running the wrapper and reading a shell # error, which would report the same fact as a red. # # THE ABSENCE IS MEASURED, not assumed from "the image has no python2". # python3-core 3.12.13 ships exactly two names in ${bindir} -- python3.12 and # python3 -> python3.12 -- and no python3* package in the feed ships a bare # `python'. Worth pinning because the obvious expectation is wrong on an # ordinary developer host: Debian and Ubuntu carry /usr/bin/python -> python3 as # a compat symlink, so this case passes on the target and FAILS in a host-side # dry run, which is the opposite of the direction a stale assertion usually # breaks in. check system-config-keyboard/no-python2-interpreter test ! -e /usr/bin/python # keyboard_cli.py is still Python 2: `print _(...)', `keys.sort()' on a view, # and string.join, which Python 3 removed outright. grep -c rather than a # compile, because these files are DATA under /usr/share -- not importable # modules -- so no interpreter ever looks at them and py_compile would be # asserting something no consumer does. check_out system-config-keyboard/cli-is-python2 "print _(" \ cat "$SHARE/keyboard_cli.py" # The three application scripts are present as data even though none can run, # because the recipe packages them and a port that silently dropped files would # be a packaging regression whatever their interpreter. check system-config-keyboard/cli-installed test -s "$SHARE/keyboard_cli.py" check system-config-keyboard/tui-installed test -s "$SHARE/keyboard_tui.py" check system-config-keyboard/gui-installed test -s "$SHARE/keyboard_gui.py" check system-config-keyboard/main-installed test -s "$SHARE/system-config-keyboard.py" # THE PLATFORM FACT THE APPLICATION DIES ON, recorded once. dbus is not on this # image, so keyboard.py's module-scope `import dbus' cannot succeed and # Keyboard() cannot be constructed -- which is why read()/write() are untested # above rather than tested and failing. A skip, not a red: nothing is broken # here, a dependency is absent by design (see the bbappend for why python3-dbus # is deliberately not pulled in -- what these calls reach is systemd-localed, # which dbus alone would not supply). if [ "$(field dbus-import)" = "ok" ]; then # dbus arrived. That is not a failure either, but it changes what this port # can claim, so it is surfaced rather than silently passing. bst_ok system-config-keyboard/dbus-present else bst_skip system-config-keyboard/keyboard-runtime \ "python3 dbus module absent ($(field dbus-import)); Keyboard() needs dbus + systemd-localed, neither on this image" fi rm -rf "$SCRATCH" 2>/dev/null