iverilog/ivtest/run_ivl.py

326 lines
11 KiB
Python

'''Functions for running Icarus Verilog
'''
import subprocess
import difflib
import os
import sys
import re
def assemble_iverilog_cmd(options: dict, cfg: dict, outfile: str) -> list:
'''Build the iverilog command line'''
res = []
if cfg['with-valgrind']:
res += ["valgrind", "--trace-children=yes"]
res += ["iverilog"+cfg['suffix'], "-o", os.path.join("work", outfile)]
res += options['iverilog_args']
res += [os.path.join(options['directory'], options['source'])]
return res
def assemble_vvp_cmd(options: dict, cfg: dict) -> list:
'''Build the vvp command line'''
res = []
if cfg['with-valgrind']:
res += ["valgrind", "--leak-check=full", "--show-reachable=yes"]
res += ["vvp"+cfg['suffix']]
res += options['vvp_args']
res.append(os.path.join("work", "a.out"))
res += options['vvp_args_extended']
return res
def get_ivl_version (suffix: str) -> int:
'''Figure out the version of the installed iverilog compler.
The return value is a list of 2 numbers, the major and minor version
numbers, or None if the version string couldn't be found.'''
# Get the output from the "iverilog -V" command for the version string.
text = subprocess.check_output(["iverilog"+suffix, "-V"])
match = re.search(b'Icarus Verilog version ([0-9]+)\\.([0-9]+)', text)
if not match:
return None
items = match.groups()
return int(items[0])
def build_runtime(it_key: str) -> None:
'''Check and prepare the runtime environment for a test
This is called in front of tests to make sure that the directory
structure is correct, and common temp files that might linger from
a previous run are removed. We need to make sure that the directories
"work" and "log" are present, and the log files related to this key
are removed.'''
try:
os.mkdir("log")
except FileExistsError:
pass
try:
os.remove(os.path.join("log", it_key + ".log"))
except FileNotFoundError:
pass
try:
os.mkdir("work")
except FileExistsError:
pass
try:
os.remove(os.path.join("work", "a.out"))
except FileNotFoundError:
pass
def get_log_file(key: str, title: str, stream: str) -> str:
'''Get the name of the log file'''
# pylint: disable-next=consider-using-f-string
res = "{key}-{title}-{stream}.log".format(key=key, title=title, stream=stream)
return res
def log_results(key, title, res) -> None:
''' Write results into log files.
Generate a log file with the name of the key and title, and
put the stdout and stderr into separate files.'''
with open(os.path.join("log", get_log_file(key, title, "stdout")), 'wb') as fd:
fd.write(res.stdout)
with open(os.path.join("log", get_log_file(key, title, "stderr")), 'wb') as fd:
fd.write(res.stderr)
def compare_files(log_path, gold_path):
'''Compare the log file and the gold file
The files are read it, line at a time, and the lines are compared.
If they differ, then write to stdout a unified diff. In any case,
return True or False to indicate the results of the test.'''
with open(log_path, 'rt', encoding='ascii') as fd:
a_raw = fd.readlines()
# Remove the valgrind lines from the log. They start with ==PID==
# and error messages are **PID**
a = list(filter(lambda item: not re.match('(==\\d+==)|(\\*\\*\\d+\\*\\*)', item), a_raw))
# Allow to omit empty gold files
if os.path.exists(gold_path):
with open(gold_path, 'rt', encoding='ascii') as fd:
b = fd.readlines()
else:
b = []
if a != b:
# pylint: disable-next=consider-using-f-string
print("{log} and {gold} differ:".format(log=log_path, gold=gold_path))
sys.stdout.writelines(difflib.unified_diff(a, b, log_path, gold_path))
return False
return True
def run_cmd(cmd: list) -> subprocess.CompletedProcess:
'''Run the given command'''
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
return res
# pylint: disable-next=invalid-name
def run_CE(options: dict, cfg: dict) -> list:
''' Run the compiler, and expect an error
In this case, we assert that the command fails to run and reports
an error. This is to check that invalid input generates errors.'''
it_key = options['key']
build_runtime(it_key)
cmd = assemble_iverilog_cmd(options, cfg, 'a.out')
res = run_cmd(cmd)
log_results(it_key, "iverilog", res)
log_list = ["iverilog-stdout", "iverilog-stderr"]
if res.returncode == 0:
return [1, "Failed - CE (no error reported)"]
if res.returncode >= 256:
return [1, "Failed - CE (execution error)"]
if options['gold'] is not None and not check_gold(options, log_list):
return [1, "Failed - CE (Gold output doesn't match actual output.)"]
return [0, "Passed - CE"]
def check_gold(options: dict, log_list: list) -> list:
'''Check if the log and gold file match'''
compared = True
for log_name in log_list:
# pylint: disable-next=consider-using-f-string
log_path = os.path.join("log", "{key}-{log}.log".format(key=options['key'],
log=log_name))
# pylint: disable-next=consider-using-f-string
gold_path = os.path.join("gold", "{gold}-{log}.gold".format(gold=options['gold'],
log=log_name))
compared = compared and compare_files(log_path, gold_path)
if compared:
return [0, "Passed"]
return [1, "Failed - Gold output doesn't match actual output."]
def check_diff(fname1: str, fname2: str, skip: int, expected_fail: bool) -> list:
'''Check if the difference files match after skipping the number of
specified lines'''
with open(fname1, 'rt', encoding='ascii') as fd:
# pylint: disable-next=unused-variable
for idx in range(skip):
fd.readline()
data1 = fd.read()
with open(fname2, 'rt', encoding='ascii') as fd:
# pylint: disable-next=unused-variable
for idx in range(skip):
fd.readline()
data2 = fd.read()
if data1 == data2:
return [0, "Passed"]
if expected_fail:
return [0, "Passed - EF"]
# pylint: disable-next=consider-using-f-string
return [1, "Failed - Files {name1} and {name2} differ.".format(name1=fname1,
name2=fname2)]
def check_run_outputs(options: dict, it_stdout: str, log_list: list,
expected_fail: bool) -> list:
'''Check the output files, and return success or failed.
This function takes an options dictionary that describes the settings, and
the output from the final command. This also takes a list of log files to check
if there are gold files present.'''
# Check the results against the gold file if it exists.
if options['gold'] is not None:
return check_gold(options, log_list)
# If there is a diff description, then compare named files instead of
# the log and a gold file.
it_diff = options['diff']
if it_diff is not None:
return check_diff(it_diff[0], it_diff[1], it_diff[2], expected_fail)
# Otherwise, look for the PASSED output string in stdout.
for line in it_stdout.splitlines():
if line == "PASSED":
return [0, "Passed"]
# If there is no PASSED output, and nothing else to check, then
# assume a failure unless a fail is expected.
if expected_fail:
return [0, "Passed - EF"]
return [1, "Failed - No PASSED output, and no gold file"]
# pylint: disable-next=unused-argument
def do_run_normal_vlog95(options: dict, cfg: dict, expected_fail: bool) -> list:
'''Run the iverilog and vvp commands.
In this case, run the compiler with the -tvlog95 flag to generate
an intermediate verilog file, then run the compiler again to generate
a vvp out. Run that vvp output to test the simulation results. Collect
the results and look for a "PASSED" string.'''
it_key = options['key']
build_runtime(it_key)
# Run the first iverilog command, to generate the intermediate verilog
# FIXME: this doesn't work since -tvlog95 is not passed
ivl1_cmd = assemble_iverilog_cmd(options, cfg, "a.out.v")
ivl1_res = run_cmd(ivl1_cmd)
log_results(it_key, "iverilog", ivl1_res)
if ivl1_res.returncode != 0:
return [1, "Failed - Compile failed"]
# Run another iverilog command to compile the code generated from the first step.
# FIXME: this doesn't currently work since the vlog95 path needs to be used
ivl2_cmd = assemble_iverilog_cmd(options, cfg, 'a.out')
ivl2_res = run_cmd(ivl2_cmd)
log_results(it_key, "iverilog-vlog95", ivl2_res)
if ivl2_res.returncode != 0:
return [1, "Failed - Compile of generated code failed"]
# Run the vvp command
vvp_cmd = assemble_vvp_cmd(options, cfg)
vvp_res = run_cmd(vvp_cmd)
log_results(it_key, "vvp", vvp_res)
if vvp_res.returncode != 0:
return [1, "Failed - vvp execution failed"]
it_stdout = vvp_res.stdout.decode('ascii')
log_list = ["iverilog-stdout", "iverilog-stderr",
"iverilog-vlog95-stdout", "iverilog-vlog95-stderr",
"vvp-stdout", "vvp-stderr"]
return check_run_outputs(options, it_stdout, log_list, expected_fail)
def do_run_normal(options: dict, cfg: dict, expected_fail: bool) -> list:
'''Run the iverilog and vvp commands.
In this case, run the compiler to generate a vvp output file, and
run the vvp command to actually execute the simulation. Collect
the results and look for a "PASSED" string.'''
it_key = options['key']
build_runtime(it_key)
# Run the iverilog command
ivl_cmd = assemble_iverilog_cmd(options, cfg, 'a.out')
ivl_res = run_cmd(ivl_cmd)
log_results(it_key, "iverilog", ivl_res)
if ivl_res.returncode != 0:
return [1, "Failed - Compile failed"]
# run the vvp command
vvp_cmd = assemble_vvp_cmd(options, cfg)
vvp_res = run_cmd(vvp_cmd)
log_results(it_key, "vvp", vvp_res)
if vvp_res.returncode != 0 and expected_fail:
return [0, "Passed - EF"]
if vvp_res.returncode >= 256:
return [1, "Failed - vvp execution error"]
if vvp_res.returncode > 0 and vvp_res.returncode < 256 and not expected_fail:
return [1, "Failed - vvp error, but expected to succeed"]
it_stdout = vvp_res.stdout.decode('ascii')
log_list = ["iverilog-stdout", "iverilog-stderr",
"vvp-stdout", "vvp-stderr"]
return check_run_outputs(options, it_stdout, log_list, expected_fail)
def run_normal(options: dict, cfg: dict) -> list:
'''Run a normal test'''
return do_run_normal(options, cfg, False)
# pylint: disable-next=invalid-name
def run_EF(options: dict, cfg: dict) -> list:
'''Run an expected fail test'''
return do_run_normal(options, cfg, True)
def run_normal_vlog95(options: dict, cfg: dict) -> list:
'''Run a vlog95 test'''
return do_run_normal_vlog95(options, cfg, False)
# pylint: disable-next=invalid-name
def run_EF_vlog95(options: dict, cfg: dict) -> list:
'''Run an expected fail vlog95 test'''
return do_run_normal_vlog95(options, cfg, True)