Skip to content
VLSI Mentor

Makefile · Part II · Module 6 · UVM and Regression Automation

UVM Test Selection

Module 5 built a flow that compiles once and runs a test. A real verification project has dozens of tests sharing one compiled image, and an engineer picks one by name from the command line. This chapter builds that path end to end — and then adds the four-line check that stops a typo from costing you a full compile and run.

Intermediate25 min readMakefileGNU MakePart IIUVMTest Selection

A verification environment is built once and used many times. The design, the testbench, the drivers, the monitors and the scoreboard all compile into a single image, and the thing that changes between runs is small: which test should run. One test writes a register and reads it back. Another hammers the bus with random traffic. Another deliberately provokes an error response.

They share everything except the few lines that decide the stimulus. So they must share the compiled image too — otherwise every test change costs a full compile, which on a real block is the difference between a two-minute answer and a twenty-minute one.

This chapter builds the path from make run TEST=axi_smoke to the simulator, and gives each test somewhere of its own to put its results.

1. How a Simulator Learns Which Test to Run

The mechanism is a plusarg: an argument starting with + that is passed to the simulation, not to the compiler. The compiler never sees it, which is exactly why it costs no rebuild.

In real UVM, the testbench's top module calls run_test() with no argument, and UVM asks the command line which test class to build:

Azvya Education Pvt. Ltd.VLSI Mentor
Reference syntax — real UVM, not executed here
// Each test registers itself with the UVM factory.
class axi_smoke_test extends uvm_test;
  `uvm_component_utils(axi_smoke_test)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  task run_phase(uvm_phase phase);
    // ... drive the smoke sequence ...
  endtask
endclass

module tb_top;
  initial run_test();          // <- the test name comes from +UVM_TESTNAME
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Reference syntax — how the three big simulators are told
./simv  +UVM_TESTNAME=axi_smoke_test +UVM_VERBOSITY=UVM_MEDIUM   # VCS
xmsim tb_top +UVM_TESTNAME=axi_smoke_test                         # Xcelium
vsim  -c tb_top +UVM_TESTNAME=axi_smoke_test -do "run -all"       # Questa

Notice what is the same in all three: +UVM_TESTNAME=<name>. The command that launches the simulation differs, the plusarg does not. That is the seam a Makefile should sit on.

The stand-in used for the measurements

Since UVM cannot run here, the measured transcripts come from a plain-Verilog testbench that reads the same plusarg and dispatches on it. It is not UVM and does not pretend to be, but it behaves identically from the Makefile's point of view:

Azvya Education Pvt. Ltd.VLSI Mentor
tb/tb_top.v — the dispatch, trimmed to the essentials
reg [8*48-1:0] testname;
integer seed;

initial begin
  if (!$value$plusargs("UVM_TESTNAME=%s", testname)) testname = "axi_smoke";
  if (!$value$plusargs("ntb_random_seed=%d", seed))  seed = 1;
  $display("UVM_INFO @ 0: reporter [RNTST] Running test %0s...", testname);

  case (testname)
    "axi_smoke":      begin /* write reg0, read it back   */ end
    "axi_burst":      begin /* write all four, read all four */ end
    "axi_random":     begin /* random data, self-checking */ end
    "axi_decode_err": begin /* poke an undecoded address  */ end
    default: $display("UVM_FATAL @ %0t: reporter [INVTST] no test named %0s in the factory",
                      $time, testname);
  endcase
end

The case on a string is doing real work, and it is worth knowing it is reliable: a packed vector filled by $value$plusargs("%s", ...) compares correctly against string literals both with == and inside a case. Measured on three names, including one that does not exist:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
+UVM_TESTNAME=axi_smoke  -> case -> smoke
+UVM_TESTNAME=axi_burst  -> case -> burst
+UVM_TESTNAME=axi_nope   -> case -> default
(no plusarg at all)      -> case -> smoke

2. The Makefile Side

The Makefile's job is small and entirely mechanical: take a variable from the command line, put it in the plusarg, and put it in the path.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the test name as a knob
BUILD    := build
FILELIST := filelists/all.f
SIMV     := $(BUILD)/simv

TEST ?= axi_smoke
SEED ?= 1

RUNDIR := $(BUILD)/$(TEST)/$(SEED)
RUNLOG := $(RUNDIR)/run.log
STATUS := $(RUNDIR)/passed.stamp

.PHONY: all compile run log clean
all: run
compile: $(SIMV)
run:     $(STATUS)

$(SIMV): $(FILELIST)
	@mkdir -p $(@D)
	@echo "  [compile] -> $@"
	@iverilog -g2012 -f $(FILELIST) -o $@

$(STATUS): $(SIMV)
	@mkdir -p $(@D)
	@echo "  [run] $(TEST) seed=$(SEED) -> $(RUNLOG)"
	@vvp $(SIMV) +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED) > $(RUNLOG)
	@grep -q "^UVM_ERROR :  0" $(RUNLOG) || { echo "  [FAIL] $(TEST) seed=$(SEED)"; exit 1; }
	@touch $@
	@echo "  [PASS] $(TEST) seed=$(SEED)"

log:
	@test -f $(RUNLOG) || { echo "no log for $(TEST) seed $(SEED)"; exit 1; }
	@cat $(RUNLOG)

clean:
	@rm -rf $(BUILD)

Everything here was built in Module 5 and is being reused rather than re-explained: ?= so the command line wins (Chapter 3.2), the run directory carrying the identity of the run (Chapter 5.3), and passed.stamp as the target rather than the log (Chapter 5.4).

The only new line is the one that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
@vvp $(SIMV) +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED) > $(RUNLOG)

One variable, used twice: once in the plusarg, once — via RUNDIR — in the path.

3. Running It

Six invocations in order, on a clean tree:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
$ make run
  [compile] -> build/simv
  [run] axi_smoke seed=1 -> build/axi_smoke/1/run.log
  [PASS] axi_smoke seed=1

$ make run TEST=axi_burst
  [run] axi_burst seed=1 -> build/axi_burst/1/run.log
  [PASS] axi_burst seed=1

$ make run TEST=axi_burst SEED=42
  [run] axi_burst seed=42 -> build/axi_burst/42/run.log
  [PASS] axi_burst seed=42

$ make run TEST=axi_burst SEED=42
make: Nothing to be done for `run'.

