Skip to content
VLSI Mentor

Python Part II · Module 33 · Testing and Debugging Automation

Debugging an Automation Failure That Only Sometimes Happens

A script that fails every time is a pleasant afternoon's work. A script that fails once in twenty is the one that costs a week, because every technique in this module assumes you can run the thing again and watch it break. The way out is not cleverness — it is to stop debugging the failure and start debugging the difference between the run that failed and the run that did not, and then to make that difference something you control. Python contributes two sources of its own, and one of them changes on every process start.

Advanced24 min readPythonPart IIAutomationDebuggingDeterminism

Part II · Module 33 · Chapter 33.14 · Debugging an Automation Failure That Only Sometimes Happens

1. The Failure That Will Not Hold Still

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SHAPES = [
    ("fails every time", "run it again", "an hour"),
    ("fails on one machine", "compare the two", "a morning"),
    ("fails for one user", "compare the environments", "a morning"),
    ("fails 1 run in 20", "nothing works", "a week"),
    ("stopped failing", "nothing works, and nobody believes you", "forever"),
]

print(f"{'the report says':<24} {'the technique that works':<40} typical cost")
for shape, tech, cost in SHAPES:
    print(f"{shape:<24} {tech:<40} {cost}")

print()
print("what the last two have in common:",
      "you cannot run the experiment")
print("so the first job is              :",
      "not to fix it — to make it reproduce")
print("and every technique in 33.1-33.13 assumes:", "it reproduces")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the report says          the technique that works                 typical cost
fails every time         run it again                             an hour
fails on one machine     compare the two                          a morning
fails for one user       compare the environments                 a morning
fails 1 run in 20        nothing works                            a week
stopped failing          nothing works, and nobody believes you   forever

what the last two have in common: you cannot run the experiment
so the first job is              : not to fix it — to make it reproduce
and every technique in 33.1-33.13 assumes: it reproduces

That last line is worth sitting with. Chapter 33.9's mutation testing needs a suite you can run repeatedly. Chapter 33.4's golden fixtures need byte-stable output. Chapter 33.8's contract tests need an exit code that means the same thing twice. An intermittent failure invalidates the premise of the entire module, and that is why this chapter is last rather than first.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# re-run cmd.sh by hand in the same run_dir
print(repr(os.environ.get("PATH")))

Those two lines are the first two moves, in that order, and they are chosen to split the problem rather than to attack it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
MOVES = [
    ("re-run cmd.sh in the same run_dir", "does it fail again?",
     "yes -> deterministic, given that state", "no -> the state differed"),
    ("print repr(os.environ.get('PATH'))", "what did the tool actually see?",
     "a difference -> the environment", "identical -> look elsewhere"),
]

print(f"{'the move':<36} {'the question':<32} ")
for move, q, yes, no in MOVES:
    print(f"{move:<36} {q:<32}")
    print(f"{'':<36}   {yes}")
    print(f"{'':<36}   {no}")

print()
print("what move 1 needs from 33.13:", "cmd.sh, written BEFORE the tool ran")
print("what it would cost without it:", "reconstructing the command line")
print("and that reconstruction is   :", "the thing you cannot verify")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the move                             the question                     
re-run cmd.sh in the same run_dir    does it fail again?             
                                       yes -> deterministic, given that state
                                       no -> the state differed
print repr(os.environ.get('PATH'))   what did the tool actually see? 
                                       a difference -> the environment
                                       identical -> look elsewhere

what move 1 needs from 33.13: cmd.sh, written BEFORE the tool ran
what it would cost without it: reconstructing the command line
and that reconstruction is   : the thing you cannot verify

Move one is only possible because of the previous chapter. cmd.sh records the working directory, the environment variables and the exact argument vector before the tool launches, so it survives the run that was killed. Without it, "re-run it by hand" means reconstructing a command line from a log — and a reconstruction that fails to reproduce the bug tells you nothing, because you cannot distinguish "the bug is intermittent" from "I rebuilt the command wrong".

That is the whole value of Chapter 33.13 Section 8 arriving a chapter later: the cheapest artefact in the run directory is the one that makes debugging possible at all.

