[util] Add templates to autogen ISR testutils

This adds two templates to autogenerate ISR testutil implementations (in
C) and a meson.build file to build the ISR testutil library.
Additionally, this commit updates the `util/autogen_testutils.py` script
to function with the new templates.

This partially addresses #9038.

Signed-off-by: Timothy Trippel <ttrippel@google.com>
diff --git a/util/autogen_testutils.py b/util/autogen_testutils.py
index 5284a9d..4c25bdd 100755
--- a/util/autogen_testutils.py
+++ b/util/autogen_testutils.py
@@ -13,6 +13,7 @@
 $ util/autogen_testutils.py
 """
 
+import argparse
 import glob
 import logging
 import shutil
@@ -20,8 +21,10 @@
 import sys
 from pathlib import Path
 
+import hjson
 from mako.template import Template
 
+import topgen.lib as topgen_lib
 from autogen_banner import get_autogen_banner
 from make_new_dif.ip import Ip
 
@@ -36,13 +39,36 @@
             "ERROR: clang-format must be installed to format "
             " autogenerated code to pass OpenTitan CI checks.")
 
+    # Parse command line args.
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--topcfg_path",
+        "-t",
+        type=Path,
+        default=(REPO_TOP / "hw/top_earlgrey/data/top_earlgrey.hjson"),
+        help="path of the top hjson file.",
+    )
+    args = parser.parse_args()
+
+    # Parse toplevel Hjson to get IPs that are templated / generated with IPgen.
+    try:
+        topcfg_text = args.topcfg_path.read_text()
+    except FileNotFoundError:
+        logging.error(f"hjson {args.topcfg_path} could not be found.")
+        sys.exit(1)
+    topcfg = hjson.loads(topcfg_text, use_decimal=True)
+    templated_modules = topgen_lib.get_templated_modules(topcfg)
+    ipgen_modules = topgen_lib.get_ipgen_modules(topcfg)
+
     # Define input/output directories.
     autogen_dif_directory = REPO_TOP / "sw/device/lib/dif/autogen"
     testutils_templates_dir = REPO_TOP / "util/autogen_testutils"
     autogen_testutils_dir = REPO_TOP / "sw/device/lib/testing/autogen"
 
     # Create list of IPs to generate shared testutils code for. This is all IPs
-    # that have a DIF library, that the testutils functions can use.
+    # that have a DIF library, that the testutils functions can use. Note, the
+    # templates will take care of only generating ISR testutil functions for IPs
+    # that can actually generate interrupts.
     ips_with_difs = []
     for autogen_dif_filename in glob.iglob(str(autogen_dif_directory / "*.h")):
         # NOTE: the line below takes as input a file path
@@ -51,14 +77,18 @@
         ip_name_snake = Path(autogen_dif_filename).stem[4:-8]
         # NOTE: ip.name_long_* not needed for auto-generated files which
         # are the only files (re-)generated in batch mode.
-        ips_with_difs.append(Ip(ip_name_snake, "AUTOGEN"))
+        ips_with_difs.append(
+            Ip(ip_name_snake, "AUTOGEN", templated_modules, ipgen_modules))
     ips_with_difs.sort(key=lambda ip: ip.name_snake)
 
     # Create output directories if needed.
     autogen_testutils_dir.mkdir(exist_ok=True)
 
     # Auto-generate testutils files.
-    for suffix in [".h", ".c"]:
+    for suffix in [".h", ".c", ".build"]:
+        comment_syntax = "#" if suffix == ".build" else "//"
+        if suffix == ".build":
+            comment_syntax = "#"
         for testutils_template_path_str in glob.iglob(
                 str(testutils_templates_dir / f"*{suffix}.tpl")):
             testutils_template_path = Path(testutils_template_path_str)
@@ -71,15 +101,17 @@
                 testutils_template.render(ips_with_difs=ips_with_difs,
                                           autogen_banner=get_autogen_banner(
                                               "util/autogen_testutils.py",
-                                              comment="//")))
+                                              comment=comment_syntax)))
 
             # Format autogenerated file with clang-format.
-            try:
-                subprocess.check_call(["clang-format", "-i", testutils])
-            except subprocess.CalledProcessError:
-                logging.error(
-                    f"failed to format {testutils} with clang-format.")
-                sys.exit(1)
+            # Note: do not format meson build file.
+            if testutils.suffix != ".build":
+                try:
+                    subprocess.check_call(["clang-format", "-i", testutils])
+                except subprocess.CalledProcessError:
+                    logging.error(
+                        f"failed to format {testutils} with clang-format.")
+                    sys.exit(1)
 
             print("testutils \"{}\" successfully written to {}.".format(
                 suffix, str(testutils)))
diff --git a/util/autogen_testutils/isr_testutils.c.tpl b/util/autogen_testutils/isr_testutils.c.tpl
new file mode 100644
index 0000000..b613760
--- /dev/null
+++ b/util/autogen_testutils/isr_testutils.c.tpl
@@ -0,0 +1,71 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+${autogen_banner}
+
+#include "sw/device/lib/testing/autogen/isr_testutils.h"
+
+#include "sw/device/lib/dif/dif_rv_plic.h"
+% for ip in ips_with_difs:
+  % if ip.irqs:
+    #include "sw/device/lib/dif/dif_${ip.name_snake}.h"
+  % endif
+% endfor
+#include "sw/device/lib/testing/check.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h" // Generated.
+
+% for ip in ips_with_difs:
+  % if ip.irqs:
+    void isr_testutils_${ip.name_snake}_isr(
+      plic_isr_ctx_t plic_ctx,
+      ${ip.name_snake}_isr_ctx ${ip.name_snake}_ctx,
+      top_earlgrey_plic_peripheral_t *peripheral_serviced,
+      dif_${ip.name_snake}_irq_t *irq_serviced) {
+
+      // Claim the IRQ at the PLIC.
+      dif_rv_plic_irq_id_t plic_irq_id;
+      CHECK_DIF_OK(dif_rv_plic_irq_claim(
+        plic_ctx.rv_plic,
+        plic_ctx.hart_id,
+        &plic_irq_id));
+
+      // Get the peripheral the IRQ belongs to.
+      *peripheral_serviced = (top_earlgrey_plic_peripheral_t)
+        top_earlgrey_plic_interrupt_for_peripheral[plic_irq_id];
+
+      // Get the IRQ that was fired from the PLIC IRQ ID.
+      dif_${ip.name_snake}_irq_t irq = (dif_${ip.name_snake}_irq_t)(plic_irq_id -
+        ${ip.name_snake}_ctx.plic_${ip.name_snake}_start_irq_id);
+      *irq_serviced = irq;
+
+      // Check if it is supposed to be the only IRQ fired.
+      if (${ip.name_snake}_ctx.is_only_irq) {
+        dif_${ip.name_snake}_irq_state_snapshot_t snapshot;
+        CHECK_DIF_OK(dif_${ip.name_snake}_irq_get_state(
+          ${ip.name_snake}_ctx.${ip.name_snake},
+        % if ip.name_snake == "rv_timer":
+          plic_ctx.hart_id,
+        % endif
+          &snapshot));
+        CHECK(snapshot == (dif_${ip.name_snake}_irq_state_snapshot_t)(1 << irq),
+          "Only ${ip.name_snake} IRQ %d expected to fire. Actual IRQ state = %x",
+          irq, snapshot);
+      }
+
+      // Acknowledge the IRQ at the peripheral.
+      CHECK_DIF_OK(dif_${ip.name_snake}_irq_acknowledge(
+          ${ip.name_snake}_ctx.${ip.name_snake},
+          irq));
+
+      // Complete the IRQ at the PLIC.
+      CHECK_DIF_OK(dif_rv_plic_irq_complete(
+          plic_ctx.rv_plic,
+          plic_ctx.hart_id,
+          plic_irq_id));
+    }
+
+  % endif
+% endfor
+
diff --git a/util/autogen_testutils/isr_testutils.h.tpl b/util/autogen_testutils/isr_testutils.h.tpl
index 9e54895..1576e9a 100644
--- a/util/autogen_testutils/isr_testutils.h.tpl
+++ b/util/autogen_testutils/isr_testutils.h.tpl
@@ -7,23 +7,71 @@
 #ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_AUTOGEN_ISR_TESTUTILS_H_
 #define OPENTITAN_SW_DEVICE_LIB_TESTING_AUTOGEN_ISR_TESTUTILS_H_
 
+#include "sw/device/lib/dif/dif_rv_plic.h"
 % for ip in ips_with_difs:
   % if ip.irqs:
-  /* 
-   * Services an ${ip.name_snake} IRQ at the IP that raised it, and verifies this
-   * matches the IRQ that was raised at the PLIC.
-   *  
-   * @param ${ip.name_snake} A(n) ${ip.name_snake} DIF handle. 
-   * @param plic_irq_id The triggered PLIC IRQ ID. 
-   * @param plic_${ip.name_snake}_start_irq_id The PLIC IRQ ID where ${ip.name_snake} starts. 
-   * @param expected_${ip.name_snake}_irq The expected ${ip.name_snake} IRQ. 
-   * @param is_only_irq This is the only IRQ expected to be raised.
+    #include "sw/device/lib/dif/dif_${ip.name_snake}.h"
+  % endif
+% endfor
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h" // Generated.
+
+/**
+ * A handle to a PLIC ISR context struct.
+ */
+typedef struct plic_isr_ctx {
+  /**
+   * A handle to a rv_plic.
    */
-  void isr_testutils_${ip.name_snake}(dif_${ip.name_snake}_t *${ip.name_snake}, 
-                                      dif_rv_plic_irq_id_t plic_irq_id, 
-                                      dif_rv_plic_irq_id_t plic_${ip.name_snake}_start_irq_id, 
-                                      dif_${ip.name_snake}_irq_id_t expected_${ip.name_snake}_irq,
-                                      bool is_only_irq);
+  dif_rv_plic_t *rv_plic;
+  /**
+   * The HART ID associated with the PLIC (correspond to a PLIC "target").
+   */
+  uint32_t hart_id;
+} plic_isr_ctx_t;
+
+% for ip in ips_with_difs:
+  % if ip.irqs:
+    /**
+     * A handle to a ${ip.name_snake} ISR context struct.
+     */
+    typedef struct ${ip.name_snake}_isr_ctx {
+      /**
+       * A handle to a ${ip.name_snake}.
+       */
+      dif_${ip.name_snake}_t *${ip.name_snake};
+      /**
+       * The PLIC IRQ ID where this ${ip.name_snake} instance's IRQs start.
+       */
+      dif_rv_plic_irq_id_t plic_${ip.name_snake}_start_irq_id;
+      /**
+       * The ${ip.name_snake} IRQ that is expected to be encountered in the ISR.
+       */
+      dif_${ip.name_snake}_irq_t expected_irq;
+      /**
+       * Whether or not a single IRQ is expected to be encountered in the ISR.
+       */
+      bool is_only_irq;
+    } ${ip.name_snake}_isr_ctx;
+
+  % endif
+% endfor
+
+% for ip in ips_with_difs:
+  % if ip.irqs:
+    /**
+     * Services an ${ip.name_snake} IRQ.
+     *
+     * @param plic_ctx A PLIC ISR context handle.
+     * @param ${ip.name_snake}_ctx A(n) ${ip.name_snake} ISR context handle.
+     * @param[out] peripheral_serviced Out param for the peripheral that was serviced.
+     * @param[out] irq_serviced Out param for the IRQ that was serviced.
+     */
+    void isr_testutils_${ip.name_snake}_isr(
+      plic_isr_ctx_t plic_ctx,
+      ${ip.name_snake}_isr_ctx ${ip.name_snake}_ctx,
+      top_earlgrey_plic_peripheral_t *peripheral_serviced,
+      dif_${ip.name_snake}_irq_t *irq_serviced);
 
   % endif
 % endfor
diff --git a/util/autogen_testutils/meson.build.tpl b/util/autogen_testutils/meson.build.tpl
new file mode 100644
index 0000000..5f19009
--- /dev/null
+++ b/util/autogen_testutils/meson.build.tpl
@@ -0,0 +1,20 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+${autogen_banner}
+
+# ISR test utilities
+sw_lib_testing_isr_testutils = declare_dependency(
+  link_with: static_library(
+    'sw_lib_testing_isr_testutils',
+    sources: ['isr_testutils.c'],
+    dependencies: [
+% for ip in ips_with_difs:
+  % if ip.irqs:
+      sw_lib_dif_${ip.name_snake},
+  % endif
+% endfor
+    ],
+  ),
+)