$ make run TEST=axi_flaky SEED=5
  [run] axi_flaky seed=5 -> build/axi_flaky/5/run.log
  [FAIL] axi_flaky seed=5
make: *** [build/axi_flaky/5/passed.stamp] Error 1

$ make run TEST=axi_nosuch
  [run] axi_nosuch seed=1 -> build/axi_nosuch/1/run.log
  [FAIL] axi_nosuch seed=1
make: *** [build/axi_nosuch/1/passed.stamp] Error 1

The second invocation is the one to dwell on. There is no [compile] line. A completely different test ran against the image built for the first one, because the test name never reached the compiler. That is the whole point of a plusarg, and it is the behaviour that makes an interactive debug session bearable.

The resulting tree keeps every run apart:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — find build -name '*.log' -o -name '*.stamp' | sort
build/axi_burst/1/passed.stamp
build/axi_burst/1/run.log
build/axi_burst/42/passed.stamp
build/axi_burst/42/run.log
build/axi_flaky/5/run.log
build/axi_nosuch/1/run.log
build/axi_smoke/1/passed.stamp
build/axi_smoke/1/run.log

Note the two directories with a log and no stamp: those are the two failures. The log is there to read, and nothing claims they passed. That is 5.4's design doing its job.

Proving the image really is untouched

"The image is reused" is the chapter's central claim, and the absence of a [compile] line is only indirect evidence — a recipe could in principle rebuild quietly. So check the file itself. Run three different tests and take a digest of the image after each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run TEST=axi_smoke  > /dev/null; md5 -q build/simv
make run TEST=axi_burst  > /dev/null; md5 -q build/simv
make run TEST=axi_random SEED=7 > /dev/null; md5 -q build/simv
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  after axi_smoke     : d91b5051a91f6629bb8813d1f51520e7
  after axi_burst     : d91b5051a91f6629bb8813d1f51520e7
  after axi_random    : d91b5051a91f6629bb8813d1f51520e7
  IDENTICAL - one image served three tests

