Tcl · Module 25
Regression Bookkeeping in Tcl
A regression is believable when you can go back to any single run and see what it did. Two defects in this chapter's own code show how easily that stops being true.
Advanced23 min readtcledapart-iisimulationregressionverification
Part II · Module 25 · Chapter 25.5 · Regression Bookkeeping in Tcl
Chapter 25.4 made one run report its verdict honestly. A regression is hundreds of those, and the new problem is not running them — it is being able to believe the result afterwards.
Everything in this chapter ran for real: Icarus Verilog driving the Module 25 FIFO, with Tcl as the regression layer. Both of the defects it discusses are defects I found in my own code while writing it, which is why they are here rather than a list of good practices.
1. What a run's record must contain
proc run_one {binary test seed logdir} {
set log [file join $logdir "[run_id $binary $test $seed].log"]
set t0 [clock milliseconds]
set ran [catch {exec vvp $binary +test=$test +seed=$seed} out]
set ms [expr {[clock milliseconds] - $t0}]
set fh [open $log w] ; puts $fh $out ; close $fh
if {$ran != 0} {
return [dict create test $test seed $seed binary $binary \
verdict SIM-ERROR ms $ms log $log]
}
return [dict create test $test seed $seed binary $binary \
verdict [verdict $out] ms $ms log $log]
}Six fields, and each one answers a question somebody asks later:
| Field | The question it answers |
|---|---|
test | which test was this |
seed | how do I reproduce it |
binary | which build — which RTL, which compile options |
verdict | what did it conclude |
ms | is it getting slower, and did it time out |
log | where is the evidence |
seed is the one people omit and the one that matters most. A randomised test that fails without a recorded seed is a failure nobody can reproduce, which makes it a failure nobody can fix — and the plusarg mechanism from Chapter 25.4 exists precisely so one binary can serve every seed.
verdict comes from the three-outcome function of Chapter 25.4 §5, and SIM-ERROR is a fourth value for "the simulator itself failed" — because Chapter 25.4 §7's argument about routing failures to different people applies to a row in a table as much as to an exit status.
ms is recorded in every row but is left out of the printed tables in this chapter, because it differs on every run — which is itself the point of recording it. A duration that drifts upward across nightlies is how a test that has started timing out announces itself before it starts failing, and a duration that collapses to near zero is how you notice a run that died immediately. It belongs in the manifest and in a trend, not in a table you compare by eye.
2. The regression
set PLAN {
{sim/rand_good.vvp rand {1 2 3 4}}
{sim/good.vvp smoke {1}}
{sim/rand_bad.vvp rand {1 2 3 4}}
}=== running the regression ===
TEST SEED VERDICT LOG
rand 1 PASSED rand_s1.log
rand 2 PASSED rand_s2.log
rand 3 PASSED rand_s3.log
rand 4 PASSED rand_s4.log
smoke 1 PASSED smoke_s1.log
rand 1 FAILED rand_s1.log
rand 2 FAILED rand_s2.log
rand 3 FAILED rand_s3.log
rand 4 FAILED rand_s4.log
=== the summary ===
PASSED 5 of 9
FAILED 4 of 9
NO-VERDICT 0 of 9
SIM-ERROR 0 of 9The summary reports all four categories, including the two that are zero — for Chapter 25.3 §5's reason. A regression report that lists only the categories with members cannot tell you that NO-VERDICT has stopped being checked.
And every line carries its denominator, of 9, which is the rule from Chapter 22.7 that has now appeared in every module of Part II.
Now look at the LOG column.
3. Nine rows, five logs
rand_s1.log appears twice. So do rand_s2, rand_s3 and rand_s4.
=== rows against distinct log files ===
rows in the manifest : 9
distinct log files : 5
=== which names collide ===
rand_s1.log written by 2 runs:
sim/rand_good.vvp
sim/rand_bad.vvp
rand_s2.log written by 2 runs:
sim/rand_good.vvp
sim/rand_bad.vvp
rand_s3.log written by 2 runs:
sim/rand_good.vvp
sim/rand_bad.vvp
rand_s4.log written by 2 runs:
sim/rand_good.vvp
sim/rand_bad.vvp4. The fix, and a check that catches it
Name the file after everything that distinguishes the run:
# The identity of a run is test + seed + BINARY. A name built from fewer
# fields than that collides, and the collision overwrites evidence silently.
proc run_id {binary test seed} {
return "${test}_[file rootname [file tail $binary]]_s${seed}"
}=== with the run identity in the name ===
rows in the manifest : 9
distinct log files : 9
TEST SEED BINARY VERDICT LOG
rand 1 rand_good.vvp PASSED rand_rand_good_s1.log
rand 2 rand_good.vvp PASSED rand_rand_good_s2.log
rand 3 rand_good.vvp PASSED rand_rand_good_s3.log
rand 4 rand_good.vvp PASSED rand_rand_good_s4.log
smoke 1 good.vvp PASSED smoke_good_s1.log
rand 1 rand_bad.vvp FAILED rand_rand_bad_s1.log
rand 2 rand_bad.vvp FAILED rand_rand_bad_s2.log
rand 3 rand_bad.vvp FAILED rand_rand_bad_s3.log
rand 4 rand_bad.vvp FAILED rand_rand_bad_s4.logBut a fix is not the lesson. The lesson is that nothing was checking, and the check is three lines:
# An independent count: one log per row, or evidence was overwritten.
set logs {}
foreach r $results { lappend logs [dict get $r log] }
if {[llength [lsort -unique $logs]] != [llength $results]} {
error "[llength $results] run(s) wrote only [llength [lsort -unique $logs]] log file(s) — log names collide" \
{} {REG LOGCOLLISION}
}Proven able to fail, by putting the weak run_id back:
=== and the self-check fires when the name is too weak ===
1 / 9 run(s) wrote only 5 log file(s) — log names collide5. The second defect: a test that could not fail
The regression above is green on four seeds of rand against the good design and red on four against the bad one, which is what a regression should look like.
An earlier version of that same testbench was green on all eight.
=== the broken DUT against two benches that differ in ONE line ===
SEED stimulus gated on DUT full/empty gated on the model's count
1 TEST PASSED TEST FAILED
2 TEST PASSED TEST FAILED
3 TEST PASSED TEST FAILED
4 TEST PASSED TEST FAILED
5 TEST PASSED TEST FAILED
6 TEST PASSED TEST FAILED
7 TEST PASSED TEST FAILED
8 TEST PASSED TEST FAILED
defect caught on 0 of 8 seeds / 8 of 8 seedsThe one line:
push = do_push && !full; // gated on a DUT OUTPUT
push = do_push && (mcount < 4); // gated on the MODEL's occupancyThe rule that falls out of it:
A testbench must decide its stimulus and compute its expected values from its own model, never from a DUT output. The moment a DUT output appears in either, the DUT is being compared against itself.
That is not always easy — flow control exists precisely so a producer can respond to a consumer — and the honest resolution is that a bench which must respond to a DUT output should check that output against the model independently:
// respond to the DUT, but also check it
check("full matches the model", full, (mcount == 4));
push = do_push && (mcount < 4);Now the handshake signal is both used and verified, and a wrong full is caught by the check rather than silently steering the stimulus.
6. What this does to the number "0 of 8"
Worth drawing out, because it changes how to read any regression result.
0 of 8 seeds caught the defect looks like a coverage problem — run more seeds, run longer, widen the constraints. It is not. No number of seeds would have caught it, because the defect was not reachable through that bench at all.
So a green regression has two possible explanations and the result alone cannot distinguish them:
- The design is correct.
- The tests cannot see the bug.
The way to tell them apart is to make the regression fail on purpose: inject a defect you know about and confirm the suite reports it. If it does not, you have measured your suite rather than your design. That is what the rand_bad.vvp entry in §2's plan is doing in a regression that also runs rand_good.vvp — a deliberately broken build whose job is to stay red.
The same argument appeared in Chapter 24.7 §1: a specimen suite of only broken inputs cannot distinguish a working parser from one that refuses everything, so the good report stays in the suite. Here it is the mirror image — the broken build stays in the suite, so a suite that has stopped catching anything is visible.
7. The manifest
=== the manifest ===
test,seed,binary,verdict,log
rand,1,sim/rand_good.vvp,PASSED,logs/pre/rand_s1.log
rand,2,sim/rand_good.vvp,PASSED,logs/pre/rand_s2.log
rand,3,sim/rand_good.vvp,PASSED,logs/pre/rand_s3.log
rand,4,sim/rand_good.vvp,PASSED,logs/pre/rand_s4.log
smoke,1,sim/good.vvp,PASSED,logs/pre/smoke_s1.log
rand,1,sim/rand_bad.vvp,FAILED,logs/pre/rand_s1.log
rand,2,sim/rand_bad.vvp,FAILED,logs/pre/rand_s2.log
rand,3,sim/rand_bad.vvp,FAILED,logs/pre/rand_s3.log
rand,4,sim/rand_bad.vvp,FAILED,logs/pre/rand_s4.logThat is the pre-fix manifest, kept deliberately, because it is the artefact in which the §3 defect is visible: rand_s1.log on two rows with different verdicts. A manifest makes a collision findable — which is the argument for writing one even when a summary line would do.
Written with the quoting rules of Chapter 24.6, since an ECO-style reason field or a test name with a comma would otherwise corrupt the row. And the units belong in the column names for Chapter 24.6 §6's reason — ms is already named for its unit, which is why that column is safe and a column called duration would not be.
What a regression manifest should also carry, and this one does not:
- The git revision of the RTL and of the testbench. Without it, "which build" is a filename rather than a fact. This is Chapter 24.2 §6's provenance argument and Chapter 23.6's netlist-and-log pairing, arriving a third time.
- The tool version. A regression result belongs to a simulator release.
- A start timestamp, so two nightlies can be told apart when both are in the same directory.
- The plusargs in full, not just the seed, since a test's behaviour may depend on others.
8. The review
Before believing a regression result:
- Is the seed recorded for every run? A failure without one is not reproducible.
- Is the binary recorded, so "which RTL" has an answer?
- Does every row point at a distinct log, and is something checking that?
- Are all verdict categories reported, including the zeros — especially
NO-VERDICT? - Does the suite contain a deliberately broken build that must stay red?
- Does any testbench decide stimulus or expected values from a DUT output?
- Is the provenance there — RTL revision, testbench revision, tool version, timestamp?
- Does the driver's own exit status reflect the regression's outcome, with the Chapter 15.4 caveat that a count of failures is not a usable exit status?
That last point closes Module 25 where Chapter 25.4 started: the mechanism that reports the result is the mechanism most likely to be wrong about it, and the only defence is a second measurement that does not share its assumptions.
9. Exercises
Exercise 1. Name the six fields of a run's record and the question each answers.
Exercise 2. Why is the seed the field people omit and the one that matters most?
Exercise 3. Nine rows produced five log files. What was wrong with the name?
Exercise 4. Why is a PASSED row pointing at a FAILED log the worst kind of regression defect?
Exercise 5. Why can the runner not detect the collision by examining individual runs?
Exercise 6. Why lsort -unique rather than a dict of seen names?
Exercise 7. The bench gated push on the DUT's full. Trace why that made the defect unobservable.
Exercise 8. Both benches pass the correct DUT. Why does that matter to the argument?
Exercise 9. Why would more seeds not have caught the defect?
Exercise 10. What are the two explanations for a green regression, and how do you tell them apart?
Exercise 11. A bench genuinely must respond to a DUT handshake output. How do you keep that from becoming a tautology?
Exercise 12. Why keep the pre-fix manifest in this chapter?
Where this fits
Part of the Tcl curriculum.
