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 Foundations18
  • UVM Phases & Simulation Lifecycle94
  • Factory & Configuration37
  • Transaction-Level Modeling (TLM)42
  • UVM Sequences & Stimulus44
  • UVM Agents & Protocol VIP71
  • Environment & Test Classes55
    • Environment Architecture
    • Environment Composition Hub: Scaling Reusable Env Topologies
    • Scoreboard + Coverage Hub: Integrated Checking Pipelines
    • Tests: Base Classes, Selection and Startup
      • Base Test Pattern: One Contract for Every Scenario
      • Scenario Inheritance: Specialize Intent, Not Infrastructure
      • Factory Overrides in Tests: Explicit, Local, Reviewable
      • Shared run_phase: One End-of-Test Policy for Every Test
      • +UVM_TESTNAME and Plusargs: Runtime Test Selection
      • Top Module and run_test Startup
      • Designing a Test Library
      • Test Library Organization: Scalable Structure and Naming
      • Test Base Debug: Symptom-First Hierarchy Triage
    • Config Distribution Hub: Test-to-Env Policy Propagation
    • Virtual Sequencer Hub: Env-Level Multi-Agent Coordination
    • Plusargs & Test Selection Hub: Runtime Control Without Recompile
    • End-of-Test Hub: Objections, Readiness, and Fail-Fast Shutdown
    • Top Module Hub: Startup Contracts from HDL to UVM
  • Checking & Coverage in UVM37
  • UVM Register Abstraction Layer (RAL)53
  • Advanced UVM in Practice44
  • Debug & Regression10
  • Coding Practice47
  • Senior Prep: Architecture, Leadership and Interviews92
  • Verification in Industry Practice10
Loading lesson

‹Tests: Base Classes, Selection and Startup
  1. Topics
  2. UVM Verification
  3. Environment & Test Classes
  4. Tests: Base Classes, Selection and Startup
  5. Designing a Test Library

Part 7 · Environment & Tests · Intermediate

Designing a Test Library

Growing nine tests into a library: the test hierarchy and intermediate bases, which knobs live in the base test, naming, regression tiers, avoiding copy-paste tests, and when to add a new test, a new sequence or a new plusarg.