Byte-identical, and the modification time never moved either. Three tests, two seeds, one compile. That is the number to quote when somebody proposes putting the test name in a +define+.

Four rows, each flowing left to right from a place the test name appears to what that gives you. Row one, the command line TEST equals axi smoke, is the one knob a person types. Row two, the plusarg plus UVM TESTNAME, is read at run time by the simulation. Row three, the run directory build slash test slash seed, keeps each test's results apart. Row four, the compiler, is greyed out because the test name never reaches it, which is why no rebuild happens.TEST=axi_smokewhat a person typesOne knobno file is edited+UVM_TESTNAMEread at run timeWhich test runschosen by the factorybuild/TEST/SEEDone dir per runResults kept apartlog, stamp, wavesThe compilernever sees the nameNo rebuildthe image is reused12
Figure 1 — the four things a test name touches, and the one thing it must not. The name is a run-time value, so it reaches the simulation and the output path but never the compiler — which is why a different test needs no rebuild.

4. When the Test Name Is Wrong

Look again at the last invocation above. make run TEST=axi_nosuch did something unhelpful: it ran the simulator, which dutifully reported that no such test exists, and only then did Make fail.

The log explains itself clearly enough:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — build/axi_nosuch/1/run.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_nosuch...
UVM_INFO @ 0: reporter [SEED] seed=1
UVM_FATAL @ 12000: reporter [INVTST] no test named axi_nosuch in the factory

--- UVM Report Summary ---

** Report counts by severity
UVM_INFO :  2
UVM_WARNING :  0
UVM_ERROR :  1
UVM_FATAL :  0
tb/tb_top.v:123: $finish called at 32000 (1ps)

That is a real UVM behaviour too: a name that is not in the factory produces a fatal at the start of the run. Here it cost almost nothing. On a real environment it costs the full elaboration and start-up before telling you that you typed axi_smok.

It also leaves rubbish behind. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
unguarded typo:   55 ms  (simulator ran, then Make failed)
unguarded also left build/axi_nope/ behind

A directory named after a test that does not exist, which will sit in build/ confusing the next person who lists it.

5. Checking the Name First

The fix is to tell the Makefile which test names are legal. Keep them in a file, because Chapter 6.3 is going to need exactly the same list to drive a whole regression:

Azvya Education Pvt. Ltd.VLSI Mentor
tests/testlist.f
# One test per line. Comments and blank lines are ignored.
axi_smoke
axi_burst
axi_random
axi_decode_err

axi_flaky

Read it once with :=, then refuse to proceed if TEST is not in it:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — validating the test name
TESTLIST := tests/testlist.f

# Read the list once, dropping comments and blank lines.
TESTS := $(shell grep -vE '^[[:space:]]*(\#|$$)' $(TESTLIST))

# Fail while the Makefile is being READ, before anything is built or run.
ifneq ($(TEST),$(filter $(TEST),$(TESTS)))
  $(error unknown test "$(TEST)" -- known tests are: $(TESTS))
endif

.PHONY: tests
tests:
	@echo "$(TESTS)" | tr ' ' '\n' | sed 's/^/  /'

Three pieces, all from Part I. $(shell ...) assigned with := runs the command once (Chapter 3.4). $(filter $(TEST),$(TESTS)) returns $(TEST) if it is in the list and nothing otherwise, so comparing the two is a membership test. ifneq with $(error) aborts at parse time (Chapter 4.2 and Chapter 4.4 §9).

What Make read:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make tests
  axi_smoke
  axi_burst
  axi_random
  axi_decode_err
  axi_flaky

Five names: the comment and the blank line are gone. And now the typo:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run TEST=axi_smok
Makefile.guard:17: *** unknown test "axi_smok" -- known tests are:
axi_smoke axi_burst axi_random axi_decode_err axi_flaky.  Stop.

It names what you typed, lists what you could have meant, and nothing was compiled or run. Measured against the unguarded version:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
guarded   typo:   22 ms  (no simulator invoked)
unguarded typo:   55 ms  (simulator ran, then Make failed)

