Skip to content
VLSI Mentor

Perl Part II · Module 29 · Practical VLSI Automation Projects

Project: Maintaining a Legacy Flow Script

Most Perl an engineer touches at work was written by someone else, years ago, without tests, and other tools depend on exactly what it prints. The job is rarely to rewrite it. It is to change one thing and leave everything else as it was. This last project does that to an old regression-report script: it records everything the script does, measures why the obvious clean-ups are behaviour changes, makes a clarity edit that provably changes nothing, then predicts the exact diff of the requested feature before writing it — and checks that the change is that diff, line for line.

Advanced24 min readPerlPart IIProjectsLegacy Code

Part II · Module 29 · Chapter 29.7 · Project: Maintaining a Legacy Flow Script

1. The Brief

This project's deliverable is not a new tool. It is a change to an existing one, with the evidence that the change is safe.

The requestRuns whose simulation died before finishing are being reported as PASS. Report them as CRASH, and count them as failures.
The scriptlegacy.pl, an old report script that other tools read. Its header says do not change the output format, the dashboard reads it!!
The ruleNothing a caller can observe may change, except what the request asks for
Evidence requireda baseline of everything observable; an empty diff after any tidying; a written prediction of the feature's diff; proof that the actual diff equals the prediction

The request is Chapter 21.2's finding, arriving as a bug report against code that predates it: a log with no errors and no finish marker is a run that stopped, not a run that passed.

The method assembles three earlier chapters into one change: reading an inherited script (28.1), writing down what it actually does (28.2), and refactoring a script you must not break (27.6). This project applies them to a requested behaviour change, and adds a written prediction of that change's exact diff, checked line for line.

2. The Script As Found

The script below was written for this chapter in the style of an old flow script, and it is not anyone's real code. Every habit in it is common in inherited Perl: no use strict, global variables everywhere, bareword filehandles, two-argument open, output in hash order, and a file written as a side effect.

Azvya Education Pvt. Ltd.VLSI Mentor
legacy.pl
#!/usr/bin/perl
#
# regress_report -- summarise the nightly sim logs
#
# usage: legacy.pl [-q] logdir/sim.log ...
#   -q   quiet, only print the totals
#
# (c) the verification team.  do not change the output format,
# the dashboard reads it!!
#

$quiet = 0;
if ($ARGV[0] eq "-q") { $quiet = 1; shift; }

$total = 0; $npass = 0; $nfail = 0; $nfat = 0;

