Skip to content
VLSI Mentor

Python Part II · Module 20 · Simulation and UVM Log Parsing

Classifying a Run: Pass, Fail, Timeout, Never Started

The parser now knows how many errors there were, what failed first, and what was happening around it. Turning that into a verdict looks like the easy part, and it is where regression reporting goes wrong — because the obvious rule, no errors means pass, is satisfied perfectly by a run that produced no errors by producing nothing at all. A hung test and a failed elaboration both have zero errors. Over four logs with one genuinely clean run between them, a two-outcome verdict reports three passes, and every one of those passes is a test nobody will look at again.

Intermediate17 min readPythonPart IILog ParsingUVMVerification Flow

Part II · Module 20 · Chapter 20.11 · Classifying a Run: Pass, Fail, Timeout, Never Started

1. Four Logs

Azvya Education Pvt. Ltd.VLSI Mentor
pass.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_reset_test...
UVM_INFO scb.sv(120) @ 160000: uvm_test_top.env.scb [CHK] 96 compared
--- UVM Report Summary ---
UVM_ERROR :    0
UVM_FATAL :    0
TEST DONE
Simulation complete via $finish(1) at time 200000 NS
Azvya Education Pvt. Ltd.VLSI Mentor
fail.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake
--- UVM Report Summary ---
UVM_ERROR :    1
UVM_FATAL :    0
TEST DONE
Simulation complete via $finish(1) at time 200000 NS
Azvya Education Pvt. Ltd.VLSI Mentor
hang.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_burst_test...
UVM_INFO drv.sv(60) @ 141700: uvm_test_top.env.agt.drv [WR] issuing write
UVM_INFO drv.sv(60) @ 141800: uvm_test_top.env.agt.drv [WR] waiting for resp
Azvya Education Pvt. Ltd.VLSI Mentor
norun.log
xcelium> run
xmelab: *E,CUVWSP: module worklib.tb_top not found
xcelium: *F,NOELAB: elaboration failed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

UVM_FAIL = re.compile(r"^UVM_(ERROR|FATAL)\b.*@\s+\d+:")


def count_failures(path):
    errors = fatals = 0
    with open(path, encoding="utf-8") as handle:
        for line in handle:
            m = UVM_FAIL.match(line)
            if m is None:
                continue
            if m.group(1) == "ERROR":
                errors += 1
            else:
                fatals += 1
    return errors, fatals


