[test] Implement rom_e2e_asm_watchdog bite tests

Test that the watchdog's bite resets the core. This is achieved outside
of GDB, by inspecting its output.

This commit adds the --gdb-expect-output-sequence flag to the GDB test
coordinator script. When the script sees GDB print each of the given
lines, in sequence, it will terminate GDB on the spot and consider it a
success.

This enables us to assert that GDB reaches an echo line from the GDB
script and *then* sees the core reset.

Signed-off-by: Dan McArdle <dmcardle@google.com>
diff --git a/rules/opentitan_gdb_test.bzl b/rules/opentitan_gdb_test.bzl
index 4f8c1d6..327098c 100644
--- a/rules/opentitan_gdb_test.bzl
+++ b/rules/opentitan_gdb_test.bzl
@@ -30,6 +30,9 @@
     ]
     if ctx.attr.exit_success_pattern != None:
         args.append(("--exit-success-pattern", ctx.attr.exit_success_pattern))
+    for output in ctx.attr.gdb_expect_output_sequence:
+        args.append(("--gdb-expect-output-sequence", output))
+
     arg_lines = ["{}={}".format(flag, shell.quote(value)) for flag, value in args]
     test_script += " \\\n".join(arg_lines)
 
@@ -80,6 +83,7 @@
             allow_single_file = True,
         ),
         "rom_kind": attr.string(mandatory = True, values = ["Rom", "TestRom"]),
