Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
    • Callbacks, Command Line, Seeds and Versions
    • Synchronisation and Reporting
      • Events, Barriers and Pools
      • Report Catchers and Waivers
      • Custom Report Servers and the Verdict Line
      • Heartbeats and Watchdogs
      • Transaction Recording
    • Testbenches at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Synchronisation and Reporting
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Synchronisation and Reporting
  5. Custom Report Servers and the Verdict Line

Part 10 · Advanced Topics · Intermediate

Custom Report Servers and the Verdict Line

Extending uvm_default_report_server for a verdict that survives a fatal, actions, the quit count, per-ID log files, a JSON summary for regression tools, and why a regression keys off one verdict line.

On this page

    A regression script decided pass or fail by counting lines that start with UVM_ERROR in each log. It worked for two years. Then the farm started killing jobs that ran out of memory, and a killed simulation writes no errors at all. For a week, every test that crashed was reported as passed, because a log with no error lines looked exactly like a clean one.

    This lesson builds the opposite: one verdict line that a run prints on every way out, plus a machine-readable summary, so the regression keys off a positive statement of PASS and treats its absence as a failure. It also covers the report server's other controls, actions, the quit count and log files, at the level a senior engineer is expected to know them.

    The idea: the referee's scorecard

    Every message, from every component, ends at one object: the global report server. It is the referee holding the only scorecard. It counts messages by severity and by ID, applies the quit count, and at the end calls report_summarize(), which prints the familiar summary. Because there is only one server and it sees everything, it is the one place where a verdict can be computed correctly.

    The library calls report_summarize() on two paths: at the end of run_test() after the report phases, and from uvm_root::die(), which runs on a UVM_FATAL, on the quit count, and on any message with the UVM_EXIT action, just before $finish. A line printed there survives a fatal. A banner printed in report_phase does not, because a fatal skips the remaining phases.

    The picture: three ways a run ends

    Flowchart: a normal end runs the report phases then report_summarize; a fatal, exit action or quit count goes through die and pre_abort to report_summarize; both produce the verdict line and summary. A crash produces nothing. The regression passes a test only if the verdict line says PASS.
    Normal ends and fatal ends both reach report_summarize, so a verdict printed there always appears. A crash prints nothing, which the regression must read as FAIL.

    Build it: the bench's verdict server

    dma_report_server extends uvm_default_report_server and adds one line after the standard summary:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // snippet: from the course DMA bench
    // A report server that always ends the log with one verdict line, even when the
    // run stops on a fatal (the library calls report_summarize on the way out).
    class dma_report_server extends uvm_default_report_server;
      function new(string name = "dma_report_server");
        super.new(name);
      endfunction
    
      virtual function void report_summarize(UVM_FILE file = 0);
        int n_err = get_severity_count(UVM_ERROR);
        int n_fat = get_severity_count(UVM_FATAL);
        super.report_summarize(file);
        $display("TEST VERDICT: %s (UVM_ERROR=%0d, UVM_FATAL=%0d)",
                 (n_err == 0 && n_fat == 0) ? "PASS" : "FAIL", n_err, n_fat);
      endfunction
    endclass

    install_verdict_server() makes it the global server. uvm_report_server::set_server() copies the counts from the server it replaces, so messages reported before the install are not lost. The phase-7 tests call it in their constructor, the earliest point a test can run code:

    design.sv
    1
    2
    3
    4
    5
    // snippet: from dma_pkg in the course DMA bench (dma_tb.sv)
    function automatic void install_verdict_server();
      dma_report_server srv = new();
      uvm_report_server::set_server(srv);
    endfunction
    design.sv
    1
    2
    3
    4
    5
    // snippet: from dma_verdict_test in the course DMA bench (dma_tb.sv)
    function new(string name, uvm_component parent);
      super.new(name, parent);
      install_verdict_server();
    endfunction

    Watch it run: PASS, and FAIL after a fatal

    Diagram
    +UVM_TESTNAME=dma_verdict_test
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0
    TEST VERDICT: PASS (UVM_ERROR=0, UVM_FATAL=0)
    
    +UVM_TESTNAME=dma_verdict_fatal_test
    UVM_FATAL base/uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue
    UVM_ERROR :    0
    UVM_FATAL :    1
    TEST VERDICT: FAIL (UVM_ERROR=0, UVM_FATAL=1)
    • The passing run ends with the standard counts and then the verdict line.

    • The fatal run never reached report_phase; the timeout fatal went through die(), which still called report_summarize(), so the verdict line is there and says FAIL.

    Note

    report_summarize(UVM_FILE file = 0) in the bench compiles on both libraries: UVM 1.2 declares the default as 0, and IEEE 1800.2 as UVM_STDOUT. An override must match the base signature's argument list; the default value may differ.

    Actions: what the server does with a message

    Each message carries an action, a bit mask the component's report handler looked up from its severity and ID. The server executes it:

    Action

    Effect

    Default for

    UVM_DISPLAY

    Print to standard output

    every severity

    UVM_LOG

    Write to the file handle set for that severity or ID

    none

    UVM_COUNT

    Count toward the quit count

    UVM_ERROR

    UVM_EXIT

    Call die(): summarize and $finish

    UVM_FATAL

    UVM_STOP

    Call $stop, handing control to the simulator prompt

    none

    UVM_CALL_HOOK

    Call the component's report_hook methods first (legacy)

    none

    UVM_RM_RECORD

    Record the message in the transaction database

    none

    UVM_NO_ACTION

    Do nothing: the message is dropped

    none

    The server counts every message it executes, whatever the action, which is why warnings appear in the summary counts. UVM_COUNT is only about the quit count. UVM_STOP is useful interactively: set it on one ID with +uvm_set_action=uvm_test_top.env.sb,SB_MISMATCH,UVM_ERROR,UVM_DISPLAY|UVM_STOP and the simulator stops at the first mismatch with the waveform at the right time.

    The quit count: stop after N

    set_max_quit_count(n) on the server (or +UVM_MAX_QUIT_COUNT=n,NO, where NO forbids later overrides) ends the run after n messages with UVM_COUNT. The quit count goes through die(), so the verdict line appears. The scratch run caps a run that would report six mismatches at three, and also routes that ID to its own log file:

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: scratch run (sync_tb.sv), quit_test's end_of_elaboration_phase
    uvm_report_server srv = uvm_report_server::get_server();
    srv.set_max_quit_count(3);
    sb_log = $fopen("sb_mismatch.log", "w");
    set_report_id_file("SB_MISMATCH", sb_log);
    set_report_id_action("SB_MISMATCH", UVM_DISPLAY | UVM_LOG | UVM_COUNT);
    Diagram
    scratch run: +UVM_TESTNAME=quit_test
    UVM_ERROR sync_tb.sv(287) @ 10000: uvm_test_top [SB_MISMATCH] word mismatch at 10000
    UVM_ERROR sync_tb.sv(287) @ 20000: uvm_test_top [SB_MISMATCH] word mismatch at 20000
    UVM_ERROR sync_tb.sv(287) @ 30000: uvm_test_top [SB_MISMATCH] word mismatch at 30000
    --- UVM Report Summary ---
    Quit count reached!
    Quit count :     3 of     3
    UVM_ERROR :    3
    TEST VERDICT: FAIL (UVM_ERROR=3, UVM_FATAL=0)

    The same three lines were also written to sb_mismatch.log. The set_report_*_file calls exist per severity, per ID and per severity-and-ID, plus set_report_default_file, each with a _hier form for a whole subtree. Setting UVM_LOG without a file writes to the default file, which is 0, meaning standard output: the message lands in the main log anyway. Set the file and the action together.

    A machine-readable summary

    A regression dashboard wants fields, not prose. Extending report_summarize() once more writes a JSON record per run. The seed comes from the launcher as a plusarg: the launcher chose it, and there is no portable IEEE call that returns the simulator's seed ($get_initial_random_seed is a vendor extension).

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // snippet: verified in a scratch run (sync_tb.sv)
    class json_report_server extends uvm_default_report_server;
      string json_file = "uvm_summary.json";
      function new(string name = "json_report_server");
        super.new(name);
      endfunction
      virtual function void report_summarize(UVM_FILE file = 0);
        string ids[$];
        string test = "unknown", seed = "unknown", id_list = "";
        int n_info = get_severity_count(UVM_INFO);       // read before the summary adds its own info
        int n_warn = get_severity_count(UVM_WARNING);
        int n_err  = get_severity_count(UVM_ERROR);
        int n_fat  = get_severity_count(UVM_FATAL);
        string verdict = (n_err == 0 && n_fat == 0) ? "PASS" : "FAIL";
        int fd;
        super.report_summarize(file);
        void'($value$plusargs("UVM_TESTNAME=%s", test));
        void'($value$plusargs("SEED=%s", seed));          // the launcher's copy of the seed, for the record only;
                                                         // the simulator's own seed switch is tool-specific
        get_id_set(ids);
        foreach (ids[i])
          id_list = {id_list, (i == 0) ? "" : ", ", $sformatf("\"%s\": %0d", ids[i], get_id_count(ids[i]))};
        fd = $fopen(json_file, "w");
        $fdisplay(fd, "{\"test\": \"%s\", \"seed\": \"%s\", \"verdict\": \"%s\", \"end_time\": \"%0t\",",
                  test, seed, verdict, $time);
        $fdisplay(fd, " \"counts\": {\"info\": %0d, \"warning\": %0d, \"error\": %0d, \"fatal\": %0d},",
                  n_info, n_warn, n_err, n_fat);
        $fdisplay(fd, " \"ids\": {%s}}", id_list);
        $fclose(fd);
        $display("TEST VERDICT: %s (UVM_ERROR=%0d, UVM_FATAL=%0d)", verdict, n_err, n_fat);
      endfunction
    endclass
    Diagram
    scratch run: +UVM_TESTNAME=json_test +SEED=1234   (uvm_summary.json; the library's own IDs trimmed)
    {"test": "json_test", "seed": "1234", "verdict": "FAIL", "end_time": "20000",
     "counts": {"info": 4, "warning": 3, "error": 1, "fatal": 0},
     "ids": {"CFG": 1, "RNTST": 1, "SB_MISMATCH": 1, "UVM/RELNOTES": 1, "UVM/REPORT/SERVER": 1}}

    The per-ID counts are the triage key: a regression of 2,000 tests that groups failures by their first error ID turns 140 red tests into four bugs. A CSV row (test,seed,verdict,errors,fatals,first_error_id) appended to one shared file is the lowest-tech version of the same idea. Escape any quote characters in IDs before writing real JSON, and write the file from report_summarize(), so a fatal run produces it too: the heartbeat scratch run in the next lesson ends on a fatal and its JSON was written.

    Why the regression keys off one line

    • Absence means failure. The script looks for TEST VERDICT: PASS. A crash, a kill or a truncated log has no verdict line and is a FAIL by construction, which is the bug from the opening.

    • The counts, not the text. Grepping for UVM_ERROR also matches message text and misses demoted or caught messages. The server's counts already reflect overrides and catchers, which is the definition of the test result.

    • One definition, in one place. Whether warnings fail a test, or a non-zero catcher summary needs review, is decided once in the server class every test installs, not in a dozen scripts.

    • Also check the scoreboard's own evidence. A verdict of PASS with zero words compared is still a false pass; the bench's SB_EMPTY error exists for that reason.

    What breaks: the PASS banner that vanished

    A team printed *** TEST PASSED *** or *** TEST FAILED *** from the base test's report_phase, and the regression grepped for PASSED. It took a while to notice that a test ending on a uvm_fatal printed neither: the fatal called die(), the report phases never ran, and the script classified the test as "no result", which a colleague's filter then silently dropped from the report. Fatals were exactly the failures they most needed to see. Moving the verdict into a report server's report_summarize(), as in dma_verdict_fatal_test above, made every exit print it.

    Interview

    "How does your regression decide that a test passed?"

    A weak answer: "It greps the log for UVM_ERROR and UVM_FATAL and checks they are zero."

    A strong answer: "Each test installs a report server whose report_summarize prints one verdict line and writes a JSON summary with the test, seed, counts and per-ID counts. The library calls report_summarize on a normal end and from die() on a fatal or the quit count, so the line always appears unless the simulator crashed, and the regression treats a missing line as a failure. The counts come from the server, after severity overrides and catchers, so they are the real result. Failures are bucketed by first error ID for triage, and a PASS with zero scoreboard compares is caught separately by the scoreboard's own empty-run error."

    Expect the follow-ups: Why not print the verdict in report_phase? (a fatal skips it) and What does set_server copy? (the severity and ID counts of the old server, so early messages are kept).

    Check yourself A test sets +UVM_MAX_QUIT_COUNT=5 and hits 5 errors at 2 µs. Does its check_phase run? Does the verdict line print?

    No and yes. The quit count adds UVM_EXIT to the fifth counted message, which calls die(): the remaining phases are skipped, but die() calls report_summarize(), so a verdict printed there appears (FAIL).

    Check yourself Someone sets set_report_id_action_hier("SB_MISMATCH", UVM_LOG) on the env to declutter the log, but forgets the file. What changes?

    Less than they hoped, and one thing they did not want. With no file set, UVM_LOG writes to the default file 0, which is standard output, so the mismatches still appear in the main log. But the action no longer includes UVM_COUNT, so mismatches stop counting toward the quit count, and a runaway mismatch loop no longer ends early. The server still counts them by severity, so the verdict still fails. Set the file together with the action, and keep UVM_COUNT on errors.

    Check yourself Why does the JSON server read the severity counts before calling super.report_summarize()?

    The default summary is itself reported as a UVM_INFO message, which the server counts. Reading afterwards makes the info count one higher than the summary that was just printed; reading first keeps the JSON consistent with the printed counts.

    Next: a verdict is only useful if the run ends. The next lesson ends hung runs in microseconds: Heartbeats and Watchdogs.

    Related topics

    • Up: Synchronisation and Reporting

    • run_test, timeouts and how a simulation ends

    • Command-line control

    • Seed control and replay

    Previous

    176 / 280

    Next: Heartbeats and Watchdogs

    Up next

    Up nextHeartbeats and Watchdogs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Events, Barriers and Pools
    • Transaction Recording