. "$BST_LIB" require phytool phytool/present # phytool 2+git (SRCREV 8882328c08, meta-networking recipes-support/phytool) -- # Tobias Waldekranz's MDIO register accessor. Three C files and ONE kernel # entry point: everything it does is SIOCGMIIREG / SIOCSMIIREG on an AF_INET # datagram socket, with `iface/addr/reg` parsed out of argv into the # mii_ioctl_data the call carries. It ships two applets from one binary -- # /usr/bin/phytool and the /usr/bin/mv6tool symlink -- selected by __progname. # # THIS IMAGE HAS NO PHY LAYER AT ALL, and that is a property of the kernel # rather than of the port. wasm32_defconfig does turn on the netdev stack -- # CONFIG_NET, CONFIG_INET, CONFIG_UNIX, CONFIG_NETDEVICES, CONFIG_NET_CORE and # CONFIG_VIRTIO_NET are all set -- but CONFIG_ETHERNET is not, and nothing in # it selects PHYLIB: the built vmlinux.wasm carries no mdio_bus, no phy_device, # no phy_attach and no phylink symbol whatsoever (measured: 0 matches). So # there is no MDIO bus for a PHY to sit on, and no netdev here implements # ndo_eth_ioctl -- neither loopback nor virtio_net does. net/core/dev_ioctl.c # refuses such a netdev with -EOPNOTSUPP, which is what every ioctl-reaching # case below sees. # # WHICH INTERFACES EXIST IS A PROPERTY OF THE RUNTIME, NOT OF THE IMAGE, so # nothing below is written against a fixed set. Under the local functional # verify the guest is presented no virtio-mmio net device and /sys/class/net # holds `lo` alone (type 772, ARPHRD_LOOPBACK); a boot that IS given one gains # an ARPHRD_ETHER interface -- which still has no PHY behind it. That is # exactly why the probe below keys on the `phydev` link a PHY-attached netdev # publishes, and NOT on the ARPHRD type: keying on the type would red on a # perfectly healthy virtio-net machine. # # THE TICKET'S TWO PREMISES ABOUT THIS ARE BOTH WRONG, and the suite asserts # what the tool does rather than what the ticket hoped: # - "The SIOCGMIIREG / SIOCSMIIREG ioctls are not implemented behind it" is # right, but not for the stated reason and not only for the virtio # interface: there is no PHY layer in this kernel, so no interface of any # kind can answer, loopback included. # - phytool does NOT reject a malformed register specifier or an # out-of-range address. parse_phy_id() checks the ADDRESS and the segment # COUNT; the register goes through a bare strtoul(reg, NULL, 0) whose # result is never inspected, so `lo/0/zzz` silently becomes register 0 and # `lo/0x20/0` silently becomes address 32. Both reach the ioctl. That # non-rejection is asserted below, because it is the behaviour a reader # needs to know and the one a future version would change. # # So the suite splits three ways: # # ASSERTED FOR REAL, needing no PHY -- both applets' usage text and the # stream and status it uses; every location the parser rejects; every form # it accepts and hands to the ioctl; the DOCUMENTED STATUS for an unknown # interface; and the two behaviours that only appear when no PHY answers # (below). # # ASSERTED FOR REAL BECAUSE NO PHY ANSWERS -- the ioctl path end to end. An # unknown interface name is refused with -ENODEV and a real one with # -EOPNOTSUPP, and that DIFFERENCE is the proof that the name parsed out of # argv reached __dev_get_by_name: a location parser that dropped or mangled # the interface field would answer ENODEV for both. The summary `print` form # then exposes phytool's own arithmetic on the failure -- see the id cases. # # PROBED, then skipped -- register VALUES: a read that returns a number and # a write that lands. Those need a PHY on an MDIO bus, and no package port # can supply one (unlike 99-mbpoll.test, which ships its own Modbus slave, # the far end here is a kernel bus driver and a chip, not a userspace peer). # The probe is phytool's OWN read, run over every interface the machine has # and cross-read against each interface's ARPHRD type in sysfs, so a probe # that fails IS the port failing rather than a proxy for it. # # Every expectation that does not depend on the kernel was executed against a # native x86-64 build of the identical source (src/phytool_mknative.sh) before # it was written down, rc and output compared. # --- the package ------------------------------------------------------------- check phytool/path test -x /usr/bin/phytool check phytool/mv6-symlink test -L /usr/bin/mv6tool check_eq phytool/mv6-target "phytool" "$(readlink /usr/bin/mv6tool 2>&1)" check_out phytool/on-path "/usr/bin/phytool" command -v phytool check_out phytool/mv6-on-path "/usr/bin/mv6tool" command -v mv6tool # _case : ok iff exits AND its # combined output contains . bst.sh has check_out for the rc-0 form and # check_rc for the status alone; nearly every assertion here needs both halves, # because phytool answers almost everything with status 1 and the whole of the # information is in the string -- and because its ONE rc-0-on-failure path # (below) is only visible if the status is asserted beside the text. _case() { _cid=$1; _cwant=$2; _cn=$3; shift 3 _co=$("$@" 2>&1); _cr=$? case "$_co" in *"$_cn"*) _ch=1 ;; *) _ch=0 ;; esac if [ "$_cr" -eq "$_cwant" ] && [ "$_ch" -eq 1 ]; then bst_ok "$_cid" elif bst_execfail "$_co"; then bst_unrun "$_cid" "$_co" else bst_notok "$_cid" "rc=$_cr want=$_cwant needle:'$_cn' got:'$(_bst_1line "$_co")'"; fi } # _silent : ok iff exits AND printed # NOTHING on either stream. phytool has four such paths and they are worth # asserting as silence rather than as status alone -- a version that started # printing usage there would still exit 1 and satisfy a status-only case. _silent() { _sid=$1; _swant=$2; shift 2 _so=$("$@" 2>&1); _sr=$? if [ "$_sr" -eq "$_swant" ] && [ -z "$_so" ]; then bst_ok "$_sid" elif bst_execfail "$_so"; then bst_unrun "$_sid" "$_so" else bst_notok "$_sid" "rc=$_sr want=$_swant want-silent got:'$(_bst_1line "$_so")'"; fi } # --- applet dispatch (__progname) -------------------------------------------- # One binary, two applets, chosen by comparing __progname against the applet # table. This is the only thing the mv6tool symlink does, and it is invisible # to any case that runs phytool alone: a build whose libc did not export # __progname, or an install that copied the binary instead of linking it, # would fall through to applets[0] and answer the PHYTOOL usage under the # mv6tool name. The two banners are disjoint, so each is asserted to carry its # own first line AND not to carry the other's. _case phytool/usage-first-line 1 "Usage: phytool read IFACE/ADDR/REG" phytool _case phytool/mv6-first-line 1 "Usage: mv6tool read LOCATION/REG" mv6tool _mv6o=$(mv6tool 2>&1) case "$_mv6o" in *"IFACE/ADDR/REG"*) bst_notok phytool/mv6-not-phytool "mv6tool answered the phytool banner: applet dispatch fell through to applets[0]" ;; *) bst_ok phytool/mv6-not-phytool ;; esac _pho=$(phytool 2>&1) case "$_pho" in *"LOCATION := IFACE"*) bst_notok phytool/phy-not-mv6 "phytool answered the mv6tool banner" ;; *) bst_ok phytool/phy-not-mv6 ;; esac # Both cases above rest on the dispatch being by NAME, so prove that directly # rather than assuming it. Run the same binary under a third name and it must # fall back to applets[0] (phytool); run a COPY -- not the shipped symlink -- # named mv6tool and it must still select the mv6 applet. Together those are # what make `mv6-not-phytool` discriminating: without them a build in which # both applets printed the same thing would pass every case above. _tmp=${TMPDIR:-/tmp} _alias=$_tmp/bst-phytool-alias.$$ _m2=$_tmp/mv6tool if cp /usr/bin/phytool "$_alias" 2>/dev/null && chmod +x "$_alias" 2>/dev/null; then _ao=$("$_alias" 2>&1) # usage() interpolates __progname into every form line, so the fallback is # proved by the GRAMMAR it printed, not by the name -- a third name prints # its own argv[0] beside the phytool grammar. Measured: a copy at # $TMPDIR/bst-phytool-alias. answers # "Usage: bst-phytool-alias. read IFACE/ADDR/REG". case "$_ao" in *"read IFACE/ADDR/REG"*"ADDR := C22 | C45"*) bst_ok phytool/applet-fallback ;; *) bst_notok phytool/applet-fallback "a name that is neither applet did not fall back to applets[0]: '$(_bst_1line "$_ao")'" ;; esac # ...and that interpolation is itself the proof that __progname reached the # banner: a libc that left it empty, or a build that hard-coded the name, # would not name the file it was actually invoked as. _aname=${_alias##*/} case "$_ao" in *"Usage: $_aname read"*) bst_ok phytool/usage-names-argv0 ;; *) bst_notok phytool/usage-names-argv0 "the banner does not name the invoked program ($_aname): '$(_bst_1line "$_ao")'" ;; esac rm -f "$_alias" else bst_notok phytool/applet-fallback "cannot stage a copy of /usr/bin/phytool at $_alias" bst_notok phytool/usage-names-argv0 "cannot stage a copy of /usr/bin/phytool at $_alias" fi if cp /usr/bin/phytool "$_m2" 2>/dev/null && chmod +x "$_m2" 2>/dev/null; then _mo2=$("$_m2" 2>&1) case "$_mo2" in *"Usage: mv6tool read LOCATION/REG"*) bst_ok phytool/applet-by-progname ;; *) bst_notok phytool/applet-by-progname "a copy named mv6tool did not select the mv6 applet, so __progname is not what dispatches: '$(_bst_1line "$_mo2")'" ;; esac rm -f "$_m2" else bst_notok phytool/applet-by-progname "cannot stage a copy of /usr/bin/phytool at $_m2" fi # --- the usage banner: stream and status ------------------------------------- # usage() is reached by running either applet with no arguments at all. It # prints to STDOUT -- not stderr, which is where a usage message is more often # sent -- and exits 1. Each of the three is asserted separately. _uso=$(phytool 2>/dev/null); _use=$(phytool 2>&1 >/dev/null) case "$_uso" in *"Usage: phytool read"*) bst_ok phytool/usage-on-stdout ;; *) bst_notok phytool/usage-on-stdout "banner not on stdout: '$(_bst_1line "$_uso")'" ;; esac if [ -z "$_use" ]; then bst_ok phytool/usage-stderr-empty else bst_notok phytool/usage-stderr-empty "usage() writes to stderr: '$(_bst_1line "$_use")'"; fi check_rc phytool/usage-rc 1 phytool check_rc phytool/mv6-usage-rc 1 mv6tool # --- the usage GRAMMAR, swept whole ------------------------------------------ # The grammar block is the tool's whole documentation of its argument syntax # and every line of it is a separate printf argument in one format string. # Sweeping it as a table is how it fails: a line dropped from the format string # still leaves a banner that a single "does it print usage" case accepts. _sweep_bad= _sweep() { _sid=$1; _stext=$2; shift 2 _sweep_bad= for _k in "$@"; do case "$_stext" in *"$_k"*) ;; *) _sweep_bad="$_sweep_bad [$_k]" ;; esac done if [ -z "$_sweep_bad" ]; then bst_ok "$_sid" else bst_notok "$_sid" "missing from the banner:$_sweep_bad"; fi } _sweep phytool/usage-forms "$_pho" \ "phytool read IFACE/ADDR/REG" \ "phytool write IFACE/ADDR/REG <0-0xffff>" \ "phytool print IFACE/ADDR[/REG]" _sweep phytool/usage-grammar "$_pho" \ "ADDR := C22 | C45" \ "C22 := <0-0x1f>" \ "C45 := <0-0x1f>:<0-0x1f>" \ "REG := <0-0x1f>" _sweep phytool/usage-examples "$_pho" \ "phytool read eth0/0:3/1" \ "phytool write eth0/0xa/0 0x1140" \ "phytool print eth0/0x1c" _sweep phytool/usage-prose "$_pho" \ "Not all MDIO drivers support the \`port:device\` Clause 45 address" \ "simple register level" \ "the register is optional" \ "https://github.com/wkz/phytool/issues" _sweep phytool/mv6-usage-forms "$_mv6o" \ "mv6tool read LOCATION/REG" \ "mv6tool write LOCATION/REG <0-0xffff>" \ "mv6tool print LOCATION[/REG]" \ "mv6tool print IFACE" _sweep phytool/mv6-usage-grammar "$_mv6o" \ "LOCATION := IFACE/ | DEV/" \ "DEV := <0-0x1f>" \ "ADDR := <0-0x1f>" \ "N := <0-0xa>" \ "G := <0-2>" _sweep phytool/mv6-usage-examples "$_mv6o" \ "mv6tool read cpu0/port/0" \ "mv6tool write 1/global1/2 0x1337" \ "mv6tool print eth2-1" # --- the four SILENT failures ------------------------------------------------ # read/write/print with no location at all, and write with a location and no # value, all return 1 from main() without printing anything -- they never reach # parse_loc, and neither of the two paths that print does. Asserted as silence. _silent phytool/read-no-loc 1 phytool read _silent phytool/write-no-loc 1 phytool write _silent phytool/print-no-loc 1 phytool print _silent phytool/write-no-value 1 phytool write lo/0/0 # --- the locations the parser REJECTS ---------------------------------------- # loc_segments() splits on '/' and the strict flag decides the minimum count: # read and write demand three segments, print two. parse_phy_id() then demands # that the address consume its whole token, either as a plain number or as the # `port:device` Clause 45 pair. Everything below fails one of those two, and # every one of them is refused by the same string on stderr with status 1. _case phytool/reject-read-2seg 1 "error: bad location format" phytool read eth0/0 _case phytool/reject-write-2seg 1 "error: bad location format" phytool write eth0/0 0x1140 _case phytool/reject-print-1seg 1 "error: bad location format" phytool print eth0 _case phytool/reject-bad-addr 1 "error: bad location format" phytool read eth0/abc/0 _case phytool/reject-bad-c45-dev 1 "error: bad location format" phytool read eth0/0:abc/0 _case phytool/reject-addr-junk 1 "error: bad location format" phytool read eth0/0x1f%/0 # The refusal is on STDERR and the tool prints nothing on stdout for it. _rso=$(phytool read eth0/0 2>/dev/null) if [ -z "$_rso" ]; then bst_ok phytool/reject-stdout-empty else bst_notok phytool/reject-stdout-empty "bad-location message on stdout: '$(_bst_1line "$_rso")'"; fi # --- the unknown-command path is DEAD CODE ----------------------------------- # main() dispatches read/write/print and its final `else` calls print with the # whole of argv[1..] -- so the fprintf(stderr, "error: unknown command ...") # and usage() call that follow it are UNREACHABLE. `phytool frobnicate` is # therefore a one-segment PRINT, refused as a bad location, and no version of # it prints the words "unknown command". Ours to record, not to fix: it is # third-party code and no user is harmed by it, so it is kept in our tree per # docs/conventions/no-public-upstream-reports.md. _case phytool/unknown-cmd-is-print 1 "error: bad location format" phytool frobnicate _uco=$(phytool frobnicate 2>&1) case "$_uco" in *"unknown command"*) bst_notok phytool/unknown-cmd-dead-code "the unreachable branch printed: '$(_bst_1line "$_uco")'" ;; *) bst_ok phytool/unknown-cmd-dead-code ;; esac # --- mv6tool's own location grammar ------------------------------------------ # mv6tool_parse_loc() tries three things in order: an IFACE whose sysfs carries # phys_switch_id/phys_port_id, then a numeric switch id matched against every # interface's phys_switch_id, then the plain phytool grammar as a fallback. # This image has no DSA switch, so no interface carries phys_switch_id and the # glob matches nothing -- every form below reaches the fallback and is refused # there. That is the assertion: it proves the two switch paths were TRIED and # declined, since a build in which the fallback was reached first would answer # the same way for `1/global1/0` but differently for a real interface name. _case phytool/mv6-reject-2seg 1 "error: bad location format" mv6tool read 1/global1 _case phytool/mv6-reject-global 1 "error: bad location format" mv6tool read 1/global1/0 _case phytool/mv6-reject-port 1 "error: bad location format" mv6tool print lo/port/0 _case phytool/mv6-reject-iface 1 "error: bad location format" mv6tool print lo _case phytool/mv6-reject-serdes 1 "error: bad location format" mv6tool read 1/serdes/0 _case phytool/mv6-reject-write 1 "error: bad location format" mv6tool write 1/global1/2 0x1337 # --- probe: is there an MDIO-attached PHY on this machine? -------------------- # The probe is phytool's own read, byte for byte the operation the skipped # cases would perform, run over every interface in sysfs. It exits 0 and prints # a 0x-prefixed value when a PHY answered, and non-zero otherwise. # # The cross-read is the `phydev` link: net-sysfs publishes /sys/class/net// # phydev exactly when a PHY device is attached to that netdev, so it is the # kernel's own answer to the question this suite is asking. The ARPHRD type is # NOT that answer -- virtio_net is ARPHRD_ETHER and has no PHY -- so it is # collected as evidence and never used as the trigger. _ifs= _first= _answered= _phy= for _d in /sys/class/net/*; do [ -e "$_d" ] || continue _n=${_d##*/} [ -n "$_first" ] || _first=$_n _t=$(cat "$_d/type" 2>/dev/null) [ -e "$_d/phydev" ] && _phy="$_phy $_n" _po=$(phytool read "$_n/0/0" 2>&1); _pr=$? [ "$_pr" -eq 0 ] && _answered="$_answered $_n" _ifs="$_ifs $_n(type=$_t,phydev=$([ -e "$_d/phydev" ] && echo yes || echo no),rc=$_pr,$(_bst_1line "$_po"))" done [ -n "$_ifs" ] || _ifs=" (no interfaces in /sys/class/net)" _mdiobus=$(ls /sys/class/mdio_bus 2>/dev/null | tr '\n' ' ') [ -n "$_mdiobus" ] || _mdiobus="(no /sys/class/mdio_bus)" # The probe passes in both self-consistent worlds -- no PHY attached anywhere, # or a PHY that answers -- and reds only on the contradiction: the kernel # advertising an attached PHY that phytool cannot reach, which is a finding # rather than the expected shape of this image. if [ -n "$_answered" ]; then bst_ok phytool/probe-mdio elif [ -z "$_phy" ]; then bst_ok phytool/probe-mdio else bst_notok phytool/probe-mdio \ "the kernel advertises an attached PHY on$_phy but none answered SIOCGMIIREG --$_ifs" fi # The interface the ioctl cases below are aimed at. Its identity does not # matter to them -- what matters is that it EXISTS, so that the ENODEV and # EOPNOTSUPP arms are genuinely different. _if=lo [ -d /sys/class/net/lo ] || _if=$_first if [ -z "$_if" ]; then for _skid in ioctl-unknown-iface ioctl-unknown-write ioctl-present-iface \ ioctl-present-write ioctl-errno-distinct print-summary-rc \ print-summary-id print-id-is-errno print-reg-rc \ accept-c45 accept-oor-addr accept-oor-reg accept-bad-reg; do bst_skip "phytool/$_skid" "this image has no network interface at all, so no ioctl-reaching case can run --$_ifs" done elif [ -n "$_answered" ]; then for _skid in ioctl-present-iface ioctl-present-write ioctl-errno-distinct \ print-summary-rc print-summary-id print-id-is-errno print-reg-rc; do bst_skip "phytool/$_skid" "a PHY answers on$_answered, so these cases assert the no-PHY refusal path which is not reachable here" done else # ---- the ioctl path, end to end, with no PHY -------------------------- # An interface name no netdev carries is refused by __dev_get_by_name with # -ENODEV; a real one is found and then refused by dev_eth_ioctl with # -EOPNOTSUPP because loopback has no ndo_eth_ioctl. phytool prints the # NEGATED errno, so those read as (-19) and (-95). _case phytool/ioctl-unknown-iface 1 "error: phy_read (-19)" phytool read bstnosuch0/0/0 _case phytool/ioctl-unknown-write 1 "error: phy_write (-19)" phytool write bstnosuch0/0/0 0x1140 _case phytool/ioctl-present-iface 1 "error: phy_read (-95)" phytool read "$_if/0/0" _case phytool/ioctl-present-write 1 "error: phy_write (-95)" phytool write "$_if/0/0" 0x1140 # The two arms are DIFFERENT, and that difference is the assertion: it is # what proves the interface field parsed out of argv actually reached the # kernel's netdev lookup. A parser that dropped it, or that always passed # the same bytes, would answer identically for both. _e_unknown=$(phytool read bstnosuch0/0/0 2>&1) _e_present=$(phytool read "$_if/0/0" 2>&1) if [ "$_e_unknown" != "$_e_present" ]; then bst_ok phytool/ioctl-errno-distinct else bst_notok phytool/ioctl-errno-distinct \ "an unknown interface and $_if answered identically ('$(_bst_1line "$_e_present")'), so the parsed name may not reach the ioctl"; fi # ---- what the SUMMARY print does when nothing answers ------------------ # `print IFACE/ADDR` with no register takes print_phy_ieee()'s summary # path. It calls phy_id() (two reads), prints the id, and then pretty-prints # registers 0 and 1 -- and it DISCARDS every error on the way, so it exits 0 # after six failed reads. That rc-0-on-total-failure is upstream behaviour, # not a port artifact; it is asserted rather than filed, per # docs/conventions/no-public-upstream-reports.md. _case phytool/print-summary-rc 0 "ieee-phy: id:" phytool print "$_if/0" # The id it prints is arithmetic on the failure, not a chip's answer. # phy_id() stores each phy_read() return into a uint16_t, so a negative # errno is truncated: -95 becomes 0xffa1, and the two halves are shifted # together into 0xffa1ffa1. The literal pins today's value; the derivation # below pins the MECHANISM, so a kernel that starts refusing with a # different errno reds the literal alone and says so. _case phytool/print-summary-id 0 "ieee-phy: id:0xffa1ffa1" phytool print "$_if/0" _errno=$(phytool read "$_if/0/0" 2>&1 | sed -n 's/^error: phy_read (\(-[0-9][0-9]*\))$/\1/p') if [ -n "$_errno" ]; then _half=$(printf '%04x' $(( (65536 + _errno) % 65536 ))) _case phytool/print-id-is-errno 0 "ieee-phy: id:0x$_half$_half" phytool print "$_if/0" else bst_notok phytool/print-id-is-errno \ "could not read an errno out of phytool's own message: '$(_bst_1line "$_e_present")'" fi # Naming a register takes ieee_one() instead, which propagates the failure # all the way out -- so the SAME location that exits 0 above exits 1 here. _case phytool/print-reg-rc 1 "error: phy_read (-95)" phytool print "$_if/0/1" # ---- what the parser ACCEPTS and hands to the ioctl -------------------- # Each of these reaches the ioctl -- proved by the refusal string, which # only the ioctl can produce -- rather than being rejected by the parser. # The last three are the non-rejections the ticket expected to be # rejections: the documented ranges (<0-0x1f>) are documentation only. _case phytool/accept-c45 1 "error: phy_read (-95)" phytool read "$_if/0:3/1" _case phytool/accept-oor-addr 1 "error: phy_read (-95)" phytool read "$_if/0x20/0" _case phytool/accept-oor-reg 1 "error: phy_read (-95)" phytool read "$_if/0/0x20" _case phytool/accept-bad-reg 1 "error: phy_read (-95)" phytool read "$_if/0/zzz" fi # ---- the register VALUES: the only thing that needs a PHY ------------------- # A read that returns a number, a write that lands, and the pretty-printer's # BMCR/BMSR decode of real register contents. Unlike 99-mbpoll.test, which # ships its own Modbus slave because the far end is a userspace peer, the far # end here is a kernel MDIO bus driver and a chip on it -- nothing a package # port can supply. Skipped only on the probe above. if [ -n "$_answered" ]; then _pv=$(phytool read "${_answered# }/0/0" 2>&1) case "$_pv" in 0x????) bst_ok phytool/read-value ;; *) bst_notok phytool/read-value "a PHY answered the probe but the read did not print a 4-digit hex word: '$(_bst_1line "$_pv")'" ;; esac _case phytool/print-bmcr 0 "reg:BMCR(0x00)" phytool print "${_answered# }/0/0" _case phytool/print-bmsr 0 "reg:BMSR(0x01)" phytool print "${_answered# }/0/1" else _why="no MDIO-attached PHY on this image -- nothing in wasm32_defconfig selects PHYLIB, so the built kernel carries no mdio_bus/phy_device symbol and no netdev implements ndo_eth_ioctl; no interface answered phytool's own SIOCGMIIREG read and none publishes a phydev link. mdio_bus: $_mdiobus. Interfaces seen:$_ifs" bst_skip phytool/read-value "$_why" bst_skip phytool/print-bmcr "$_why" bst_skip phytool/print-bmsr "$_why" bst_skip phytool/write-value "$_why" fi