Two tests from the same regression, both broken. One reports PASS after 0 ns of simulated time with nothing driven. The other holds a farm slot for nine hours until the job scheduler kills it, leaving no summary and no clue. Both are end-of-test bugs, and UVM has a precise answer for each: the first never raised an objection, and the second had no timeout short enough to matter.
run_test is the stage manager. It decides which show is on, opens the doors, lets the show run until the cast says it is done, then reads the reviews and closes the theatre. Two other things can close the theatre early: a fatal problem (someone pulls the fire alarm), or a timeout (the building's closing time). On an early close, the audience leaves at once: there is a quick summary at the door, but the critics' review, report_phase, never happens.
The bench's top module ends with run_test("dma_smoke_test");. That name is only a default. If the command line has +UVM_TESTNAME=dma_rand_test, run_test uses that instead: the plusarg beats the argument. That is how one compiled bench runs a whole regression of different tests. run_test then asks the factory to create the named class with the instance name uvm_test_top, so a misspelled name is a fatal at time 0, not a silent fallback.
When the phases are done, run_test calls report_summarize() on the report server, which prints the --- UVM Report Summary --- block with the counts by severity and by id, and then calls $finish if finish_on_completion is 1 (the default). Set it to 0 and run_test returns to your initial block instead, which is the place for post-processing that must happen after UVM is done:
// snippet
initial begin
// config_db sets for the interfaces, as in the bench's top
uvm_root::get().finish_on_completion = 0;
run_test("dma_smoke_test");
// UVM is done: every phase has run and the summary is printed
$display("post-processing coverage files...");
$finish;
endThe bench's timeout test is written to hang: it runs a job with the interrupt disabled and waits for an interrupt that never comes. It survives because it sets a timeout: a watchdog that ends run_phase with a fatal at a fixed simulated time.
// snippet: from the course DMA bench
// A test that would hang: the interrupt is never enabled, so it never fires.
// The global timeout ends the run with a fatal instead of burning the regression slot.
class dma_timeout_test extends dma_base_test;
`uvm_component_utils(dma_timeout_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_root::get().set_timeout(20us, 1); // replaces a +UVM_TIMEOUT=...,YES value; ,NO locks the command line
endfunction
task run_phase(uvm_phase phase);
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
if (!job.randomize() with { len == 4; irq_en == 0; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 4, status); // waits for an interrupt that never comes
phase.drop_objection(this);
endtask
endclassuvm_root::get().set_timeout(20us, 1) sets the limit to 20 µs. The second argument says whether a later set_timeout may change it. Without any call, the default limit is 9200 s of simulated time, long enough to burn a farm slot.
The watchdog starts with run_phase and measures from there, so the limit is a total, not a per-sequence budget.
On the command line, +UVM_TIMEOUT=<time>,<YES|NO> sets the same limit. The time is a plain integer in the time unit the UVM package was compiled with (picoseconds in this bench's run, which is why the log below says 20000000), so +UVM_TIMEOUT=10ms does not mean what it looks like. YES or NO is the same overridable flag.
The order of the two matters. The library applies +UVM_TIMEOUT in uvm_root's own build_phase, which runs before the test's build_phase (top-down). So a test that calls set_timeout(..., 1) replaces a command-line value given with YES. To make the command line win over the test, pass NO, which locks the value; the test's later call is then refused with an informational message. That is what the comment in the listing means by "can still override".
The timeout test's log, trimmed to the lines that matter:
UVM_INFO @ 0: reporter [RNTST] Running test dma_timeout_test...
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue
--- UVM Report Summary ---
** Report counts by severity
UVM_ERROR : 0
UVM_FATAL : 1
** Report counts by id
[PH_TIMEOUT] 1
[RNTST] 1"Explicit" means a limit set by set_timeout or +UVM_TIMEOUT; the default limit says "Default". The time is printed in the UVM package's unit.
The scoreboard's [SB] ... words matched line is missing. It is printed in report_phase, and after a fatal the remaining phases do not run. The summary appears only because die() calls report_summarize() itself.
A banner printed in report_phase ("TEST PASSED") would also be missing, which is why regression scripts judge a test by the summary's counts, never by a banner.
die() also calls pre_abort() on every component, bottom-up, before summarising. That is the hook for writing something useful on the way out, such as the scoreboard's pending queue.
The report server counts every message by severity. Messages whose action includes UVM_COUNT (by default every UVM_ERROR) also count toward a quit count: when it reaches its limit, the server calls die() and the test ends exactly as after a fatal. The default limit is 0, meaning no limit. Set it with +UVM_MAX_QUIT_COUNT=10,NO on the command line, or uvm_report_server::get_server().set_max_quit_count(10) in code, to stop a test that has already failed from producing a 2 GB log.
A variant of the smoke test that runs one DMA job but never raises an objection:
UVM_INFO @ 0: reporter [RNTST] Running test dma_noobj_test...
UVM_ERROR dma_tb.sv(407) @ 0: uvm_test_top.env.sb [SB_EMPTY] no words were compared: the test checked nothing
UVM_INFO dma_tb.sv(411) @ 0: uvm_test_top.env.sb [SB] 0 words matched, 0 mismatched
UVM_ERROR : 1
UVM_FATAL : 0Nothing objected, so the run phase's count was zero from the start, and run_phase ended at time 0 (after the drain time, here zero). The test's run_phase thread had started its register writes, but it was killed with every other run thread. The same thing happens when the test starts its work with fork ... join_none and returns without raising: the forked threads are killed with it. This is an early end, not a hang.
The test is saved only by the scoreboard's SB_EMPTY check in check_phase. Without it, this run would report UVM_ERROR : 0 and pass. Every bench needs both halves: an objection raised before the first wait, and a check that fails a run that compared nothing.
The other half of the opening, the nine-hour hang, was a test waiting on an interrupt that a configuration change had disabled, with no timeout below the 9200 s default. The team set a base-test timeout of ten times the longest test's normal run time, and the same bug now ends in seconds with a PH_TIMEOUT line that points straight at run_phase.
+UVM_TESTNAME=dma_rand_test, and the top calls run_test("dma_smoke_test"). Which test runs, and what does the log say?dma_rand_test: the plusarg beats the argument. The log's RNTST line says Running test dma_rand_test....
No banner. A fatal calls die(), which runs pre_abort and report_summarize and then $finish; report_phase is skipped. The summary's UVM_FATAL : 1 is the reliable verdict.
set_timeout(1ms, 1) in build. One long test needs 5 ms. What is the cleanest way to give it more time from the command line?+UVM_TIMEOUT=<5 ms in the UVM time unit>,NO. The plusarg is applied first, in uvm_root's build; the NO locks it, so the base test's later set_timeout is refused. With YES, the base test would override it back to 1 ms.
Next: objections decide when the run phase ends, and they have more rules than raise and drop. The Objections Deep Dive covers counting, propagation, drain time and the end-of-test strategies.