On this page

    A year into a project, the DMA bench has 140 tests. Forty of them are copies of dma_rand_test with one constraint changed. Two of them are, by accident, the same test under different names. The subsystem team wrote their own program_dma because the block helpers would not work for them. Nobody planned any of this; each test was a reasonable addition on the day it was written. What decisions, made early, would have kept the library small and the regression honest?

    The idea: a test library is a product with users

    A test library is the set of test classes plus the lists that say which of them run, with how many seeds, and when. Its users are the people who add tests, the people who read regression results, and the people one level up who want to reuse your stimulus. Each of them needs something different: an obvious place to add a scenario, a name that says what failed, and stimulus that is not locked inside a test class.

    A good kitchen's recipe book is the analogy. Base preparations (stocks, doughs) are written once and referenced. Dishes are named for what they are, so a cook can find one. Variations are written as "as above, but", not copied out in full. And the menu, what is served tonight, is a separate list from the book. Tests are the dishes, sequences are the base preparations, and regression lists are the menus.

    The picture: a library that grows without copying

    Tree: uvm_test has dma_base_test and dma_subsys_test; dma_base_test has functional tests, an error family under dma_err_base_test, and a stress family; shared sequences feed both base classes.
    A test hierarchy with one intermediate base for a family that shares setup and checking. Stimulus lives in sequences that both the block and subsystem tests use, so the subsystem test does not copy the block helpers.

    Build it: five decisions

    1. Depth: one base, plus a base per family

    Keep the hierarchy shallow: the project base test, then an intermediate base only where three or more tests share setup or checking, then leaf tests. Error tests are the classic family: they all program one length and send fewer words, they all expect STATUS.ERR instead of DONE, and they all check that the error was reported. That shared expectation belongs in one class.

    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
    // snippet
    // A family base: every error test runs one job and expects ERR, not DONE.
    class dma_err_base_test extends dma_base_test;
      `uvm_component_utils(dma_err_base_test)
      int unsigned prog_len = 8;     // what the test programs
      int unsigned sent_len = 3;     // what it actually sends
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      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 == local::prog_len; })
          `uvm_fatal("RAND", "dma_job_cfg randomize failed")
        run_job(job, sent_len, status);
        if (status[2] !== 1'b1) `uvm_error("NOERR", $sformatf("%s: ERR not set, STATUS=%08h", get_type_name(), status))
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    A leaf error test now sets two numbers, or registers one override, and inherits the expectation. Deeper hierarchies (a base for every feature, a base for every base) make it hard to know which run_phase actually runs; when you need to open four files to read one test, the hierarchy is too deep.

    2. What lives in the base test

    Belongs in the base test

    Belongs in a derived test or below

    Env creation through the factory, required handles with fatals

    Which stimulus runs and in what order

    Default config objects and the defaults, scenario, command line, validate order

    Scenario knob values

    End-of-test policy, drain time, global timeout

    Factory overrides for this scenario

    Topology print, knob logging, verdict line

    What to check beyond the scoreboard

    Thin helpers that only start a sequence

    Multi-step jobs: those go in sequences

    The last row is the one the bench breaks, on purpose, so you can see the cost. run_job is a multi-step job written as a test task. dma_subsys_test extends uvm_test, not dma_base_test (its env is a different class), so it could not call run_job and wrote its own program_dma. Had the job been a sequence that takes an APB sequencer handle, both test classes would start the same object on different sequencers:

    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
    // snippet
    class dma_prog_seq extends uvm_sequence #(apb_item);
      `uvm_object_utils(dma_prog_seq)
      bit [31:0] key;
      int unsigned len;
      bit irq_en = 1;
    
      function new(string name = "dma_prog_seq");
        super.new(name);
      endfunction
    
      task body();
        write(KEY, key);
        write(LEN, len);
        write(CTRL, {30'b0, irq_en, 1'b1});
      endtask
    
      task write(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(m_sequencer, this);                 // same sequencer, this as parent
      endtask
    endclass
    
    // block test:      prog.start(env.apb.sqr);
    // subsystem test:  prog.start(env.dma1.apb.sqr);  prog.start(env.dma0.apb.sqr);

    3. Names that say what failed

    Use one pattern and enforce it in review: <block>_<feature>_<variant>_test, for example dma_err_truncate_test, dma_stress_backpressure_test. The bench's names, dma_<what>_test, are the short form of the same idea. A name is good if a failure line in a regression report tells you which feature to look at without opening the file. Avoid names that describe history (dma_new_rand_test, dma_rand2_test) or a person; neither tells anyone what is tested.

    4. Regression lists are data, separate from the tests

    Which tests run, with how many seeds, and with which plusargs, is a list, kept next to the bench and reviewed like code. Tiers keep the feedback fast:

    Diagram
    # tier      test                          seeds  plusargs
    smoke       dma_smoke_test                1
    smoke       dma_rand_test                 5
    nightly     dma_rand_test                 200    +DMA_JOBS=10
    nightly     dma_err_truncate_test         20
    nightly     dma_stress_backpressure_test  50     +UVM_VERBOSITY=UVM_LOW
    nightly     dma_stress_reset_test         50
    weekly      dma_rand_test                 2000   +DMA_JOBS=50
    directed    dma_timeout_test              1      # expects PH_TIMEOUT: judged by a rule, not by "0 fatals"

    The smoke tier runs on every commit and must finish in minutes. The nightly tier is where random tests earn their seeds. The last line matters: a test that is supposed to end in a fatal needs its own pass rule in the regression tool, or it either fails every night or teaches people to ignore fatals.

    5. New test, new sequence, new plusarg, or new config field?

    The change is ...

    Add a ...

    Because

    a value within one scenario (job count, max length)

    plusarg on an existing test

    The scenario is the same; the regression list can vary it.

    a new shape of stimulus (a new job pattern, a new interrupt flow)

    sequence or virtual sequence

    Stimulus in a sequence is reusable by other tests and at subsystem level.

    a topology or behaviour switch (agent mode, extra checker)

    config object field

    The env reads it in build; tests and plusargs set it.

    a different override, checking rule or end-of-test rule, or a line of its own in the regression report

    test

    It is a different scenario, and it deserves a name.

    Watch it run

    A healthy library leaves a fingerprint in every log: the test name, the knobs it ran with, and proof of checking. With the base test from Base Test Pattern and the knob logging from +UVM_TESTNAME and Plusargs, one random run reads like this (values depend on the seed):

    Diagram
    UVM_INFO @ 0: reporter [RNTST] Running test dma_rand_test...
    UVM_INFO ... @ 0: uvm_test_top [KNOBS] n_jobs=3 max_len=16
    UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
    ...
    UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0

    The RNTST, KNOBS and JOB lines are the replay bundle in the log itself; the SB line is proof of work. A regression tool can parse all four.

    What breaks: the copied test that ran its parent

    An engineer needs a variant of the truncate test that cuts at beat 5. They copy dma_truncate_test into a new file, rename the class to dma_trunc5_test, change the beat, and add it to the nightly list. It passes every night. Weeks later, a bug at beat 5 escapes.

    The copied class still said uvm_component_utils(dma_truncate_test). The macro registers whatever name it is given, so the new class never registered itself, and its get_type_name() returned the old name. Selecting +UVM_TESTNAME=dma_trunc5_test would have failed with INVTST, but the list had been edited in a hurry with the old name, so the nightly run executed the original test under the old name and nobody noticed the duplicate. Two defences close this: a lint rule that the class name and the macro argument match, and a family base, so a variant is five lines that set a value instead of a copied file that can drift.

    Interview

    "You inherit a bench with 300 tests and a regression that takes two days. What do you do?"

    A weak answer: "Delete the tests that never fail and run the rest." Tests that never fail may be guarding against regressions; deleting by pass rate is guesswork.

    A strong answer: "First, measure: map each test to the testplan features and to coverage it uniquely hits, and find duplicates by comparing what they configure, not their names. Then restructure: fold copies into family bases with knobs, move multi-step stimulus into sequences so tests become thin, and turn value variations into plusargs on one test with more seeds. Then tier the regression: a smoke tier on every commit, nightly random with seeds weighted by coverage contribution, and a weekly long tail. Pass rules require zero errors and fatals, the expected RNTST line and a non-zero compare count."

    Follow-ups: How do you find two tests that are really the same? (diff their effective configuration and overrides from the logs, and their coverage contribution). When do you add a test instead of a seed? (when the scenario needs a different override, check or end-of-test rule).

    Check yourself A designer asks for "the random test, but with LEN always 16". New test, new sequence, plusarg or config field?

    A plusarg, if the test reads one for its length range: it is a value within the same scenario, and the regression list can run it with its own seeds. If the test does not support such a knob yet, adding it is a one-time change that serves every later request like this one, which is better than a new test class that differs by one constraint.

    Check yourself The subsystem team wants to reuse dma_vseq for dma0 and dma1. What in the block bench makes that hard, and what would you change?

    dma_vseq takes an APB sequencer, a stream sequencer and an interrupt interface. For dma1 in the chain, in_agt is passive and has no sequencer, because dma0 produces its data. The fix is to split the vseq: register programming and interrupt handling (which work for both DMAs) in one sequence, data in another, so the subsystem test can program both DMAs and stream only into dma0. That is a library decision: stimulus that assumes it owns every interface does not travel up a level.

    Next: with the environment and the tests designed, the bench needs its judges. Checking & Coverage builds the scoreboard, the predictor and the coverage model on the same bench.

    Related topics

    • Environment Architecture

    • Scenario Inheritance

    • Registration macros

    • Virtual sequences without a virtual sequencer

    Previous

    333 / 658

    Next: Test Library Organization: Scalable Structure and Naming

    Up next

    Up nextTest Library Organization: Scalable Structure and Naming

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Base Test Pattern: One Contract for Every Scenario
    • Scenario Inheritance: Specialize Intent, Not Infrastructure
    • Factory Overrides in Tests: Explicit, Local, Reviewable