The guard has a hole, and it is worth seeing

That guard looks complete. It is not. Try an empty test name — which is what a wrapper script produces when the variable it meant to pass was never set:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run TEST= , with the guard in place
  [run]  seed=1 -> build//1/run.log
  [FAIL]  seed=1
make: *** [build/1/passed.stamp] Error 1

It ran. The guard did nothing, and the run landed in a directory called build/1/ because the empty name collapsed the path. The exit status was 2, so it failed — but it failed after the run, which is exactly what the guard was supposed to prevent.

The reason is worth understanding, because it is a general trap with $(filter):

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  TEST=[]
  filter result=[]
  are they equal? YES

With TEST empty, $(filter $(TEST),$(TESTS)) is empty too — and ifneq is comparing empty against empty, which are equal, so the condition does not fire. A membership test written as a comparison against $(filter) always passes for the empty string. (Writing it the other way round — $(if $(filter $(TEST),$(TESTS)),,$(error ...)), which tests whether the filter result is non-empty — does catch it. Chapter 7.2 §2 measures both forms side by side.)

One more line fixes it, and it has to come first:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the guard, completed
$(if $(strip $(TEST)),,$(error TEST is empty -- known tests are: $(TESTS)))
ifneq ($(TEST),$(filter $(TEST),$(TESTS)))
  $(error unknown test "$(TEST)" -- known tests are: $(TESTS))
endif
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
$ make run TEST=
Makefile.guard:16: *** TEST is empty -- known tests are:
axi_smoke axi_burst axi_random axi_decode_err axi_flaky.  Stop.

$ make run TEST=axi_smok
Makefile.guard:18: *** unknown test "axi_smok" -- known tests are:
axi_smoke axi_burst axi_random axi_decode_err axi_flaky.  Stop.

$ make run TEST=axi_smoke
  [run] axi_smoke seed=1 -> build/axi_smoke/1/run.log
  [PASS] axi_smoke seed=1

Empty rejected, typo rejected, real name accepted. And while we are here, two cases the $(filter) test does handle correctly on its own — a name with a space in it, and a name that is merely a prefix of a real test:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
$ make run TEST="axi smoke"
*** unknown test "axi smoke" -- known tests are: axi_smoke axi_burst ...

$ make run TEST=axi
*** unknown test "axi" -- known tests are: axi_smoke axi_burst ...

$(filter) matches whole words, so axi is not treated as a shortening of axi_smoke. That is the behaviour you want, and it is worth having checked rather than assumed.

6. The # That Breaks $(shell ...)

That \# in the grep pattern is not decoration, and getting it wrong produces one of Make's most confusing errors. Write the pattern the way you would in a shell:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TESTS := $(shell grep -vE '^[[:space:]]*(#|$$)' $(TESTLIST))

and Make says:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile:7: *** unterminated call to function `shell': missing `)'.  Stop.

There is nothing wrong with your brackets. A # starts a comment in a Makefile, even inside quotes, even inside a function call. Make threw away everything from the # onwards, including the closing ), and then complained about the bracket you did write.

Three ways out, all measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — what works and what does not
# BROKEN - the # begins a comment and eats the rest of the line
A := $(shell grep -vE '^[[:space:]]*(#|$$)' $(L))

# WORKS - escape it
B := $(shell grep -vE '^[[:space:]]*(\#|$$)' $(L))

# WORKS - keep the character in a variable
HASH := \#
C := $(shell grep -vE "^[[:space:]]*($(HASH)|$$)" $(L))
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
--- unescaped # ---
t.mk:2: *** unterminated call to function `shell': missing `)'.  Stop.
--- escaped \# ---
B=[axi_smoke axi_burst axi_random axi_decode_err axi_flaky]
--- # held in a variable ---
C=[axi_smoke axi_burst axi_random axi_decode_err axi_flaky]

And to be sure it is the character rather than anything about the regular expression, the same bare # inside a completely different command fails in exactly the same way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D := $(shell sed -e 's/[[:space:]]*#.*//' -e '/^[[:space:]]*$$/d' $(L))
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
t4.mk:2: *** unterminated call to function `shell': missing `)'.  Stop.

