Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 33 · Debugging Failed Jobs and Broken Flows

Reproducing a Single Failure From a Regression

Debugging starts when a failure happens on demand, and the usual way of getting there — retyping the command from the log — is how real failures get labelled intermittent. This chapter measures one omitted plusarg turning a genuine failure into a clean pass, builds a replay that re-executes the recorded argv verbatim, and shows why that argv must be stored one token per line.

Advanced26 min readLinuxPart IIDebuggingReproducibilityQuoting

Part II · Module 33 · Chapter 33.2 · Reproducing a Single Failure

Chapter 33.1's triage ends, for the design layer, with the sentence the failure is reproducible from source. This chapter is about the gap between that being true and you having done it.

A failure you cannot make happen on demand is not a failure you can debug. Every technique in the rest of this module assumes a run that fails every time you ask it to.

1. The Failure That Stops Reproducing

The fixture has a test called latefail, which fails at cycle 150 and not before. The regression runs it with +CYCLES=200:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TB  test=latefail seed=1 cycles=200 verbosity=LOW
TB  ERROR t=1535 late check failed at cycle 150
TB  TEST FAILED  test=latefail checks=1 errors=1
tb/tb_arb.v:165: $finish called at 2025 (1s)

So you reproduce it the way everybody does — by reading the log's first line and typing the command:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cd /tmp/scratch
$ vvp /proj/logs/sim.vvp +TESTNAME=latefail +SEED=1
TB  test=latefail seed=1 cycles=40 verbosity=LOW
TB  TEST PASSED  test=latefail checks=1 errors=0

It passes. Same test, same seed, same image. The reason is in the first line of each log and is easy to read past:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  the regression used: cycles=200
  this run used      : cycles=40

+CYCLES was not in the command you typed, so the testbench applied its default. And the default is 40, which never reaches cycle 150:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CYCLES=40   -> TEST PASSED
  CYCLES=100  -> TEST PASSED
  CYCLES=160  -> TEST FAILED  ERROR t=1535 late check failed at cycle 150
  CYCLES=200  -> TEST FAILED  ERROR t=1535 late check failed at cycle 150
  no +CYCLES  -> TEST PASSED   (the TB defaults to 40)

2. Replay the Record, Not Your Memory of It

The fix is not to be more careful. It is to stop retyping.

Chapter 33.1's runone already records the resolved argv, one token per line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat cap/lf200/cmd.args
vvp
/…/m33/logs/sim.vvp
+TESTNAME=latefail
+SEED=1
+CYCLES=200

And that file can be executed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# repro RUNDIR [OUTDIR] -- re-execute one recorded run, verbatim, and say
# whether it produced the same result.
#
# Reads cmd.args, which holds ONE ARGV TOKEN PER LINE. A single quoted string
# cannot be replayed safely: re-splitting it guesses where the boundaries were,
# and guesses wrongly for any token containing a space.
set -u
src=${1:-}
[ -n "$src" ] || { echo "repro: need a run directory" >&2; exit 2; }
[ -r "$src/cmd.args" ] || { echo "repro: '$src' has no cmd.args -- nothing to replay" >&2; exit 2; }
[ -r "$src/run.log" ]  || { echo "repro: '$src' has no run.log -- nothing to compare against" >&2; exit 2; }
out=${2:-$src/repro}
rm -rf "$out"; mkdir -p "$out" || exit 2

