Python Part II · Module 20 · Simulation and UVM Log Parsing
From a Hard-Coded Path to a Variable
Two strings are buried in the checker's body: the name of the log and the text it looks for. Lifting them to the top of the file is the smallest refactor in this module and the one most people do without thinking about it — which is a pity, because doing it attentively reveals that the two values are not the same kind of thing at all. One of them changes on every single run and is really an argument wearing a constant's clothes. The other changes when the verification methodology changes, which is almost never. Telling them apart is what stops a script accumulating a block of eleven settings at the top of which three should have been parameters.
Foundation14 min readPythonPart IILog ParsingVerification FlowRefactoring
Part II · Module 20 · Chapter 20.4 · From a Hard-Coded Path to a Variable
1. The Two Buried Strings
Here is the checker as Chapter 20.3 left it, with the two literals marked:
errors = 0
with open("run.log", encoding="utf-8") as handle:
# ^^^^^^^^^ the log
for line in handle:
if "UVM_ERROR" in line:
# ^^^^^^^^^^^ the marker
errors += 1Both are inside the body. To check a different log you edit line 2; to count warnings instead you edit line 4. Neither edit is hard, and both require opening the file, finding the right line, and not breaking anything around it.
The canonical first refactor pulls them out:
log_path = "sim.log"
marker = "UVM_ERROR"2. Lifted
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_WARNING mon.sv(51) @ 99300: late sample ignored
UVM_ERROR drv.sv(88) @ 141900: bad handshake
UVM_ERROR drv.sv(104) @ 180400: timeout
UVM_INFO seq.sv(140) @ 200000: doneLOG_PATH = "run.log"
MARKER = "UVM_ERROR"
ENCODING = "utf-8"
errors = 0
with open(LOG_PATH, encoding=ENCODING) as handle:
for line in handle:
if MARKER in line:
errors += 1
print(f"{'FAIL' if errors else 'PASS'} {MARKER}={errors} log={LOG_PATH}")FAIL UVM_ERROR=2 log=run.logThree things changed and one of them was not in the canonical sketch.
The names are upper case. Chapter 2.14 established the convention: a name in capitals at module level says this is a setting, not a working variable, and nothing below reassigns it. Python does not enforce it; readers rely on it completely.
ENCODING came along too. It was a third hard-coded value hiding in plain sight, and Chapter 20.3 Section 3 argued it is a real decision. A refactor that lifts two of three magic values and leaves the third makes the file less honest about what is configurable, not more.
The output now reports what it used. log=run.log costs nothing and answers the first question anyone asks about a surprising result — which file did you actually read? Chapter 19.17 Step 1 is the same instinct: when something looks wrong, show the input.
3. Who Changes It, and How Often
Now the part worth slowing down for. Those three constants look alike and behave completely differently:
| Value | Who changes it | How often |
|---|---|---|
LOG_PATH | whoever runs the script | every single run |
MARKER | the author | when the methodology changes |
ENCODING | the author | essentially never |
That table is the whole chapter. The path is not a setting. It is an input that has been written down in the wrong place — and every time somebody opens the script to change line 1 before running it, they are doing by hand what Chapter 20.5 makes the shell do.
The test to apply to any value you are about to lift:
Every run, or by the caller — it is an argument. Once a project, by the author — it is a constant. Never — it is not a value at all; leave it in the code.
The third row is worth stating because over-lifting is a real failure mode. SEVERITY_SEPARATOR = ":" at the top of a file is not configuration; it is a fact about UVM's output format, and giving it a name implies somebody might reasonably change it. They cannot.
4. The Marker Earns More as a Parameter
MARKER passes the constant test — but there is a better place for it still, because the counting does not care which string it counts:
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_WARNING mon.sv(51) @ 99300: late sample ignored
UVM_ERROR drv.sv(88) @ 141900: bad handshake
UVM_ERROR drv.sv(104) @ 180400: timeout
UVM_INFO seq.sv(140) @ 200000: doneLOG_PATH = "run.log"
ENCODING = "utf-8"
def count(path, marker):
"""How many lines of this log contain this marker. A fact, not a verdict."""
n = 0
with open(path, encoding=ENCODING) as handle:
for line in handle:
if marker in line:
n += 1
return n
for marker in ["UVM_ERROR", "UVM_WARNING", "UVM_FATAL", "UVM_"]:
print(f"{marker:<14} {count(LOG_PATH, marker)}")UVM_ERROR 2
UVM_WARNING 1
UVM_FATAL 0
UVM_ 5One function, four questions, no edit. That is the reuse a parameter buys and a constant does not — and it is Chapter 20.1 Section 4's split arriving as a practical benefit rather than an argument: because count produces a fact and takes the marker as input, it is the same function for every severity.
Note UVM_FATAL reading 0 and UVM_ reading 5. Both are correct for this extract, which is the body of a run with no report summary. Chapter 20.2 Section 7 showed what the same function returns once the summary block is present, and it is not 0.
5. What the Default Argument Buys
count(path, marker) now forces every caller to say which marker, including the ninety percent of callers who want errors. Chapter 11.5's defaults fix that without giving up the parameter:
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_WARNING mon.sv(51) @ 99300: late sample ignored
UVM_ERROR drv.sv(88) @ 141900: bad handshake
UVM_ERROR drv.sv(104) @ 180400: timeout
UVM_INFO seq.sv(140) @ 200000: doneERROR_MARKER = "UVM_ERROR"
ENCODING = "utf-8"
def count(path, marker=ERROR_MARKER):
"""How many lines of this log contain this marker."""
n = 0
with open(path, encoding=ENCODING) as handle:
for line in handle:
if marker in line:
n += 1
return n
print("default :", count("run.log"))
print("explicit :", count("run.log", "UVM_WARNING"))
print("keyword :", count("run.log", marker="UVM_FATAL"))default : 2
explicit : 1
keyword : 0The common case is short and the uncommon one is still possible. The default is named rather than typed in the signature as marker="UVM_ERROR", so the constant still exists in exactly one place and a reader of the constants block sees it.
6. What This Refactor Did Not Do
Honest accounting, because the next chapter depends on it:
It did not make the script reusable from the command line. Running it on a different log is still an edit.
It did not make it testable. count is now testable — that is real — but the script's top-level still names one specific file, so the thing you would actually run has no inputs.
It did not reduce the number of ways to get it wrong. The marker is still a substring test, and Chapter 20.2's defect is untouched. Naming a wrong value does not make it right, and a constants block at the top of a file can make a wrong value look considered.
What it did do is real and worth having: the two decisions are now visible in one place, and the counting has become a function that does not know which marker it is counting. The first makes the script readable by someone who did not write it. The second is what Chapter 20.7 will extend without touching the loop.
7. Predict, Run, Observe
MARKER = "UVM_ERROR"
LINES = ["UVM_ERROR a", "UVM_WARNING b", "UVM_ERROR : 2", "uvm_error c"]
def count(lines, marker=MARKER):
n = 0
for line in lines:
if marker in line:
n += 1
return n
# A
print("A:", count(LINES))
# B
print("B:", count(LINES, "UVM_"))
# C
print("C:", count(LINES, "uvm_error"))
# D
print("D:", count(LINES, MARKER.lower()))
# E
MARKER = "UVM_WARNING"
print("E:", count(LINES))
# F
print("F:", count(LINES, ""))The verified output:
A: 2
B: 3
C: 1
D: 1
E: 2
F: 4A — the default marker, two matches including the summary-shaped line.
B — UVM_ matches three of the four; the lower-case one does not start with it.
C and D — case matters, and MARKER.lower() is the same string as the literal.
E — reassigning MARKER after the function was defined changes nothing. A default argument is evaluated once, when the def runs — Chapter 11.5's rule, and the reason a "constant" that something reassigns is a bug waiting to happen.
F — the empty string is in every string, so every line matches. A marker that arrives empty from a config file turns the checker into a line counter, silently.
8. Exercises
Exercise 1 — lift them. Take your checker and move the path, the marker and the encoding to named constants at the top. Confirm the output is unchanged.
Exercise 2 — report the input. Add the log path to the output line. Say which question that answers before anyone has to ask it.
Exercise 3 — the three-way test. For five values in a script of your own, say whether each is an argument, a constant, or something that should stay in the code. Justify the third category with one example.
Exercise 4 — over-lifting. Find a value in your script that should not be a constant. Say what giving it a name would wrongly imply.
Exercise 5 — parameter beats constant. Convert your marker from a constant into a parameter and answer three different questions with one function and no edits.
Exercise 6 — the naming test. Write a function whose name would become a lie if one of its constants became a parameter. Rename it so the parameter fits.
Exercise 7 — a named default. Give your function a default argument that refers to the module constant. Call it three ways: default, positional and keyword.
Exercise 8 — defaults are evaluated once. Reassign the constant after the def and call the function again. Record what happens and explain it in one sentence.
Exercise 9 — the empty marker. Call your counter with "". Record the result and say what would have to go wrong upstream to produce it.
Exercise 10 — what is still broken. List three things this refactor did not fix in your script. For each, name the chapter that does.
9. Summary
- Two literals are buried in the checker: the log path and the marker. The encoding is a third, and lifting only two is worse than lifting none.
- Upper-case names at module level say setting, not working variable — Chapter 2.14's convention, which readers rely on and Python does not enforce.
- Report the input in the output.
log=run.loganswers the first question anyone asks about a surprising result. - The three values are different kinds of thing. The path changes every run; the marker changes once a project; the encoding never.
- The test: who changes it and how often. Every run or by the caller means an argument; once a project by the author means a constant; never means leave it in the code.
- The path is an input written down in the wrong place. Editing line 1 before each run is doing by hand what Chapter 20.5 gives to the shell.
- A constants block is not an interface. This refactor moved the edit; it did not remove it.
- Over-lifting is real. Naming a fact about a tool's output format implies somebody could change it, and they cannot.
- A parameter buys reuse a constant does not — one
countfunction answers error, warning and fatal with no edit. - The second test: if the function would still make sense with a different value, it is a parameter; if changing it would make the function's name a lie, it is a constant.
- A named default keeps the common call short and the constant in one place.
- A default argument is evaluated once, at
deftime — reassigning the constant afterwards changes nothing. - An empty marker matches every line, turning the checker into a line counter with no error.
The next chapter removes the edit entirely: the log path arrives from the command line, and the script stops being about any particular file.
Where this fits
Part of the Python curriculum.
