Skip to content
VLSI Mentor

Tcl · Module 25

Headless Batch Simulation

In batch there is nobody to read the log, so the exit status is the whole report. It does not say what you think it says, and this chapter measures exactly what it does say.

Intermediate21 min readtcledapart-iisimulationregressionexit-status

Part II · Module 25 · Chapter 25.4 · Headless Batch Simulation

Interactively, a failing test is obvious: you read the transcript. In batch — a nightly regression, a merge gate, a thousand runs on a farm — nobody reads anything. The exit status is the entire report.

So the question is what the exit status actually means. It is measurable, and the answer is not what most people assume.

1. Running without a GUI

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vsim -c -do run.do work.fifo_tb        # Questa family: -c is "console", no GUI
vvp sim/fifo_tb.vvp +test=smoke        # Icarus: there is no GUI to suppress

The first line is the registry's own sample and is not executed anywhere in this module — no Questa exists on the machine these chapters were verified on (Chapter 25.1 §1). The second line is real, and everything measured below came from it.

Three things change when the GUI goes away, and all three are the reason this chapter exists:

  • Nothing is interactive. A command that would have opened a dialog or waited at a prompt either fails or hangs, and a hang in a batch farm is worse than a failure because it consumes a licence until a timeout kills it.
  • The log is the only record. Whatever was not printed did not happen, as far as anyone will ever know.
  • The exit status is the only thing the caller sees. A regression driver running 500 simulations reads 500 integers.

2. What the exit status reports

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== what exit status does each ending give? ===
    MODE     EXIT  LAST LINE OF OUTPUT
    finish   0     TEST FAILED
    fatal    1     TEST FAILED
    fatal1   1     TEST FAILED
    fatal2   1     TEST FAILED

3. The same testbench, two designs

The abstract version is unconvincing, so here is a real one. One testbench, one injected defect — the FIFO's full asserts one slot early:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign full  = (count == DEPTH - 1);   // DEFECT: asserts one slot early
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the same testbench against two DUTs ===
  --- good DUT ---
    test=fill_drain seed=1
    fill_drain: 9 checks, 0 errors
    TEST PASSED
    tb/fifo_tb.v:68: $finish called at 170 (1s)
    vvp exit status = 0
  --- bad DUT ---
    test=fill_drain seed=1
    FAIL 90 count after 4 pushes: got 3 expected 4
    FAIL 170 fifo order: got 18 expected 19
    fill_drain: 9 checks, 2 errors
    TEST FAILED
    tb/fifo_tb.v:68: $finish called at 170 (1s)
    vvp exit status = 0

Two errors found, the defect correctly diagnosed, TEST FAILED printed — and exit status 0. The testbench did its job perfectly. The only thing wrong is the channel the verdict travelled on.

One detail in that output is worth noticing, because it is a nice piece of design-understanding: full after 4 pushes did not fail, only count did. With full asserting at 3, the fourth push is blocked, so count stays at 3 — and full is indeed 1 at that moment, so the full check passes. A single defect can satisfy one of your checks while violating another, which is why the count of checks matters as much as the count of failures.

4. exec cannot tell the difference either