Same error, sed instead of grep. The lesson is about Make, not about either tool: a literal # anywhere on a Makefile line must be written \#.

7. Why the List Lives in a File

It would be shorter to write TESTS := axi_smoke axi_burst axi_random directly in the Makefile. The file is better for three reasons that all show up later:

The simulator may need it too. Some flows generate a UVM test package, or a documentation page, or a CI job matrix from the same list. One file, many readers — the same argument Chapter 5.2 made for the source filelist.

It changes more often than the Makefile. Adding a test should not require editing build logic, and a diff that touches only tests/testlist.f is obviously safe to approve.

Comments and grouping become possible. A list file can be organised:

Azvya Education Pvt. Ltd.VLSI Mentor
tests/testlist.f — organised
# ---- smoke: must pass before anything else ----
axi_smoke
axi_burst

# ---- random: self-checking, seed-sensitive ----
axi_random
axi_flaky

# ---- error injection ----
axi_decode_err

The grep drops every comment and blank line, so the structure costs nothing at build time. Chapter 6.3 builds the regression on top of exactly this file.

8. Giving the Flow a Front Door

Two small phony targets make the flow usable by somebody who has never read the Makefile:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — discoverability
.PHONY: help tests
help:
	@echo "targets:"
	@echo "  make tests                      list the tests that can be run"
	@echo "  make run TEST=<name> [SEED=<n>] run one test"
	@echo "  make log TEST=<name> [SEED=<n>] print that run's log"
	@echo "  make clean                      remove $(BUILD)/"
	@echo ""
	@echo "tests: $(TESTS)"
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make help
targets:
  make tests                      list the tests that can be run
  make run TEST=<name> [SEED=<n>] run one test
  make log TEST=<name> [SEED=<n>] print that run's log
  make clean                      remove build/

tests: axi_smoke axi_burst axi_random axi_decode_err axi_flaky

This is not decoration. A flow that cannot tell you what it can do gets used for one target and ignored for the rest, and the engineer who needed make log re-runs the whole test instead because they did not know it existed. Both help and tests are phony and have no prerequisites, so neither can ever start a simulation — the same rule 5.4 §7 set for make log.

9. Common Mistakes

Putting the test name in a +define+. A define is read at compile time, so every test change becomes a rebuild — Chapter 5.2 §2. The test name is a run-time value and belongs in a plusarg.

One shared output directory for every test. Then the second test overwrites the first one's log. The test name belongs in the path (§2).

No validation of the test name. A typo costs a full compile and run, and leaves a directory named after a test that does not exist (§4).

Comparing a variable against its own $(filter) result. $(filter ,$(TESTS)) is empty and so is $(TEST), so the comparison succeeds and an unset variable sails through — measured, it ran and wrote to build/1/ (§5).

Writing a literal # in a $(shell ...). Make treats it as a comment and reports an unterminated function call that has nothing to do with your brackets (§6).

Assigning $(shell ...) with =. It re-runs the command at every expansion. Use := (Chapter 3.1), and Chapter 6.2 §8 shows what that mistake does to a seed.

Giving help or tests prerequisites. Asking what the flow can do should never launch a simulation (§8).

Trusting the simulator's exit status for a bad test name. The run reported UVM_FATAL in its log and the tool still exited 0 — the verdict comes from reading the log (Chapter 5.4 §5).

10. Check Your Understanding

Question 1. An engineer runs make run TEST=axi_smoke, then make run TEST=axi_burst. The second command prints no [compile] line. Explain why that is correct rather than a sign the flow is broken.

Question 2. A Makefile contains TESTS := $(shell grep -v '^#' tests/testlist.f) and Make reports unterminated call to function 'shell': missing ')'. The brackets are balanced. What is wrong, and what are two ways to fix it?

Question 3. Why should the test-name check use $(error) inside ifneq rather than a test-and-exit 1 line at the top of the run recipe?

Question 4. The test list is read with := rather than =. What would go wrong with = on a list of 200 tests?

11. Exercise

