UVM Logging to File
Report handler config, UVM_LOG action, per-severity routing, tiered logging architecture.
UVM Fundamentals · Module 21
§1 — The Problem With Relying on Transcripts
Here's a scenario that plays out constantly on verification teams. You're running overnight regression — 800 tests across a compute farm. A test fails. The CI system flags it. You click the job, look for the simulation transcript… and there's nothing. The simulator's stdout was captured only to console during the run. It's gone. The test failed, you know it failed, but you have no idea what failed, which transaction triggered the error, or what the debug messages said leading up to it.
This happens to every team exactly once. After that, the first thing anyone touches when setting up a new verification environment is file logging. Not because it's intellectually interesting — it isn't — but because losing debug information from a failing regression test is genuinely expensive.
UVM's report infrastructure handles this elegantly once you understand how message routing works. Every uvm_info`, uvm_warning, ``uvm_error, and ``uvm_fatalthat fires during simulation can be simultaneously written to the console transcript AND to one or more log files — with different files for different severities, different verbosities, or different components. The mechanism that makes this possible is theUVM_LOGaction flag combined with a file handle registered throughset_report_default_file()`.
§2 — The Message Routing Pipeline — Intuition First
When a UVM message fires, it doesn't go directly to the console. It travels through a two-stage pipeline: the uvm_report_handler (one per component, applies verbosity and ID filters) and the uvm_report_server (global singleton, applies actions). Actions are what determine the destination of each message. File logging happens when you add the UVM_LOG bit to a severity's action set.
| Action Bit | Value | Effect |
|---|---|---|
UVM_NO_ACTION | 0 | Drop the message entirely — nothing happens |
UVM_DISPLAY | 1 | Print to console (simulator transcript / stdout) |
UVM_LOG | 2 | Write to the registered file handle — the key to file logging |
UVM_COUNT | 4 | Increment the report server's severity counter |
UVM_EXIT | 8 | Terminate simulation after this message |
UVM_CALL_HOOK | 16 | Invoke user-registered report hook |
UVM_DISPLAY | UVM_LOG | 3 | Both console AND file — the most common production setup |
The file handle itself comes from SystemVerilog's $fopen(). You get an integer descriptor, register it with the UVM component using set_report_default_file(fd), and then configure the action to include UVM_LOG. From that point forward, every message from that component (and its children, if you use the hierarchical version) that triggers the LOG action gets written to that file.
§3 — The File Logging API — All the Methods
// ── Step 1: Open a file and get the descriptor ────────────────────────
integer log_fd = $fopen("simulation.log", "w"); // overwrite each run
integer err_fd = $fopen("errors.log", "w");
integer pass_fd = $fopen("result.log", "w");
// ── Step 2: Register the file with a component ────────────────────────
// set_report_default_file: ALL messages from this component → fd
set_report_default_file(log_fd);
// Hierarchical: this component AND all its children in the hierarchy
uvm_root::get().set_report_default_file_hier(log_fd);
// ── Step 3: Configure actions to include UVM_LOG ─────────────────────
// Per-severity action (replaces the severity's current action set)
set_report_severity_action(UVM_INFO, UVM_DISPLAY | UVM_LOG);
set_report_severity_action(UVM_WARNING, UVM_DISPLAY | UVM_LOG | UVM_COUNT);
set_report_severity_action(UVM_ERROR, UVM_DISPLAY | UVM_LOG | UVM_COUNT);
set_report_severity_action(UVM_FATAL, UVM_DISPLAY | UVM_LOG | UVM_COUNT | UVM_EXIT);
// Hierarchical version — propagates down to all children
uvm_root::get().set_report_severity_action_hier(UVM_INFO, UVM_DISPLAY | UVM_LOG);
// ── Per-severity, different files ─────────────────────────────────────
// Errors go to errors.log; everything goes to simulation.log
set_report_default_file(log_fd); // base: all messages → log_fd
set_report_severity_file(UVM_ERROR, err_fd); // errors ALSO → err_fd
set_report_severity_file(UVM_FATAL, err_fd); // fatals ALSO → err_fd
set_report_severity_action(UVM_ERROR, UVM_DISPLAY | UVM_LOG); // LOG writes to severity file
// ── Per-ID, specific message to its own file ──────────────────────────
integer scb_fd = $fopen("scoreboard_trace.log", "w");
set_report_id_file("SCB", scb_fd);
set_report_id_action("SCB", UVM_DISPLAY | UVM_LOG);
// ── File-only (no console) — useful for very high verbosity debug ─────
set_report_severity_action(UVM_INFO, UVM_LOG); // UVM_LOG only — no DISPLAY
// ── Close files properly ──────────────────────────────────────────────
// In final_phase — always close what you open
$fclose(log_fd);
$fclose(err_fd);
$fclose(pass_fd);| Method | Scope | What It Sets |
|---|---|---|
set_report_default_file(fd) | This component | Default file for ALL messages from this component |
set_report_default_file_hier(fd) | This + all children | Default file for all messages in the subtree |
set_report_severity_file(sev, fd) | This component | Override file for a specific severity level |
set_report_id_file(id, fd) | This component | Override file for a specific message ID string |
set_report_severity_id_file(sev, id, fd) | This component | Most specific: override for a severity+ID combination |
set_report_severity_action(sev, action) | This component | Set the action mask (include/exclude UVM_LOG) |
set_report_severity_action_hier(sev, action) | This + all children | Propagate action change down the entire hierarchy |
§4 — Message Routing Diagrams
apb_monitor`uvm_info("MON", msg, UVM_MEDIUM)Report HandlerVerbosity filterID filterSeverity overrideFile registrationReport ServerAction lookupCount incrementDISPLAY → consoleLOG → file handleConsole (DISPLAY)Simulator transcriptstdout / transcript windowsimulation.log (LOG)set_report_default_file(fd)errors.log (severity file)set_report_severity_file()UVM_DISPLAY alone → console onlyUVM_DISPLAY | UVM_LOG → console + file ← most common production settingUVM_LOG alone → file only, no console ← for ultra-high verbosity debug files Figure 1 — UVM message routing pipeline. A message passes through the per-component report handler (filtering) then the global report server (action dispatch). The server routes to console when UVM_DISPLAY is set and to file when UVM_LOG is set. Both can be active simultaneously.
Action Routing Decision Table
| Action Set | Console Output | File Output | Use Case |
|---|---|---|---|
UVM_DISPLAY | YES | NO | Development / interactive debug. Default for most messages. |
UVM_LOG | NO | YES | Background regression — silent console, write-to-file only. Good for ultra-verbose debug traces. |
UVM_DISPLAY | UVM_LOG | YES | YES | Production regression — both CI system and debug engineer get the full picture. |
UVM_NO_ACTION | NO | NO | Suppress completely. Use for suppressing known false alarms in regression. |
§5 — Code Examples — From Basic to Production
Example 1 — Single Log File for All Messages
class base_test extends uvm_test;
`uvm_component_utils(base_test)
integer sim_log; // file descriptor — stored as class member for final_phase
function new(string n, uvm_component p); super.new(n,p); endfunction
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
// Open the log — "w" overwrites each run (safer than append for regression)
sim_log = $fopen("simulation.log", "w");
if (sim_log == 0) begin
`uvm_fatal("LOG", "Cannot open simulation.log — check write permissions")
end
// Write a header so we know which test this log belongs to
$fwrite(sim_log, "==== Test: %s ====\n", get_type_name());
$fwrite(sim_log, "==== Start: %0t ====\n\n", $time);
// Register with uvm_root so ALL components in the hierarchy use this file
uvm_root::get().set_report_default_file_hier(sim_log);
// Add UVM_LOG to each severity — now every message goes console AND file
uvm_root::get().set_report_severity_action_hier(UVM_INFO, UVM_DISPLAY | UVM_LOG);
uvm_root::get().set_report_severity_action_hier(UVM_WARNING, UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_ERROR, UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_FATAL, UVM_DISPLAY | UVM_LOG | UVM_COUNT | UVM_EXIT);
`uvm_info("LOG", "File logging active → simulation.log", UVM_NONE)
endfunction
// CRITICAL: close the file — unflushed buffers may lose last messages otherwise
function void final_phase(uvm_phase phase);
super.final_phase(phase);
if (sim_log) $fclose(sim_log);
endfunction
endclassExample 2 — Tiered Logging: Three Files, Three Purposes
class production_base_test extends uvm_test;
`uvm_component_utils(production_base_test)
integer debug_fd; // everything at HIGH verbosity — debug engineer
integer error_fd; // warnings and above only — triage engineer
integer result_fd; // PASS/FAIL stamp only — CI pipeline parser
function new(string n, uvm_component p); super.new(n,p); endfunction
function void start_of_simulation_phase(uvm_phase phase);
string test_name = get_type_name();
super.start_of_simulation_phase(phase);
// Open all three files
debug_fd = $fopen({test_name, "_debug.log"}, "w");
error_fd = $fopen({test_name, "_errors.log"}, "w");
result_fd = $fopen({test_name, "_result.log"}, "w");
// Write structured headers for CI parsing
$fwrite(result_fd, "TEST_NAME=%s\n", test_name);
$fwrite(result_fd, "START_TIME=%0t\n", $time);
$fwrite(result_fd, "STATUS=RUNNING\n");
$fflush(result_fd); // flush so CI can watch progress in real time
// ── TIER 1: debug.log — everything at HIGH verbosity ──────────
uvm_root::get().set_report_default_file_hier(debug_fd);
uvm_root::get().set_report_severity_action_hier(UVM_INFO,
UVM_LOG); // UVM_LOG only — no console spam, file only
uvm_root::get().set_report_verbosity_level_hier(UVM_HIGH);
// ── TIER 2: errors.log — only warnings and above ──────────────
uvm_root::get().set_report_severity_file_hier(UVM_WARNING, error_fd);
uvm_root::get().set_report_severity_file_hier(UVM_ERROR, error_fd);
uvm_root::get().set_report_severity_file_hier(UVM_FATAL, error_fd);
// Errors also go to console (on top of debug.log via default file)
uvm_root::get().set_report_severity_action_hier(UVM_WARNING,
UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_ERROR,
UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_FATAL,
UVM_DISPLAY | UVM_LOG | UVM_COUNT | UVM_EXIT);
endfunction
function void report_phase(uvm_phase phase);
uvm_report_server svr = uvm_report_server::get_server();
string status;
super.report_phase(phase);
// Write CI-parseable result — this is what the regression dashboard reads
status = (svr.get_severity_count(UVM_ERROR) == 0) ? "PASS" : "FAIL";
$fwrite(result_fd, "STATUS=%s\n", status);
$fwrite(result_fd, "ERRORS=%0d\n", svr.get_severity_count(UVM_ERROR));
$fwrite(result_fd, "WARNINGS=%0d\n", svr.get_severity_count(UVM_WARNING));
$fwrite(result_fd, "END_TIME=%0t\n", $time);
endfunction
function void final_phase(uvm_phase phase);
super.final_phase(phase);
$fclose(debug_fd); $fclose(error_fd); $fclose(result_fd);
endfunction
endclassExample 3 — Custom Report Catcher for Expected Errors
// ── uvm_report_catcher: intercept messages before they hit the server ─
// Use case: error injection tests — expected errors should not count as failures
class expected_error_catcher extends uvm_report_catcher;
`uvm_object_utils(expected_error_catcher)
int caught_count = 0;
string expected_id = "PROT_ERR"; // message ID we expect to see
function new(string n="expected_error_catcher"); super.new(n); endfunction
function action_e catch();
// Check if this is the error we expect during error injection
if (get_severity() == UVM_ERROR && get_id() == expected_id) begin
caught_count++;
// Demote from ERROR to INFO — does not count against pass/fail
set_severity(UVM_INFO);
set_message($sformatf("[EXPECTED #%0d] %s", caught_count, get_message()));
return CAUGHT; // CAUGHT: message is modified but NOT suppressed
end
return THROW; // THROW: all other messages proceed normally
endfunction
endclass
// ── Register in the test ──────────────────────────────────────────────
class error_inject_test extends production_base_test;
`uvm_component_utils(error_inject_test)
expected_error_catcher catcher;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
catcher = expected_error_catcher::type_id::create("catcher");
uvm_report_cb::add(null, catcher); // null = global — intercepts all components
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("INJECT",$sformatf("Caught and demoted %0d expected errors",
catcher.caught_count),UVM_NONE)
endfunction
endclass§6 — Simulation Thinking — Timing, Flushing, and Edge Cases
| Question | Answer |
|---|---|
| When should I open files? | start_of_simulation_phase — hierarchy is built, so hierarchy methods work. build_phase works but hierarchy methods won't propagate to children not yet built. |
| When should I close files? | final_phase — the last phase. Closing in report_phase means later messages from other components' report phases may be lost. |
| What happens if I don't close files? | The OS closes file handles when the process exits, but unflushed write buffers may not be written. The last few KB of log can be lost on simulator crash. Always $fclose(). |
| Does UVM buffer writes? | UVM calls SystemVerilog's $fdisplay() internally for LOG actions. Most simulators buffer this. Use $fflush(fd) after critical writes if you need real-time visibility (e.g., the result file for CI polling). |
| What if $fopen() fails? | Returns 0. Always check: if (fd == 0) uvm_fatal(...)`. Common causes: wrong directory, no write permission, full disk. |
| Can I log to multiple files simultaneously? | Yes — use set_report_default_file() for the base file, then set_report_severity_file() or set_report_id_file() to route specific messages to additional files. Severity file takes precedence over default file for that severity. |
§7 — Real CI and Regression Usage — Production Patterns
Pattern: CI-Parseable Result File
## ── CI Script: parse UVM result file ────────────────────────────────
## result.log format (written by production_base_test above):
## TEST_NAME=smoke_test
## START_TIME=0
## STATUS=PASS
## ERRORS=0
## WARNINGS=3
## END_TIME=1200ns
## Parse in bash:
STATUS=$(grep "^STATUS=" smoke_test_result.log | cut -d= -f2)
ERRORS=$(grep "^ERRORS=" smoke_test_result.log | cut -d= -f2)
if [ "$STATUS" == "PASS" ] && [ "$ERRORS" == "0" ]; then
echo "PASS: $TEST_NAME"
exit 0
else
echo "FAIL: $TEST_NAME (errors=$ERRORS)"
exit 1
fi
## ── Makefile target: regression with log archiving ───────────────────
TESTS = smoke_test data_test corner_test stress_test
regression:
@mkdir -p logs/$(RUN_ID)
@for test in $(TESTS); do \
vsim -c tb_top \
+UVM_TESTNAME=$$test \
+UVM_VERBOSITY=UVM_MEDIUM \
-do "run -all; quit -f" \
> logs/$(RUN_ID)/$$test.transcript 2>&1 & \
done
@wait ## wait for all parallel simulations
@python3 scripts/aggregate_results.py logs/$(RUN_ID)/
## ── Python aggregator: collect all result.log files ─────────────────
## scripts/aggregate_results.py reads every *_result.log,
## builds a table of PASS/FAIL/ERRORS/WARNINGS, emails on failures§8 — Common Bugs and Debugging Scenarios
Bug 1 — Forgotten $fclose() Truncates Log on Crash
Symptom: The log file exists but the last few hundred lines are missing. The simulation crashed (uvm_fatal) but the crash message doesn't appear in the file. This is the most common file logging bug.
Root Cause: SystemVerilog file I/O is buffered. If the simulator crashes or exits abnormally before final_phase, the write buffer is never flushed. The file appears to end mid-simulation.
Fix: Add $fflush(log_fd) after every critical write, or configure the file in unbuffered mode. At minimum, ensure $fclose() is in final_phase.
// ── Bug 2: Wrong phase for set_report_*_hier() ────────────────────────
// ❌ WRONG — called in build_phase before children exist
function void build_phase(uvm_phase phase);
super.build_phase(phase); // children built by super call
integer fd = $fopen("sim.log", "w");
uvm_root::get().set_report_default_file_hier(fd);
// At this point, the env's agents haven't been created yet
// _hier propagation applies to children that exist AT CALL TIME
// Agents created by super.build_phase() DO get the setting
// BUT: if you call this BEFORE super.build_phase(), no children exist yet
endfunction
// ✓ CORRECT — call AFTER super.build_phase() so all children exist
// OR: use start_of_simulation_phase (all build_phases have completed)
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
integer fd = $fopen("sim.log", "w");
uvm_root::get().set_report_default_file_hier(fd);
// Now the ENTIRE hierarchy exists — _hier propagates correctly
endfunction
// ── Bug 3: Forgetting to set UVM_LOG action ───────────────────────────
// ❌ WRONG — file registered, but action doesn't include UVM_LOG
uvm_root::get().set_report_default_file_hier(fd);
// Default UVM_INFO action = UVM_DISPLAY only (no LOG bit)
// File is registered but nothing writes to it — log file stays empty
// ✓ CORRECT — BOTH file registration AND action must be set
uvm_root::get().set_report_default_file_hier(fd);
uvm_root::get().set_report_severity_action_hier(UVM_INFO,
UVM_DISPLAY | UVM_LOG); // MUST include UVM_LOG
// ── Bug 4: uvm_report_catcher returning THROW instead of CAUGHT ───────
// ❌ WRONG — demoting severity but returning THROW
function action_e catch();
if (get_severity() == UVM_ERROR && get_id() == "EXPECTED") begin
set_severity(UVM_INFO);
return THROW; // ❌ THROW with modified severity still counts as error!
end
return THROW;
endfunction
// ✓ CORRECT — use CAUGHT to indicate the message has been handled
function action_e catch();
if (get_severity() == UVM_ERROR && get_id() == "EXPECTED") begin
set_severity(UVM_INFO); // demote
return CAUGHT; // ✓ CAUGHT: message is handled (still displays, not counted)
end
return THROW; // other messages: proceed normally
endfunction§9 — Ready-to-Run Complete Demo
Ready to Run — Questa / VCS / Xcelium
// logging_demo.sv — complete UVM file logging demonstration
// Questa : vlog -sv logging_demo.sv && vsim -c logging_demo_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm logging_demo.sv && ./simv
// Xcelium: xrun -sv -uvm logging_demo.sv -input "run; exit"
//
// After running: check sim_debug.log, sim_errors.log, sim_result.log
`include "uvm_macros.svh"
import uvm_pkg::*;
class demo_comp extends uvm_component;
`uvm_component_utils(demo_comp)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info ("COMP", "Info message — goes to debug log and console", UVM_MEDIUM)
`uvm_warning("COMP", "Warning — goes to errors log and console")
`uvm_error ("COMP", "Error — goes to errors log and console")
`uvm_info ("COMP", "Another info — file only (high verbosity)", UVM_HIGH)
#10;
phase.drop_objection(this);
endtask
endclass
class demo_test extends uvm_test;
`uvm_component_utils(demo_test)
demo_comp comp;
integer debug_fd, error_fd, result_fd;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
comp = demo_comp::type_id::create("comp", this);
endfunction
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
debug_fd = $fopen("sim_debug.log", "w");
error_fd = $fopen("sim_errors.log", "w");
result_fd = $fopen("sim_result.log", "w");
$fwrite(result_fd, "TEST=demo_test\nSTATUS=RUNNING\n");
$fflush(result_fd);
// All messages → debug log (file only at HIGH verbosity)
uvm_root::get().set_report_default_file_hier(debug_fd);
uvm_root::get().set_report_verbosity_level_hier(UVM_HIGH);
uvm_root::get().set_report_severity_action_hier(UVM_INFO,
UVM_LOG); // file only, no console
// Warnings and errors also → error log AND console
uvm_root::get().set_report_severity_file_hier(UVM_WARNING, error_fd);
uvm_root::get().set_report_severity_file_hier(UVM_ERROR, error_fd);
uvm_root::get().set_report_severity_action_hier(UVM_WARNING,
UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_ERROR,
UVM_DISPLAY | UVM_LOG | UVM_COUNT);
uvm_root::get().set_report_severity_action_hier(UVM_FATAL,
UVM_DISPLAY | UVM_LOG | UVM_COUNT | UVM_EXIT);
`uvm_info("TEST","Logging configured: debug→file-only, errors→console+file",UVM_NONE)
endfunction
function void report_phase(uvm_phase phase);
uvm_report_server svr = uvm_report_server::get_server();
super.report_phase(phase);
$fwrite(result_fd, "STATUS=%s\nERRORS=%0d\nWARNINGS=%0d\n",
(svr.get_severity_count(UVM_ERROR) == 0) ? "PASS" : "FAIL",
svr.get_severity_count(UVM_ERROR),
svr.get_severity_count(UVM_WARNING));
endfunction
function void final_phase(uvm_phase phase);
super.final_phase(phase);
$fclose(debug_fd); $fclose(error_fd); $fclose(result_fd);
endfunction
endclass
module logging_demo_top;
initial run_test("demo_test");
endmodule
// ── After running, check these files: ─────────────────────────────────
// sim_debug.log : all messages including UVM_HIGH info (no console output)
// sim_errors.log : only WARNING and ERROR messages
// sim_result.log : STATUS=FAIL ERRORS=1 WARNINGS=1
//
// Console shows only:
// UVM_WARNING ... Warning — goes to errors log and console
// UVM_ERROR ... Error — goes to errors log and console
// (no INFO messages on console — they went to file only)§10 — Interview Questions
- What is the UVM_LOG action bit and why is it not enough on its own to start file logging?
UVM_LOGis the action bit (value=2) that tells the report server to write a message to a file. On its own, it's necessary but not sufficient — you also need to register a file handle withset_report_default_file(fd). If UVM_LOG is set but no file handle is registered, UVM uses a null/zero handle which silently discards the message. If a file handle is registered but UVM_LOG is not in the action mask, nothing is written. Both must be configured together. - What is the difference between set_report_default_file() and set_report_default_file_hier()?
set_report_default_file(fd)applies only to the component it's called on — messages from that component go tofd, but its children continue to use their own registered files (or no file if none registered).set_report_default_file_hier(fd)propagates down the entire component tree from the called component, setting every component in the subtree to usefd. Call the hierarchical version onuvm_root::get()instart_of_simulation_phaseto configure the entire testbench at once. - In a uvm_report_catcher, what is the difference between returning CAUGHT and THROW? Which one should you return when demoting an expected error?
THROWpasses the message (possibly with modifications from set_severity()/set_message()) on to the normal action processing — it will still count against severity counters if the action includes UVM_COUNT.CAUGHTmeans the catcher has handled the message — UVM still processes and displays it based on current actions, but it treats the potentially-modified severity as the final one. When demoting an expected error to info, useCAUGHTafter callingset_severity(UVM_INFO). This way the message still appears in the log as INFO-level (visible, not suppressed) but does not increment the UVM_ERROR counter that drives pass/fail. - Why should $fclose() be in final_phase and not report_phase? UVM phases execute across all components in a specific order. report_phase is a function phase that runs bottom-up — your test's report_phase may complete before other components' report_phases fire their last messages. Closing the file in report_phase means those subsequent messages find a closed file handle and are lost. final_phase runs after all other phases including all report_phases, so closing in final_phase guarantees no more messages will arrive after close. It's the only safe place.
§11 — Best Practices — The Production Standard
| Practice | Reasoning |
|---|---|
| Open files in start_of_simulation_phase, not build_phase | Hierarchy is fully built. Hierarchical methods propagate correctly to all children. |
| Close files in final_phase, not report_phase | Prevents data loss if other components' report_phases fire after your close. |
| Check $fopen() return value and fatal on zero | Silent failure (fd==0) produces an empty log file with no error — hard to debug. A fatal immediately identifies the problem. |
| Write a header to every log file | Test name, timestamp, simulator version. When you have 800 log files in a regression, headers are the only way to know which test a log belongs to without checking the filename. |
| Write a structured result file for CI parsing | CI systems should NOT grep for "UVM_ERROR" strings in transcripts — too fragile. A structured key=value result file is the right interface between UVM and CI. |
| Use $fflush() on the result file immediately after writing | CI systems may poll for test completion in real time. Unflushed buffers make it look like the test hasn't finished. |
| In base_test, default to UVM_DISPLAY | UVM_LOG for errors | Engineers running interactively see errors on console. CI systems have a persistent file to grep. Both get what they need from the same configuration. |
| For high-verbosity debug logs, use UVM_LOG only (no DISPLAY) | A 500MB transcript file on the CI node is expensive. Write verbose logs to file silently, let the regression stay clean on console. |
File logging is infrastructure. Nobody notices it when it works. Everyone notices it when it doesn't — specifically at 2am when a regression has been running for six hours and you're staring at a failed test with no log file and wondering what happened. Set it up once, in the base test class, and never think about it again. That's the goal.