iverilog/ivtest/flow_reg.py

211 lines
6.5 KiB
Python

''' Regression runner for the flow (-tflow) dataflow exporter target.
For each test named in flow.list, this compiles flow/<name>.v with
`iverilog -tflow` and validates the resulting flowtracer1.verilog.v0
JSON: every output must be well-formed and carry the expected schema,
a top, and a non-empty module/hierarchy set; per-test checks then
assert the dataflow facts the exporter is meant to capture (clocked
processes, port-map continuity, continuous-assign cone reads, and
generate frames).
Run with: python3 flow_reg.py '''
import os
import re
import json
import subprocess
# Name of the iverilog command. May vary in different installations.
IVERILOG = "iverilog"
def get_tests() -> list:
'''Read the test names from flow.list (first word of each line).'''
match_prog = re.compile(r"^([a-zA-Z0-9_.]+).*$")
tests = []
with open("flow.list", encoding='ascii') as fd:
for line in fd:
if line[0] == "#":
continue
match = match_prog.search(line)
if match:
tests.append(match.group(1))
return tests
# ---- per-test structural checks ----------------------------------------
# Each returns (ok, message). `d` is the parsed .flow document.
def _module(d, name):
for m in d["modules"]:
if m["name"] == name:
return m
return None
def _find_in_hier(d, pred):
'''Depth-first search of hierarchy[] for a node matching pred.'''
stack = list(d["hierarchy"])
while stack:
n = stack.pop()
if pred(n):
return n
stack.extend(n.get("children", []))
return None
def check_basic(d):
if d["top"] != "top":
return False, "top != 'top'"
cnt = _module(d, "counter")
if not cnt:
return False, "no 'counter' module"
proc = [p for p in cnt["processes"]
if "count" in p["drives"] and p.get("sensitivity")]
if not proc:
return False, "no clocked process driving count"
# port_map continuity: u_cnt.count maps to parent net q
node = _find_in_hier(d, lambda n: n.get("module") == "counter")
if not node:
return False, "counter instance not in hierarchy"
pm = {a["formal"]: a["actuals"] for a in node["port_map"]}
if "q" not in pm.get("count", []):
return False, "port_map count->q missing"
return True, "clocked process + port_map ok"
def check_comb(d):
if d["top"] != "chip":
return False, "top != 'chip'"
dp = _module(d, "datapath")
if not dp:
return False, "no 'datapath' module"
res = [a for a in dp["assignments"]
if a["target"] == "result" and "alu_out" in a["reads"]]
if not res:
return False, "assign result<-alu_out (cone) missing"
return True, "continuous-assign cone read ok"
def check_generate(d):
frame = _find_in_hier(d, lambda n: n.get("scope") is True
and "generate" in n)
if not frame:
return False, "no generate frame in hierarchy"
kind = frame["generate"].get("kind")
if kind not in ("for", "if"):
return False, "generate.kind not for/if"
return True, "generate frame ({}) ok".format(kind)
def check_genpath(d):
# if-generate with whole-signal connection: the instance under the
# frame must resolve its actuals to the parent nets.
inst = _find_in_hier(d, lambda n: n.get("module") == "inv8")
if not inst:
return False, "inv8 instance not found under frame"
pm = {a["formal"]: a["actuals"] for a in inst["port_map"]}
if "din" not in pm.get("d", []) or "dout" not in pm.get("q", []):
return False, "port_map through frame not resolved"
return True, "generate-frame port_map ok"
CHECKS = {
"flow_basic": check_basic,
"flow_comb": check_comb,
"flow_generate": check_generate,
"flow_genpath": check_genpath,
}
def run_test(test_name: str) -> bool:
'''Compile one test with -tflow and validate its JSON.'''
src = "flow/" + test_name + ".v"
out = "flow/" + test_name + ".flow"
cmd = IVERILOG + " -g2012 -tflow -o" + out + " " + src
rc = subprocess.call(cmd + " > log/" + test_name + ".log 2>&1", shell=True)
if rc != 0:
return False
try:
with open(out, encoding='ascii') as fd:
doc = json.load(fd)
except (OSError, ValueError):
return False
# Invariants common to every well-formed output.
if doc.get("schema") != "flowtracer1.verilog.v0":
return False
if not doc.get("top") or not doc.get("modules") or not doc.get("hierarchy"):
return False
# Every position is the compact "start:begin:end" byte-offset string.
if not _positions_well_formed(doc):
return False
check = CHECKS.get(test_name)
if check:
ok, _msg = check(doc)
return ok
return True
_POS_RE = re.compile(r"^[0-9]*:[0-9]*:[0-9]*$")
def _positions_well_formed(node) -> bool:
'''Every "pos"/"inst_pos" value, anywhere in the document, must be the
compact "start:begin:end" byte-offset string (parts may be empty).'''
if isinstance(node, dict):
for key, val in node.items():
if key in ("pos", "inst_pos"):
if not (isinstance(val, str) and _POS_RE.match(val)):
return False
elif not _positions_well_formed(val):
return False
return True
if isinstance(node, list):
return all(_positions_well_formed(x) for x in node)
return True
def get_ivl_version() -> list:
'''Get the iverilog version.'''
text = subprocess.check_output([IVERILOG, "-V"])
match = re.search(b'Icarus Verilog version ([0-9]+)\\.([0-9]+)', text)
if not match:
return None
items = match.groups()
return [str(items[0], 'ascii'), str(items[1], 'ascii')]
def run_tests(tests: list):
'''Run all tests in the list.'''
ivl_ver = get_ivl_version()
print("Running flow tests for Icarus Verilog version: {}.{}".format(
ivl_ver[0], ivl_ver[1]))
print("=" * 50)
if not os.path.exists("log"):
os.mkdir("log")
count_passed = 0
count_failed = 0
width = max(len(name) for name in tests)
for test in tests:
passed = run_test(test)
if passed:
count_passed += 1
res = "Passed"
else:
count_failed += 1
res = "Failed"
print("{name:>{width}}: {res}.".format(name=test, width=width, res=res))
print("=" * 50)
print("Tests results: Passed {}, Failed {}".format(count_passed, count_failed))
if __name__ == "__main__":
run_tests(get_tests())