Reorganize how tb infrstructure selects toolchains

This commit is contained in:
Alex Mykyta
2023-10-22 11:04:43 -07:00
parent 683fc4d0ac
commit d689bb7077
24 changed files with 323 additions and 179 deletions

View File

@@ -1,4 +1,4 @@
from typing import Optional, List
from typing import Optional
import unittest
import os
import glob
@@ -49,41 +49,37 @@ class BaseTestCase(unittest.TestCase):
def _load_request(self, request):
self.request = request
@classmethod
def get_testcase_dir(cls) -> str:
class_dir = os.path.dirname(inspect.getfile(cls))
def get_testcase_dir(self) -> str:
class_dir = os.path.dirname(inspect.getfile(self.__class__))
return class_dir
@classmethod
def get_run_dir(cls) -> str:
this_dir = cls.get_testcase_dir()
run_dir = os.path.join(this_dir, "run.out", cls.__name__)
def get_run_dir(self) -> str:
this_dir = self.get_testcase_dir()
run_dir = os.path.join(this_dir, "run.out", self.__class__.__name__)
return run_dir
@classmethod
def _write_params(cls) -> None:
def _write_params(self) -> None:
"""
Write out the class parameters to a file so that it is easier to debug
how a testcase was parameterized
"""
path = os.path.join(cls.get_run_dir(), "params.txt")
path = os.path.join(self.get_run_dir(), "params.txt")
with open(path, 'w') as f:
for k, v in cls.__dict__.items():
for k, v in self.__class__.__dict__.items():
if k.startswith("_") or callable(v):
continue
f.write(f"{k}: {repr(v)}\n")
@classmethod
def _export_regblock(cls):
def _export_regblock(self):
"""
Call the peakrdl_regblock exporter to generate the DUT
"""
this_dir = cls.get_testcase_dir()
this_dir = self.get_testcase_dir()
if cls.rdl_file:
rdl_file = cls.rdl_file
if self.rdl_file:
rdl_file = self.rdl_file
else:
# Find any *.rdl file in testcase dir
rdl_file = glob.glob(os.path.join(this_dir, "*.rdl"))[0]
@@ -98,45 +94,33 @@ class BaseTestCase(unittest.TestCase):
rdlc.compile_file(udp_file)
rdlc.compile_file(rdl_file)
root = rdlc.elaborate(cls.rdl_elab_target, "regblock", cls.rdl_elab_params)
root = rdlc.elaborate(self.rdl_elab_target, "regblock", self.rdl_elab_params)
cls.exporter.export(
self.exporter.export(
root,
cls.get_run_dir(),
self.get_run_dir(),
module_name="regblock",
package_name="regblock_pkg",
cpuif_cls=cls.cpuif.cpuif_cls,
retime_read_fanin=cls.retime_read_fanin,
retime_read_response=cls.retime_read_response,
reuse_hwif_typedefs=cls.reuse_hwif_typedefs,
retime_external_reg=cls.retime_external,
retime_external_regfile=cls.retime_external,
retime_external_mem=cls.retime_external,
retime_external_addrmap=cls.retime_external,
default_reset_activelow=cls.default_reset_activelow,
default_reset_async=cls.default_reset_async,
cpuif_cls=self.cpuif.cpuif_cls,
retime_read_fanin=self.retime_read_fanin,
retime_read_response=self.retime_read_response,
reuse_hwif_typedefs=self.reuse_hwif_typedefs,
retime_external_reg=self.retime_external,
retime_external_regfile=self.retime_external,
retime_external_mem=self.retime_external,
retime_external_addrmap=self.retime_external,
default_reset_activelow=self.default_reset_activelow,
default_reset_async=self.default_reset_async,
)
@classmethod
def setUpClass(cls):
def setUp(self) -> None:
# Create fresh build dir
run_dir = cls.get_run_dir()
run_dir = self.get_run_dir()
if os.path.exists(run_dir):
shutil.rmtree(run_dir)
pathlib.Path(run_dir).mkdir(parents=True, exist_ok=True)
cls._write_params()
self._write_params()
# Convert testcase RDL file --> SV
cls._export_regblock()
def setUp(self) -> None:
# cd into the run directory
self.original_cwd = os.getcwd()
os.chdir(self.get_run_dir())
def run_test(self, plusargs:List[str] = None) -> None:
simulator = self.simulator_cls(testcase_cls_inst=self)
simulator.run(plusargs)
self._export_regblock()

View File

@@ -66,7 +66,7 @@ class CpuifTestMode:
return self._get_file_paths("rtl_files")
def get_tb_inst(self, tb_cls: 'SimTestCase', exporter: 'RegblockExporter') -> str:
def get_tb_inst(self, testcase: 'SimTestCase', exporter: 'RegblockExporter') -> str:
class_dir = self._get_class_dir_of_variable("tb_template")
loader = jj.FileSystemLoader(class_dir)
jj_env = jj.Environment(
@@ -77,7 +77,7 @@ class CpuifTestMode:
context = {
"cpuif": self,
"cls": tb_cls,
"testcase": testcase,
"exporter": exporter,
"type": type,
}

View File

@@ -1,24 +1,21 @@
from typing import List
import os
import jinja2 as jj
import pytest
from .sv_line_anchor import SVLineAnchor
from .simulators.questa import Questa
from .simulators import StubSimulator
from .simulators import get_simulator_cls
from .base_testcase import BaseTestCase
SIM_CLS = Questa
if os.environ.get("STUB_SIMULATOR", False):
SIM_CLS = StubSimulator
class SimTestCase(BaseTestCase):
#: Abort test if it exceeds this number of clock cycles
timeout_clk_cycles = 5000
simulator_cls = SIM_CLS
incompatible_sim_tools = set()
tb_template_file = "tb_template.sv"
@@ -32,17 +29,14 @@ class SimTestCase(BaseTestCase):
clocking_hwif_in = True
clocking_hwif_out = True
@classmethod
def get_extra_tb_files(cls) -> List[str]:
def get_extra_tb_files(self) -> List[str]:
paths = []
for path in cls.extra_tb_files:
path = os.path.join(cls.get_testcase_dir(), path)
for path in self.extra_tb_files:
path = os.path.join(self.get_testcase_dir(), path)
paths.append(path)
return paths
@classmethod
def _generate_tb(cls):
def _generate_tb(self):
"""
Render the testbench template into actual tb.sv
"""
@@ -57,32 +51,38 @@ class SimTestCase(BaseTestCase):
)
context = {
"cls": cls,
"exporter": cls.exporter,
"testcase": self,
"exporter": self.exporter,
}
# template path needs to be relative to the Jinja loader root
template_path = os.path.join(cls.get_testcase_dir(), cls.tb_template_file)
template_path = os.path.join(self.get_testcase_dir(), self.tb_template_file)
template_path = os.path.relpath(template_path, template_root_path)
template = jj_env.get_template(template_path)
output_path = os.path.join(cls.get_run_dir(), "tb.sv")
output_path = os.path.join(self.get_run_dir(), "tb.sv")
stream = template.stream(context)
stream.dump(output_path)
@classmethod
def setUpClass(cls):
super().setUpClass()
def setUp(self):
name = self.request.config.getoption("--sim-tool")
if name in self.incompatible_sim_tools:
pytest.skip()
simulator_cls = get_simulator_cls(name)
if simulator_cls is None:
pytest.skip()
super().setUp()
# Create testbench from template
cls._generate_tb()
self._generate_tb()
simulator = cls.simulator_cls(testcase_cls=cls)
simulator = simulator_cls(self)
# cd into the build directory
cwd = os.getcwd()
os.chdir(cls.get_run_dir())
os.chdir(self.get_run_dir())
try:
simulator.compile()
finally:
@@ -91,5 +91,16 @@ class SimTestCase(BaseTestCase):
def run_test(self, plusargs:List[str] = None) -> None:
simulator = self.simulator_cls(testcase_cls_inst=self)
simulator.run(plusargs)
name = self.request.config.getoption("--sim-tool")
simulator_cls = get_simulator_cls(name)
simulator = simulator_cls(self)
# cd into the build directory
cwd = os.getcwd()
os.chdir(self.get_run_dir())
try:
simulator.run(plusargs)
finally:
# cd back
os.chdir(cwd)

View File

@@ -1,34 +1,37 @@
from typing import Type, TYPE_CHECKING, List
from typing import Type, Optional, List
import functools
if TYPE_CHECKING:
from ..sim_testcase import SimTestCase
from .base import Simulator
from .questa import Questa
from .xilinx import Xilinx
from .stub import StubSimulator
class Simulator:
ALL_SIMULATORS: List[Simulator]
ALL_SIMULATORS = [
Questa,
Xilinx,
StubSimulator,
]
def __init__(self, testcase_cls: 'Type[SimTestCase]' = None, testcase_cls_inst: 'SimTestCase' = None) -> None:
self.testcase_cls = testcase_cls
self.testcase_cls_inst = testcase_cls_inst
@functools.lru_cache()
def get_simulator_cls(name: str) -> Optional[Type[Simulator]]:
if name == "skip":
return None
@property
def tb_files(self) -> List[str]:
files = []
files.extend(self.testcase_cls.cpuif.get_sim_files())
files.extend(self.testcase_cls.get_extra_tb_files())
files.append("regblock_pkg.sv")
files.append("regblock.sv")
files.append("tb.sv")
if name == "auto":
# Find the first simulator that is installed
for sim_cls in ALL_SIMULATORS:
if sim_cls is StubSimulator:
# Never offer the stub as an automatic option
continue
if sim_cls.is_installed():
return sim_cls
raise ValueError("Could not find any installed simulators")
return files
def compile(self) -> None:
raise NotImplementedError
def run(self, plusargs:List[str] = None) -> None:
raise NotImplementedError
class StubSimulator(Simulator):
def compile(self) -> None:
pass
def run(self, plusargs:List[str] = None) -> None:
pass
# Look up which explicit simulator name was specified
for sim_cls in ALL_SIMULATORS:
if sim_cls.name == name:
if not sim_cls.is_installed():
raise ValueError("Simulator '%s' is not installed" % sim_cls.name)
return sim_cls
raise RuntimeError

View File

@@ -0,0 +1,31 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from ..sim_testcase import SimTestCase
class Simulator:
name = ""
@classmethod
def is_installed(cls) -> bool:
raise NotImplementedError
def __init__(self, testcase: 'SimTestCase' = None) -> None:
self.testcase = testcase
@property
def tb_files(self) -> List[str]:
files = []
files.extend(self.testcase.cpuif.get_sim_files())
files.extend(self.testcase.get_extra_tb_files())
files.append("regblock_pkg.sv")
files.append("regblock.sv")
files.append("tb.sv")
return files
def compile(self) -> None:
raise NotImplementedError
def run(self, plusargs:List[str] = None) -> None:
raise NotImplementedError

View File

@@ -1,10 +1,20 @@
from typing import List
import subprocess
import os
import shutil
from . import Simulator
from .base import Simulator
class Questa(Simulator):
name = "questa"
@classmethod
def is_installed(cls) -> bool:
return (
shutil.which("vlog") is not None
and shutil.which("vsim") is not None
)
def compile(self) -> None:
cmd = [
"vlog", "-sv", "-quiet", "-l", "build.log",
@@ -31,7 +41,7 @@ class Questa(Simulator):
def run(self, plusargs:List[str] = None) -> None:
plusargs = plusargs or []
test_name = self.testcase_cls_inst.request.node.name
test_name = self.testcase.request.node.name
# call vsim
cmd = [
@@ -54,11 +64,11 @@ class Questa(Simulator):
def assertSimLogPass(self, path: str):
self.testcase_cls_inst.assertTrue(os.path.isfile(path))
self.testcase.assertTrue(os.path.isfile(path))
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("# ** Error"):
self.testcase_cls_inst.fail(line)
self.testcase.fail(line)
elif line.startswith("# ** Fatal"):
self.testcase_cls_inst.fail(line)
self.testcase.fail(line)

View File

@@ -0,0 +1,17 @@
from typing import List
from .base import Simulator
class StubSimulator(Simulator):
name = "stub"
@classmethod
def is_installed(cls) -> bool:
# Always available!
return True
def compile(self) -> None:
pass
def run(self, plusargs: List[str] = None) -> None:
pass

View File

@@ -1,22 +1,34 @@
from typing import List
import subprocess
import os
import shutil
from . import Simulator
from .base import Simulator
class Xilinx(Simulator):
"""
Don't bother using the Xilinx simulator... Its buggy and extraordinarily slow.
As observed in v2021.1:
As observed in v2023.2:
- clocking block assignments do not seem to actually simulate correctly.
assignment statements get ignored or the values get mangled.
- Streaming operators have all sorts of limitations.
Keeping this here in case someday it works better...
"""
name = "xilinx"
@classmethod
def is_installed(cls) -> bool:
return (
shutil.which("xvlog") is not None
and shutil.which("xelab") is not None
and shutil.which("xsim") is not None
)
def compile(self) -> None:
cmd = [
"xvlog", "--sv",
"--log", "compile.log",
"--include", os.path.join(os.path.dirname(__file__), ".."),
"--define", "XSIM",
]
@@ -25,6 +37,7 @@ class Xilinx(Simulator):
cmd = [
"xelab",
"--log", "elaborate.log",
"--timescale", "1ns/1ps",
"--debug", "all",
"tb",
@@ -35,7 +48,7 @@ class Xilinx(Simulator):
def run(self, plusargs:List[str] = None) -> None:
plusargs = plusargs or []
test_name = self.testcase_cls_inst.request.node.name
test_name = self.testcase.request.node.name
# call vsim
cmd = [
@@ -54,13 +67,13 @@ class Xilinx(Simulator):
def assertSimLogPass(self, path: str):
self.testcase_cls_inst.assertTrue(os.path.isfile(path))
self.testcase.assertTrue(os.path.isfile(path))
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("Error:"):
self.testcase_cls_inst.fail(line)
self.testcase.fail(line)
elif line.startswith("Fatal:"):
self.testcase_cls_inst.fail(line)
self.testcase.fail(line)
elif line.startswith("FATAL_ERROR:"):
self.testcase_cls_inst.fail(line)
self.testcase.fail(line)

View File

@@ -1,12 +1,11 @@
from typing import List
import subprocess
import os
import pytest
from .base_testcase import BaseTestCase
from .synthesizers import get_synthesizer_cls
@pytest.mark.skipif(os.environ.get("SKIP_SYNTH_TESTS", False), reason="user skipped")
class SynthTestCase(BaseTestCase):
def _get_synth_files(self) -> List[str]:
@@ -17,20 +16,24 @@ class SynthTestCase(BaseTestCase):
return files
def setUp(self) -> None:
name = self.request.config.getoption("--synth-tool")
synth_cls = get_synthesizer_cls(name)
if synth_cls is None:
pytest.skip()
super().setUp()
def run_synth(self) -> None:
script = os.path.join(
os.path.dirname(__file__),
"synthesis/vivado/run.tcl"
)
name = self.request.config.getoption("--synth-tool")
synth_cls = get_synthesizer_cls(name)
synth = synth_cls(self)
cmd = [
"vivado", "-nojournal", "-notrace",
"-mode", "batch",
"-log", "out.log",
"-source", script,
"-tclargs"
]
cmd.extend(self._get_synth_files())
# cd into the build directory
cwd = os.getcwd()
os.chdir(self.get_run_dir())
subprocess.run(cmd, check=True)
try:
synth.run()
finally:
# cd back
os.chdir(cwd)

View File

@@ -0,0 +1,30 @@
from typing import List, Optional, Type
import functools
from .base import Synthesizer
from .vivado import Vivado
ALL_SYNTHESIZERS: List[Synthesizer]
ALL_SYNTHESIZERS = [
Vivado,
]
@functools.lru_cache()
def get_synthesizer_cls(name: str) -> Optional[Type[Synthesizer]]:
if name == "skip":
return None
if name == "auto":
# Find the first tool that is installed
for synth_cls in ALL_SYNTHESIZERS:
if synth_cls.is_installed():
return synth_cls
raise ValueError("Could not find any installed synthesis tools")
# Look up which explicit synth tool name was specified
for synth_cls in ALL_SYNTHESIZERS:
if synth_cls.name == name:
if not synth_cls.is_installed():
raise ValueError("Synthesis tool '%s' is not installed" % synth_cls.name)
return synth_cls
raise RuntimeError

View File

@@ -0,0 +1,17 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from ..synth_testcase import SynthTestCase
class Synthesizer:
name = ""
@classmethod
def is_installed(cls) -> bool:
raise NotImplementedError
def __init__(self, testcase: 'SynthTestCase' = None) -> None:
self.testcase = testcase
def run(self) -> None:
raise NotImplementedError

View File

@@ -0,0 +1,29 @@
import os
import subprocess
import shutil
from .base import Synthesizer
class Vivado(Synthesizer):
name = "vivado"
@classmethod
def is_installed(cls) -> bool:
return shutil.which("vivado") is not None
def run(self) -> None:
script = os.path.join(
os.path.dirname(__file__),
"vivado_scripts/run.tcl"
)
cmd = [
"vivado", "-nojournal", "-notrace",
"-mode", "batch",
"-log", "out.log",
"-source", script,
"-tclargs"
]
cmd.extend(self.testcase._get_synth_files())
subprocess.run(cmd, check=True)

View File

@@ -40,11 +40,11 @@ module tb;
default clocking cb @(posedge clk);
default input #1step output #1;
output rst;
{%- if exporter.hwif.has_input_struct and cls.clocking_hwif_in %}
{%- if exporter.hwif.has_input_struct and testcase.clocking_hwif_in %}
output hwif_in;
{%- endif %}
{%- if exporter.hwif.has_output_struct and cls.clocking_hwif_out %}
{%- if exporter.hwif.has_output_struct and testcase.clocking_hwif_out %}
input hwif_out;
{%- endif %}
@@ -61,7 +61,7 @@ module tb;
//--------------------------------------------------------------------------
// CPUIF
//--------------------------------------------------------------------------
{{cls.cpuif.get_tb_inst(cls, exporter)|indent}}
{{testcase.cpuif.get_tb_inst(testcase, exporter)|indent}}
//--------------------------------------------------------------------------
// DUT
@@ -93,7 +93,7 @@ module tb;
//--------------------------------------------------------------------------
initial begin
cb.rst <= '1;
{%- if exporter.hwif.has_input_struct and cls.init_hwif_in %}
{%- if exporter.hwif.has_input_struct and testcase.init_hwif_in %}
cb.hwif_in <= '{default: '0};
{%- endif %}
@@ -112,8 +112,8 @@ module tb;
// Monitor for timeout
//--------------------------------------------------------------------------
initial begin
##{{cls.timeout_clk_cycles}};
$fatal(1, "Test timed out after {{cls.timeout_clk_cycles}} clock cycles");
##{{testcase.timeout_clk_cycles}};
$fatal(1, "Test timed out after {{testcase.timeout_clk_cycles}} clock cycles");
end
endmodule