Build the flow in §2, add the guard from §5, and confirm each behaviour. Every transcript below is real output.

Step 1. List what can be run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make tests
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  axi_smoke
  axi_burst
  axi_random
  axi_decode_err
  axi_flaky

Step 2. Run one test, then a different one. Confirm the second has no [compile] line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run TEST=axi_smoke
make run TEST=axi_burst
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  [compile] -> build/simv
  [run] axi_smoke seed=1 -> build/axi_smoke/1/run.log
  [PASS] axi_smoke seed=1
  [run] axi_burst seed=1 -> build/axi_burst/1/run.log
  [PASS] axi_burst seed=1

Step 3. Mistype a name. Nothing should be compiled or run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run TEST=axi_smok
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
Makefile.guard:17: *** unknown test "axi_smok" -- known tests are:
axi_smoke axi_burst axi_random axi_decode_err axi_flaky.  Stop.

Step 4. Run a test that genuinely fails, and read its log without typing the path:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run TEST=axi_flaky SEED=5
make log TEST=axi_flaky SEED=5
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — log trimmed to its first three lines
  [run] axi_flaky seed=5 -> build/axi_flaky/5/run.log
  [FAIL] axi_flaky seed=5
make: *** [build/axi_flaky/5/passed.stamp] Error 1
UVM_INFO @ 0: reporter [RNTST] Running test axi_flaky...
UVM_INFO @ 0: reporter [SEED] seed=5
UVM_ERROR @ 55000: uvm_test_top [TEST] addr 4 undecoded - data lost

The UVM_ERROR line is the one the Makefile's grep reacts to; the report summary below it reads UVM_ERROR : 1, which is why no stamp was written.

Step 5. Confirm the failed run left a log and no stamp:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls build/axi_flaky/5/
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
run.log

Then change two things and predict before you look. First, remove the \ from \# in the TESTS line and run make tests — you should get the unterminated-function error, and you should be able to say why without reading §6 again. Second, change TESTS := to TESTS = and add $(warning reading the list) just above it; run make run TEST=axi_smoke and count how many times the warning appears. That count is the number of times the grep ran, and it is the argument for := in one number.

12. Remember

  • A verification environment compiles once and runs many tests. The only thing that changes between runs is which test, so that must not be a compile-time value.
  • +UVM_TESTNAME=<name> is the seam. VCS, Xcelium and Questa launch differently but all read the same plusarg, so a Makefile written around it ports between them.
  • A plusarg never reaches the compiler, which is exactly why a different test needs no rebuild — measured: no [compile] line on the second run.
  • The test name belongs in the output path too, so two tests cannot overwrite each other's results.
  • Validate the name before you spend a run. $(filter $(TEST),$(TESTS)) plus ifneq and $(error) rejects a typo at parse time, with no tool work at all.
  • An unvalidated typo costs a full compile and run and leaves a directory named after a test that does not exist.
  • Guard the empty string separately. A membership test written as ifneq ($(TEST),$(filter $(TEST),$(TESTS))) passes for an empty TEST, because empty equals empty — measured, the run proceeded into build/1/.
  • Measured: one image served three tests byte-identically (same md5, unchanged mtime), which is the evidence behind "a plusarg costs no rebuild".
  • A literal # in a Makefile line starts a comment, even inside $(shell ...) — write it \#, or hold it in a variable. The error you get names the wrong problem.
  • Keep the test list in a file, not in the Makefile: other tools read it, it changes more often than build logic, and it can carry comments.
  • Read that file with :=, so the grep runs once rather than once per expansion.
  • Give the flow a help and a tests target, both phony and both without prerequisites, so asking what it does can never start a simulation.
  • Be honest about tool limits. UVM is not installed here, so real UVM appears as labelled reference syntax and the measurements come from a stand-in that reads the same plusargs.

One test at a time, chosen by name, with its own directory and its own verdict. The next knob is the one that decides what the test actually does: in constrained-random verification the same test at two different seeds is two different stimulus streams and two genuinely different results. Chapter 6.2 makes the seed a first-class part of the flow — and measures three ways a seed can look right while reproducing nothing.

Where this fits

Part of the Makefile curriculum.