Source code for tbottest.tc.watchdog

"""
Watchdog testcases.

A watchdog that is never tested is a watchdog that may not work, and the way
to test one is to stop feeding it and see the board come back. That is what
board_lnx_watchdog_bites() does.

Generic: what feeds the watchdog is a systemd service whose name is handed
in, and what a reset looks like from the bootloader is handed in as a
callable, because only the board knows which register says so and what it
says.
"""

import typing
import tbot
from tbot.machine import linux
from tbot.context import Optional


[docs] def lnx_service_main_pid(lnx: linux.LinuxShell, service: str) -> str: """ the main PID of a systemd service, empty string when it is not running :param lnx: board linux machine :param service: name of the service """ ret, log = lnx.exec("systemctl", "show", "-p", "MainPID", "--value", service) if ret != 0: return "" pid = log.strip() return "" if pid in ("", "0") else pid
[docs] def lnx_pid_holds(lnx: linux.LinuxShell, pid: str, path: str) -> bool: """ does this process hold that file open Reads /proc/<pid>/fd rather than using fuser or lsof, so it works on an image that ships neither. :param lnx: board linux machine :param pid: process id as a string :param path: the file to look for """ ret, log = lnx.exec( "sh", "-c", f"for f in /proc/{pid}/fd/*; do readlink $f; done", ) if ret != 0: return False return path in log.split()
[docs] @tbot.testcase def board_lnx_watchdog_bites( lab: Optional[linux.LinuxShell] = None, service: str = "", device: str = "/dev/watchdog", check: typing.Optional[typing.Callable[[typing.Any], bool]] = None, boot_timeout: float = 120, ) -> bool: """ stop feeding the watchdog and prove that it resets the board The one test a watchdog cannot pass on paper. Everything up to here can look right, the service can be running and the device can be open, and the watchdog can still be doing nothing because the bootloader never started it or because the kernel is quietly feeding it instead. The run: check that the service holds the device, stop it, and wait for the board to come up in the bootloader on its own. Nobody asked it to reboot, so arriving there at all is the result. What kind of reset it was is left to the caller through check(), and the board is booted back into linux at the end so it is not left sitting at a prompt. The board and the lab host are requested once and held, the U-Boot and Linux machines are built from the board object rather than requested as roles. Requesting them takes the board with exclusive=True, and an exclusively requested instance is torn down on release, which would power the board off in the middle of the run and answer the question with a power cycle instead of a watchdog. :param lab: lab host, requested when not given :param service: the systemd service that feeds the watchdog :param device: the watchdog device that service is expected to hold :param check: called with the U-Boot machine that came up after the reset, for whatever the board can say about what kind of reset it was. Returns whether that was the expected kind. :param boot_timeout: seconds to wait for the board to come back. The budget is the watchdog timeout, which starts at the last feed just before the service was stopped, plus the boot to the prompt. Set so that a watchdog which does not bite fails the testcase instead of hanging it. """ if service == "": raise RuntimeError("please configure service") with tbot.ctx() as cx: if lab is None: lab = cx.request(tbot.role.LabHost) b = cx.request(tbot.role.Board) ub_class = cx.get_machine_class(tbot.role.BoardUBoot) lnx_class = cx.get_machine_class(tbot.role.BoardLinux) # Same machine, but it gives up waiting. Without a boot_timeout the # connector waits for the prompt forever, and a watchdog that does # not bite would hang the run rather than fail it. ub_wait_class = type( "UBootWaitForWatchdog", (ub_class,), {"boot_timeout": boot_timeout} ) retval = True had_nopoweroff = "nopoweroff" in tbot.flags # The whole point is that the board resets by itself. A teardown # that cuts the power would look exactly the same from the outside. tbot.flags.add("nopoweroff") try: with ub_class(b) as ub: with lnx_class(ub) as lnx: pid = lnx_service_main_pid(lnx, service) if pid == "": tbot.log.message( tbot.log.c( f"{service} is not running, nothing feeds {device}" ).red ) return False if not lnx_pid_holds(lnx, pid, device): tbot.log.message( tbot.log.c( f"{service} runs as pid {pid} but does " f"not hold {device}" ).red ) return False tbot.log.message( tbot.log.c(f"{service} holds {device} as pid {pid}").green ) # Every machine below has to have a boot to catch, so # the stop goes out here and the block ends with it. # There is no prompt to wait for either: the shell dies # with the board somewhere in the next seconds. lnx.ch.sendline(f"systemctl stop {service}") tbot.log.message( f"{service} stopped, waiting up to {boot_timeout:.0f}s " "for the board to reset" ) with ub_wait_class(b) as ub: tbot.log.message( tbot.log.c("the board reset itself with nobody asking it to").green ) if check is not None and not check(ub): retval = False # Back into linux, so the board is left running rather than # sitting at a prompt with an unfed watchdog. with lnx_class(ub) as lnx: pid = lnx_service_main_pid(lnx, service) if pid == "": tbot.log.message( tbot.log.c( f"{service} did not come back after the reset" ).red ) retval = False else: tbot.log.message( tbot.log.c(f"{service} is feeding {device} again").green ) finally: if not had_nopoweroff: tbot.flags.discard("nopoweroff") return retval