The natural Tcl reflex is to let catch around exec decide:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc run_sim {binary args} {
    set cmd [list vvp $binary]
    foreach a $args { lappend cmd $a }
    set code [catch {exec {*}$cmd} out]
    return [list $code $out]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== what exec reports for a passing and a failing run ===
    passing  catch code 0   output lines 4
    failing  catch code 0   output lines 6
    both report catch code 0 — the simulator ran, which is all it said

Identical. exec reports the child's exit status, and the child said 0 both times.

5. Read the verdict out of the log

If the status cannot carry the verdict, the log must:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc verdict {out} {
    set pass 0 ; set fail 0
    foreach line [split $out "\n"] {
        if {[string match "TEST PASSED*" [string trim $line]]} { incr pass }
        if {[string match "TEST FAILED*" [string trim $line]]} { incr fail }
    }
    if {$fail} { return FAILED }
    if {$pass} { return PASSED }
    return NO-VERDICT
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== so read the verdict out of the log ===
    passing  verdict PASSED
    failing  verdict FAILED

Three decisions in nine lines.

FAILED is checked first. A log containing both — a test that printed a pass line for one phase and a fail for another — must come out FAILED. Ordering the tests so the bad news wins is the only safe precedence.

string match with a trailing *, because the verdict line may carry a suffix, and string match is anchored at both ends by definition (Chapter 5.5) so the * is required rather than decorative.

The third outcome exists. NO-VERDICT is not an error case bolted on; it is what the function returns when neither line is present, and §6 is about why that matters more than either of the other two.

6. The third outcome

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and a log with no verdict at all ===
    verdict of an empty log      : NO-VERDICT
    verdict of a log that cut off: NO-VERDICT

An empty log and a truncated log both produce NO-VERDICT rather than PASSED.

7. The gate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc gate {binary args} {
    set r [run_sim $binary {*}$args]
    set ran [lindex $r 0] ; set out [lindex $r 1]
    if {$ran != 0} { return [list 2 "the simulator itself failed"] }
    switch -- [verdict $out] {
        PASSED     { return [list 0 "test passed"] }
        FAILED     { return [list 1 "test failed"] }
        NO-VERDICT { return [list 3 "no verdict in the log — the run did not finish"] }
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the gate: three outcomes, three exit statuses ===
    passing  exit 0  test passed
    failing  exit 1  test failed
    missing  exit 2  the simulator itself failed

Four distinct statuses for four distinct situations, which is Chapter 15.4's convention applied here:

StatusMeaningWhat a driver should do
0the test passedcontinue
1the test failedreport the design, keep the log
2the simulator or its inputs failedreport the flow, not the design
3no verdict — the run did not finishretry once, then report the infrastructure

1 and 2 must be different, because they go to different people. A failing test is the design team's; a missing binary, a licence timeout or a compile error is the flow's. A regression that returns the same status for both sends every failure to the wrong queue.

3 deserves its own status because it is the one that is usually retried rather than reported. A run that vanished is often infrastructure, and a driver that retries a 3 once and reports a 1 immediately is doing the right thing with both.

8. The two ways to make the status right

The gate above fixes the status outside the simulation. The other option is to fix it inside, and they are not exclusive.

In the testbench, end on $fatal rather than $finish when the test failed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (errors == 0) begin
    $display("TEST PASSED");
    $finish;
end else begin
    $display("TEST FAILED: %0d of %0d checks", errors, checks);
    $fatal(1, "test failed");
end

Now the exit status is 1 and the wrapper's job gets easier. Two caveats, both measured: $fatal gives 1 regardless of its argument (§2), so it cannot encode which failure; and $fatal stops the simulation immediately, so anything the testbench would have done afterwards — a coverage dump, a final report, closing a log file — does not happen. If the end-of-test bookkeeping matters, print it before $fatal, not after.

In the do file, end on quit -code:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {[verdict $transcript] eq "PASSED"} { quit -code 0 } else { quit -code 1 }

That is Chapter 25.2 §6's mechanism, and in the Questa family it is the natural place for the decision because the do file can see the transcript.

9. What else a batch run needs

  • A timeout. A hung simulation holds a licence until something kills it. The driver owns this, not the testbench.
  • A log per run, named for the run. Chapter 25.5 is about what "named for the run" has to mean, and it is less obvious than it sounds.
  • No $stop. It drops to an interactive prompt, which in batch means a hung job. Ban it from testbench code rather than hoping.
  • No GUI command on a batch path. view wave and relatives fail or hang headless; guard them behind the same flag that chose batch mode.
  • The plusargs recorded. +seed=7 is the difference between a reproducible failure and a ghost, and it belongs in the log and the manifest, not only in the command line that has scrolled away.

10. Exercises

Exercise 1. What three things change when the GUI goes away?

Exercise 2. What does $finish mean, and what does its exit status therefore report?

Exercise 3. What exit status does $fatal(2, "msg") give, and why is the 2 misleading?

Exercise 4. The bad DUT failed count after 4 pushes but passed full after 4 pushes. Explain.

Exercise 5. Why can catch {exec vvp ...} not distinguish a passing from a failing run?

Exercise 6. vvp writes nothing to stderr. Why is that worth knowing, and what would a simulator that did write to stderr do to catch?

Exercise 7. Why is FAILED tested before PASSED in verdict?

Exercise 8. Name three real situations that produce a log with no verdict.

Exercise 9. Why must exit 1 and exit 2 be different statuses?

Exercise 10. Give two drawbacks of ending a failing test with $fatal.

Exercise 11. Why keep the wrapper's log parse even when the testbench already calls $fatal?

Where this fits

Part of the Tcl curriculum.