System Verilog Unit Testing Framework (SvUTest) is an open source framework for performing unit testing of lightweight hardware modules written in System Verilog. This project is inspired by CppUTest, Google Test and UVM.
Large digital designs often contain hundreds of blocks organized into clusters, with verification done typically done at block, cluster and top levels, generally using UVM. A single block, can itself be quite large, consisting of anywhere between a handful to a hundred modules. Testing of these lower level modules is sometimes left to the block-level (or higher) verification environment, which results in a longer turnaround. Building UVM testbenches for each lower-level module is generally not practical. While not impossible, this approach has issues that a UVM testbench can only run one test at a time and there's no inbuilt mechanism to consolidate results of multiple runs.
SvUTest is an attempt at a tool that helps designers write sanity checks on their building blocks with minimal overhead. With support for concurrent regressions and inbuilt consolidation of results, this framework enables faster design sign-off.
SvUTest is meant to be used by RTL design engineers to ensure basic sanity of small designs across a known set of input patterns. This framework not meant to be a replacement for UVM and is only recommended for small System Verilog modules with a handful of input/output interfaces and a set of input workloads whose output is known. UVM would still be the go-to solution for large designs with complex stimuli.
Building a unit test requires creating a module that contains the Design Under Test (DUT), called the test_top
, a test_case
class that describes the inputs and outputs to the DUT and finally top level module called regress_top
that creates and runs a test_list
.
Let's look at the floating-point multiplier block in examples/001_floatmul that we need to unit-test. This DUT has two input channels and an output channel, all following the valid-data-ready protocol:
input logic a_valid,
input float32_t a_payload,
output logic a_ready,
input logic b_valid,
input float32_t b_payload,
output logic b_ready,
output logic o_valid,
output float32_t o_payload,
input logic o_ready,
The valid on the output interface shall be asserted if and only if both the inputs' valids a_valid and b_valid are high.
The test_top is a System Verilog module with a single interface port of type svutest_test_ctrl_if
and a single type parameter (T_test_case
):
module floatmul_test_top
// import any other packages needed by the DUT
import svutest_pkg::*;
#(
type T_test_case = bit // Need to be overriden during instantiation
)(
svutest_test_ctrl_if.target tc
);
...
endmodule
This type parameter must be a class derived from svutest_pkg::test_case
class.
The test_top needs to instantiate the following items:
- A
done
signal to indicate that the DUT has finished operation. This signal may be driven by the DUT or may be generated by the test_top. In this example,done
is driven from the test_top. - An instance of
svutest_dut_ctrl_if
. This interface supplies the clock and reset to the interior of the test_top and collects the done signal. It is driven from therun()
method of thetest_case
class instance - An instance of
svutest_req_payload_rsp_if
interface for each channel on the DUT. These interfaces supply the input transactions to and collect the output transactions from, the DUT.svutest_req_payload_rsp_if
is a generic interface that is used to emulate valid..data..ready protocol ampng others.svutest_req_payload_rsp_if
need to be replaced with the right interface in case the protocol is different from valid-data-ready. - An instance of the DUT, with the ports on the DUT hooked to the signals from the svutest_dut_ctrl_if and svutest_req_payload_rsp_if interfaces.
- An
initial .. begin
block where theT_test_case
parameter is instantiated, and itsrun()
method is called. The constructor ofT_test_case
must accept thesvutest_test_ctrl_if
andsvutest_dut_ctrl_if
interface instances in addition to other interface instnaces declared inside the test_top. After the call to the constructor, therun()
method of thetest_case
class must be called to start the test.
With the above instantiations in place, the test_top is now complete:
module floatmul_test_top
import floatmul_pkg::*;
#(
type T_test_case = bit
)(
svutest_test_ctrl_if.target tc
);
svutest_dut_ctrl_if dc ();
svutest_req_payload_rsp_if#(float32_t) a (dc.clk, dc.rst);
svutest_req_payload_rsp_if#(float32_t) b (dc.clk, dc.rst);
svutest_req_payload_rsp_if#(float32_t) o (dc.clk, dc.rst);
logic busy;
// ---------------------------------------------------------------------- //
floatmul u_fmul (
.clk (dc.clk),
.rst (dc.rst),
.busy (busy),
.a_valid (a.valid),
.a_payload (a.payload),
.a_ready (a.ready),
.b_valid (b.valid),
.b_payload (b.payload),
.b_ready (b.ready),
.o_valid (o.valid),
.o_payload (o.payload),
.o_ready (o.ready)
);
always_comb dc.done = ~(a.valid | b.valid | o.valid | busy);
// ---------------------------------------------------------------------- //
initial begin
T_test_case test = new(tc, dc, a, b, o);
test.run();
end
endmodule
A test_top is built once per DUT.
Once the test_top is built, a test_case
class that drives the input interfaces and evaluates the output transactions needs to be created by deriving from svutest_pkg::test_case
. This class acts as a base class for all test sequences for the current DUT. svutest_pkg::test_case
class drives the clock and reset to the DUT while monitoring the done
in addition to managing the input and output interfaces of the DUT. The constructor of svutest_pkg::test_case
accepts the svutest_test_ctrl_if interface, svutest_dut_ctrl_if interface and a test name as constructor arguments:
class floatmul_utest extends test_case;
valid_data_ready_injector#(float32_t) m_a_injector;
valid_data_ready_injector#(float32_t) m_b_injector;
valid_data_ready_extractor#(float32_t) m_o_extractor;
function new (
virtual svutest_test_ctrl_if.target vif_test_ctrl,
virtual svutest_dut_ctrl_if vif_dut_ctrl,
virtual svutest_req_payload_rsp_if#(float32_t).driver vif_a,
virtual svutest_req_payload_rsp_if#(float32_t).driver vif_b,
virtual svutest_req_payload_rsp_if#(float32_t).target vif_o,
string test_case_name
);
super.new(vif_test_ctrl, vif_dut_ctrl, $sformatf("fmul:%0s", test_case_name));
m_a_injector = new(vif_a);
m_b_injector = new(vif_b);
m_o_extractor = new(vif_o);
this.add(m_a_agent);
this.add(m_b_agent);
this.add(m_o_agent);
endfunction
endclass
sv_utest_pkg
provides injectors and extractors for 2 different interface protocols:
valid_data_injector
: Data with validvalid_data_ready_injector
: Data with valid and ready
An injector or extractor needs to be created per interface, depending on the direction of the transaction. The example above creates two injectors and an extractor inside the class. The constructor of test_case
must attach each injector/extractor to the test case by calling add()
method.
Once the injectors and extractors are set up, the user needs to extend the base class for each test scenario and override two virtual functions test_case::populate()
and test_case::check()
. populate()
is used to populate the input data for all the injectors. The injector class provides a function put()
to push a transaction its internal queue. Any number of put()
calls may be made from the populate
function. The actual injection of the transactions to the DUT will be done later.
The transactions emitted from the output channels of the DUT are collected by the extractor and populated into an internal queue. This queue can be accessed from the virtual function check()
using get_queue()
method and queried for correctness of output transactions. Two macros UTEST_ASSERT(expr)
and UTEST_ASSERT_EQ(expr_lhs, expr_rhs)
are provided in svutest_defines.svh
to help the user with the line number and a failure count summary.
The following snippet shows how a simple test scenario with one transaction on each input interface:
class floatmul_test2_0_0 extends floatmul_utest;
function new (
virtual svutest_test_ctrl_if.target vif_test_ctrl,
virtual svutest_dut_ctrl_if.driver vif_dut_ctrl,
virtual svutest_req_payload_rsp_if#(float32_t).driver vif_a,
virtual svutest_req_payload_rsp_if#(float32_t).driver vif_b,
virtual svutest_req_payload_rsp_if#(float32_t).target vif_o
);
super.new(vif_test_ctrl, vif_dut_ctrl, vif_a, vif_b, vif_o, "0_0");
endfunction
function void populate ();
m_a_injector.put('{ sign: 1'b0, exponent: '0, mantissa: '0 });
m_b_injector.put('{ sign: 1'b0, exponent: '0, mantissa: '0 });
endfunction
function void check ();
float32_t queue [$] = m_o_extractor.get_queue();
`UTEST_ASSERT_EQ(queue.size(), 1)
`UTEST_ASSERT_EQ(queue[0], '1)
endfunction
endclass
Once the test_top and test_cases are set up, we need to populate a top module where we do the following:
- Create an instance of
svutest_test_ctrl_if
and thetest_top
for each test scenario - Create a instance of
test_list
and add to it all thesvutest_test_ctrl_if
instances that we created in step 1 - Call the
run()
method on the test list
module regress_top;
import svutest_pkg::*;
import floatmul_test_pkg::*;
svutest_test_ctrl_if i_floatmul_test2_0_0 ();
floatmul_test_top#(floatmul_test2_0_0) u_floatmul_test2_0_0 (i_floatmul_test2_0_0);
svutest_test_ctrl_if i_floatmul_test2_012_012 ();
floatmul_test_top#(floatmul_test2_012_012) u_floatmul_test2_012_012 (i_floatmul_test2_012_012);
initial begin
test_list list = new();
list.add(i_floatmul_test2_0_0);
list.add(i_floatmul_test2_012_012);
list.run();
end
endmodule
The list.run()
method runs all instantiated test cases, in no specific order, and prints a summary (pass +color to the simulation environment for print in color) on the console:
001_floatmul/floatmul_test_pkg.sv", 109: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_8: started at 15000s failed at 15000s
Offending '(this.m_o_agent.m_mon_queue[3].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x0
"001_floatmul/floatmul_test_pkg.sv", 111: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_12: started at 15000s failed at 15000s
Offending '(this.m_o_agent.m_mon_queue[5].payload === float32_t'{sign:0, exponent:128, mantissa:0})'
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x80000000
"001_floatmul/floatmul_test_pkg.sv", 112: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_14: started at 15000s failed at 15000s
Offending '(this.m_o_agent.m_mon_queue[6].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x0
"001_floatmul/floatmul_test_pkg.sv", 114: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_18: started at 15000s failed at 15000s
Offending '(this.m_o_agent.m_mon_queue[8].payload === float32_t'{sign:0, exponent:129, mantissa:0})'
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x81000000
15000 | fmul:0_0> DONE, PASS (2 / 2)
15000 | fmul:012_012> DONE, FAIL (6 / 10)
15000 | Status: DONE, FAIL | (Done: 2, Timeout: 0), (Pass: 1, Fail: 1, Unknown: 0)
Any number of test lists may be created, based on compile time or run time conditions.
SvUTest framework contains 5 source files:
src/svutest_if.sv
src/svutest_core_pkg.sv
src/svutest_injector_pkg.sv
src/svutest_extractor_pkg.sv
src/svutest_test_pkg.sv
src/svutest_pkg.sv
and a header file
src/defines.svh
The source files must be compiled in the order specified above by the user's eda tool like while the header file is typically picked up by providing the include path. A typical invocation from the command line would be:
<tool> src/svutest_if.sv src/svutest_core_pkg.sv src/svutest_injector_pkg.sv src/svutest_extractor_pkg.sv src/svutest_test_pkg.sv src/svutest_pkg.sv <include_path_flag> src/ path_to_other_files
This framework is still in pre-alpha stage.
See CONTRIBUTING.md.
- Report an Issue on GitHub
- Open a Discussion on GitHub
- E-mail us for general questions
SvUTest is licensed under the BSD-3-clause License. See LICENSE.txt for the full license text.