# bash 3.2 has no mapfile, so build the array a line at a time. The final line
# may lack a newline, hence the `|| [ -n "$line" ]`.
argv=()
while IFS= read -r line || [ -n "$line" ]; do argv+=("$line"); done < "$src/cmd.args"
[ ${#argv[@]} -gt 0 ] || { echo "repro: cmd.args is empty" >&2; exit 2; }

printf 'repro: replaying %d argv token(s) from %s\n' "${#argv[@]}" "$src" >&2
printf '   %s\n' "${argv[@]}" >&2

( cd "$out" && exec "${argv[@]}" ) > "$out/run.log" 2>&1
printf '%s\n' "$?" > "$out/tool.rc"

a=$(/usr/bin/grep -o 'TEST PASSED\|TEST FAILED' "$src/run.log"  | head -1)
b=$(/usr/bin/grep -o 'TEST PASSED\|TEST FAILED' "$out/run.log"  | head -1)
printf '  recorded verdict : %s\n' "${a:-none}"
printf '  replayed verdict : %s\n' "${b:-none}"
if [ "$a" = "$b" ]; then
  # the same verdict is necessary, not sufficient -- compare the whole log too
  if diff -q <(sed 's/[0-9]\{4,\}/N/g' "$src/run.log") \
             <(sed 's/[0-9]\{4,\}/N/g' "$out/run.log") >/dev/null 2>&1; then
    echo "  => REPRODUCED (same verdict, and the logs match once long numbers are normalised)"
    exit 0
  fi
  echo "  => same verdict, but the logs differ:"
  diff "$src/run.log" "$out/run.log" | sed 's/^/     /' | head -8
  exit 1
fi
echo "  => DID NOT REPRODUCE"
diff "$src/run.log" "$out/run.log" | sed 's/^/     /' | head -8
exit 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ repro cap/lf200
repro: replaying 5 argv token(s) from cap/lf200
   vvp
   /…/m33/logs/sim.vvp
   +TESTNAME=latefail
   +SEED=1
   +CYCLES=200
  recorded verdict : TEST FAILED
  replayed verdict : TEST FAILED
  => REPRODUCED (same verdict, and the logs match once long numbers are normalised)
$ echo $?
0

And it reports a non-reproduction rather than only a reproduction — here against a record whose argv is missing +CYCLES:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ repro cap/mismatch
  recorded verdict : TEST FAILED
  replayed verdict : TEST PASSED
  => DID NOT REPRODUCE
     1,4c1,3
     < TB  test=latefail seed=1 cycles=200 verbosity=LOW
     < TB  ERROR t=1535 late check failed at cycle 150
     < TB  TEST FAILED  test=latefail checks=1 errors=1
     < tb/tb_arb.v:165: $finish called at 2025 (1s)
     ---
     > TB  test=latefail seed=1 cycles=40 verbosity=LOW
     > TB  TEST PASSED  test=latefail checks=1 errors=0
$ echo $?
1

The diff is the output that matters, because it puts cycles=200 directly above cycles=40. A tool that said only "did not reproduce" would have left the engineer exactly where Section 1 left them.

3. Why One Token Per Line

The obvious way to record a command is as a line you could paste:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vvp "/proj/a dir with spaces/sim.vvp" +TESTNAME=smoke

Replaying that means splitting it again, and the shell's splitting is not the inverse of the quoting:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( cd cap/q && exec $(cat /tmp/quoted.args) )
     rc=255  "/private/tmp/…/a dir with spaces/sim.vvp: No such file or directory

Exit 255, and look at the message: the path begins with a literal ". Command substitution produced the characters, word splitting cut them at spaces, and no quote removal happened — because quote removal is a parsing step that already ran. So vvp received four arguments where one was intended, the first of them starting with a quote character.

The same argv, one token per line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ argv=(); while IFS= read -r l || [ -n "$l" ]; do argv+=("$l"); done < /tmp/perline.args
$ ( cd cap/p && exec "${argv[@]}" )
     rc=0  TB  test=smoke seed=1 cycles=40 verbosity=LOW

A newline is the one character that cannot appear in a path on this system, which makes it the only safe separator — the same argument Chapter 14.7 makes for find -print0 and xargs -0, one step weaker because a newline can appear in a filename on Linux while a NUL cannot.

Three details in those two lines of bash, all of which matter here:

IFS= read -r — IFS= so leading and trailing spaces in a token survive, -r so a backslash in a path is not an escape. Chapter 31.2 measured both.

|| [ -n "$line" ] — read returns false on a final line with no terminating newline, having already set the variable. Without this the last argv token is silently dropped, which for a command is usually the most important one.

argv+=("$line") rather than mapfile — mapfile -t argv < file is the modern one-liner and does not exist in bash 3.2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /bin/bash -c 'mapfile -t a < /etc/hosts; echo ${#a[@]}'
/bin/bash: line 0: mapfile: command not found

This machine's bash is 3.2.57, and so is every stock macOS. Chapter 21.6 is the general case; the practical consequence is that argv round-tripping has to be written the long way.

4. The Working Directory Changes the Evidence, Not the Verdict

repro runs the replay from inside a fresh directory, and it is worth being precise about what that buys, because the obvious claim is wrong.

Same argv, run from the project root instead:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  run from inside its own dir: verdict=TEST PASSED  dump landed in dump.vcd (1470 bytes)
  run from the project root  : verdict=TEST PASSED  dump landed in THE PROJECT ROOT (1470 bytes)

The verdict is identical. The working directory does not change what the simulation computes — it changes where everything the simulation writes ends up. Here that is a 1,470-byte waveform deposited in the project root, where it will be committed by somebody's git add . or deleted by somebody's clean-up.

And it changes what two runs do to each other:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one dump.vcd, two runs: hashes 1990351d vs aa0bfa94 -> the second overwrote the first

Two debugging runs from the same directory, and the first one's waveform is gone. That is Chapter 31.6's collision arriving in an interactive session rather than a parallel one — and it is more annoying here, because the thing it destroys is the artifact you were about to open.

So the rule is narrower than "always cd" and worth getting right: the working directory is part of the run's identity when the run writes relative paths, and $dumpfile("dump.vcd") is a relative path. Chapter 27.6 measured which artifacts a simulation names for itself; those are precisely the ones the cwd controls.

5. What To Do When It Genuinely Does Not Reproduce

Suppose the replay is faithful — same argv, same image verified by its manifest, same environment — and the run passes anyway. Now the non-reproduction is information.

In order of how often they turn out to be the answer:

Something in the run is not a function of its inputs. Time, a process id, a hostname, a directory listing order, an uninitialised value. The test is to run the faithful replay several times and see whether the replay is stable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ for i in 1 2 3 4 5; do repro cap/X "/tmp/rx$i" >/dev/null 2>&1; echo -n "$? "; done
0 0 0 0 0

Five identical results means the replay is deterministic and the difference is between the regression and your replay, not inside either. Five mixed results means the run itself is nondeterministic, and that is the bug to chase first — Chapter 31.3 distinguishes a seed-dependent failure, which is reproducible, from a genuinely nondeterministic one, which is not.

The original run was affected by its neighbours. Eight parallel runs share memory bandwidth, disk and a page cache. A failure that needs a loaded machine is real and will not reproduce on an idle one, and the way to find out is to reproduce it under load rather than alone.

Or the record is incomplete. Which loops back to Section 4: something that mattered was defaulted, and the fix is to the recording rather than to the investigation.

The thing not to do is run it fifty times and call the pass rate a measurement. A reproduction rate across an unfaithful replay measures nothing, and it is the step that converts a two-hour debug into a two-day one.

Practice

  1. Find a failing run in your flow and reproduce it by retyping the command from the log.
  2. Compare the first line of both logs field by field and find the difference.
  3. Build a test that fails only after N cycles and run it at four different limits.
  4. Run it with the limit omitted and record what the testbench defaults to.
  5. List the three wrong conclusions a passing reproduction invites, and the cost of each.
  6. Record an argv one token per line and write the replay that executes it.
  7. Make the replay report a non-reproduction with a diff, not just a verdict.
  8. Read its exit status through a pipe, then without one.
  9. Record the same argv as one quoted line and replay it by re-splitting.
  10. Put a space in the image's path and compare the two formats.
  11. Explain why quote removal does not happen on the re-split version.
  12. Drop the || [ -n "$line" ] and replay a file with no final newline.
  13. Try mapfile on this machine.
  14. Run the same argv from two different directories and compare the verdicts.
  15. Find where the waveform went in each case.
  16. Run twice from the same directory and check whether the first waveform survived.
  17. State the narrow rule about when the working directory is part of the identity.
  18. Name the three parts of the identity that cmd.args does not capture.
  19. Reproduce a scheduler eviction interactively and explain the result.
  20. Replay a faithful reproduction five times and interpret both possible outcomes.
  21. Reproduce a failure under load that will not reproduce idle.

Check Your Understanding

  1. Why is a failure you cannot reproduce on demand not debuggable?
  2. What made the retyped reproduction pass?
  3. What were the four measured cycle counts and their verdicts?
  4. What does the testbench do when +CYCLES is absent?
  5. Name the three wrong conclusions, and which is most expensive and why.
  6. State the general rule about a reproduction attempt that does not reproduce.
  7. Which line would have revealed the problem, and why was it missed?
  8. Why replay the record rather than retype it?
  9. Why is the diff the important part of a non-reproduction report?
  10. What did reading the status through sed give, and why?
  11. Why does that matter in an interactive check rather than only in a script?
  12. What happened to the quoted single line when it was re-split?
  13. Why is there a literal quote character in the error message?
  14. Why is a newline the right separator here, and how does it compare to -print0?
  15. What do IFS= and -r each protect?
  16. What does || [ -n "$line" ] protect, and which token is lost without it?
  17. Why not mapfile?
  18. Does the working directory change the verdict? What does it change?
  19. What did two runs from the same directory do to each other?
  20. State the narrow rule about the working directory and identity.
  21. Name the three parts of a run's identity that cmd.args does not capture.
  22. What do all three have in common with the +CYCLES case?
  23. What is the first thing to check when a faithful replay does not reproduce?
  24. What do five identical replay results tell you, and five mixed ones?
  25. Why might a real failure need a loaded machine?
  26. Why is a reproduction rate across an unfaithful replay worthless?

6. Remember

  • A failure that does not happen on demand cannot be debugged. Everything later in this module assumes one that does.
  • Retyping the command from the log is how deterministic bugs get called intermittent. One omitted +CYCLES turned a real failure into a clean pass.
  • The testbench's default was 40 and the regression used 200 — printed in both logs, two characters apart, in the line nobody reads twice.
  • A reproduction that does not reproduce is evidence about your reproduction. A negative result means nothing until the replay is known faithful.
  • Replay the recorded argv, and report a diff rather than only a verdict.
  • Read an exit status with nothing between the command and $? — including in the one-off command you use to check your own tool.
  • Store the argv one token per line. Re-splitting a quoted line gave exit 255 and a literal quote inside an argument, because quote removal has already happened.
  • IFS= read -r, || [ -n "$line" ], and argv+=(…) — there is no mapfile in bash 3.2.
  • The working directory changes the evidence, not the verdict. Two runs from one directory overwrote each other's waveform.
  • The argv does not capture the environment, the image's identity, or the resource limits — and all three, like +CYCLES, are lost precisely because they were defaulted.
  • Check whether the replay is stable before calling the run nondeterministic, and never treat a pass rate over an unfaithful replay as a measurement.

7. What Is Next

The replay works because it executes a recorded argv in a controlled directory. Chapter 33.3 takes on the next form of the same problem — a command that works perfectly when you type it and fails inside the script that runs it — and measures the four differences that account for almost every case.

Where this fits

Part of the Linux & Bash curriculum.