The Test
The Environment can be built and launched in two lines. This page is where that actually happens — and where the open problem it left behind, deciding when a test is really finished, has to get an answer.
Testbench-top
module tb_top;
bit clk, rst_n;
always #5 clk = ~clk;
fifo_if fifo_if_inst (.clk(clk), .rst_n(rst_n));
sync_fifo dut (
.clk (clk),
.rst_n (rst_n),
.wr_en (fifo_if_inst.wr_en),
.wr_data (fifo_if_inst.wr_data),
.full (fifo_if_inst.full),
.rd_en (fifo_if_inst.rd_en),
.rd_data (fifo_if_inst.rd_data),
.empty (fifo_if_inst.empty)
);
environment env;
initial begin
rst_n = 0;
#20 rst_n = 1;
env = new(fifo_if_inst, 20);
env.run();
#100; // give the last few transactions time to drain through driver/monitor/scoreboard
$display("PASS=%0d FAIL=%0d", env.sb.pass_count, env.sb.fail_count);
$finish;
end
endmodule
This is deliberately not a separate "test class" at all — no class, just an initial block in the same module that instantiates the DUT. That's the single biggest remaining gap against UVM's test/testbench-top split: a UVM test is its own class, extended per scenario, with testbench-top reduced to nothing but DUT instantiation and run_test(). Here, changing the test scenario means editing this module directly — there's no way to swap in a different stimulus count, or a different sequence of behavior, without touching the file that also owns the DUT.
The #100: an honest, fragile answer to the open problem
env.run() returns the moment the generator finishes producing 20 transactions — but, as the previous page flagged, the driver/monitor/scoreboard are still running in the background at that exact moment. #100 is a flat guess: wait what's probably long enough for the last transactions to finish draining, then check results regardless of whether they actually have.
SystemVerilog's example walkthrough used an identical #100 before its own $finish, describing it explicitly as "a simple stand-in for the more rigorous end-of-test synchronization... that a production testbench would use instead." Nothing has changed that here — a fixed delay has no relationship to how long the pipeline actually needs, works by luck on a small transaction count, and gets actively wrong (either wastefully long or, worse, too short and silently truncating real checks) the moment stimulus volume changes. UVM's objection mechanism — raise_objection()/drop_objection(), keeping a phase open for exactly as long as real work is still happening — is the actual, correct answer to this exact problem, not a nice-to-have refinement of it.
What's next
Generation, driving, observing, and checking are all wired together and running as one test. The last piece this section adds is functional coverage — tracking not just whether results were correct, but whether the test actually exercised what it needed to.