| #!/usr/bin/env python3 |
| """Runs test within Spike, Qemu and Renode simulators.""" |
| import argparse |
| import os |
| import re |
| import sys |
| import tempfile |
| |
| import io |
| import pexpect |
| |
| |
| parser = argparse.ArgumentParser( |
| description="Run a springbok test on an simulator.") |
| |
| parser.add_argument("simulator", |
| help="Select a simulator", |
| choices=["renode", "qemu", "spike"]) |
| parser.add_argument("elf", |
| help="Elf to execute on a simulator") |
| parser.add_argument("--renode-path", |
| help="Path to renode simulator") |
| parser.add_argument("--trace-output", |
| help="Path to trace output file") |
| parser.add_argument("--qemu-path", |
| help="Path to qemu simulator") |
| parser.add_argument("--spike-path", |
| help="Path to spike simulator") |
| parser.add_argument("--timeout", type=int, |
| help="Timeout for test", default=1000) |
| parser.add_argument("--quick_test", |
| help="allow quickest test time", action="store_true") |
| |
| args = parser.parse_args() |
| |
| class Simulation: # pylint: disable=too-few-public-methods |
| """ Base class for simulation """ |
| def __init__(self, simulator_cmd): |
| self.simulator_cmd = simulator_cmd |
| self.buffer = io.StringIO() |
| self.child = None |
| self.termination_strings = [ |
| "main returned", |
| "Exception occurred", |
| "ReadByte from non existing peripheral", |
| "File does not exist" |
| ] |
| # stats collected command for renode |
| self.renode_end_command = None |
| |
| def run(self, timeout=1000): |
| """ Run the simulation command and quit the simulation.""" |
| self.child = pexpect.spawn(self.simulator_cmd, encoding="utf-8") |
| self.child.logfile = self.buffer |
| try: |
| self.child.expect(self.termination_strings, timeout=timeout) |
| except pexpect.exceptions.EOF as run_hit_eof: |
| self.buffer.seek(0) |
| message = ("Runner reach EOF with the execution log: \n\n" + |
| cleanup_message(self.buffer.read())) |
| exc = pexpect.exceptions.EOF(message) |
| exc.__cause__ = None |
| raise exc from run_hit_eof |
| except pexpect.exceptions.TIMEOUT as run_hit_timeout: |
| self.buffer.seek(0) |
| message = ("Runner times out with the execution log: \n\n" + |
| cleanup_message(self.buffer.read())) |
| exc = pexpect.exceptions.EOF(message) |
| exc.__cause__ = None |
| raise exc from run_hit_timeout |
| if self.renode_end_command: |
| self.child.send(self.renode_end_command) |
| self.child.expect("(springbok)", timeout=timeout) |
| self.child.send("\nq\n") |
| self.child.expect(pexpect.EOF, timeout=timeout) |
| self.child.close() |
| self.buffer.seek(0) |
| return self.buffer.read() |
| |
| class QemuSimulation(Simulation): # pylint: disable=too-few-public-methods |
| """ Qemu simulation """ |
| def __init__(self, path, elf): |
| self.qemu_simulator_cmd = ( |
| "%(sim)s -M springbok -nographic -d springbok " |
| "-device loader,file=%(elf)s") |
| self.sim_params = {"sim": path, "elf": elf} |
| super().__init__(self.qemu_simulator_cmd % self.sim_params) |
| |
| |
| class RenodeSimulation(Simulation): # pylint: disable=too-few-public-methods |
| """ Renode Simulation """ |
| def __init__(self, path, elf): |
| # Get the ROOTDIR path if it exists |
| self.rootdir = os.environ.get("ROOTDIR", default=None) |
| if self.rootdir is None: |
| parser.error("ROOTDIR environment variable not set.") |
| renode_script = """ |
| $bin=@%(elf)s |
| path set @%(rootdir)s |
| include @sim/config/springbok.resc""" |
| |
| if args.quick_test: |
| renode_script += """ |
| sysbus.cpu2 PerformanceInMips 2000 |
| emulation SetGlobalQuantum "1" """ |
| |
| if args.trace_output: |
| renode_script += """ |
| sysbus.cpu2 EnableExecutionTracing @%(trace_file)s PCAndOpcode """ |
| |
| renode_script += """ |
| sysbus.cpu2 EnableExternalWindowMmu false |
| start |
| sysbus.vec_controlblock WriteDoubleWord 0xc 0""" |
| |
| trace_file = "" |
| if args.trace_output: |
| trace_file = os.path.realpath(args.trace_output) |
| self.script_params = { |
| "elf": os.path.realpath(elf), |
| "rootdir": self.rootdir, |
| "trace_file": trace_file |
| } |
| self.renode_script = renode_script % self.script_params |
| self.renode_args = [ |
| f"{path}", |
| "--disable-xwt", |
| " --console", |
| "--plain", |
| ] |
| self.renode_simulator_cmd = " ".join(self.renode_args) |
| super().__init__(self.renode_simulator_cmd) |
| self.renode_end_command = "\nsysbus.cpu2 ExecutedInstructions\n" |
| |
| def run(self, timeout=120): |
| file_desc, script_path = tempfile.mkstemp(suffix=".resc") |
| try: |
| with os.fdopen(file_desc, "w") as tmp: |
| tmp.write(self.renode_script) |
| tmp.flush() |
| self.simulator_cmd += f" {script_path}" |
| test_output = super().run(timeout=timeout) |
| finally: |
| os.remove(script_path) |
| return test_output |
| |
| class SpikeSimulation(Simulation): # pylint: disable=too-few-public-methods |
| """ Spike Simulation """ |
| def __init__(self, path, elf): |
| trace_file = "" |
| if args.trace_output: |
| trace_file = os.path.realpath(args.trace_output) |
| self.sim_params = { |
| "path": path, |
| "elf": elf, |
| "trace_file": trace_file |
| } |
| self.spike_simulator_cmd = ( |
| "%(path)s -m0x34000000:0x1000000 " |
| "--pc=0x34000000 ") |
| |
| if args.trace_output: |
| self.spike_simulator_cmd += " -l --log=%(trace_file)s " |
| |
| self.spike_simulator_cmd += " %(elf)s" |
| super().__init__(self.spike_simulator_cmd % self.sim_params) |
| |
| Simulators = { |
| "qemu": QemuSimulation, |
| "renode": RenodeSimulation, |
| "spike": SpikeSimulation, |
| } |
| |
| simulators_paths = { |
| "renode": args.renode_path, |
| "qemu": args.qemu_path, |
| "spike": args.spike_path, |
| } |
| |
| def cleanup_message(message: str) -> str: |
| """ Clean up the message generated by Mono. |
| |
| The non-ascii code generated by Mono. |
| |
| Convert the opcode count from hex to decimal. |
| """ |
| ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") |
| output = ansi_escape.sub("", message) |
| op_count_out = re.compile( |
| r"(?P<op_count>0x[0-9A-Fa-f]+)\r\r\r\n\(springbok\)") |
| op_count = op_count_out.search(output) |
| if op_count: |
| op_count = int(op_count.group(1), 16) |
| output = op_count_out.sub( |
| f"Renode total instruction count: {op_count}\n", output) |
| return output |
| |
| def main(): |
| """ Run a test and check for Pass or Fail """ |
| simulator_path = simulators_paths[args.simulator] |
| if simulator_path is None: |
| parser.error( |
| f"Must provide path to simulator {args.simulator}, " |
| f"use argument --{args.simulator}-path") |
| |
| simulator_class = Simulators[args.simulator] |
| simulator = simulator_class(simulator_path, args.elf) |
| output = simulator.run(timeout=args.timeout) |
| output = cleanup_message(output) |
| print(output) |
| failure_strings = [ |
| "FAILED", |
| "Exception occurred", |
| "ReadByte from non existing peripheral", |
| "File does not exist" |
| ] |
| if any(x in output for x in failure_strings): |
| sys.exit(1) |
| # Grab the return code from the output string with regex |
| # Syntax: "main returned: ", <code> (<hex_code>) |
| return_string = re.compile( |
| r"\"main returned:\s\",(?P<ret_code>\s[0-9]+\s*)") |
| code = return_string.search(output) |
| sys.exit(int(code.group(1))) |
| |
| |
| if __name__ == "__main__": |
| main() |