foreach $f (@ARGV) {
    open(F, $f) || die "cant open $f\n";
    $f =~ /([^\/]+)\/[^\/]*$/;
    $t = $1;                              # the run name is the directory
    $errs = 0; $fat = 0; $warns = 0;
    while (<F>) {
        if (/^UVM_(ERROR|FATAL) .*\[/) {
            $errs++;
            $fat++ if $1 eq "FATAL";
        }
        if (/^UVM_WARNING .*\[/) { $warns++; }
        if (/^UVM_ERROR .*\[(\w+)\]/ && !$first{$t}) {
            $first{$t} = $1;
        }
    }
    close(F);
    $total++;
    if ($fat) {
        $status{$t} = "FATAL"; $nfat++;
    } elsif ($errs) {
        $status{$t} = "FAIL"; $nfail++;
    } else {
        $status{$t} = "PASS"; $npass++;
    }
    $count{$t} = $errs;
    $wcount{$t} = $warns;
}

if (!$quiet) {
    print "REGRESSION REPORT\n";
    print "-----------------\n";
    foreach $t (keys %status) {
        printf("%-14s %-6s %3d %3d %s\n", $t, $status{$t}, $count{$t},
               $wcount{$t}, $first{$t});
    }
}
print "total $total pass $npass fail $nfail fatal $nfat\n";

# the dashboard picks this up
open(O, ">last_run.txt");
foreach $t (keys %status) {
    print O "$t $status{$t}\n" if $status{$t} ne "PASS";
}
close(O);

exit($nfail + $nfat > 0 ? 1 : 0);

Read it in the order its callers experience it — arguments, the main loop, everything it emits, the exit — and write down every observable behaviour, because those are what must not change:

  • Two ways to call it: with -q it prints only the totals line.
  • STDOUT: a header, one line per run (name, status, error count, warning count, first error ID) and a totals line.
  • A file: last_run.txt, written into the current directory, listing every run that is not PASS. The comment says the dashboard picks it up.
  • The exit status: 1 if any run failed or was fatal.
  • STDERR: nothing, today.

Two quirks are visible once the code is read slowly, and both are behaviour that callers may depend on. The errors column counts fatal lines too — the first pattern matches UVM_ERROR or UVM_FATAL. And the second pattern, which looks almost the same, matches errors only.

3. The Corpus

A baseline is only as good as the inputs it runs on. The corpus has to exercise every branch the change will touch and every branch it might touch by accident.

Azvya Education Pvt. Ltd.VLSI Mentor
tb/tb_corpus.v
// tb_corpus.v -- the logs the legacy script reads: UVM-SHAPED lines (not
// UVM, which cannot run here), real simulator output, one run per
// +TEST/+SEED. What a run does depends only on its seed:
//   seed 1, 2   pass (one warning)
//   seed 3      two errors
//   seed 4      an error, then a fatal
//   seed 5      dies part-way: no report summary
`timescale 1ns/1ps

module tb_corpus;
  integer seed, n_err, n_warn, n_fatal;
  reg [8*16-1:0] test;

  task summary;
    begin
      $display("--- UVM Report Summary ---");
      $display("UVM_WARNING :%5d", n_warn);
      $display("UVM_ERROR :%5d", n_err);
      $display("UVM_FATAL :%5d", n_fatal);
    end
  endtask

  initial begin
    n_err = 0; n_warn = 0; n_fatal = 0;
    if (!$value$plusargs("TEST=%s", test)) test = "smoke";
    if (!$value$plusargs("SEED=%d", seed)) seed = 1;
    $display("UVM_INFO @ 0: reporter [RNTST] Running test %0s...", test);
    #10 $display("UVM_WARNING tb(0) @ %0t: env [LATE] response 2 cycles late", $time);
    n_warn = n_warn + 1;
    if (seed == 3) begin
      #10 $display("UVM_ERROR tb(0) @ %0t: env.scbd [SCBD] data mismatch exp=%0d got=0", $time, seed * 7);
      #10 $display("UVM_ERROR tb(0) @ %0t: env.scbd [SCBD] data mismatch exp=%0d got=0", $time, seed * 9);
      n_err = n_err + 2;
    end
    if (seed == 4) begin
      #10 $display("UVM_ERROR tb(0) @ %0t: env.axi [AXI] burst crossed a 4KB boundary", $time);
      #10 $display("UVM_FATAL tb(0) @ %0t: env.axi [AXI] cannot continue", $time);
      n_err = n_err + 1; n_fatal = n_fatal + 1;
    end
    if (seed == 5) begin
      #10 $display("UVM_INFO tb(0) @ %0t: env [PHASE] main_phase executing", $time);
      $finish;                         // dies: no summary
    end
    #10 summary;
    $finish;
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
make_corpus.sh
#!/bin/bash
# make_corpus.sh DIR -- simulate 4 tests x 5 seeds; DIR/<test>_s<seed>/sim.log
set -u
out=${1:?usage: make_corpus.sh DIR}
iverilog -o corpus.vvp tb/tb_corpus.v || exit 1
for test in smoke burst csr fifo; do
    for seed in 1 2 3 4 5; do
        mkdir -p "$out/${test}_s$seed"
        vvp -n corpus.vvp +TEST=$test +SEED=$seed > "$out/${test}_s$seed/sim.log"
    done
done
rm -f corpus.vvp

Four tests, five seeds each, and each seed is one kind of run: two passes, two errors, an error followed by a fatal, and a run that dies before its report summary. The logs are real Icarus output, UVM-shaped as in Module 21. The seed-5 runs are the ones the request is about, so they are in the corpus; the fatal runs are what the two patterns disagree on, so they are in it too.

4. Making The Output Comparable

The method is to run the script before and after a change and compare. That only works if the script repeats itself.

Azvya Education Pvt. Ltd.VLSI Mentor
order.sh
#!/bin/bash
# order.sh -- run the unchanged script five times: does it repeat itself?
# (Only the verdict is printed: HOW MANY orders appear is itself random.)
bash make_corpus.sh corpus > /dev/null || exit 1
verdict() { [ "$1" -eq 1 ] && echo "one output" || echo "more than one different output"; }
raw=$(for i in 1 2 3 4 5; do perl legacy.pl corpus/*/sim.log | cksum; done | sort -u | wc -l)
srt=$(for i in 1 2 3 4 5; do perl legacy.pl corpus/*/sim.log | sort | cksum; done | sort -u | wc -l)
echo "five runs, output as printed:  $(verdict $raw)"
echo "five runs, each output sorted: $(verdict $srt)"
rm -f last_run.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
five runs, output as printed:  more than one different output
five runs, each output sorted: one output

The unchanged script, on unchanged input, does not repeat itself: it lists runs with foreach $t (keys %status), and hash order changes from one process to the next (Chapter 6.6). A naive diff against a baseline would report differences on a change that changed nothing, and an engineer who sees a noisy diff every time learns to stop reading it.

So the snapshot sorts every output before recording it. That is safe for one reason worth stating: since the order was random in production, no caller can have relied on any particular order. The snapshot then records everything section 2 listed — both ways of calling the script, the exit status, STDOUT, STDERR, and every file the script leaves in its working directory, so a change that stops writing last_run.txt, or starts writing something new, shows up.

Azvya Education Pvt. Ltd.VLSI Mentor
snapshot.sh
#!/bin/bash
# snapshot.sh SCRIPT CORPUS -- everything a caller can observe of SCRIPT,
# for each way it is called: exit status, STDOUT, STDERR, and every file it
# leaves in its working directory. Lines are SORTED, because this script's
# output order changes from run to run (see order.sh).
script=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
corpus=$(cd "$2" && pwd)
for args in "" "-q"; do
    work=$(mktemp -d)                      # the script's working directory
    cap=$(mktemp -d)                       # what it printed
    ( cd "$work" && perl "$script" $args "$corpus"/*/sim.log > "$cap/stdout" 2> "$cap/stderr" )
    status=$?
    echo "### legacy.pl ${args:-(no options)}"
    echo "exit $status"
    echo "--- stdout"; sort "$cap/stdout"
    echo "--- stderr"; sort "$cap/stderr"
    for f in $(cd "$work" && ls -A | sort); do
        echo "--- file $f"; sort "$work/$f"
    done
    rm -rf "$work" "$cap"
done

5. The Method

Figure 29.7.1 — Two edits, each proved before the next

An eight-step method: list every observable behaviour; make the output comparable by sorting what is random; record a baseline of exit status, output, errors and files; make a clarity edit of comments and sorting only; prove the clarity edit changed nothing; write down the predicted diff of the feature; make the smallest edit; prove the actual diff equals the prediction.Change one thing1List the behaviourevery caller, every output2Make it comparablesort what was random3Record a baselineexit, output, files4Clarity editcomments and sorting5Prove: no changethe diff is empty6Predict the diffbefore the code7Smallest editone behaviour8Prove: as predictedline for line
Everything observable is listed and recorded first, made comparable by sorting what was random. The clarity edit comes alone and must produce an empty diff. Only then is the feature's diff predicted, in writing, before the code is touched; the smallest edit is made; and the actual diff is checked against the prediction, line for line.

Two edits, never one. A clarity edit and a behaviour change made together produce one diff, and nobody can tell which lines came from which. Made separately, the first must produce no diff at all, and the second must produce exactly the predicted one.

6. Three Clean-Ups That Are Behaviour Changes

Everyone who opens a script like this wants to make the same three improvements first. Each is measured here against the baseline.

Azvya Education Pvt. Ltd.VLSI Mentor
tidyups.sh
#!/bin/bash
# tidyups.sh -- the three clean-ups everyone wants to make first, each
# measured against the baseline of the script as found.
bash make_corpus.sh corpus > /dev/null || exit 1
bash snapshot.sh legacy.pl corpus > baseline.txt
mkdir -p try

echo "== A. add 'use strict;'"
perl -pe 's/^(\$quiet = 0;)/use strict;\n$1/' legacy.pl > try/strict.pl
errors=$(perl -c try/strict.pl 2>&1 | grep -c 'Global symbol')
names=$(perl -c try/strict.pl 2>&1 | grep -o 'Global symbol "[^"]*"' | sort -u | wc -l | tr -d ' ')
echo "  it no longer compiles: $errors errors, naming $names different variables"

echo "== B. turn on warnings (-w on the #! line)"
perl -pe 's{^#!/usr/bin/perl$}{#!/usr/bin/perl -w}' legacy.pl > try/warn.pl
bash snapshot.sh try/warn.pl corpus > warn.txt
diff baseline.txt warn.txt | grep '^[<>]' | sed 's/"[a-z]*_s[0-9]"/"RUN"/; s# at .*/try/# at try/#' |
    sort | uniq -c | sed 's/^/ /'

echo "== C. make the two message patterns one (they look the same)"
perl -pe 's{^(\s+if \(/)\^UVM_\(ERROR\|FATAL\) \.\*\\\[/\)}{$1^UVM_ERROR .*\\[(\\w+)\\]/)}' legacy.pl > try/onepat.pl
grep -n 'if (/^UVM_ERROR' try/onepat.pl | sed 's/^/  /'
bash snapshot.sh try/onepat.pl corpus > onepat.txt
diff baseline.txt onepat.txt | grep '^[<>]' > onepat.diff
echo "  $(grep -c . onepat.diff) changed lines; the first ten:"
head -10 onepat.diff | sed 's/^/    /'
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
== A. add 'use strict;'
  it no longer compiles: 60 errors, naming 14 different variables
== B. turn on warnings (-w on the #! line)
   12 > Use of uninitialized value $first{"RUN"} in printf at try/warn.pl line 50.
== C. make the two message patterns one (they look the same)
  23:        if (/^UVM_ERROR .*\[(\w+)\]/) {
  28:        if (/^UVM_ERROR .*\[(\w+)\]/ && !$first{$t}) {
  28 changed lines; the first ten:
    < burst_s4       FATAL    2   1 AXI
    > burst_s4       FAIL     1   1 AXI
    < csr_s4         FATAL    2   1 AXI
    > csr_s4         FAIL     1   1 AXI
    < fifo_s4        FATAL    2   1 AXI
    > fifo_s4        FAIL     1   1 AXI
    < smoke_s4       FATAL    2   1 AXI
    > smoke_s4       FAIL     1   1 AXI
    < total 20 pass 12 fail 4 fatal 4
    > total 20 pass 12 fail 8 fatal 0

A. use strict stops it compiling. Sixty errors naming fourteen variables, because every variable in the script is a global — exactly what Chapter 1.10's strict mode exists to refuse. Strictness is the right goal, and converting the script to it is a real piece of work — every variable needs a declaration at the right scope — with its own baseline and its own empty diff. It is not something to do in passing while fixing a bug.

B. Warnings change STDERR. Twelve new lines of the kind Chapter 1.11 describes, one for every run with no first error, because printf is handed an undefined $first{...}. STDOUT is unchanged — but STDERR is observable too, and flows exist that treat any STDERR output as failure, or mail it to someone. Turning warnings on is a behaviour change, and a good one to make on purpose.

C. The two patterns are not the same. Replacing the counting pattern with the first-ID pattern "because they do the same thing" changes 28 lines: every fatal run becomes FAIL, with half its errors, and the totals line reports no fatal runs at all. The two patterns differed in one alternation, (ERROR|FATAL) versus ERROR, and that difference was carrying the script's whole notion of a fatal run. A difference between two copies of almost-identical code is a question to answer, not a typo to fix.

7. The Clarity Edit

"Leave the file a little clearer" means something specific here: make the next reader faster, and change nothing they can observe. This edit writes down what the script actually does — including the quirk and the bug — marks the two patterns as different on purpose, and sorts the run lines.

Azvya Education Pvt. Ltd.VLSI Mentor
clarity.patch
--- legacy.pl
+++ legacy.pl
@@ -8,6 +8,18 @@
 # (c) the verification team.  do not change the output format,
 # the dashboard reads it!!
 #
+# WHAT IT ACTUALLY DOES -- written down 2026-10-13 from a baseline over
+# corpus/ (snapshot.sh), before anything here was changed:
+#   stdout   a header, one line per run (name, status, errors, warnings,
+#            first error ID), then "total N pass N fail N fatal N"
+#   -q       only the totals line
+#   file     last_run.txt in the CURRENT directory: every run not PASS
+#   exit     1 if any run is FAIL or FATAL, else 0
+#   quirks   the errors column counts UVM_FATAL lines too;
+#            a log that stops before its report summary counts as PASS;
+#            runs were listed in hash order, different on every run --
+#            they are sorted now, which no reader could have relied on
+#
 
 $quiet = 0;
 if ($ARGV[0] eq "-q") { $quiet = 1; shift; }
@@ -20,6 +32,8 @@
     $t = $1;                              # the run name is the directory
     $errs = 0; $fat = 0; $warns = 0;
     while (<F>) {
+        # NOTE the two message patterns below are NOT the same, on purpose:
+        # this one counts errors AND fatals; the first-ID one takes errors only
         if (/^UVM_(ERROR|FATAL) .*\[/) {
             $errs++;
             $fat++ if $1 eq "FATAL";
@@ -45,7 +59,7 @@
 if (!$quiet) {
     print "REGRESSION REPORT\n";
     print "-----------------\n";
-    foreach $t (keys %status) {
+    foreach $t (sort keys %status) {
         printf("%-14s %-6s %3d %3d %s\n", $t, $status{$t}, $count{$t},
                $wcount{$t}, $first{$t});
     }
@@ -54,7 +68,7 @@
 
 # the dashboard picks this up
 open(O, ">last_run.txt");
-foreach $t (keys %status) {
+foreach $t (sort keys %status) {
     print O "$t $status{$t}\n" if $status{$t} ne "PASS";
 }
 close(O);

The sort is the only line that touches behaviour, and section 4 argued why it is safe: the old order was random, so no caller relied on it. Now the order is the same on every run, and the next person who maintains this script can compare outputs without sorting them first.

The comment block is the most valuable part of the edit. It records the side-effect file, both ways of calling the script, the exit rule and the quirks as observed, with the date and the corpus they were observed on — so they are facts with a source, not folklore.

8. Predict The Diff, Then Make The Change

Before touching the code, write down the exact diff the request should produce. Working it out forces every consequence into the open: the four seed-5 run lines change status; the totals move four runs from pass to fail; last_run.txt gains four lines — in both calling modes, because -q writes the file too, which is easy to forget until the prediction has to list it.

Azvya Education Pvt. Ltd.VLSI Mentor
prediction.txt
# prediction.txt -- every line expected to leave (-) or join (+) the
# snapshot when crash.patch is applied, written BEFORE applying it.
# Each line ends in $ so that a trailing space can be seen.
-burst_s5       PASS     0   1 $
-csr_s5         PASS     0   1 $
-fifo_s5        PASS     0   1 $
-smoke_s5       PASS     0   1 $
-total 20 pass 12 fail 4 fatal 4$
-total 20 pass 12 fail 4 fatal 4$
+burst_s5       CRASH    0   1 $
+csr_s5         CRASH    0   1 $
+fifo_s5        CRASH    0   1 $
+smoke_s5       CRASH    0   1 $
+total 20 pass 8 fail 8 fatal 4$
+total 20 pass 8 fail 8 fatal 4$
+burst_s5 CRASH$
+csr_s5 CRASH$
+fifo_s5 CRASH$
+smoke_s5 CRASH$
+burst_s5 CRASH$
+csr_s5 CRASH$
+fifo_s5 CRASH$
+smoke_s5 CRASH$

Every line ends in $, as cat -e shows line ends. My first prediction did not, and it failed: typed by hand, it had lost the trailing space that printf leaves after the empty first-ID field. A trailing space is invisible in an editor and real in a byte comparison, so the comparison makes it visible instead of ignoring it.

The change itself is small. It reads the summary marker, decides CRASH before anything else, counts it under fail so that the totals line keeps its exact shape, and updates the comment block from section 7, because the behaviour it documents has changed.

Azvya Education Pvt. Ltd.VLSI Mentor
crash.patch
--- legacy.pl
+++ legacy.pl
@@ -14,9 +14,10 @@
 #            first error ID), then "total N pass N fail N fatal N"
 #   -q       only the totals line
 #   file     last_run.txt in the CURRENT directory: every run not PASS
-#   exit     1 if any run is FAIL or FATAL, else 0
+#   exit     1 if any run is FAIL, FATAL or CRASH, else 0
 #   quirks   the errors column counts UVM_FATAL lines too;
-#            a log that stops before its report summary counts as PASS;
+#            a log that stops before its report summary is CRASH, and
+#            is counted under "fail" (until 2026-10-13 it counted as PASS);
 #            runs were listed in hash order, different on every run --
 #            they are sorted now, which no reader could have relied on
 #
@@ -30,7 +31,7 @@
     open(F, $f) || die "cant open $f\n";
     $f =~ /([^\/]+)\/[^\/]*$/;
     $t = $1;                              # the run name is the directory
-    $errs = 0; $fat = 0; $warns = 0;
+    $errs = 0; $fat = 0; $warns = 0; $summary = 0;
     while (<F>) {
         # NOTE the two message patterns below are NOT the same, on purpose:
         # this one counts errors AND fatals; the first-ID one takes errors only
@@ -39,13 +40,16 @@
             $fat++ if $1 eq "FATAL";
         }
         if (/^UVM_WARNING .*\[/) { $warns++; }
+        $summary = 1 if /^--- UVM Report Summary ---/;
         if (/^UVM_ERROR .*\[(\w+)\]/ && !$first{$t}) {
             $first{$t} = $1;
         }
     }
     close(F);
     $total++;
-    if ($fat) {
+    if (!$summary) {                      # it never finished: not a pass
+        $status{$t} = "CRASH"; $nfail++;  # counted with the failures
+    } elsif ($fat) {
         $status{$t} = "FATAL"; $nfat++;
     } elsif ($errs) {
         $status{$t} = "FAIL"; $nfail++;

One part of the request is a format change that no snapshot can approve: CRASH is a new value in the status column and in last_run.txt. The totals line keeps its shape on purpose, but whoever owns the dashboard that reads last_run.txt must confirm it accepts a status it has never seen. A maintainer names the consumers that could not be checked rather than assuming they are fine.

check.sh compares the lines that changed between two snapshots with the prediction, and maintain.sh runs the whole method:

Azvya Education Pvt. Ltd.VLSI Mentor
check.sh
#!/bin/bash
# check.sh BEFORE AFTER PREDICTION -- do the lines that changed between two
# snapshots match, exactly, the lines predicted before the edit was made?
# Every line gets a $ at its end, as `cat -e` shows it, so that a trailing
# space is part of the comparison AND visible in any message.
tmp=$(mktemp -d)
diff "$1" "$2" | grep '^[<>]' | sed 's/^< /-/; s/^> /+/; s/$/$/' | sort > "$tmp/actual"
grep -v '^#' "$3" | sort > "$tmp/wanted"
if cmp -s "$tmp/actual" "$tmp/wanted"; then
    echo "  the change is exactly the prediction: $(grep -c . "$tmp/wanted") line(s)"
    status=0
else
    echo "  the change is NOT the prediction:"
    diff "$tmp/wanted" "$tmp/actual" | grep '^[<>]' |
        sed 's/^< /    predicted, did not happen: /; s/^> /    happened, not predicted:   /'
    status=1
fi
rm -rf "$tmp"
exit $status
Azvya Education Pvt. Ltd.VLSI Mentor
maintain.sh
#!/bin/bash
# maintain.sh -- baseline, the clarity edit, then the one feature, each
# step proved against the snapshot before the next one starts.
bash make_corpus.sh corpus > /dev/null || exit 1
mkdir -p work && cp legacy.pl work/legacy.pl

echo "== 1. baseline of the script as found"
bash snapshot.sh work/legacy.pl corpus > baseline.txt
grep -c . baseline.txt | sed 's/^/  lines recorded: /'

echo "== 2. the clarity edit"
patch -s work/legacy.pl < clarity.patch || exit 1
bash snapshot.sh work/legacy.pl corpus > after_clarity.txt
if diff baseline.txt after_clarity.txt > /dev/null; then
    echo "  no observable change"
else
    echo "  CHANGED:"; diff baseline.txt after_clarity.txt | sed 's/^/    /'; exit 1
fi

echo "== 3. the feature: a run that never finished is CRASH, not PASS"
patch -s work/legacy.pl < crash.patch || exit 1
bash snapshot.sh work/legacy.pl corpus > after_crash.txt
bash check.sh after_clarity.txt after_crash.txt prediction.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
== 1. baseline of the script as found
  lines recorded: 50
== 2. the clarity edit
  no observable change
== 3. the feature: a run that never finished is CRASH, not PASS
  the change is exactly the prediction: 20 line(s)

The baseline, an empty diff after the clarity edit, and a feature diff that is exactly the twenty predicted lines. That last line is a stronger statement than "the tests pass". It says the change did what was intended, everything it did was intended, and every caller-visible effect was listed in advance.

9. Proving The Check Can Fail

mutate.sh makes the same change five wrong ways, each a plausible slip, and asks the check about each.

Azvya Education Pvt. Ltd.VLSI Mentor
mutate.sh
#!/bin/bash
# mutate.sh -- five WRONG ways to make the change. Each is crash.patch plus
# one more edit; check.sh must refuse every one whose effect differs from
# the prediction.
bash make_corpus.sh corpus > /dev/null || exit 1
mkdir -p m
cp legacy.pl m/clear.pl && patch -s m/clear.pl < clarity.patch
bash snapshot.sh m/clear.pl corpus > m/before.txt
wrong() {   # LABEL PERL-SUBSTITUTION
    cp m/clear.pl m/try.pl && patch -s m/try.pl < crash.patch
    perl -0pi -e "$2" m/try.pl
    if ! perl -c m/try.pl > /dev/null 2>&1; then printf '%-46s INVALID\n' "$1"; return; fi
    bash snapshot.sh m/try.pl corpus > m/after.txt
    if bash check.sh m/before.txt m/after.txt prediction.txt > /dev/null; then
        printf '%-46s ACCEPTED by the check\n' "$1"
    else
        printf '%-46s REFUSED (%s line(s) differ from the prediction)\n' "$1" \
            "$(bash check.sh m/before.txt m/after.txt prediction.txt | grep -c 'predicted\|happened')"
    fi
}
wrong "1 crash = no \$finish line"            's/\$summary = 1 if \/\^--- UVM Report Summary ---\//\$summary = 1 if \/\\\$finish called\//'
wrong "2 a new 'crash N' field in the totals" 's/(\$status\{\$t\} = "CRASH"; )\$nfail\+\+;/$1\$ncrash++;/; s/fatal \$nfat\\n"/fatal \$nfat crash \$ncrash\\n"/'
wrong "3 CRASH counted under nothing"         's/(\$status\{\$t\} = "CRASH"; )\$nfail\+\+;/$1/'
wrong "4 the pattern clean-up slipped in too" 's{/\^UVM_\(ERROR\|FATAL\) \.\*\\\[/}{/^UVM_ERROR .*\\[(\\w+)\\]/}'
wrong "5 crash tested after fatal"            's/    if \(!\$summary\) \{(.*?)\n    \} elsif \(\$fat\) \{(.*?)\n    \} elsif/    if (\$fat) {$2\n    } elsif (!\$summary) {$1\n    } elsif/s'
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
1 crash = no $finish line                      REFUSED (20 line(s) differ from the prediction)
2 a new 'crash N' field in the totals          REFUSED (4 line(s) differ from the prediction)
3 CRASH counted under nothing                  REFUSED (4 line(s) differ from the prediction)
4 the pattern clean-up slipped in too          REFUSED (28 line(s) differ from the prediction)
5 crash tested after fatal                     ACCEPTED by the check

Four are refused, each for a reason worth knowing:

  • "No $finish line" changes nothing at all. Icarus prints its own $finish called line even for a run that died, so that test never fires. All twenty predicted lines fail to happen. Chapter 21.2 explains why the summary block, not the simulator's finish line, is the evidence.
  • A new crash N field changes the totals line's shape — exactly what the header warned against.
  • Counting a crash under nothing leaves the totals summing to less than the total.
  • Slipping the pattern clean-up in brings section 6's 28 lines with it, and the check lists them.

The fifth is accepted, and it should be. Testing for a crash after testing for a fatal gives a different answer only for a run that logs a fatal and then dies. The corpus has no such run, so on this corpus the two orders are indistinguishable, and the check cannot prefer one. A characterisation check proves that the change is right on the corpus, and the corpus decides what can be proved. The fix belongs in the corpus — exercise 1.

10. What This Method Does Not Do

  • It cannot see what the corpus does not exercise. Section 9's accepted mutant is the proof. Adding the run you are worried about to the corpus is part of the change.
  • It cannot vouch for output order once the snapshot sorts. The sort was argued safe here, from the fact that the old order was random; in a script with a deterministic order, the snapshot must keep it.
  • It cannot approve a new value that a downstream reader might reject. That needs the reader's owner.
  • It is not a test suite. The snapshot and the prediction prove one change. Keeping the corpus, the snapshot script and a current baseline beside the script turns them into a regression check for the next change, which is the cheapest test suite a legacy script will ever get.
  • It does not make the script modern. use strict, warnings and real tests are each worth doing — as separate, deliberate changes, each with its own baseline and its own empty or predicted diff.

11. Exercises

1. Close the gap. Add a sixth seed that logs a fatal and then dies before its summary. Update the prediction, rerun mutate.sh, and show that mutant 5 is now refused. Which status should that run get, and why?

2. Strict, safely. Convert legacy.pl to use strict with my declarations. Your evidence is an empty diff against the post-clarity baseline. Which variable needs the most thought about its scope?

3. Warnings, on purpose. Turn warnings on and fix the twelve uninitialized warnings without changing STDOUT. Write the prediction first: what exactly should the diff of STDERR be?

4. A third caller. Suppose a cron job runs the script from / instead of from a run directory. What does last_run.txt do then, and how would you extend snapshot.sh to notice?

5. The dashboard's side. Write the ten-line reader that "the dashboard" might be — reading last_run.txt — so that it accepts CRASH. Then write the one that would reject it. What should the review comment on the crash patch say?

12. What To Remember

  • The deliverable is the change plus its evidence: a baseline, an empty diff for any tidying, a written prediction, and a diff that equals it.
  • List every observable behaviour first — every way the script is called, STDOUT, STDERR, exit status and every file it writes.
  • Make the output comparable. Measured: five runs of the unchanged script did not agree, and sorted output did.
  • The obvious clean-ups are behaviour changes. Measured: use strict gave 60 compile errors, -w gave 12 new STDERR lines, and unifying two near-identical patterns changed 28 lines.
  • Two edits, never one: a clarity edit with an empty diff, then the behaviour change.
  • Predict the diff before writing the code. Measured: the change was exactly the 20 predicted lines. Writing the prediction is where the forgotten consequences show up — -q writes the file too.
  • Make line ends visible. My own prediction lost a trailing space.
  • The corpus decides what can be proved. One wrong implementation was indistinguishable on it, and the check rightly accepted it.
  • Name the consumers you could not check.

That closes Module 29, and with it the set of projects this track builds from its Part I and Part II material. Each tool had a brief before it had code, an exit status a flow can trust, a refusal for every input it could not handle completely, an independent witness for its results — a compiler, a simulator, a generator's own truth table, a re-count — and a test suite that was shown to fail. And the last project made the point the other six depend on: most of the work is on code that already exists, and the professional skill is changing it without surprising anyone who relies on it.

Where this fits

Part of the Perl curriculum.