System Verilog Unit Testing Framework (SvUTest) is an open source framework for performing rapid sanity testing of lightweight hardware modules in System Verilog. This project is inspired by CppUTest, Google Test and UVM.
- Rapid test case development
- Concurrent regressions
- Consolidation of regression status
- Inbuilt support for interface protocols like valid-ready and credit-data
SvUTest is meant to help RTL designers ensure the basic sanity of small designs whose outputs can be predicted for a given set of inputs. This is not a replacement for UVM based tests.
Let's say we need to test a module that multiplies two floating point numbers:
package floatmul_pkg; typedef struct packed{logic sign; logic [7:0] exponent; logic [23:0] mantissa} float32_t; endpackage modulefloatmul import floatmul_pkg::*; ( input logic clk, input logic rst, output logic busy, input logic a_valid, input float32_t a_data, output logic a_ready, input logic b_valid, input float32_t b_data, output logic b_ready, output logic o_valid, output float32_t o_data, input logic o_ready ); // Implementation a_ready =/* .. */; b_ready =/* .. */; o_valid =/* .. */; o_data =/* .. */endmoduleWe start by creating a test_top module which has a single interface port of type svutest_test_ctrl_if.target and a single type parameter T_test_case. It instantiates the DUT and calls the run() method of T_Test_case:
modulefloatmul_test_top import floatmul_pkg::*; #( type T_test_case = bit // Test case )( svutest_test_ctrl_if.target tc // Test control from the regress suite ); // DUT control. Injects clock + rst, and collects test-donesvutest_dut_ctrl_ifdc (); // Input and output interfaces to DUTsvutest_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; // Instantiate thefloatmulu_fmul ( .clk (dc.clk), .rst (dc.rst), .busy (busy), .a_valid (a.req), .a_data (a.req_payload), .a_ready (a.rsp), .b_valid (b.req), .b_data (b.req_payload), .b_ready (b.rsp), .o_valid (o.req), .o_data (o.req_payload), .o_ready (o.rsp) ); // Set this signal once the test is done always_comb dc.done =~(a.req | b.req | o.req | busy); initialbegin// Instantiate the test case, pass the test_ctrl and dut_ctrl ifs,// along other inputs and outputs. And call run(). T_test_case test = new(tc, dc, a, b, o); test.run(); endendmoduleOnce the test top is set up, we need to create a test case base class per dut, derived from svutest_pkg::test_case, that accepts the test_ctrl, dut_ctrl and other interfaces:
class floatmul_utest extends svutest_pkg::test_case; valid_data_ready_injector#(float32_t) m_a_injector; // Built-in injector for valid-ready protocol (input a) valid_data_ready_injector#(float32_t) m_b_injector; // Injector for input b valid_data_ready_extractor#(float32_t) m_o_extractor; // Extractor for output cfunction new ( virtual svutest_test_ctrl_if.target vif_test_ctrl, // Test Ctrl virtual svutest_dut_ctrl_if.driver vif_dut_ctrl, // Dut Ctrl virtual svutest_req_payload_rsp_if#(float32_t).driver vif_a, // a virtual svutest_req_payload_rsp_if#(float32_t).driver vif_b, // b virtual svutest_req_payload_rsp_if#(float32_t).target vif_o, // c string test_case_name // Test name ); // Pass test-ctrl, dut_ctrl and test_name to svutest_pkg::test_case::new() super.new(vif_test_ctrl, vif_dut_ctrl, $sformatf("fmul:%0s", test_case_name)); // Create injectors and extractors m_a_injector = new(vif_a); m_b_injector = new(vif_b); m_o_extractor = new(vif_o); // And register them with the test case this.add(m_a_injector); this.add(m_b_injector); this.add(m_o_extractor); endfunction endclassThe next step is to create a test case class per input-output pattern, derived from the base class:
class floatmul_utest_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// Override the populate() virtual function to set up the transcations// that will be injected per interface. This is a function that is// run once before actual simulationfunction 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// Override the check() virtual function to check the number of// outputs emitted and the expectation of each outputfunction void check (); // Get the extractor's internal queue float32_t queue [$] = m_o_extractor.get_queue(); // Check the number of entries emitted from the output `SVUTEST_ASSERT_EQ(queue.size(), 1) // Check the first entry `SVUTEST_ASSERT_EQ(queue[0], '1) endfunction endclassThe last step is to create a top module that instantiates the test cases and runs them:
moduleregress_top; import svutest_pkg::*; import floatmul_utest_pkg::*; // Create the test_ctrl interfacesvutest_test_ctrl_ifi_floatmul_test2_0_0 (); // Instantiate the test_top while passing the test_ctrl interfacefloatmul_test_top#(floatmul_test2_0_0) u_floatmul_test2_0_0 (i_floatmul_test2_0_0); // Another test case, using a macro that does the above in one line `SVUTEST(floatmul_test_top, floatmul_test2_012_012) initialbegin// Create a test-list test_list list = new(); // Add the two test cases to the list by passing the interfaces to add() list.add(i_floatmul_test2_0_0); list.add(i_floatmul_test2_012_012); // Run all tests in the test list list.run(); endendmoduleThe run() task of test_list outputs one line of summary per test case, showing the number of assertions that failed in that test case. A complete summary will be printed at the end. Any failure from SVUTEST_ASSERT_EQ macros will get printed in additional lines:
7000 | fmul:0_0> SVUTEST_ASSERT_EQ failed: /usr2/nvettuva/SvUTest/examples/001_floatmul/floatmul_utest_pkg.sv,58: Expected == 0x0, actual == 0x1 7000 | fmul:0_0> COMPLETE. Assertions: 1/2 [FAIL] 15000 | fmul:012_012> SVUTEST_ASSERT_EQ failed: /usr2/nvettuva/SvUTest/examples/001_floatmul/floatmul_utest_pkg.sv,108: Expected == 0x7f000000, actual == 0x0 15000 | fmul:012_012> SVUTEST_ASSERT_EQ failed: /usr2/nvettuva/SvUTest/examples/001_floatmul/floatmul_utest_pkg.sv,110: Expected == 0x7f000000, actual == 0x80000000 15000 | fmul:012_012> SVUTEST_ASSERT_EQ failed: /usr2/nvettuva/SvUTest/examples/001_floatmul/floatmul_utest_pkg.sv,111: Expected == 0x80000000, actual == 0x0 15000 | fmul:012_012> SVUTEST_ASSERT_EQ failed: /usr2/nvettuva/SvUTest/examples/001_floatmul/floatmul_utest_pkg.sv,113: Expected == 0x80000000, actual == 0x81000000 15000 | fmul:012_012> COMPLETE. Assertions: 6/10 [FAIL] 15000 | Status: FAIL | Total: 11, Unresponsive: 0, Timeout: 0, Unchecked: 0, Fail: 2, Pass: 9 SvUTest framework contains 2 source files:
src/svutest_if.sv src/svutest_pkg.sv and a few header files defined under src:
src/svutest_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_pkg.sv <include_path_flag> src path_to_other_files See guide.md for more information.
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.