3. Why repr

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PATHS = [
    ("/opt/tools/bin:/usr/bin", "normal"),
    ("/opt/tools/bin:/usr/bin:", "trailing colon"),
    ("/opt/tools/bin::/usr/bin", "empty element"),
    ("/opt/tools/bin :/usr/bin", "trailing space on an element"),
    ("/opt/tools/bin:/usr/bin\n", "trailing newline"),
]

print("constructed PATH values — the machine's real PATH is not published\n")
for value, label in PATHS:
    parts = value.split(":")
    cwd_on_path = any(p == "" or p == "." for p in parts)
    print(f"  {label}")
    print(f"    print()       {value}")
    print(f"    print(repr()) {value!r}")
    print(f"    elements      {len(parts)}   "
          f"current dir on PATH: {cwd_on_path}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
constructed PATH values — the machine's real PATH is not published

  normal
    print()       /opt/tools/bin:/usr/bin
    print(repr()) '/opt/tools/bin:/usr/bin'
    elements      2   current dir on PATH: False
  trailing colon
    print()       /opt/tools/bin:/usr/bin:
    print(repr()) '/opt/tools/bin:/usr/bin:'
    elements      3   current dir on PATH: True
  empty element
    print()       /opt/tools/bin::/usr/bin
    print(repr()) '/opt/tools/bin::/usr/bin'
    elements      3   current dir on PATH: True
  trailing space on an element
    print()       /opt/tools/bin :/usr/bin
    print(repr()) '/opt/tools/bin :/usr/bin'
    elements      2   current dir on PATH: False
  trailing newline
    print()       /opt/tools/bin:/usr/bin

    print(repr()) '/opt/tools/bin:/usr/bin\n'
    elements      2   current dir on PATH: False

Two of these five put the current directory on PATH, and a trailing newline is invisible without repr. A PATH ending in a colon has an empty final element, and an empty element means "here" — so a run started from a directory containing a file called iverilog picks that up instead of the real tool, and only when started from that directory.

That is an intermittent failure with a completely deterministic cause: it depends on the working directory, and the working directory depends on how the job was launched.

The trailing-space case is the one that produces the most confusing symptom. /opt/tools/bin with a space is a directory that does not exist, so the tool is simply not found on that element — and the error message says the tool is missing while echo $PATH appears to show it present. Chapter 25.5 Section 3's point about an invisible character applies to every environment variable a flow reads, which is why repr and not print is in the annotation.

4. Debug the Difference, Not the Failure

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DIFFERENCES = [
    ("the environment", "PATH, tool version vars, locale", "cmd.sh exports"),
    ("the working directory", "relative paths resolve elsewhere", "cmd.sh cd"),
    ("the inputs", "a filelist that changed under you", "hash the inputs"),
    ("the seed", "different stimulus", "record it — 33.13"),
    ("the order of work", "iteration order, completion order", "§5, §6"),
    ("the clock", "a date-named dir, a timeout", "record the start time"),
    ("concurrency", "two runs sharing a path", "a unique run_dir"),
    ("leaked state", "a previous run's file still there", "a fresh dir — 33.7"),
]

print(f"{'what can differ':<24} {'how it shows up':<38} what pins it")
for what, how, pin in DIFFERENCES:
    print(f"{what:<24} {how:<38} {pin}")

print()
print("candidate differences:", len(DIFFERENCES))
print("the method           :", "make each one identical, one at a time")
print("when they all match  :", "the failure is in the code, and reproduces")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what can differ          how it shows up                        what pins it
the environment          PATH, tool version vars, locale        cmd.sh exports
the working directory    relative paths resolve elsewhere       cmd.sh cd
the inputs               a filelist that changed under you      hash the inputs
the seed                 different stimulus                     record it — 33.13
the order of work        iteration order, completion order      §5, §6
the clock                a date-named dir, a timeout            record the start time
concurrency              two runs sharing a path                a unique run_dir
leaked state             a previous run's file still there      a fresh dir — 33.7

candidate differences: 8
the method           : make each one identical, one at a time
when they all match  : the failure is in the code, and reproduces

Eight candidates, and the reframing is the whole technique: you are not looking for a bug that happens sometimes, you are looking for an input you did not know you had. A failure that occurs on one run in twenty is perfectly deterministic with respect to something; the job is to find the something and put it in cmd.sh.

Row five is the one Python supplies for free, and it is the next two sections.

5. String Hashing Is Randomised Per Process

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
import sys
from pathlib import Path

Path("order.py").write_text(
    'severities = {"UVM_ERROR", "UVM_FATAL", "UVM_WARNING", "UVM_INFO"}\n'
    'print(",".join(severities))\n', encoding="utf-8")

RUNS = 20
orders = set()
for _ in range(RUNS):
    r = subprocess.run([sys.executable, "order.py"], stdout=subprocess.PIPE,
                       text=True)
    orders.add(r.stdout.strip())

print("the same program, run as", RUNS, "separate processes")
print("distinct iteration orders observed >= 2:", len(orders) >= 2)
print("so set order is stable within a process:", True)
print("and stable between processes           :", False)
print()
print("hash randomisation is on by default:",
      bool(sys.flags.hash_randomization))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the same program, run as 20 separate processes
distinct iteration orders observed >= 2: True
so set order is stable within a process: True
and stable between processes           : False

hash randomisation is on by default: True

The same program printed different orders on different runs. Python randomises the hash of str and bytes at interpreter start — a deliberate defence against hash-collision denial of service — so any code whose behaviour depends on set iteration order behaves differently from one process to the next.

The specific orders are not published here deliberately: they are different every time this page's examples are verified, which is exactly the property under discussion. What is stable, and therefore worth publishing, is that at least two distinct orders appear in twenty runs.

Dictionaries are not affected:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
import sys
from pathlib import Path

Path("dicts.py").write_text(
    'd = {"UVM_ERROR": 1, "UVM_FATAL": 2, "UVM_WARNING": 3, "UVM_INFO": 4}\n'
    's = {"UVM_ERROR", "UVM_FATAL", "UVM_WARNING", "UVM_INFO"}\n'
    'print("dict:", ",".join(d))\n'
    'print("set :", ",".join(s))\n', encoding="utf-8")


def collect(n, env=None):
    import os
    e = dict(os.environ)
    if env:
        e.update(env)
    dicts, sets = set(), set()
    for _ in range(n):
        r = subprocess.run([sys.executable, "dicts.py"], stdout=subprocess.PIPE,
                           text=True, env=e)
        for line in r.stdout.strip().splitlines():
            (dicts if line.startswith("dict:") else sets).add(line)
    return dicts, sets


d1, s1 = collect(12)
print("default hash seed, 12 runs")
print("  distinct dict orders        :", len(d1))
print("  distinct set orders >= 2    :", len(s1) >= 2)

d2, s2 = collect(12, {"PYTHONHASHSEED": "0"})
print()
print("PYTHONHASHSEED=0, 12 runs")
print("  distinct dict orders        :", len(d2))
print("  distinct set orders         :", len(s2))
print()
print("a dict preserves insertion order:", "always — 15.4 §2")
print("a set has no order to preserve  :", True)
print("PYTHONHASHSEED=0 makes a set    :", "reproducible, not sorted")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
default hash seed, 12 runs
  distinct dict orders        : 1
  distinct set orders >= 2    : True

PYTHONHASHSEED=0, 12 runs
  distinct dict orders        : 1
  distinct set orders         : 1

a dict preserves insertion order: always — 15.4 §2
a set has no order to preserve  : True
PYTHONHASHSEED=0 makes a set    : reproducible, not sorted

One dict order always; one set order only with the seed pinned. That gives two separate practical conclusions and they should not be conflated.

PYTHONHASHSEED=0 is a diagnostic tool. Set it, run the failing job twenty times, and if the failure becomes consistently present or consistently absent you have found the cause. It is not a fix: shipping a flow that depends on it means shipping a flow whose correctness depends on an environment variable nobody will remember.

The fix is to not depend on set order at all. sorted(severities) where an order is needed, a dict or a list where insertion order is meaningful, and a set only where the question really is membership. Chapter 20.4's advice to sort before writing a report is the same rule: if the output has an order, choose it explicitly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
severities = {"UVM_ERROR", "UVM_FATAL", "UVM_WARNING", "UVM_INFO"}

print("a set, iterated       :", "order varies per process")
print("a set, sorted         :", ",".join(sorted(severities)))
print("a list                :", ",".join(["UVM_FATAL", "UVM_ERROR",
                                           "UVM_WARNING", "UVM_INFO"]))
print()
print("which of the three is reproducible:", "the last two")
print("which carries MEANING in its order:", "the list — severity rank")
print("so for a report you want          :", "the list, not sorted()")
print("because alphabetical ranks        :", ",".join(sorted(severities)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a set, iterated       : order varies per process
a set, sorted         : UVM_ERROR,UVM_FATAL,UVM_INFO,UVM_WARNING
a list                : UVM_FATAL,UVM_ERROR,UVM_WARNING,UVM_INFO

which of the three is reproducible: the last two
which carries MEANING in its order: the list — severity rank
so for a report you want          : the list, not sorted()
because alphabetical ranks        : UVM_ERROR,UVM_FATAL,UVM_INFO,UVM_WARNING

The last line is the small trap inside the fix. sorted() makes the order reproducible and alphabetical, which puts UVM_ERROR before UVM_FATAL and UVM_INFO before UVM_WARNING — a severity ordering that is wrong in both pairs. Reproducible and correct are different properties, and reaching for sorted() only buys the first.

6. Directory Order Is Not Sorted Order

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import glob
import os
from pathlib import Path

for name in ["c_test", "b_test", "z_test", "a_test", "m_test"]:
    Path(name + ".log").write_text("UVM_INFO\n", encoding="utf-8")

listed = [p for p in os.listdir(".") if p.endswith(".log")]
globbed = [os.path.basename(p) for p in glob.glob("*.log")]

print("files created in a deliberately non-alphabetical order")
print()
print("os.listdir is sorted            :", listed == sorted(listed))
print("glob.glob is sorted             :", globbed == sorted(globbed))
print("listdir and glob agree          :", listed == globbed)
print("same SET of names               :", set(listed) == set(globbed))
print("sorted(listdir) == sorted(glob) :", sorted(listed) == sorted(globbed))
print()
print("the actual order is not published because it is:",
      "filesystem-specific")
print("which is exactly why you must not depend on it:", True)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
files created in a deliberately non-alphabetical order

os.listdir is sorted            : False
glob.glob is sorted             : False
listdir and glob agree          : True
same SET of names               : True
sorted(listdir) == sorted(glob) : True

the actual order is not published because it is: filesystem-specific
which is exactly why you must not depend on it: True

Neither is sorted here, and — the stronger statement — neither is specified either way. The order depends on the filesystem, the creation history, and sometimes on how many entries a directory has ever contained, so two machines with identical files can enumerate them differently, and the same machine can return sorted order for one directory and not for another. Section 10 shows a three-file case that came back sorted, which makes the trap concrete: a sortedness check is not a test, because passing it proves nothing about the next directory.

That turns into an intermittent failure through a specific and common shape: a sweep that writes summary.csv by iterating glob("*.log") produces rows in a different order on a different machine, and Chapter 30.3's new-versus-fixed comparison — which diffs two summaries — reports every row as changed. The results are identical and the report says everything moved.

The fix is one call, and it belongs at the point of enumeration rather than later:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import glob
from pathlib import Path

for name in ["c_test", "b_test", "z_test", "a_test", "m_test"]:
    Path(name + ".log").write_text("UVM_INFO\n", encoding="utf-8")

logs = sorted(Path(p) for p in glob.glob("*.log"))
print("sorted at enumeration:", [p.name for p in logs])
print()
print("rows in the report  :", len(logs))
print("reproducible        :", True)
print("on any filesystem   :", True)
print()
print("and for a NUMERIC suffix, sorted() is still not enough:")
names = ["run_1.log", "run_2.log", "run_10.log", "run_20.log"]
print("  sorted()      :", sorted(names))
print("  natural order :", sorted(names, key=lambda s: int(s.split('_')[1]
                                                           .split('.')[0])))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sorted at enumeration: ['a_test.log', 'b_test.log', 'c_test.log', 'm_test.log', 'z_test.log']

rows in the report  : 5
reproducible        : True
on any filesystem   : True

and for a NUMERIC suffix, sorted() is still not enough:
  sorted()      : ['run_1.log', 'run_10.log', 'run_2.log', 'run_20.log']
  natural order : ['run_1.log', 'run_2.log', 'run_10.log', 'run_20.log']

sorted() fixes reproducibility and introduces the lexicographic-versus-numeric problem Chapter 24.6 measured on sort -k2,2 — run_10 before run_2 — so a run index needs a key function or a zero-padded name. Reproducible first, then correct, and they are two separate pieces of work.

7. The Other Six Sources

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SOURCES = [
    ("completion order", "a pool writes results as runs finish",
     "collect, sort, then write", "high"),
    ("a shared path", "two runs write the same scratch file",
     "a unique run_dir per run", "high"),
    ("leaked state", "a file from the previous run is still there",
     "a fresh directory — 33.7", "high"),
    ("the clock", "a date-named dir collides at midnight",
     "include the run index", "low"),
    ("locale", "a decimal comma in a parsed number",
     "pin LC_ALL in cmd.sh", "low"),
    ("a retry that succeeded", "the run passed on attempt 2",
     "log the retry — 33.10 §4", "medium"),
]

print(f"{'source':<20} {'how it bites':<44} {'frequency':<10} fix")
for src, how, fix, freq in SOURCES:
    print(f"{src:<20} {how:<44} {freq:<10} {fix}")

print()
print("the top three share one property:", "they are all about SHARING")
print("the single best defence         :", "one fresh directory per run")
print("which 33.7 argued for on        :", "test-isolation grounds")
print("and is here required for        :", "reproducibility")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
source               how it bites                                 frequency  fix
completion order     a pool writes results as runs finish         high       collect, sort, then write
a shared path        two runs write the same scratch file         high       a unique run_dir per run
leaked state         a file from the previous run is still there  high       a fresh directory — 33.7
the clock            a date-named dir collides at midnight        low        include the run index
locale               a decimal comma in a parsed number           low        pin LC_ALL in cmd.sh
a retry that succeeded the run passed on attempt 2                  medium     log the retry — 33.10 §4

the top three share one property: they are all about SHARING
the single best defence         : one fresh directory per run
which 33.7 argued for on        : test-isolation grounds
and is here required for        : reproducibility

The last row is the one that produces the report nobody can act on. A run that failed, retried, and passed is recorded as a pass — so the sweep looks clean and the underlying flakiness is invisible. That is precisely why Chapter 33.10 Section 4 defined WARNING as the run continued but not the way you asked: without that line in the log, there is no evidence the retry ever happened, and the one-in-twenty failure has been converted into a zero-in-twenty failure that is still there.

8. The Procedure

Turning an intermittent failure into a deterministic one

A seven-stage procedure for an intermittent failure: keep the failing run directory, re-run its cmd.sh by hand, compare the recorded environment against a passing run, pin one candidate difference at a time such as the hash seed or the enumeration order, re-run many times to measure whether the failure rate changed, stop once it reproduces every time, and then write the regression test that would have caught it.01KEEP THE FAILING RUN DIR33.13's evidence, not deleted02RE-RUN CMD.SH BY HANDsame dir, same env03DIFF AGAINST A PASSING RUNrepr every value — §304PIN ONE DIFFERENCEPYTHONHASHSEED, sorted(), the seed05MEASURE THE RATE, NOT ONCE20 runs: did the rate move?06IT FAILS EVERY TIMEnow it is an ordinary bug07WRITE THE TESTthe difference is the fixture
Steps 2 and 3 are the only ones that need the previous chapter's cmd.sh. The loop exits at step 6 with a bug that reproduces — at which point every technique in 33.1 to 33.13 applies, and the hard part is over.

Step 5 is the one people skip, and skipping it is how a week gets lost. A one-in-twenty failure that does not appear in three runs after a change has told you almost nothing — the probability of that under no change at all is better than eight in ten. An intermittent bug can only be measured as a rate, so a candidate fix needs enough runs to distinguish the rate you had from the rate you have.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
P_FAIL = 0.05           # one run in twenty

print("a 1-in-20 failure, and you run N times after a 'fix' that did nothing")
print()
print(f"{'runs':>6}  {'chance of seeing zero failures':>32}")
for n in (1, 3, 5, 10, 20, 40, 60, 100):
    p_none = (1 - P_FAIL) ** n
    print(f"{n:>6}  {p_none:>31.1%}")

print()
print("runs needed for 'no failures' to mean something:", "about 60")
print("runs most people do                            :", 3)
print("conclusion drawn from 3 clean runs              :", "fixed")
print("probability that conclusion is wrong            :", "about 86%")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 1-in-20 failure, and you run N times after a 'fix' that did nothing

  runs    chance of seeing zero failures
     1                            95.0%
     3                            85.7%
     5                            77.4%
    10                            59.9%
    20                            35.8%
    40                            12.9%
    60                             4.6%
   100                             0.6%

runs needed for 'no failures' to mean something: about 60
runs most people do                            : 3
conclusion drawn from 3 clean runs              : fixed
probability that conclusion is wrong            : about 86%

Three clean runs leave an 86% chance that nothing was fixed. Sixty runs bring that below 5%, which is the number worth remembering: for a one-in-twenty failure, about sixty clean runs is the evidence threshold, and three is theatre.

That arithmetic is the same one Chapter 29.2 applied to coverage thresholds — a number is only evidence against the sample size that produced it — and it is why step 5 says measure the rate.

9. Once It Reproduces, It Is an Ordinary Bug

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

SEVERITY_RANK = ["UVM_FATAL", "UVM_ERROR", "UVM_WARNING", "UVM_INFO"]


def worst_severity_buggy(lines):
    """Iterates a SET — the order decides the answer. The §5 bug."""
    found = {s for line in lines for s in SEVERITY_RANK if line.startswith(s)}
    for s in found:                     # <- set iteration order
        return s
    return "NONE"


def worst_severity_fixed(lines):
    """Iterates the RANK — the order is the specification."""
    found = {s for line in lines for s in SEVERITY_RANK if line.startswith(s)}
    for s in SEVERITY_RANK:             # <- explicit, meaningful order
        if s in found:
            return s
    return "NONE"


LOG = ["UVM_INFO top.sv(1) start",
       "UVM_WARNING drv.sv(9) late",
       "UVM_ERROR drv.sv(88) bad",
       "TEST DONE"]

buggy = worst_severity_buggy(LOG)
fixed = worst_severity_fixed(LOG)

print("the log contains :", ["UVM_INFO", "UVM_WARNING", "UVM_ERROR"])
print("correct answer   :", "UVM_ERROR")
print()
print("buggy version returned something in the set:", buggy in
      {"UVM_INFO", "UVM_WARNING", "UVM_ERROR"})
print("buggy version is always correct            :", False)
print("fixed version returned                     :", fixed)
print("fixed version depends on iteration order   :", False)
print()
print("the test that pins it:")
print("  assert worst_severity(LOG) == 'UVM_ERROR'   # every run")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the log contains : ['UVM_INFO', 'UVM_WARNING', 'UVM_ERROR']
correct answer   : UVM_ERROR

buggy version returned something in the set: True
buggy version is always correct            : False
fixed version returned                     : UVM_ERROR
fixed version depends on iteration order   : False

the test that pins it:
  assert worst_severity(LOG) == 'UVM_ERROR'   # every run

There is the one-in-twenty failure, as four lines of ordinary code. worst_severity_buggy returns whichever severity the set happened to yield first, so it is right whenever that is UVM_ERROR and wrong otherwise — a classifier that reports UVM_INFO for a log containing an error, occasionally, for reasons invisible in the code.

Note that the buggy version is not random in a way a test would catch by accident. Within one process it is perfectly consistent, so a test suite run once passes or fails and then keeps doing the same thing all day. Chapter 33.9's mutation experiment would not find it either, because it is not a missing check — it is a dependency on something nobody declared as an input.

And the fix is not defensive coding. It is replacing an implicit order with the explicit one that was always the specification: severity rank is a list, written down, and iterating it is both correct and reproducible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PINS = [
    ("sorted() at every enumeration", "listdir, glob, set", "reproducible"),
    ("an explicit rank list", "anywhere order means something", "correct"),
    ("a unique run_dir per run", "no sharing", "isolated"),
    ("record seed, env, argv in cmd.sh", "every input written down", "replayable"),
    ("log retries at WARNING", "flakiness stays visible", "measurable"),
    ("collect, then write", "not in completion order", "reproducible"),
]

print(f"{'what you pin':<36} {'where':<36} what it buys")
for pin, where, buys in PINS:
    print(f"{pin:<36} {where:<36} {buys}")

print()
print("none of these is a debugging technique:", True)
print("all of them are                       :", "ways not to need one")
print("the module's thesis                   :",
      "an intermittent failure is an undeclared input")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what you pin                         where                                what it buys
sorted() at every enumeration        listdir, glob, set                   reproducible
an explicit rank list                anywhere order means something       correct
a unique run_dir per run             no sharing                           isolated
record seed, env, argv in cmd.sh     every input written down             replayable
log retries at WARNING               flakiness stays visible              measurable
collect, then write                  not in completion order              reproducible

none of these is a debugging technique: True
all of them are                       : ways not to need one
the module's thesis                   : an intermittent failure is an undeclared input

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import glob
import os
import subprocess
import sys
from pathlib import Path

Path("o.py").write_text(
    'print(",".join({"a", "b", "c", "d", "e", "f"}))\n', encoding="utf-8")


def distinct(n, env=None):
    e = dict(os.environ)
    if env:
        e.update(env)
    seen = set()
    for _ in range(n):
        seen.add(subprocess.run([sys.executable, "o.py"],
                                stdout=subprocess.PIPE, text=True,
                                env=e).stdout.strip())
    return len(seen)


# A
print("A:", distinct(20) >= 2)

# B
print("B:", distinct(20, {"PYTHONHASHSEED": "0"}))

# C
for n in ["z.log", "a.log", "m.log"]:
    Path(n).write_text("x", encoding="utf-8")
g = glob.glob("*.log")
print("C:", set(g) == {"z.log", "a.log", "m.log"}, len(g))

# D
d = {"z": 1, "a": 2, "m": 3}
print("D:", list(d), list(d) == sorted(d))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: True
B: 1
C: True 3
D: ['z', 'a', 'm'] False

A — twenty processes, more than one set order. The failure's source, as one boolean.

B — with the seed pinned, exactly one order in twenty runs. That is the bisection tool, not the fix.

C — the set of names and the count are guaranteed; the order is not, which is why neither value here is about ordering. When these three files were enumerated they happened to come back in sorted order, where Section 6's five files did not — and that is the hazard stated precisely. An unspecified order is not reliably wrong, it is reliably unreliable, so a check asserting g == sorted(g) can pass on your machine, pass in review, and fail in the sweep. The only safe move is to sort and never ask.

D — the dict keeps insertion order, which is z, a, m and therefore not sorted. Reproducible and unsorted at the same time, which is the distinction Section 5 ended on: a dict solves the determinism problem and says nothing about whether the order is the one you wanted.

11. Exercises

Exercise 1 — five shapes. Classify the last intermittent failure you saw.

Exercise 2 — the invalidated premise. Explain why an intermittent failure breaks Chapter 33.9's method.

Exercise 3 — move one. Re-run a failing job's cmd.sh by hand and record whether it fails again.

Exercise 4 — without cmd.sh. Say why a reconstruction that does not reproduce proves nothing.

Exercise 5 — repr your PATH. Check your own for a trailing colon, an empty element and trailing spaces.

Exercise 6 — the current directory. Explain how an empty PATH element makes a failure depend on where the job started.

Exercise 7 — the invisible space. Describe the symptom of /opt/tools/bin and why echo $PATH hides it.

Exercise 8 — record, do not inspect. Add the environment variables your flow depends on to cmd.sh.

Exercise 9 — eight candidates. For your own flow, say which of Section 4's eight could differ between two runs.

Exercise 10 — an undeclared input. Restate your failure as "it depends on X, and X is not in cmd.sh".

Exercise 11 — twenty processes. Reproduce the set-order measurement.

Exercise 12 — why randomised. Say in one sentence why Python randomises string hashes.

Exercise 13 — dicts. Confirm dict order is stable across processes and explain why.

Exercise 14 — the diagnostic. Use PYTHONHASHSEED=0 on a flaky job and say what each outcome would tell you.

Exercise 15 — not a fix. Explain why shipping a flow that needs it is a bad idea.

Exercise 16 — audit your sets. Find a place where you iterate a set and decide what order it should be.

Exercise 17 — sorted() is not rank. Show that alphabetical order gets UVM severities wrong.

Exercise 18 — listdir. Confirm os.listdir is not sorted on your filesystem.

Exercise 19 — every row changed. Explain how enumeration order makes Chapter 30.3's diff useless.

Exercise 20 — sort at enumeration. Fix one place, and say why there rather than later.

Exercise 21 — run_10 before run_2. Add a key function or zero-pad your run names.

Exercise 22 — the top three. Explain what completion order, shared paths and leaked state have in common.

Exercise 23 — the retry that passed. Check whether your flow logs retries, and what the report says if it does not.

Exercise 24 — three runs prove nothing. Compute the probability of three clean runs for your own failure rate.

Exercise 25 — how many runs. Work out the number you would need for 95% confidence.

Exercise 26 — measure the rate. Re-run a candidate fix enough times to compare rates, and write both down.

Exercise 27 — the buggy classifier. Write worst_severity_buggy and explain why one process is consistent.

Exercise 28 — why mutation misses it. Say why Chapter 33.9's method would not find this bug.

Exercise 29 — explicit over defensive. Rewrite one implicit order as an explicit one.

Exercise 30 — pin six things. Apply Section 9's list to your own flow and say which you already had.

Exercise 31 — the test that would have caught it. Write it, and confirm it fails against the buggy version under at least one hash seed.

Exercise 32 — the module, applied. Pick one script and run every technique from 33.1 to 33.14 against it. Write down what you found.

12. Summary

  • An intermittent failure invalidates the premise of this whole module, because every other technique assumes you can run the thing again and watch it break.
  • The first job is not to fix it — it is to make it reproduce.
  • Move one is only possible because of cmd.sh. Without a record written before the tool launched, "re-run it by hand" means reconstructing a command line you cannot verify.
  • repr your environment values. A trailing colon or an empty element puts the current directory on PATH; a trailing space makes a directory that does not exist; a trailing newline is invisible.
  • Record the environment into the run directory, rather than inspecting it later in a shell you have just configured.
  • Debug the difference, not the failure. A one-in-twenty failure is deterministic with respect to something — you are looking for an input you did not know you had.
  • String hashing is randomised per process, so set iteration order changes between runs of the same program. Twenty runs produced more than one order.
  • Dictionaries are insertion-ordered and stable across processes.
  • PYTHONHASHSEED=0 is a diagnostic, not a fix. Pin it to find the cause; do not ship a flow that depends on it.
  • sorted() buys reproducibility, not correctness — alphabetical puts UVM_ERROR before UVM_FATAL, which is the wrong severity rank.
  • os.listdir and glob.glob return filesystem order, which is not specified either way — so two machines with identical files can enumerate them differently, a report diff then shows every row as changed, and a check asserting the order is sorted can pass by luck.
  • Sort at the point of enumeration, and remember that sorted() puts run_10 before run_2.
  • The three highest-frequency sources are all about sharing — completion order, a shared path, leaked state — and one fresh directory per run defends against all three.
  • A retry that succeeded hides the flakiness entirely unless it is logged at WARNING.
  • Three clean runs leave an 86% chance nothing was fixed. For a one-in-twenty failure, about sixty clean runs is the evidence threshold.
  • An intermittent bug can only be measured as a rate, so a candidate fix needs enough runs to distinguish the old rate from the new one.
  • Once it reproduces, it is an ordinary bug — and the fix is usually replacing an implicit order with the explicit one that was always the specification.
  • An intermittent failure is an undeclared input. Every item on the pin list is a way of not needing a debugging technique at all.

That closes Module 33. The scripts you write from here are tested, logged, validated before they start, honest about what they leave behind when they stop, and reproducible when they misbehave — which is the difference between automation somebody maintains and automation somebody rewrites.

Where this fits

Part of the Python curriculum.