Every UVM course teaches the rule "register factory overrides before super.build_phase". The DMA bench follows it: dma_truncate_test overrides the stream driver on the line above super.build_phase(phase). In an interview, you are asked: "and if the override came after super, would the test still work?" Most candidates say no. For this test, the answer is yes, and the reason is the most useful thing to understand about build.
A manager who hires someone signs an offer letter today. The new hire's first day, when they set up their desk and hire their team, comes later, after the manager has finished their own work. In UVM, type_id::create() is the offer letter: it asks the factory (the registry that maps a requested type to the type actually built) for an object and runs its constructor. The child's build_phase, where it creates its children, is the first day, and the phaser schedules it later in the top-down walk.
A factory override changes what future create() calls return. It never changes an object that already exists. So the only question about an override's placement is: has the create I want to change already happened? Because each level's children are created in the next level's build, a test's build_phase runs before almost every create in the tree.
// snippet: from the course DMA bench
// Factory override: the stream driver cuts each packet short, so the DMA must
// stop early and report ERR. No sequence or env file changes.
class dma_truncate_test extends dma_base_test;
`uvm_component_utils(dma_truncate_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
axis_driver::type_id::set_type_override(axis_truncating_driver::get_type());
super.build_phase(phase); // the env is created after the override
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 == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 8, status);
if (status[2] !== 1'b1) `uvm_error("STATUS", $sformatf("ERR not set after a short packet: STATUS=%08h", status))
else `uvm_info("TRUNC", "DMA reported ERR for the short packet, as expected", UVM_LOW)
#100ns;
phase.drop_objection(this);
endtask
endclasssuper.build_phase(phase) runs dma_base_test::build_phase, which creates env. That is the only create that happens inside the test's own build. The driver is created two levels further down, in in_agt's build, which the phaser runs after the test's build has returned. So the override reaches the driver from either position. The bench's comment, "the env is created after the override", names the one create that would be affected by the order.
Where the override is registered | Affects | Too late for |
|---|---|---|
Test build_phase, before | Every create in the tree, including | Nothing |
Test build_phase, after | Everything below env: agents, drivers, scoreboard |
|
Env build_phase, after it created | The agent's children (built later) |
|
connect_phase or later | Nothing in this tree | Everything: build is over |
Test hierarchies add one twist. When dma_truncate_test calls super.build_phase, it runs dma_base_test's build, which creates the env. If a mid-level test sat between them and registered an override after its own super call, that override would arrive after the env was created, exactly like the second row. Each level's "after super" is after every create its ancestors make.
"Before super" is still the right habit, because it is never wrong and it keeps working when a base test starts creating more things. But knowing why lets you read a test and say precisely what an override can reach.
A type override replaces a type everywhere it is created. An instance override replaces it only at one path, which is usually what a scenario test wants: truncate the input stream, leave every other stream driver alone. The timing rule is identical, because both are consulted at create() time. Only the path needs care:
// snippet: inside dma_truncate_test::build_phase
// Component method: the path is relative to this component (uvm_test_top).
set_inst_override_by_type("env.in_agt.drv",
axis_driver::get_type(), axis_truncating_driver::get_type());The component method set_inst_override_by_type takes a path relative to the component that calls it, so "env.in_agt.drv" from the test means uvm_test_top.env.in_agt.drv. The factory's own method of the same name takes a full path from uvm_test_top. Mixing the two forms is the most common reason an instance override silently matches nothing.
Calling new() on a component is legal SystemVerilog; component constructors are public. It simply skips the factory, so no override can ever replace that instance. type_id::create(name, parent) asks the factory first. Every child in the bench is created that way, which is why the truncate test can swap the driver without editing the agent. The factory section covers create-time resolution in depth.
The truncate test's topology shows the substituted type, and its log shows the substituted behaviour:
in_agt axis_agent - @264
drv axis_truncating_driver - @636
...
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatchedA copy of the test with the override moved below super.build_phase(phase) produces the same three lines, with the same axis_truncating_driver in the topology. That is the measured answer to the interview question.
A team derived dma_env2 from dma_env with an extra coverage collector, and wrote a test that extends the smoke test with super.build_phase(phase) first, then dma_env::type_id::set_type_override(dma_env2::get_type()), following the "super first" rule from the signatures lesson. Coverage stayed at zero. A log line added after the override told the story:
UVM_INFO dma_tb.sv(...) @ 0: uvm_test_top [ENV] env type after build: dma_env
...
env dma_env - @242The env is the one component created inside super.build_phase. By the time the override was registered, env already existed as a dma_env, and an override never changes an existing object. Moving the override above super.build_phase fixed it. The general rule: an override must be registered before the create it targets, and in a test the creates that happen inside super are the ones at risk.
dma_scoreboard with dma_scoreboard_eot on the line after super.build_phase(phase). Does the env get the new scoreboard?Yes. sb is created in the env's build_phase, which the phaser runs after the test's build_phase returns. Only env itself is created inside the test's super call, so only an override of the env type would be too late.
dma_env::build_phase, the line in_agt = axis_agent::type_id::create(...) is followed by an override of axis_driver. Which drivers are affected?in_agt's driver. Each agent creates its driver in its own build_phase, which runs after the env's build returns, so the override is already in place when in_agt creates its driver, even though in_agt itself was created before the override. out_agt is passive and builds no driver, and the APB agent creates an apb_driver, which this override does not mention.
env.in_agt read as null on the line right after the test creates env?Because creating env only ran env's constructor. env's build_phase, which creates in_agt, runs later, after the test's build_phase returns. Top-down means the parent's whole build finishes before any child's build begins.
Next: the other thing a parent hands down in build is configuration. config_db Gets and Conditional Construction shows how the DMA bench passes its interfaces and its passive setting, and the wildcard that catches everyone once.