Improve cocotb Bazel rules

- Introduces cocotb_test_suite, which defines a test suite that can
  target both Verilator and VCS from one rule. This rule creates a
  target which will run the whole suite, and then also accepts a
  testcases list which will be used to create single-test targets.
- Add utils/update_all_cocotb_tests.py to keep the aforementioned
  testcase lists up-to-date.
- Rules generated by this way are highly queriable to find lists of
  tests to run, e.g.
  For finding all suites that use the RvvCoreMiniAxi HDL on VCS:
    - bazel query 'kind("cocotb_test", //...) intersect attr("tags", "vcs_cocotb_test_suite", //...) intersect attr("hdl_toplevel", "^RvvCoreMiniAxi$", //...)'
  For finding all individual tests that use the RvvCoreMiniAxi HDL on VCS:
    - bazel query 'kind("cocotb_test", //...) intersect attr("tags", "vcs_cocotb_single_test", //...) intersect attr("hdl_toplevel", "^RvvCoreMiniAxi$", //...)'

Change-Id: I5c639b703c3c062ef37480d3aac32418761fcab2
diff --git a/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py b/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
index cad8813..4dfa085 100644
--- a/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
+++ b/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
@@ -17,6 +17,7 @@
 import tempfile
 import threading
 
+from bazel_tools.tools.python.runfiles import runfiles
 from enum import Enum
 from cocotb.triggers import ClockCycles
 from pyocd.board.board import Board
@@ -349,6 +350,8 @@
 
         def exec_gdb():
             with tempfile.NamedTemporaryFile(mode='w+') as cmdfile:
+                r = runfiles.Create()
+                gdb_path = r.Rlocation("kelvin_hw/toolchain/gdb")
                 cmds_pre = [
                     'set architecture riscv:rv32',
                     'target remote :3333',
@@ -361,7 +364,7 @@
                     cmdfile.write(f'{cmd}\n')
                 cmdfile.flush()
                 args = [
-                    '../../../toolchain/gdb',
+                    gdb_path,
                     '-x',
                     cmdfile.name,
                     elf.name,
diff --git a/rules/coco_tb.bzl b/rules/coco_tb.bzl
index 229b7ca..e746cb5 100644
--- a/rules/coco_tb.bzl
+++ b/rules/coco_tb.bzl
@@ -12,13 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Convinence wrapper for Verilator driven cocotb."""
+"""Convenience wrapper for Verilator driven cocotb."""
 
 load("@kelvin_hw//third_party/python:requirements.bzl", "requirement")
 load("@rules_hdl//cocotb:cocotb.bzl", "cocotb_test")
 load("@rules_python//python:defs.bzl", "py_library")
 
 def _verilator_cocotb_model_impl(ctx):
+    """Implementation of the verilator_cocotb_model rule."""
     cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc
     ar_executable = cc_toolchain.ar_executable
     compiler_executable = cc_toolchain.compiler_executable
@@ -89,7 +90,7 @@
     return [
         DefaultInfo(
             files = depset([output_file, make_log]),
-            runfiles = ctx.runfiles(files=[output_file, make_log]),
+            runfiles = ctx.runfiles(files = [output_file, make_log]),
             executable = output_file,
         ),
         OutputGroupInfo(
@@ -98,6 +99,19 @@
     ]
 
 verilator_cocotb_model = rule(
+    doc = """Builds a verilator model for cocotb.
+
+    This rule takes a verilog source file and a toplevel module name and
+    builds a verilator model that can be used with cocotb.
+
+    It returns a DefaultInfo provider with an executable that can be run
+    to execute the simulation.
+
+    Attributes:
+        verilog_source: The verilog source file to build the model from.
+        hdl_toplevel: The name of the toplevel module.
+        cflags: A list of flags to pass to the compiler.
+    """,
     implementation = _verilator_cocotb_model_impl,
     attrs = {
         "verilog_source": attr.label(allow_single_file = True, mandatory = True),
@@ -127,15 +141,30 @@
     toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
 )
 
-def verilator_cocotb_test(name,
-                          model,
-                          hdl_toplevel,
-                          test_module,
-                          deps=[],
-                          data=[],
-                          **kwargs):
+def verilator_cocotb_test(
+        name,
+        model,
+        hdl_toplevel,
+        test_module,
+        deps = [],
+        data = [],
+        **kwargs):
+    """Runs a cocotb test with a verilator model.
+
+    This is a wrapper around the cocotb_test rule that is specific to
+    verilator.
+
+    Args:
+        name: The name of the test.
+        model: The verilator_cocotb_model target to use.
+        hdl_toplevel: The name of the toplevel module.
+        test_module: The python module that contains the test.
+        deps: Additional dependencies for the test.
+        data: Data dependencies for the test.
+        **kwargs: Additional arguments to pass to the cocotb_test rule.
+    """
     kwargs.update(
-        hdl_toplevel_lang="verilog",
+        hdl_toplevel_lang = "verilog",
         sim_name = "verilator",
         sim = [
             "@verilator//:verilator",
@@ -163,23 +192,89 @@
         deps = [
             ":{}_test_data".format(name),
         ],
-        **kwargs,
+        **kwargs
     )
 
-def vcs_cocotb_test(name,
-                    hdl_toplevel,
-                    test_module,
-                    testcases=[],
-                    deps=[],
-                    data=[],
-                    **kwargs):
-    tags = kwargs.pop("tags", [])
+def _verilator_cocotb_test_suite(
+        name,
+        model,
+        testcases = [],
+        testcases_vname = "",
+        tests_kwargs = {},
+        **kwargs):
+    """Runs a cocotb test with a verilator model.
+
+    This is a wrapper around the cocotb_test rule that is specific to
+    verilator.
+
+    Args:
+        name: The name of the test.
+        model: The verilator_cocotb_model target to use.
+        testcases: A list of testcases to run. A test will be generated for each
+          testcase.
+        tests_kwargs: A dictionary of arguments to pass to the cocotb_test rule.
+        **kwargs: Additional arguments to pass to the cocotb_test rule.
+    """
+    all_tests_kwargs = dict(tests_kwargs)
+    all_tests_kwargs.update(kwargs)
+
+    if testcases:
+        test_targets = []
+        for tc in testcases:
+            tc_tests_kwargs = dict(all_tests_kwargs)
+            tags = list(tc_tests_kwargs.pop("tags", []))
+            tags.append("manual")
+            tags.append("verilator_cocotb_single_test")
+            verilator_cocotb_test(
+                name = "{}_{}".format(name, tc),
+                model = model,
+                testcase = [tc],
+                tags = tags,
+                **tc_tests_kwargs
+            )
+            test_targets.append(":{}_{}".format(name, tc))
+
+    # Generate a meta-target for all tests.
+    meta_target_kwargs = dict(all_tests_kwargs)
+    tags = list(meta_target_kwargs.pop("tags", []))
+    tags.append("verilator_cocotb_test_suite")
+    if testcases_vname:
+        tags.append("testcases_vname={}".format(testcases_vname))
+    verilator_cocotb_test(
+        name = name,
+        model = model,
+        tags = tags,
+        **meta_target_kwargs
+    )
+
+def vcs_cocotb_test(
+        name,
+        hdl_toplevel,
+        test_module,
+        deps = [],
+        data = [],
+        **kwargs):
+    """Runs a cocotb test with a vcs model.
+
+    This is a wrapper around the cocotb_test rule that is specific to
+    vcs.
+
+    Args:
+        name: The name of the test.
+        hdl_toplevel: The name of the toplevel module.
+        test_module: The python module that contains the test.
+        deps: Additional dependencies for the test.
+        data: Data dependencies for the test.
+        **kwargs: Additional arguments to pass to the cocotb_test rule.
+    """
+    tags = list(kwargs.pop("tags", []))
     tags.append("vcs")
     kwargs.update(
-        hdl_toplevel_lang="verilog",
+        hdl_toplevel_lang = "verilog",
         sim_name = "vcs",
         sim = [],
-        tags = tags)
+        tags = tags,
+    )
 
     # Wrap in py_library so we can forward data
     py_library(
@@ -193,15 +288,133 @@
         data = data,
     )
 
-    test_args = kwargs.pop("test_args", [""])
-    [cocotb_test(
-        name = "{}_{}".format(name, tc),
+    cocotb_test(
+        name = name,
         hdl_toplevel = hdl_toplevel,
         test_module = test_module,
-        testcase = [tc],
-        test_args = ["{} -cm_name {}".format(test_args[0], tc)] + test_args[1:],
         deps = [
             ":{}_test_data".format(name),
         ],
-        **kwargs,
-    ) for tc in testcases]
+        **kwargs
+    )
+
+def _vcs_cocotb_test_suite(
+        name,
+        verilog_sources,
+        testcases = [],
+        testcases_vname = "",
+        tests_kwargs = {},
+        **kwargs):
+    """Runs a cocotb test with a vcs model.
+
+    This is a wrapper around the cocotb_test rule that is specific to
+    vcs.
+
+    Args:
+        name: The name of the test.
+        verilog_sources: The verilog sources to use for the test.
+        testcases: A list of testcases to run. A test will be generated for each
+          testcase.
+        tests_kwargs: A dictionary of arguments to pass to the cocotb_test rule.
+        **kwargs: Additional arguments to pass to the cocotb_test rule.
+    """
+    all_tests_kwargs = dict(tests_kwargs)
+    all_tests_kwargs.update(kwargs)
+
+    hdl_toplevel = all_tests_kwargs.get("hdl_toplevel")
+    if not hdl_toplevel:
+        fail("hdl_toplevel must be specified in tests_kwargs")
+
+    if testcases:
+        test_targets = []
+        for tc in testcases:
+            tc_tests_kwargs = dict(all_tests_kwargs)
+            tags = list(tc_tests_kwargs.pop("tags", []))
+            tags.append("manual")
+            tags.append("vcs_cocotb_single_test")
+            test_args = tc_tests_kwargs.pop("test_args", [""])
+            vcs_cocotb_test(
+                name = "{}_{}".format(name, tc),
+                testcase = [tc],
+                tags = tags,
+                test_args = ["{} -cm_name {}".format(test_args[0], tc)] + test_args[1:],
+                verilog_sources = verilog_sources,
+                **tc_tests_kwargs
+            )
+            test_targets.append(":{}_{}".format(name, tc))
+
+    # Generate a meta-target for all tests.
+    meta_target_kwargs = dict(all_tests_kwargs)
+    tags = list(meta_target_kwargs.pop("tags", []))
+    tags.append("vcs_cocotb_test_suite")
+    vcs_cocotb_test(
+        name = name,
+        tags = tags,
+        verilog_sources = verilog_sources,
+        **meta_target_kwargs
+    )
+
+def cocotb_test_suite(name, testcases, simulators = ["verilator"], **kwargs):
+    """Runs a cocotb test with a verilator or vcs model.
+
+    This is a wrapper around the cocotb_test rule that is specific to
+    verilator.
+
+    Args:
+        name: The name of the test.
+        simulators: A list of simulators to run the test with.
+          Supported simulators are "verilator" and "vcs".
+        **kwargs: Additional arguments to pass to the cocotb_test rule.
+          These can be prefixed with the simulator name to apply them to
+          only that simulator.
+    """
+
+    # Pop tests_kwargs from kwargs, if it exists.
+    tests_kwargs = kwargs.pop("tests_kwargs", {})
+    testcases_vname = kwargs.pop("testcases_vname", "")
+    for sim in simulators:
+        sim_kwargs = {}
+        sim_tests_kwargs = dict(tests_kwargs)
+
+        # Partition kwargs into sim_kwargs
+        for key, value in kwargs.items():
+            if key.startswith(sim):
+                sim_kwargs[key.replace(sim + "_", "")] = value
+
+        # Partition tests_kwargs into sim_tests_kwargs
+        for key, value in tests_kwargs.items():
+            if key.startswith(sim):
+                sim_tests_kwargs[key.replace(sim + "_", "")] = value
+
+        # Remove sim-specific kwargs from tests_kwargs
+        for key, value in tests_kwargs.items():
+            if key.startswith(sim):
+                if key in sim_tests_kwargs:
+                    sim_tests_kwargs.pop(key)
+
+        if sim == "verilator":
+            model = sim_kwargs.pop("model", None)
+            if not model:
+                fail("verilator_model must be specified for verilator tests")
+            _verilator_cocotb_test_suite(
+                name = name,
+                model = model,
+                testcases = testcases,
+                testcases_vname = testcases_vname,
+                tests_kwargs = sim_tests_kwargs,
+                **sim_kwargs
+            )
+        elif sim == "vcs":
+            verilog_sources = sim_kwargs.pop("verilog_sources", [])
+            if not verilog_sources:
+                fail("vcs_verilog_sources must be specified for vcs tests")
+            _vcs_cocotb_test_suite(
+                name = "{}_{}".format(sim, name),
+                verilog_sources = verilog_sources,
+                testcases = testcases,
+                testcases_vname = testcases_vname,
+                tests_kwargs = sim_tests_kwargs,
+                **sim_kwargs
+            )
+        else:
+            fail("Unknown simulator: {}".format(sim))
\ No newline at end of file
diff --git a/tests/cocotb/BUILD b/tests/cocotb/BUILD
index f27a000..673daad 100644
--- a/tests/cocotb/BUILD
+++ b/tests/cocotb/BUILD
@@ -12,109 +12,56 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-load("//rules:coco_tb.bzl", "vcs_cocotb_test", "verilator_cocotb_test", "verilator_cocotb_model")
-load("//rules:kelvin_v2.bzl", "kelvin_v2_binary")
-load("//rules:utils.bzl", "template_rule")
 load("@kelvin_hw//third_party/python:requirements.bzl", "requirement")
 load("@rules_pkg//:pkg.bzl", "pkg_zip")
+load("//rules:coco_tb.bzl", "cocotb_test_suite", "verilator_cocotb_model")
+load("//rules:kelvin_v2.bzl", "kelvin_v2_binary")
+load("//rules:utils.bzl", "template_rule")
+
+load(
+    "//tests/cocotb:build_defs.bzl",
+    "VCS_BUILD_ARGS",
+    "VCS_DEFINES",
+    "VCS_TEST_ARGS",
+    "VERILATOR_BUILD_ARGS",
+)
 
 package(default_visibility = ["//visibility:public"])
 
 COCOTB_TEST_BINARY_TARGETS = glob(["**/*.elf"]) + glob(["**/*.o"]) + [
-        ":align_test.elf",
-        ":float_csr_interlock_test.elf",
-        ":finish_txn_before_halt.elf",
-        ":stress_test.elf",
-        ":wfi_slot_0.elf",
-        ":wfi_slot_1.elf",
-        ":wfi_slot_2.elf",
-        ":wfi_slot_3.elf",
-    ]
-
-VERILATOR_BUILD_ARGS = [
-    "-Wno-WIDTH",
-    "-Wno-CASEINCOMPLETE",
-    "-Wno-LATCH",
-    "-Wno-SIDEEFFECT",
-    "-Wno-MULTIDRIVEN",
-    "-Wno-UNOPTFLAT",
-    # Warnings that we disable for fpnew
-    "-Wno-ASCRANGE",
-    "-Wno-WIDTHEXPAND",
-    "-Wno-WIDTHTRUNC",
-    "-Wno-UNSIGNED",
-    "-DUSE_GENERIC=\"\"",
+    ":align_test.elf",
+    ":float_csr_interlock_test.elf",
+    ":finish_txn_before_halt.elf",
+    ":stress_test.elf",
+    ":wfi_slot_0.elf",
+    ":wfi_slot_1.elf",
+    ":wfi_slot_2.elf",
+    ":wfi_slot_3.elf",
 ]
 
 verilator_cocotb_model(
     name = "core_mini_axi_model",
+    cflags = VERILATOR_BUILD_ARGS,
     hdl_toplevel = "CoreMiniAxi",
     verilog_source = "//hdl/chisel/src/kelvin:CoreMiniAxi.sv",
-    cflags = VERILATOR_BUILD_ARGS,
 )
 
 verilator_cocotb_model(
     name = "core_mini_debug_axi_model",
+    cflags = VERILATOR_BUILD_ARGS,
     hdl_toplevel = "CoreMiniDebugAxi",
     verilog_source = "//hdl/chisel/src/kelvin:CoreMiniDebugAxi.sv",
-    cflags = VERILATOR_BUILD_ARGS,
 )
 
 verilator_cocotb_model(
     name = "rvv_core_mini_axi_model",
+    cflags = VERILATOR_BUILD_ARGS,
     hdl_toplevel = "RvvCoreMiniAxi",
     verilog_source = "//hdl/chisel/src/kelvin:RvvCoreMiniAxi.sv",
-    cflags = VERILATOR_BUILD_ARGS,
 )
 
-template_rule(
-    verilator_cocotb_test,
-    {
-        "core_mini_axi_sim_cocotb": {
-            "hdl_toplevel": "CoreMiniAxi",
-            "model": ":core_mini_axi_model",
-            "size": "enormous",
-        },
-        "rvv_core_mini_axi_sim_cocotb": {
-            "hdl_toplevel": "RvvCoreMiniAxi",
-            "model": ":rvv_core_mini_axi_model",
-            "size": "enormous",
-            "tags": ["manual"], # This suite takes a really long time
-        },
-    },
-    waves = True,
-    seed = "42",
-    test_module = ["core_mini_axi_sim.py"],
-    deps = [
-        "//kelvin_test_utils:core_mini_axi_sim_interface",
-        requirement("tqdm"),
-        "@bazel_tools//tools/python/runfiles",
-    ],
-    data = COCOTB_TEST_BINARY_TARGETS,
-)
-
-verilator_cocotb_test(
-    name = "core_mini_axi_debug_cocotb",
-    hdl_toplevel = "CoreMiniDebugAxi",
-    model = ":core_mini_debug_axi_model",
-    waves = True,
-    seed = "42",
-    test_module = ["core_mini_axi_debug.py"],
-    size = "enormous",
-    deps = [
-        "//kelvin_test_utils:core_mini_axi_sim_interface",
-        "//kelvin_test_utils:core_mini_axi_pyocd_gdbserver",
-        "@bazel_tools//tools/python/runfiles",
-    ],
-    data = [
-        ":fptr.elf",
-        ":math.elf",
-        ":noop.elf",
-        ":registers.elf",
-    ] + COCOTB_TEST_BINARY_TARGETS,
-)
-
-TESTCASES = [
+# BEGIN_TESTCASES_FOR_core_mini_axi_sim_cocotb
+CORE_MINI_AXI_SIM_TESTCASES = [
     "core_mini_axi_basic_write_read_memory",
     "core_mini_axi_run_wfi_in_all_slots",
     "core_mini_axi_slow_bready",
@@ -130,76 +77,193 @@
     "core_mini_axi_burst_types_test",
     "core_mini_axi_float_csr_test",
 ]
+# END_TESTCASES_FOR_core_mini_axi_sim_cocotb
 
-template_rule(
-    vcs_cocotb_test,
-    {
-        "vcs_core_mini_axi_sim_cocotb": {
-            "hdl_toplevel": "CoreMiniAxi",
-            "verilog_sources": [
-                "//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"
-            ],
-        },
-        "vcs_rvv_core_mini_axi_sim_cocotb": {
-            "hdl_toplevel": "RvvCoreMiniAxi",
-            "verilog_sources": [
-                "//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"
-            ],
-        },
-    },
-    size = "large",
-    defines = {
-        "USE_GENERIC" : "",
-    },
-    waves = True,
-    seed = "42",
-    test_module = ["core_mini_axi_sim.py"],
-    testcases = TESTCASES,
-    build_args = [
-        "-timescale=1ns/1ps",
-        "-kdb",
-        "+vcs+fsdbon",
-        "-debug_access+all",
-        "-cm",
-        "line+cond+tgl+branch+assert",
-        "-cm_hier",
-        "../tests/cocotb/coverage_exclude.cfg",
-    ],
-    test_args = [
-        "+vcs+fsdbon",
-        "-cm",
-        "line+cond+tgl+branch+assert",
-    ],
-    deps = [
+# BEGIN_TESTCASES_FOR_rvv_core_mini_axi_sim_cocotb
+RVV_CORE_MINI_AXI_SIM_TESTCASES = [
+    "core_mini_axi_basic_write_read_memory",
+    "core_mini_axi_run_wfi_in_all_slots",
+    "core_mini_axi_slow_bready",
+    "core_mini_axi_write_read_memory_stress_test",
+    "core_mini_axi_master_write_alignment",
+    "core_mini_axi_finish_txn_before_halt_test",
+    "core_mini_axi_riscv_tests",
+    "core_mini_axi_riscv_dv",
+    "core_mini_axi_csr_test",
+    "core_mini_axi_exceptions_test",
+    "core_mini_axi_kelvin_isa_test",
+    "core_mini_axi_rand_instr_test",
+    "core_mini_axi_burst_types_test",
+    "core_mini_axi_float_csr_test",
+]
+# END_TESTCASES_FOR_rvv_core_mini_axi_sim_cocotb
+
+CORE_MINI_AXI_SIM_COMMON_TEST_KWARGS = {
+    "waves": True,
+    "seed": "42",
+    "test_module": ["core_mini_axi_sim.py"],
+    "deps": [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
         requirement("tqdm"),
         "@bazel_tools//tools/python/runfiles",
     ],
-    data = COCOTB_TEST_BINARY_TARGETS + [
-        ":coverage_exclude.cfg",
+    "data": COCOTB_TEST_BINARY_TARGETS,
+    "size": "enormous",
+}
+
+template_rule(
+    cocotb_test_suite,
+    {
+        "core_mini_axi_sim_cocotb": {
+            "tests_kwargs": dict(CORE_MINI_AXI_SIM_COMMON_TEST_KWARGS, hdl_toplevel = "CoreMiniAxi"),
+            "vcs_verilog_sources": ["//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"],
+            "verilator_model": ":core_mini_axi_model",
+            "testcases": CORE_MINI_AXI_SIM_TESTCASES,
+            "testcases_vname": "CORE_MINI_AXI_SIM_TESTCASES",
+        },
+        "rvv_core_mini_axi_sim_cocotb": {
+            "tests_kwargs": dict(CORE_MINI_AXI_SIM_COMMON_TEST_KWARGS, hdl_toplevel = "RvvCoreMiniAxi", tags = ["manual"]),
+            "vcs_verilog_sources": ["//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"],
+            "verilator_model": ":rvv_core_mini_axi_model",
+            "testcases": RVV_CORE_MINI_AXI_SIM_TESTCASES,
+            "testcases_vname": "RVV_CORE_MINI_AXI_SIM_TESTCASES",
+        },
+    },
+    simulators = ["verilator", "vcs"],
+    vcs_data = COCOTB_TEST_BINARY_TARGETS + [":coverage_exclude.cfg"],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+)
+
+# BEGIN_TESTCASES_FOR_core_mini_axi_debug_cocotb
+CORE_MINI_AXI_DEBUG_TESTCASES = [
+    "core_mini_axi_debug_gdbserver",
+    "core_mini_axi_debug_dmactive",
+    "core_mini_axi_debug_probe_impl",
+    "core_mini_axi_debug_ndmreset",
+    "core_mini_axi_debug_halt_resume",
+    "core_mini_axi_debug_hartsel",
+    "core_mini_axi_debug_abstract_access_registers",
+    "core_mini_axi_debug_abstract_access_nonexistent_register",
+    "core_mini_axi_debug_single_step",
+    "core_mini_axi_debug_breakpoint",
+]
+# END_TESTCASES_FOR_core_mini_axi_debug_cocotb
+
+# BEGIN_TESTCASES_FOR_rvv_assembly_cocotb_test
+RVV_ASSEMBLY_TESTCASES = [
+    "core_mini_rvv_load",
+    "core_mini_rvv_add",
+    "core_mini_vstart_store",
+]
+# END_TESTCASES_FOR_rvv_assembly_cocotb_test
+
+# BEGIN_TESTCASES_FOR_rvv_load_store_test
+RVV_LOAD_STORE_TESTCASES = [
+    "load8_stride2_m1",
+    "load8_stride2_m1_partial",
+    "load8_stride2_mf4",
+    "load16_stride4_m1",
+    "load16_stride4_m1_partial",
+    "load16_stride4_mf2",
+    "load32_stride8_m1",
+    "load32_stride8_m1_partial",
+    "load_store8_unit_m2",
+    "load_store16_unit_m2",
+    "load_store32_unit_m2",
+    "load8_segment2_unit_m1",
+    "load16_segment2_unit_m1",
+    "load32_segment2_unit_m1",
+    "load8_segment2_unit_m2",
+    "load16_segment2_unit_m2",
+    "load32_segment2_unit_m2",
+    "load8_segment2_stride6_m1",
+    "load16_segment2_stride6_m1",
+    "load8_indexed_m1",
+    "store8_indexed_m1",
+]
+# END_TESTCASES_FOR_rvv_load_store_test
+
+# BEGIN_TESTCASES_FOR_rvv_arithmetic_cocotb_test
+RVV_ARITHMETIC_TESTCASES = [
+    "arithmetic_m1_vanilla_ops",
+    "reduction_m1_vanilla_ops",
+]
+# END_TESTCASES_FOR_rvv_arithmetic_cocotb_test
+
+cocotb_test_suite(
+    name = "core_mini_axi_debug_cocotb",
+    simulators = [
+        "verilator",
+        "vcs",
     ],
+    testcases = CORE_MINI_AXI_DEBUG_TESTCASES,
+    testcases_vname = "CORE_MINI_AXI_DEBUG_TESTCASES",
+    tests_kwargs = {
+        "hdl_toplevel": "CoreMiniDebugAxi",
+        "waves": True,
+        "seed": "42",
+        "test_module": ["core_mini_axi_debug.py"],
+        "size": "enormous",
+        "deps": [
+            "//kelvin_test_utils:core_mini_axi_sim_interface",
+            "//kelvin_test_utils:core_mini_axi_pyocd_gdbserver",
+            "@bazel_tools//tools/python/runfiles",
+        ],
+        "data": [
+            ":fptr.elf",
+            ":math.elf",
+            ":noop.elf",
+            ":registers.elf",
+        ],
+    },
+    vcs_data = [
+            ":fptr.elf",
+            ":math.elf",
+            ":noop.elf",
+            ":registers.elf",
+    ] + [":coverage_exclude.cfg"],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:core_mini_axi_debug_cc_library_verilog"],
+    verilator_model = ":core_mini_debug_axi_model",
 )
 
 RVV_TEST_BINARY_TARGETS = [
-        "//tests/cocotb/rvv:rvv_load.elf",
-        "//tests/cocotb/rvv:rvv_add.elf",
-        "//tests/cocotb/rvv:vstart_store.elf",
-        ]
+    "//tests/cocotb/rvv:rvv_load.elf",
+    "//tests/cocotb/rvv:rvv_add.elf",
+    "//tests/cocotb/rvv:vstart_store.elf",
+]
 
-verilator_cocotb_test(
+cocotb_test_suite(
     name = "rvv_assembly_cocotb_test",
-    hdl_toplevel = "RvvCoreMiniAxi",
-    model = ":rvv_core_mini_axi_model",
-    waves = True,
-    seed = "42",
-    test_module = ["rvv_assembly_cocotb_test.py"],
-    deps = [
-        "//kelvin_test_utils:core_mini_axi_sim_interface",
-        requirement("tqdm"),
-        "@bazel_tools//tools/python/runfiles",
+    simulators = [
+        "verilator",
+        "vcs",
     ],
-    data = RVV_TEST_BINARY_TARGETS,
-    size = "large",
+    testcases = RVV_ASSEMBLY_TESTCASES,
+    testcases_vname = "RVV_ASSEMBLY_TESTCASES",
+    tests_kwargs = {
+        "hdl_toplevel": "RvvCoreMiniAxi",
+        "waves": True,
+        "seed": "42",
+        "test_module": ["rvv_assembly_cocotb_test.py"],
+        "deps": [
+            "//kelvin_test_utils:core_mini_axi_sim_interface",
+            requirement("tqdm"),
+            "@bazel_tools//tools/python/runfiles",
+        ],
+        "data": RVV_TEST_BINARY_TARGETS,
+        "size": "large",
+    },
+    vcs_data = RVV_TEST_BINARY_TARGETS + [":coverage_exclude.cfg"],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"],
+    verilator_model = ":rvv_core_mini_axi_model",
 )
 
 kelvin_v2_binary(
@@ -210,7 +274,6 @@
     semihosting = True,
 )
 
-
 kelvin_v2_binary(
     name = "wfi_slot_0",
     srcs = [
@@ -218,7 +281,6 @@
     ],
 )
 
-
 kelvin_v2_binary(
     name = "wfi_slot_1",
     srcs = [
@@ -289,37 +351,61 @@
     ],
 )
 
-verilator_cocotb_test(
+cocotb_test_suite(
     name = "rvv_load_store_test",
-    hdl_toplevel = "RvvCoreMiniAxi",
-    model = ":rvv_core_mini_axi_model",
-    waves = True,
-    seed = "42",
-    test_module = ["rvv_load_store_test.py"],
-    deps = [
-        "//kelvin_test_utils:sim_test_fixture",
-        "@bazel_tools//tools/python/runfiles",
+    simulators = [
+        "verilator",
+        "vcs",
     ],
-    data = [
-        "//tests/cocotb/rvv/load_store:rvv_load_store_tests",
-    ],
-    size = "large",
+    testcases = RVV_LOAD_STORE_TESTCASES,
+    testcases_vname = "RVV_LOAD_STORE_TESTCASES",
+    tests_kwargs = {
+        "hdl_toplevel": "RvvCoreMiniAxi",
+        "waves": True,
+        "seed": "42",
+        "test_module": ["rvv_load_store_test.py"],
+        "deps": [
+            "//kelvin_test_utils:sim_test_fixture",
+            "@bazel_tools//tools/python/runfiles",
+        ],
+        "data": ["//tests/cocotb/rvv/load_store:rvv_load_store_tests"],
+        "size": "large",
+    },
+    vcs_data = ["//tests/cocotb/rvv/load_store:rvv_load_store_tests"] + [":coverage_exclude.cfg"],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"],
+    verilator_model = ":rvv_core_mini_axi_model",
 )
 
-verilator_cocotb_test(
+cocotb_test_suite(
     name = "rvv_arithmetic_cocotb_test",
-    hdl_toplevel = "RvvCoreMiniAxi",
-    model = ":rvv_core_mini_axi_model",
-    waves = True,
-    seed = "42",
-    test_module = ["rvv_arithmetic_cocotb_test.py"],
-    deps = [
-        "//kelvin_test_utils:sim_test_fixture",
-        "@bazel_tools//tools/python/runfiles",
-        requirement("tqdm"),
+    simulators = [
+        "verilator",
+        "vcs",
     ],
-    data = [ "//tests/cocotb/rvv/arithmetics:rvv_arith_tests" ],
-    size = "large",
+    testcases = RVV_ARITHMETIC_TESTCASES,
+    testcases_vname = "RVV_ARITHMETIC_TESTCASES",
+    tests_kwargs = {
+        "hdl_toplevel": "RvvCoreMiniAxi",
+        "waves": True,
+        "seed": "42",
+        "test_module": ["rvv_arithmetic_cocotb_test.py"],
+        "deps": [
+            "//kelvin_test_utils:sim_test_fixture",
+            "@bazel_tools//tools/python/runfiles",
+            requirement("tqdm"),
+        ],
+        "data": ["//tests/cocotb/rvv/arithmetics:rvv_arith_tests"],
+        "size": "large",
+    },
+    vcs_data = ["//tests/cocotb/rvv/arithmetics:rvv_arith_tests"] + [":coverage_exclude.cfg"],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"],
+    verilator_model = ":rvv_core_mini_axi_model",
 )
 
 pkg_zip(
@@ -328,4 +414,4 @@
         "//tests/cocotb/rvv/arithmetics:rvv_arith_tests",
         "//tests/cocotb/rvv/load_store:rvv_load_store_tests",
     ],
-)
\ No newline at end of file
+)
diff --git a/tests/cocotb/build_defs.bzl b/tests/cocotb/build_defs.bzl
new file mode 100644
index 0000000..ff0816b
--- /dev/null
+++ b/tests/cocotb/build_defs.bzl
@@ -0,0 +1,49 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common build arguments for cocotb tests."""
+
+VERILATOR_BUILD_ARGS = [
+    "-Wno-WIDTH",
+    "-Wno-CASEINCOMPLETE",
+    "-Wno-LATCH",
+    "-Wno-SIDEEFFECT",
+    "-Wno-MULTIDRIVEN",
+    "-Wno-UNOPTFLAT",
+    # Warnings that we disable for fpnew
+    "-Wno-ASCRANGE",
+    "-Wno-WIDTHEXPAND",
+    "-Wno-WIDTHTRUNC",
+    "-Wno-UNSIGNED",
+    "-DUSE_GENERIC=\"\"",
+]
+
+VCS_BUILD_ARGS = [
+    "-timescale=1ns/1ps",
+    "-kdb",
+    "+vcs+fsdbon",
+    "-debug_access+all",
+    "-cm",
+    "line+cond+tgl+branch+assert",
+    "-cm_hier",
+    "../tests/cocotb/coverage_exclude.cfg",
+]
+
+VCS_TEST_ARGS = [
+    "+vcs+fsdbon",
+    "-cm",
+    "line+cond+tgl+branch+assert",
+]
+
+VCS_DEFINES = {"USE_GENERIC": ""}
diff --git a/tests/cocotb/tutorial/BUILD b/tests/cocotb/tutorial/BUILD
index 504b9f6..bc85393 100644
--- a/tests/cocotb/tutorial/BUILD
+++ b/tests/cocotb/tutorial/BUILD
@@ -12,38 +12,86 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-load("//rules:coco_tb.bzl", "verilator_cocotb_test")
+load("//rules:coco_tb.bzl", "cocotb_test_suite")
 load("//rules:kelvin_v2.bzl", "kelvin_v2_binary")
-
-verilator_cocotb_test(
-    name = "tutorial",
-    waves = True,
-    hdl_toplevel = "CoreMiniAxi",
-    seed = "42",
-    test_module = ["tutorial.py"],
-    model = "//tests/cocotb:core_mini_axi_model",
-    deps = [
-        "//kelvin_test_utils:core_mini_axi_sim_interface",
-    ],
-    data = glob(["**/*.elf"]),
+load(
+    "//tests/cocotb:build_defs.bzl",
+    "VCS_BUILD_ARGS",
+    "VCS_DEFINES",
+    "VCS_TEST_ARGS",
 )
 
-verilator_cocotb_test(
-    name = "hello_world_float_core_mini_axi",
-    waves = True,
-    hdl_toplevel = "CoreMiniAxi",
-    seed = "42",
-    test_module = ["hello_world_float_core_mini_axi.py"],
-    model = "//tests/cocotb:core_mini_axi_model",
-    deps = [
-        "//kelvin_test_utils:core_mini_axi_sim_interface",
-        "@bazel_tools//tools/python/runfiles",
+# BEGIN_TESTCASES_FOR_tutorial
+TUTORIAL_TESTCASES = [
+    "core_mini_axi_tutorial",
+]
+# END_TESTCASES_FOR_tutorial
+
+# BEGIN_TESTCASES_FOR_hello_world_float_core_mini_axi
+HELLO_WORLD_TESTCASES = [
+    "core_mini_axi_tutorial",
+]
+# END_TESTCASES_FOR_hello_world_float_core_mini_axi
+
+cocotb_test_suite(
+    name = "tutorial",
+    simulators = [
+        "verilator",
+        "vcs",
     ],
-    data = ["//examples:kelvin_v2_hello_world_add_floats.elf"],
+    testcases = TUTORIAL_TESTCASES,
+    testcases_vname = "TUTORIAL_TESTCASES",
+    tests_kwargs = {
+        "waves": True,
+        "hdl_toplevel": "CoreMiniAxi",
+        "seed": "42",
+        "test_module": ["tutorial.py"],
+        "deps": [
+            "//kelvin_test_utils:core_mini_axi_sim_interface",
+        ],
+        "data": glob(["**/*.elf"]),
+    },
+    vcs_data = [
+        "//tests/cocotb:coverage_exclude.cfg",
+    ],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"],
+    verilator_model = "//tests/cocotb:core_mini_axi_model",
+)
+
+cocotb_test_suite(
+    name = "hello_world_float_core_mini_axi",
+    simulators = [
+        "verilator",
+        "vcs",
+    ],
+    testcases = HELLO_WORLD_TESTCASES,
+    testcases_vname = "HELLO_WORLD_TESTCASES",
+    tests_kwargs = {
+        "waves": True,
+        "hdl_toplevel": "CoreMiniAxi",
+        "seed": "42",
+        "test_module": ["hello_world_float_core_mini_axi.py"],
+        "deps": [
+            "//kelvin_test_utils:core_mini_axi_sim_interface",
+            "@bazel_tools//tools/python/runfiles",
+        ],
+        "data": ["//examples:kelvin_v2_hello_world_add_floats.elf"],
+    },
+    vcs_data = [
+        "//examples:kelvin_v2_hello_world_add_floats.elf",
+        "//tests/cocotb:coverage_exclude.cfg",
+    ],
+    vcs_build_args = VCS_BUILD_ARGS,
+    vcs_test_args = VCS_TEST_ARGS,
+    vcs_defines = VCS_DEFINES,
+    vcs_verilog_sources = ["//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"],
+    verilator_model = "//tests/cocotb:core_mini_axi_model",
 )
 
 kelvin_v2_binary(
-    name="kelvin_v2_program",
+    name = "kelvin_v2_program",
     srcs = ["program.cc"],
 )
-
diff --git a/utils/update_all_cocotb_tests.py b/utils/update_all_cocotb_tests.py
new file mode 100755
index 0000000..1780598
--- /dev/null
+++ b/utils/update_all_cocotb_tests.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+import subprocess
+import os
+import xml.etree.ElementTree as ET
+from collections import defaultdict
+
+
+def get_workspace_root():
+    return subprocess.check_output(['bazel', 'info',
+                                    'workspace']).decode('utf-8').strip()
+
+
+def get_all_cocotb_test_suites():
+    xml_output = subprocess.check_output([
+        'bazel', 'query',
+        'kind("cocotb_test", //...) intersect attr("tags", "verilator_cocotb_test_suite", //...)',
+        '--output=xml'
+    ]).decode('utf-8').strip()
+    if not xml_output:
+        return []
+    return ET.fromstring(xml_output)
+
+
+def get_test_suite_info(suite_rule):
+    test_module = None
+    variable_name = None
+
+    for child in suite_rule:
+        if child.tag == 'list' and child.attrib.get('name') == 'test_module':
+            for label in child:
+                if label.tag == 'label':
+                    test_module = label.attrib.get('value')
+                    break
+        if child.tag == 'list' and child.attrib.get('name') == 'tags':
+            for string_attr in child:
+                if string_attr.tag == 'string':
+                    value = string_attr.attrib.get('value', '')
+                    if value.startswith("testcases_vname="):
+                        variable_name = value.split("=")[1]
+                        break
+        if test_module and variable_name:
+            break
+
+    return test_module, variable_name
+
+
+def main():
+    workspace_root = get_workspace_root()
+    os.chdir(workspace_root)
+
+    all_suites_xml = get_all_cocotb_test_suites()
+
+    if all_suites_xml is None:
+        print("No cocotb test suites found.")
+        return
+
+    for suite_rule in all_suites_xml.findall('rule'):
+        suite_name = suite_rule.get('name').split(':')[1]
+        print(f"Processing suite: {suite_name}")
+
+        build_file = suite_rule.get('location').split(':')[0]
+        test_module_label, variable_name = get_test_suite_info(suite_rule)
+
+        if not test_module_label or not variable_name:
+            print(
+                f"Warning: Could not extract 'test_module' or 'testcases_vname' from {suite_name}"
+            )
+            continue
+
+        # The test_module_label is a bazel label, e.g. //tests/cocotb:core_mini_axi_sim.py
+        # We need to convert it to a file path.
+        test_module_path = test_module_label.replace('//',
+                                                     '').replace(':', '/')
+
+        test_file_path = os.path.join(workspace_root, test_module_path)
+
+        print(f"Updating testcases for {suite_name}...")
+
+        update_script_path = os.path.join(workspace_root, "utils",
+                                          "update_cocotb_tests.py")
+        update_command = [
+            'python3',
+            update_script_path,
+            f'--build_file={build_file}',
+            f'--test_file={test_file_path}',
+            f'--variable_name={variable_name}',
+            f'--name={suite_name}',
+        ]
+
+        subprocess.run(update_command, check=True)
+        print(f"Successfully updated {suite_name}.")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/utils/update_cocotb_tests.py b/utils/update_cocotb_tests.py
new file mode 100644
index 0000000..9267cbc
--- /dev/null
+++ b/utils/update_cocotb_tests.py
@@ -0,0 +1,59 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import re
+
+
+def find_cocotb_tests(filename):
+    with open(filename, "r") as f:
+        source = f.read()
+    return re.findall(r"@cocotb\.test\(\)\s+async def\s+(\w+)", source)
+
+
+def update_build_file(build_file, test_file, variable_name, name):
+    test_names = find_cocotb_tests(test_file)
+    with open(build_file, "r") as f:
+        lines = f.readlines()
+
+    start_marker = f"# BEGIN_TESTCASES_FOR_{name}\n"
+    end_marker = f"# END_TESTCASES_FOR_{name}\n"
+
+    try:
+        start_index = lines.index(start_marker)
+        end_index = lines.index(end_marker)
+    except ValueError:
+        print(f"Error: Markers not found for {name} in {build_file}")
+        return
+
+    new_lines = lines[:start_index + 1]
+    new_lines.append(f'{variable_name} = [\n')
+    for name in test_names:
+        new_lines.append(f'    "{name}",\n')
+    new_lines.append(']\n')
+    new_lines.extend(lines[end_index:])
+
+    with open(build_file, "w") as f:
+        f.writelines(new_lines)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--build_file", required=True)
+    parser.add_argument("--test_file", required=True)
+    parser.add_argument("--variable_name", required=True)
+    parser.add_argument("--name", required=True)
+    args = parser.parse_args()
+    update_build_file(args.build_file, args.test_file, args.variable_name,
+                      args.name)