awk Part III · Module 12 · AWK in EDA Automation Pipelines
Nightly Regression Automation
This is the job nobody watches. It starts at two in the morning, reads a directory of logs that may or may not exist, and leaves behind one row of metrics and one exit status — which is everything anyone will know about the run. Every failure mode in Part III converges here, and the chapter's centrepiece is a bug that was in its own first draft: reading a log with getline has three possible outcomes, minus one is truthy and zero is falsy, and the obvious test therefore reports a log that cannot be opened as readable and an empty log as a pass.
Advanced19 min readawkPart IIIRegressiongetlineExit Status
Part III · Module 12 · Chapter 12.6 · Nightly Regression Automation
1. Why This Matters
Everything else in this track produces output a person reads. This job does not. It runs unattended, and what survives it is a metrics row and an exit status — so any state it fails to distinguish is a state nobody will ever hear about.
That makes the failure modes from Part III cumulative rather than independent. A missing log (Chapter 9.5), an empty capture that equals zero (12.1), a split argument list (12.3), a count inflated by a summary line (9.3) and a figure quoted without its denominator (11.8) can all be present in one run, and each one reports success.
2. A Run Directory That Contains the Real Cases
rm -rf run && mkdir -p run/logs
cat > run/tests.txt <<'EOF'
alu_smoke
alu_random
fifo_basic
dma_stress
cdc_sync
io_loopback
EOF
printf 'UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke...\nUVM_INFO ./env/mon.sv(40) @ 10: top.mon [XACT] ok\n--- UVM Report Summary ---\nUVM_ERROR : 0\nUVM_FATAL : 0\n' > "run/logs/alu_smoke.log"
printf 'UVM_INFO @ 0: reporter [RNTST] Running test alu_random...\nUVM_ERROR ./env/sb.sv(148) @ 24: top.sb [MISMATCH] bad\nUVM_ERROR ./env/sb.sv(148) @ 48: top.sb [MISMATCH] bad\n--- UVM Report Summary ---\nUVM_ERROR : 2\nUVM_FATAL : 0\n' > "run/logs/alu_random.log"
printf 'UVM_INFO @ 0: reporter [RNTST] Running test fifo_basic...\nUVM_FATAL ./env/drv.sv(12) @ 8: top.drv [NOCLK] clock never started\n' > "run/logs/fifo_basic.log"
: > "run/logs/dma_stress.log"
printf 'UVM_INFO @ 0: reporter [RNTST] Running test io_loopback...\nUVM_ERROR ./env/sb.sv(7) @ 11: top.sb [MISMATCH] q\n--- UVM Report Summary ---\nUVM_ERROR : 5\nUVM_FATAL : 0\n' > "run/logs/io_loopback.log"
wc -l < run/tests.txt | awk '{ printf " %d test(s) in the list\n", $1 }'
ls run/logs | awk 'END { printf " %d log file(s) on disk\n", NR }' 6 test(s) in the list
5 log file(s) on diskSix tests, five logs. The five cover a passing run, a failing run, a run that died on a UVM_FATAL before printing a summary, a log that is zero bytes, and a log whose summary claims five errors while containing one. cdc_sync has no log at all.
Those are not contrived. They are, respectively: a pass, a fail, a testbench that could not start, a job killed before it wrote anything, and a log truncated or edited after the fact.
3. getline Has Three Outcomes
Reading a per-test log means naming the file rather than passing it as an argument, which is getline < file — and its return value is the whole of this section.
awk 'BEGIN {
split("run/logs/alu_smoke.log run/logs/dma_stress.log run/logs/cdc_sync.log", f, " ")
for (i = 1; i <= 3; i++) {
r = (getline line < f[i])
printf " %-28s getline returned %2d -> %s\n", f[i], r,
(r > 0 ? "a record was read" : (r == 0 ? "END OF FILE (file exists, is empty)" : "ERROR (cannot open)"))
close(f[i])
}
}' run/logs/alu_smoke.log getline returned 1 -> a record was read
run/logs/dma_stress.log getline returned 0 -> END OF FILE (file exists, is empty)
run/logs/cdc_sync.log getline returned -1 -> ERROR (cannot open)Three values for three different situations. And now the trap:
awk 'BEGIN {
split("run/logs/alu_smoke.log run/logs/dma_stress.log run/logs/cdc_sync.log", f, " ")
for (i = 1; i <= 3; i++) {
r = (getline line < f[i]); close(f[i])
printf " %-28s (r < 0)=%-5s (r > 0)=%-5s if(r) would be %s\n", f[i],
(r < 0 ? "yes" : "no"), (r > 0 ? "yes" : "no"), (r ? "TRUE" : "false")
}
}' run/logs/alu_smoke.log (r < 0)=no (r > 0)=yes if(r) would be TRUE
run/logs/dma_stress.log (r < 0)=no (r > 0)=no if(r) would be false
run/logs/cdc_sync.log (r < 0)=yes (r > 0)=no if(r) would be TRUE4. Enumerate From the Test List
The second structural decision, and Chapter 9.5 section 6 established it: the row count comes from the test list, not from the logs.
awk 'NR == FNR { want[++n] = $1; next } END { printf " %d test(s) expected\n", n }' run/tests.txt /dev/null
ls run/logs | awk 'END { printf " %d log(s) found -> %d test(s) would be invisible to a log-driven loop\n", NR, 6 - NR }' 6 test(s) expected
5 log(s) found -> 1 test(s) would be invisible to a log-driven loopA loop over run/logs/*.log produces five rows and no indication that a sixth test exists. The test list is the only place the expectation is written down.
5. The Procedure
6. The Six States
awk 'NR == FNR { want[++n] = $1; next }
END {
for (i = 1; i <= n; i++) {
t = want[i]; f = "run/logs/" t ".log"
e = 0; fa = 0; dec = ""; nrec = 0
r = (getline l < f)
if (r < 0) { printf " %-14s NO LOG (never ran)\n", t; close(f); continue }
while (r > 0) {
nrec++
if (l ~ /^UVM_ERROR (\.|\/|@)/) e++
if (l ~ /^UVM_FATAL (\.|\/|@)/) fa++
if (l ~ /^UVM_ERROR :/) { split(l, a, ":"); dec = a[2] + 0 }
r = (getline l < f)
}
close(f)
if (nrec == 0) st = "EMPTY (started, produced nothing)"
else if (fa) st = "FATAL (incomplete)"
else if (dec != "" && dec + 0 != e) st = sprintf("INCONSISTENT (records %d, summary %d)", e, dec)
else if (dec == "") st = "NO SUMMARY (incomplete)"
else if (e) st = sprintf("FAIL (%d error(s))", e)
else st = "PASS"
printf " %-14s %s\n", t, st
}
}' run/tests.txt /dev/null alu_smoke PASS
alu_random FAIL (2 error(s))
fifo_basic FATAL (incomplete)
dma_stress EMPTY (started, produced nothing)
cdc_sync NO LOG (never ran)
io_loopback INCONSISTENT (records 1, summary 5)Six tests, six distinct states, and only two of them are about the design.
PASS and FAIL go to whoever owns the RTL. The other four go to whoever owns the flow:
FATAL (incomplete)— the testbench could not start. The log has aUVM_FATALand no summary, so no error count from it means anything.EMPTY— the job started and wrote nothing.NO LOG— the job never started.INCONSISTENT— the log's own summary disagrees with its records, so the log has been truncated or edited and neither number is trustworthy.
The order of the tests in that if chain is deliberate. nrec == 0 comes first because an empty log cannot have a summary to cross-check; fa comes before the error count because a run that died has an incomplete count; and dec == "" is a separate state from dec != e because a missing summary and a wrong summary are different faults.
7. The Status a Flow Can Act On
awk 'NR == FNR { want[++n] = $1; next }
END {
for (i = 1; i <= n; i++) {
t = want[i]; f = "run/logs/" t ".log"
e = 0; fa = 0; dec = ""; nrec = 0
r = (getline l < f)
if (r < 0) { nolog++; close(f); continue }
while (r > 0) { nrec++
if (l ~ /^UVM_ERROR (\.|\/|@)/) e++
if (l ~ /^UVM_FATAL (\.|\/|@)/) fa++
if (l ~ /^UVM_ERROR :/) { split(l, a, ":"); dec = a[2] + 0 }
r = (getline l < f) }
close(f)
if (nrec == 0) { empty++; continue }
if (fa) { fatal++; continue }
if (dec == "" || dec + 0 != e) { bad++; continue }
ran++; errs += e; if (e) failed++
}
broken = nolog + empty + fatal + bad
printf " tests=%d ran=%d passed=%d failed=%d errors=%d\n", n, ran, ran - failed, failed, errs
printf " unusable=%d (nolog=%d empty=%d fatal=%d inconsistent=%d)\n", broken, nolog+0, empty+0, fatal+0, bad+0
printf " pass rate: %d of %d that ran (%.1f%%), %d of %d attempted (%.1f%%)\n", ran-failed, ran, 100*(ran-failed)/ran, ran-failed, n, 100*(ran-failed)/n
printf " verdict: %s\n", (broken ? "FLOW BROKEN -- exit 2" : (failed ? "TESTS FAILED -- exit 1" : "CLEAN -- exit 0"))
}' run/tests.txt /dev/null tests=6 ran=2 passed=1 failed=1 errors=2
unusable=4 (nolog=1 empty=1 fatal=1 inconsistent=1)
pass rate: 1 of 2 that ran (50.0%), 1 of 6 attempted (16.7%)
verdict: FLOW BROKEN -- exit 28. Common Mistakes
if (getline line < f). -1 is truthy and 0 is falsy — section 3.
Falling through for the non-negative case. The empty log then reads as a pass, which was this chapter's own first draft.
Looping over the logs. The test whose log is absent produces no row — section 4.
Two states instead of six. Four of the six here are flow problems, not design problems.
Cross-checking before testing for emptiness. An empty log has no summary to check.
Treating a FATAL log's error count as a count. The run stopped; the count is a fragment.
Quoting the run-rate pass figure alone. It improves as more tests fail to start — section 7.
One non-zero exit status. exit 1 and exit 2 have different owners.
Reporting percentages without the counts. 1 of 2 is comparable; 50.0% is not.
9. Exercises
Exercise 1 — The per-test table
awk 'NR == FNR { want[++n] = $1; next }
END { for (i = 1; i <= n; i++) { t = want[i]; f = "run/logs/" t ".log"; e = 0; nrec = 0
r = (getline l < f)
if (r < 0) { printf "%-14s %s\n", t, "NO LOG"; close(f); continue }
while (r > 0) { nrec++; if (l ~ /^UVM_ERROR (\.|\/|@)/) e++; r = (getline l < f) }
close(f)
printf "%-14s %s\n", t, (nrec == 0 ? "EMPTY" : sprintf("%d error(s)", e)) } }' run/tests.txt /dev/nullalu_smoke 0 error(s)
alu_random 2 error(s)
fifo_basic 0 error(s)
dma_stress EMPTY
cdc_sync NO LOG
io_loopback 1 error(s)Note fifo_basic reading 0 error(s) — it died on a UVM_FATAL, which this simplified version does not check. Section 6's chain does.
Exercise 2 — Counts with both denominators
awk 'NR == FNR { want[++n] = $1; next }
END { for (i = 1; i <= n; i++) { t = want[i]; f = "run/logs/" t ".log"; nrec = 0
r = (getline l < f); if (r < 0) { close(f); continue }
while (r > 0) { nrec++; r = (getline l < f) }; close(f)
if (nrec) ran++ }
printf "%d of %d test(s) produced a non-empty log (%.1f%%)\n", ran, n, 100*ran/n }' run/tests.txt /dev/null4 of 6 test(s) produced a non-empty log (66.7%)Exercise 3 — Debug an empty log reported as a pass
awk 'BEGIN { f = "run/logs/dma_stress.log"
r = (getline l < f); close(f)
printf " getline returned %d\n", r
printf " (r < 0) catches it? %s\n", (r < 0 ? "yes" : "NO -- it is 0, not negative")
printf " if (r) treats it as? %s\n", (r ? "readable" : "unreadable") }' getline returned 0
(r < 0) catches it? NO -- it is 0, not negative
if (r) treats it as? unreadableA test for r < 0 alone lets the empty log through to the counting branch, where it finds zero errors and looks like a pass. The fix is an explicit r == 0 branch, or counting records and testing nrec == 0 after the loop.
Exercise 4 — Debug a missing log reported as readable
awk 'BEGIN { f = "run/logs/cdc_sync.log"
r = (getline l < f); close(f)
printf " getline returned %d, and if (r) is %s\n", r, (r ? "TRUE" : "false")
printf " so `if (getline l < f) { ...read... } else { print \"no log\" }` takes the READ branch\n" }' getline returned -1, and if (r) is TRUE
so `if (getline l < f) { ...read... } else { print "no log" }` takes the READ branchThe read branch then loops on a getline that keeps returning -1, so a while (getline …) written the same way does not terminate — which is the other reason to compare against zero explicitly.
Exercise 5 — Engineering application
The nightly reduction, with the metrics row written in a form that is comparable between runs.
# nightly.awk — reduce a regression run to one metrics row.
#
# Usage: awk -v run=2026-10-12 -f nightly.awk run/tests.txt
# Output: one key=value row on stdout, per-test detail on stderr
# Exit: 0 clean 1 tests failed 2 the flow broke (nothing can be concluded)
#
# Enumerates from the TEST LIST, classifies on all three getline outcomes, and
# cross-checks each log against its own UVM summary.
{ want[++n] = $1 }
END {
if (n == 0) { print "ERROR: empty test list" > "/dev/stderr"; exit 2 }
for (i = 1; i <= n; i++) {
t = want[i]; f = "run/logs/" t ".log"
e = 0; fa = 0; dec = ""; nrec = 0
r = (getline l < f)
if (r < 0) { close(f); nolog++; printf "%-14s NO LOG\n", t > "/dev/stderr"; continue }
while (r > 0) {
nrec++
if (l ~ /^UVM_ERROR (\.|\/|@)/) e++
if (l ~ /^UVM_FATAL (\.|\/|@)/) fa++
if (l ~ /^UVM_ERROR :/) { split(l, a, ":"); dec = a[2] + 0 }
r = (getline l < f)
}
close(f)
if (nrec == 0) { empty++; printf "%-14s EMPTY\n", t > "/dev/stderr"; continue }
if (fa) { fatal++; printf "%-14s FATAL\n", t > "/dev/stderr"; continue }
if (dec == "") { nosum++; printf "%-14s NO SUMMARY\n", t > "/dev/stderr"; continue }
if (dec + 0 != e) { incons++; printf "%-14s INCONSISTENT records=%d summary=%d\n", t, e, dec > "/dev/stderr"; continue }
ran++; errs += e
if (e) { failed++; printf "%-14s FAIL %d error(s)\n", t, e > "/dev/stderr" }
else { printf "%-14s PASS\n", t > "/dev/stderr" }
}
unusable = nolog + empty + fatal + nosum + incons
# one row, fixed fields, counts not percentages -- Chapter 11.8
printf "run=%s tests=%d ran=%d passed=%d failed=%d errors=%d unusable=%d nolog=%d empty=%d fatal=%d nosummary=%d inconsistent=%d\n",
(run == "" ? "unknown" : run), n, ran + 0, ran - failed, failed + 0, errs + 0,
unusable + 0, nolog + 0, empty + 0, fatal + 0, nosum + 0, incons + 0
if (unusable) exit 2
if (failed) exit 1
}awk -v run=2026-10-12 -f nightly.awk run/tests.txt 2>detail.txt
echo "status: $?"
cat detail.txtrun=2026-10-12 tests=6 ran=2 passed=1 failed=1 errors=2 unusable=4 nolog=1 empty=1 fatal=1 nosummary=0 inconsistent=1
status: 2
alu_smoke PASS
alu_random FAIL 2 error(s)
fifo_basic FATAL
dma_stress EMPTY
cdc_sync NO LOG
io_loopback INCONSISTENT records=1 summary=5One row on stdout for a metrics store, the per-test detail on stderr for a human, and status 2 because four tests produced nothing usable.
The row has fixed fields in a fixed order with no percentages, which is what makes two nights comparable:
awk -v run=2026-10-12 -f nightly.awk run/tests.txt 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) { split($i, kv, "="); printf " %-13s %s\n", kv[1], kv[2] } }' run 2026-10-12
tests 6
ran 2
passed 1
failed 1
errors 2
unusable 4
nolog 1
empty 1
fatal 1
nosummary 0
inconsistent 1Twelve fields, every one a count. Any ratio a reader wants can be computed from them; none is computable from a ratio.
Exercise 6 — Challenge: compare a two-state reduction against the six-state one
Build the reduction almost everyone writes — a loop over the logs, errors counted, pass or fail — and compare what it says about this night against section 7.
for f in run/logs/*.log; do
e=$(awk '/^UVM_ERROR (\.|\/|@)/ { n++ } END { print n+0 }' "$f")
printf " %-24s %s\n" "$(basename "$f")" "$([ "$e" -eq 0 ] && echo PASS || echo "FAIL ($e)")"
done alu_random.log FAIL (2)
alu_smoke.log PASS
dma_stress.log PASS
fifo_basic.log PASS
io_loopback.log FAIL (1)n=0; p=0
for f in run/logs/*.log; do
e=$(awk '/^UVM_ERROR (\.|\/|@)/ { n++ } END { print n+0 }' "$f")
n=$((n+1)); [ "$e" -eq 0 ] && p=$((p+1))
done
awk -v n=$n -v p=$p 'BEGIN { printf " %d of %d passing (%.1f%%)\n", p, n, 100*p/n }' 3 of 5 passing (60.0%)Sixty per cent passing. Section 7's reading of the same night was 1 of 6 attempted — 16.7% — with the verdict FLOW BROKEN.
Four differences, each a state the two-state version cannot represent:
cdc_syncis absent from the output entirely, because the loop iterates over logs rather than over the test list. Five rows for six tests, and nothing says a sixth exists.dma_stressreadsPASS. Its log is zero bytes, so the pattern matched nothing and the count is zero. The test started and produced nothing.fifo_basicreadsPASS. It died on aUVM_FATALbefore the testbench started, andUVM_FATALis notUVM_ERROR, so no error was counted.io_loopbackreadsFAIL (1), which is right about the verdict and wrong to be trusted. Its own summary claims five errors against the one record present:
awk '/^UVM_ERROR/ { printf " [%s]\n", substr($0, 1, 43) }' run/logs/io_loopback.log [UVM_ERROR ./env/sb.sv(7) @ 11: top.sb [MISM]
[UVM_ERROR : 5]The log has been truncated or edited, so 1 is not the error count — it is the number of records that survived. Section 6 classifies it INCONSISTENT and refuses to use either number.
So of five logs the two-state reduction gets two verdicts wrong, trusts a third it should refuse, and never mentions the sixth test. The arithmetic is correct throughout; the categories are not there.
That is Part III's conclusion in one comparison: a reduction reports a state it cannot represent as whichever of its available outcomes is nearer to hand, and PASS is nearly always the nearer one. Section 6's six states exist because each of the four non-design states has a different owner and a different fix, and section 7's two denominators exist because 60.0% and 16.7% are both honest descriptions of the same night.
10. Summary
- This job is unwatched, so a state it cannot distinguish is a state nobody hears about.
getline < freturns 1, 0 or −1 — a record, end of file, or cannot open.−1is truthy and0is falsy, soif (getline …)calls a missing log readable and an empty one unreadable.- A
while (getline …)written that way does not terminate on a missing file. - This chapter's own first draft classified the empty log as
PASSby testing only< 0and falling through. - Enumerate from the test list. A log-driven loop gave five rows for six tests.
- Six states, not two — and four of them belong to the flow, not the design.
- Order the classification: emptiness before the summary cross-check,
FATALbefore the error count, a missing summary separate from a wrong one. - Two pass rates, both printed with denominators. The run-rate figure improves as more tests fail to start.
- Grade the exit status.
1means tests failed;2means nothing can be concluded. - A two-state reduction reported this night as 60% passing against a true 1 of 6 attempted, getting two verdicts wrong and omitting a test.
That closes Module 12 and with it Part III. Across Modules 9 to 12 the same conclusion arrived from logs, timing reports, synthesis and coverage reports, and now from the flow itself: a reduction is only as good as the states it can distinguish, and every check worth having is made on the input rather than on the shape of the answer.
Where this fits
Part of the awk curriculum.