+        "gdb_expect_output_sequence": attr.string_list(),
         "_coordinator": attr.label(
             default = "//rules/scripts:gdb_test_coordinator",
             cfg = "exec",
diff --git a/rules/scripts/gdb_test_coordinator.py b/rules/scripts/gdb_test_coordinator.py
index bbf597b..e89037a 100644
--- a/rules/scripts/gdb_test_coordinator.py
+++ b/rules/scripts/gdb_test_coordinator.py
@@ -15,7 +15,7 @@
 import selectors
 import subprocess
 import sys
-from typing import Dict, List, NewType, Optional, TextIO, Tuple
+from typing import Callable, Dict, List, NewType, Optional, TextIO, Tuple
 
 import rich
 import typer
@@ -34,8 +34,11 @@
         self.names: Dict[subprocess.Popen, str] = {}
         self.console = rich.console.Console(color_system="256")
 
-    def run(self, command: List[str], label: str,
-            style: ConsoleStyle) -> subprocess.Popen:
+    def run(self,
+            command: List[str],
+            label: str,
+            style: ConsoleStyle,
+            callback: Callable[[str], None] = None) -> subprocess.Popen:
         proc = subprocess.Popen(command,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.STDOUT,
@@ -49,6 +52,9 @@
             self.console.print(f"[{label}] ", style=style, end='')
             print(line, flush=True)
 
+            if callback is not None:
+                callback(line)
+
         self.selector.register(proc.stdout, selectors.EVENT_READ, echo)
         return proc
 
@@ -102,6 +108,7 @@
          openocd_earlgrey_config: str = typer.Option(...),
          openocd_jtag_adapter_config: str = typer.Option(...),
          gdb_path: str = typer.Option(...),
+         gdb_expect_output_sequence: List[str] = typer.Option(None),
          gdb_script_path: str = typer.Option(...),
          bitstream_path: str = typer.Option(...),
          opentitantool_path: str = typer.Option(...),
@@ -144,6 +151,21 @@
     if exit_success_pattern is not None:
         console_command.append("--exit-success=" + exit_success_pattern)
 
+    # When `gdb_expect_output_sequence` is non-empty, change the definition of
+    # success for GDB. If the given lines are a subsequence of GDB's output,
+    # then GDB was successful, regardless of its exit status.
+    gdb_alternative_success_mode = gdb_expect_output_sequence != []
+
+    def gdb_maybe_consume_expected_line(line: str):
+        """Pops the front of `gdb_expect_output_sequence` if `line` matches.
+        """
+        if gdb_expect_output_sequence == []:
+            return
+        assert gdb_alternative_success_mode
+        want_line = gdb_expect_output_sequence[0]
+        if want_line.rstrip() == line.rstrip():
+            gdb_expect_output_sequence.pop(0)
+
     # Wait until we've finished loading the bitstream.
     subprocess.run(load_bitstream_command, check=True)
 
@@ -158,7 +180,10 @@
     background.block_until_line_contains(
         openocd, "Examined RISC-V core; found 1 harts")
 
-    background.run(gdb_command, "GDB", COLOR_GREEN)
+    gdb = background.run(gdb_command,
+                         "GDB",
+                         COLOR_GREEN,
+                         callback=gdb_maybe_consume_expected_line)
     background.run(console_command, "CONSOLE", COLOR_RED)
 
     while not background.empty():
@@ -166,6 +191,15 @@
 
         proc = background.pop()
 
+        # If we are defining GDB's success by checking output lines and we've
+        # seen all of the expected lines, kill GDB and ignore its return code.
+        if proc == gdb and gdb_alternative_success_mode and gdb_expect_output_sequence == []:
+            print("Terminating GDB now that it has printed the expected lines")
+            gdb.terminate()
+            gdb.wait()
+            background.forget(gdb)
+            continue
+
         # When OpenOCD is the only remaining process, send it the TERM signal
         # and wait for it to exit. GDB will exit naturally at the end of its
         # script. The opentitantool console will either time out or exit due to
diff --git a/sw/device/silicon_creator/rom/e2e/BUILD b/sw/device/silicon_creator/rom/e2e/BUILD
index a45668a..c1f482d 100644
--- a/sw/device/silicon_creator/rom/e2e/BUILD
+++ b/sw/device/silicon_creator/rom/e2e/BUILD
@@ -1574,7 +1574,7 @@
 # been artificially inflated.
 [
     opentitan_gdb_fpga_cw310_test(
-        name = "rom_e2e_asm_watchdog_fpga_cw310_test_otp_" + otp_name,
+        name = "rom_e2e_asm_watchdog_bark_fpga_cw310_test_otp_" + otp_name,
         timeout = "short",
         gdb_script = """
             target extended-remote :3333
@@ -1645,13 +1645,82 @@
     for otp_name in OTP_CFGS_EXEC_DISABLED
 ]
 
+# Ensure that the watchdog's bite restarts the ROM.
+[
+    opentitan_gdb_fpga_cw310_test(
+        name = "rom_e2e_asm_watchdog_bite_fpga_cw310_test_otp_" + otp_name,
+        timeout = "short",
+        gdb_expect_output_sequence = [
+            ":::: Wait for interrupt.",
+            "Hart 0 unexpectedly reset!",
+        ],
+        gdb_script = """
+            target extended-remote :3333
+
+            echo :::: Send OpenOCD the 'reset halt' command.\\n
+            monitor reset halt
+
+            echo :::: Load ROM symbols into GDB.\\n
+            file rom.elf
+
+            echo :::: Run until we check whether ROM execution is enabled.\\n
+            break kRomStartBootMaybeHalt
+            continue
+
+            printf ":::: PC=%p. Expected PC=%p.\\n", $pc, kRomStartBootMaybeHalt
+            if $pc != kRomStartBootMaybeHalt
+                quit 42
+            end
+
+            echo :::: Pretend execution is enabled.\\n
+            set $pc = kRomStartBootExecEn
+
+            echo :::: Run until right after configuring the watchdog timer.\\n
+            break kRomStartWatchdogEnabled
+            continue
+
+            printf ":::: PC=%p. Expected PC=%p.\\n", $pc, kRomStartWatchdogEnabled
+            if $pc != kRomStartWatchdogEnabled
+                quit 43
+            end
+
+            echo :::: Set breakpoint on NMI handler.\\n
+            delete breakpoints
+            break _asm_exception_handler
+
+            echo :::: Wait for interrupt.\\n
+            set $pc = kRomStartBootMaybeHalt
+            echo :::: Continue.\\n
+            continue
+
+            # Not reached.
+            quit 123
+        """,
+        gdb_script_symlinks = {
+            "//sw/device/silicon_creator/rom:rom_fpga_cw310.elf": "rom.elf",
+        },
+        rom_bitstream = ":rom_otp_{}_exec_disabled".format(otp_name),
+        rom_kind = "Rom",
+        tags = [
+            "cw310",
+            "exclusive",
+            "vivado",
+        ],
+    )
+    for otp_name in OTP_CFGS_EXEC_DISABLED
+]
+
 test_suite(
     name = "rom_e2e_asm_watchdog",
     tags = [
         "cw310",
         "vivado",
     ],
-    tests = ["rom_e2e_asm_watchdog_fpga_cw310_test_otp_" + otp_name for otp_name in OTP_CFGS_EXEC_DISABLED],
+    tests = [
+        "rom_e2e_asm_watchdog_" + type + "_fpga_cw310_test_otp_" + otp_name
+        for otp_name in OTP_CFGS_EXEC_DISABLED
+        for type in ("bite", "bark")
+    ],
 )
 
 # Shutdown Redact Test