print(f"{'log':<12} {'errors':>7} {'fatals':>7}   two-outcome verdict")
for path in ["pass.log", "fail.log", "hang.log", "norun.log"]:
    errors, fatals = count_failures(path)
    verdict = "PASS" if errors == 0 and fatals == 0 else "FAIL"
    print(f"{path:<12} {errors:>7} {fatals:>7}   {verdict}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
log           errors  fatals   two-outcome verdict
pass.log           0       0   PASS
fail.log           1       0   FAIL
hang.log           0       0   PASS
norun.log          0       0   PASS

Three passes out of four, and one of these runs passed.

hang.log is a test that stopped producing output and was killed by the job scheduler hours later. norun.log is a simulation that never started — elaboration failed because a module was missing, so the testbench was never built, let alone run.

Both have zero errors. Both satisfy "no errors means pass" perfectly, and they satisfy it for the same reason: a run that produced no errors because it produced nothing at all is indistinguishable, by error count, from a run that produced no errors because everything worked.

2. Four Outcomes

Four outcomes in two rows. Top row: PASS, meaning the run started, finished, and reported no failures; and FAIL, meaning it started, finished, and reported at least one error, fatal or assertion failure. An arrow labelled failures runs from PASS to FAIL. Bottom row: NO FINISH, meaning the run started but no end-of-simulation banner was ever written, which is a hang or a kill; and NO RUN, meaning the simulation never started at all, because compilation or elaboration failed. An arrow labelled never began runs from NO FINISH to NO RUN.PASSstarted, finished, nothingfailedFAILfinished, and somethingfailedNO FINISHstarted, no end-of-simbannerNO RUNelaboration never produceda testfailuresnever began12
Figure 1 — the four states a simulation log can be in, and the evidence that puts it in each. The two on the bottom row are the ones a two-outcome verdict silently folds into PASS, and they send an engineer to two completely different places.

The two on the bottom row are not shades of failure. They are different questions, and the reason to keep them apart is that they send a person somewhere different:

  • FAIL — the design or the testbench is wrong. Open the first failure and its context.
  • NO_FINISH — the test hung, or the scheduler killed it. Look at the last thing it was doing, and at the timeout.
  • NO_RUN — nothing ran. Look at the compile and elaborate step; the log you are reading is not a simulation log at all.

Collapsing NO_RUN into FAIL sends an engineer to debug RTL for a missing file in a filelist. Collapsing either into PASS means nobody goes anywhere.

3. The Evidence for Each

Each outcome rests on something present or absent in the log:

OutcomeStartedFinishedFailuresEvidence
PASSyesyesnonebanner, end-of-sim, clean tallies
FAILyesyessomebanner, end-of-sim, a failure
NO_FINISHyesnoanythe test announced itself and stopped
NO_RUNnono—no test banner at all

Started is the UVM report server's RNTST banner — the line that says Running test <name>. It is written once the testbench has been built and the test object created, so its presence means a test genuinely began.

Finished is an end-of-simulation marker. There are several spellings — TEST DONE, Simulation complete, $finish — and like the seed in Chapter 20.8 they are tool- and testbench-specific, so a list is the right shape.

Note what is not in that table: the error count plays no part in distinguishing the bottom two rows. A hung test may well have logged errors before it hung, and it is still NO_FINISH.

4. The Classifier

Azvya Education Pvt. Ltd.VLSI Mentor
pass.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_reset_test...
UVM_INFO scb.sv(120) @ 160000: uvm_test_top.env.scb [CHK] 96 compared
TEST DONE
Simulation complete via $finish(1) at time 200000 NS
Azvya Education Pvt. Ltd.VLSI Mentor
fail.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake
TEST DONE
Simulation complete via $finish(1) at time 200000 NS
Azvya Education Pvt. Ltd.VLSI Mentor
hang.log
UVM_INFO @ 0: reporter [RNTST] Running test axi_burst_test...
UVM_INFO drv.sv(60) @ 141700: uvm_test_top.env.agt.drv [WR] issuing write
UVM_INFO drv.sv(60) @ 141800: uvm_test_top.env.agt.drv [WR] waiting for resp
Azvya Education Pvt. Ltd.VLSI Mentor
norun.log
xcelium> run
xmelab: *E,CUVWSP: module worklib.tb_top not found
xcelium: *F,NOELAB: elaboration failed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

UVM_FAIL = re.compile(r"^UVM_(?P<sev>ERROR|FATAL)\b.*@\s+\d+:")
STARTED = re.compile(r"\[RNTST\] Running test (?P<test>\w+)")
FINISHED = re.compile(r"TEST DONE|Simulation complete|\$finish")


def scan(path):
    """One pass. Facts only — no verdict."""
    facts = {"errors": 0, "fatals": 0, "started": False,
             "finished": False, "test": None}
    with open(path, encoding="utf-8") as handle:
        for line in handle:
            m = UVM_FAIL.match(line)
            if m is not None:
                facts["errors" if m["sev"] == "ERROR" else "fatals"] += 1
            s = STARTED.search(line)
            if s is not None:
                facts["started"] = True
                facts["test"] = s["test"]
            if FINISHED.search(line):
                facts["finished"] = True
    return facts


def classify(facts):
    """Policy. One of PASS, FAIL, NO_FINISH, NO_RUN."""
    if not facts["started"]:
        return "NO_RUN"
    if not facts["finished"]:
        return "NO_FINISH"
    return "FAIL" if (facts["errors"] or facts["fatals"]) else "PASS"


print(f"{'log':<12} {'status':<10} {'err':>4} {'fat':>4} {'started':>8} {'finished':>9}  test")
for path in ["pass.log", "fail.log", "hang.log", "norun.log"]:
    facts = scan(path)
    print(f"{path:<12} {classify(facts):<10} {facts['errors']:>4} {facts['fatals']:>4} "
          f"{str(facts['started']):>8} {str(facts['finished']):>9}  {facts['test']}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
log          status      err  fat  started  finished  test
pass.log     PASS          0    0     True      True  axi_reset_test
fail.log     FAIL          1    0     True      True  axi_smoke_test
hang.log     NO_FINISH     0    0     True     False  axi_burst_test
norun.log    NO_RUN        0    0    False     False  None

Four logs, four different answers, and each one tells somebody where to look.

The split between scan and classify is Chapter 20.1 Section 4's, and here it earns something specific: classify takes facts, not a path. It never opens a file, so the four-row table above is also its entire test suite — four dicts and four expected strings, no logs required. Chapter 19.18's sample-list testing applies directly.

And classify contains the canonical annotation almost verbatim:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if not finished:
    return "NO_FINISH"
return "PASS" if not (errors or fatals) else "FAIL"

with NO_RUN added in front of it. That addition is this chapter's one departure from the canonical sketch, and it is deliberate — the sketch covers three of the four outcomes, and the fourth is the one that produces the worst report, because a log from a simulation that never started is not a simulation log.

5. The Order of the Checks

started, then finished, then failures. That order is not stylistic:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FACTS = {"errors": 3, "fatals": 1, "started": False, "finished": False}


def right_order(f):
    if not f["started"]:
        return "NO_RUN"
    if not f["finished"]:
        return "NO_FINISH"
    return "FAIL" if (f["errors"] or f["fatals"]) else "PASS"


def wrong_order(f):
    if f["errors"] or f["fatals"]:
        return "FAIL"
    if not f["started"]:
        return "NO_RUN"
    if not f["finished"]:
        return "NO_FINISH"
    return "PASS"


print("right order:", right_order(FACTS))
print("wrong order:", wrong_order(FACTS))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
right order: NO_RUN
wrong order: FAIL

A log that never started a test, with three errors in it — which is exactly what an elaboration failure looks like, because the tool's own *E, messages can be mistaken for errors by a loose pattern.

wrong_order reports FAIL, which sounds harmless and is not: it says the test failed, so somebody opens the waveform for a test that was never built. right_order reports NO_RUN, and they go and look at the filelist.

Each earlier question makes the later ones meaningless rather than merely false. "How many errors did it have" is not a slightly-wrong question to ask of a run that never began; it is a question with no answer, and answering it anyway is how a report acquires numbers that mean nothing.

6. Four Statuses, One Summary

Over a sweep, the four outcomes are what makes the summary honest:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RUNS = [
    {"test": "axi_reset_test", "status": "PASS"},
    {"test": "axi_smoke_test", "status": "FAIL"},
    {"test": "axi_burst_test", "status": "NO_FINISH"},
    {"test": "axi_wrap_test", "status": "PASS"},
    {"test": None, "status": "NO_RUN"},
    {"test": "axi_excl_test", "status": "FAIL"},
]

ORDER = ["PASS", "FAIL", "NO_FINISH", "NO_RUN"]

counts = dict.fromkeys(ORDER, 0)
for run in RUNS:
    counts[run["status"]] += 1

total = len(RUNS)
for status in ORDER:
    print(f"{status:<10} {counts[status]:>3}")
print(f"{'total':<10} {total:>3}")
print()
print("clean          :", counts["PASS"] == total)
print("needs RTL debug:", counts["FAIL"])
print("needs flow work:", counts["NO_FINISH"] + counts["NO_RUN"])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS         2
FAIL         2
NO_FINISH    1
NO_RUN       1
total        6

clean          : False
needs RTL debug: 2
needs flow work: 2

Two rows at the bottom that a two-outcome report cannot produce. needs flow work: 2 is the number that gets a regression fixed, and in a two-outcome world those two runs are indistinguishable from the two genuine passes — the summary would read "4 passed, 2 failed" and the four would include a hang and a build failure.

dict.fromkeys(ORDER, 0) again, so a sweep with no hangs prints NO_FINISH 0 rather than omitting the row. A missing row in a summary reads as "not applicable"; a zero reads as "checked, none found".

7. What the Log Cannot Tell You

Honest limits, because three of the four outcomes are inferred from absence, and absence has more than one cause:

A log with no end-of-simulation banner might be a hang, a test killed by the scheduler's time limit, a job killed for memory, a full disk, an NFS write that never landed, or a file somebody copied while it was still being written. All of them look identical.

A log with no test banner might be an elaboration failure, or a simulation whose output was redirected somewhere else, or an empty file created by a wrapper script that then died.

An empty file is NO_RUN by these rules, and that is the right default — but it tells you nothing about why.

The parser should not guess between those, and the fix is not a cleverer pattern. It is more evidence, from outside the log: the runner's own exit status, the wall-clock time the job took, and whether the scheduler reported a kill. A test that ran for exactly the timeout is a hang; one that ran for nine seconds and stopped is something else. That evidence belongs to whatever launched the simulation, and Chapter 20.12 is about combining it with what the log says.

8. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def classify(f):
    if not f["started"]:
        return "NO_RUN"
    if not f["finished"]:
        return "NO_FINISH"
    return "FAIL" if (f["errors"] or f["fatals"]) else "PASS"


def facts(started, finished, errors=0, fatals=0):
    return {"started": started, "finished": finished, "errors": errors, "fatals": fatals}


# A
print("A:", classify(facts(True, True)))

# B
print("B:", classify(facts(True, True, errors=1)))

# C
print("C:", classify(facts(True, False)))

# D
print("D:", classify(facts(True, False, errors=40)))

# E
print("E:", classify(facts(False, False, errors=3)))

# F
print("F:", classify(facts(False, True)))

# G
print("G:", classify(facts(True, True, fatals=1)))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: PASS
B: FAIL
C: NO_FINISH
D: NO_FINISH
E: NO_RUN
F: NO_RUN
G: FAIL

A and B — the two outcomes a simple verdict gets right.

C and D — a hang is NO_FINISH whether it logged forty errors first or none. The status says where to look; the count is still in the record.

E — never started, with three errors. NO_RUN beats FAIL, which is Section 5's ordering.

F — finished but never started is a contradiction the data can produce — an end-of-sim marker matched in a tool banner, say — and the classifier handles it rather than being surprised. NO_RUN is the right call: without a test banner there is no test.

G — a fatal is a failure like an error, which is why the test is errors or fatals and not just errors.

9. Exercises

Exercise 1 — three passes. Build four logs matching Section 1 and run a two-outcome verdict over them. Record how many it calls PASS.

Exercise 2 — why both. Explain in one sentence the property the hang and the never-started run share that makes a two-outcome verdict pass them both.

Exercise 3 — where each one sends you. For each of the four outcomes, write the one-line instruction you would put next to it in a summary.

Exercise 4 — the evidence table. For a log of your own, identify the started marker and every end-of-simulation spelling your flow produces.

Exercise 5 — facts and policy. Write scan and classify separately. Confirm classify never opens a file.

Exercise 6 — the test suite you got free. Test classify with eight dicts and no logs at all. Include a case for each outcome and one contradiction.

Exercise 7 — the wrong order. Put the failure check first and run it on facts with started=False and three errors. Record the status and say who gets sent where.

Exercise 8 — keep the counts. Produce two NO_FINISH records, one with 40 errors and one with none. Say what each suggests.

Exercise 9 — the summary. Count a sweep by status with dict.fromkeys. Print a zero row and say why a zero reads differently from a missing row.

Exercise 10 — the flow number. Add needs flow work to your summary and say what a two-outcome report would have shown instead.

Exercise 11 — absence has causes. List four distinct things that produce a log with no end-of-simulation banner. Say which piece of evidence outside the log would separate them.

Exercise 12 — observation, not conclusion. Argue in three sentences for or against renaming NO_FINISH to TIMEOUT.

10. Summary

  • Two outcomes are not enough. Over four logs with one clean run, a two-outcome verdict reports three passes.
  • A hang and a never-started run both have zero errors, because a run that produced nothing produced no errors.
  • Four outcomes: PASS, FAIL, NO_FINISH, NO_RUN.
  • They send people to different places — the design, the testbench and timeout, or the compile and elaborate step.
  • Started is the UVM RNTST banner; finished is an end-of-simulation marker with several tool-specific spellings, so a list.
  • The error count plays no part in distinguishing the bottom two rows. A hung test that logged errors first is still NO_FINISH.
  • classify takes facts, not a path, so a handful of dicts is its whole test suite.
  • The canonical sketch covers three outcomes; NO_RUN is added in front because a log from a simulation that never started is not a simulation log.
  • Order the checks started, finished, failures. Each earlier state makes the later questions meaningless rather than merely false.
  • With the order reversed, an elaboration failure reports FAIL and somebody opens a waveform for a test that was never built.
  • Keep the counts on every record, including NO_RUN and NO_FINISH — the status says where to look, the counts say what you will find.
  • needs flow work is the number a two-outcome report cannot produce, and it is the one that gets a regression fixed.
  • Three of the four outcomes are inferred from absence, and absence is also a full disk, an NFS stall, a killed job or a part-copied file.
  • The fix is more evidence, not a cleverer pattern — the runner's exit status and wall clock.
  • NO_FINISH says what was observed; TIMEOUT is a conclusion and is wrong for the full-disk case.

The next chapter brings in that outside evidence, and works out how much of a hang a log can diagnose on its own.

Where this fits

Part of the Python curriculum.