blob: c7eb49881a56f159359ca5cb2e6eea2505fb0179 [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
'''Run tests to make sure the simulator works as expected
We expect a test in each directory below ./simple. This test should comprise a
single assembly file (called <something>.s) and a file called expected.txt,
containing a list of expected register values.
'''
import os
import re
import subprocess
from typing import Any, Dict, List, Tuple
_OTBN_DIR = os.path.join(os.path.dirname(__file__), '../../..')
_UTIL_DIR = os.path.join(_OTBN_DIR, 'util')
_SIM_DIR = os.path.join(os.path.dirname(__file__), '..')
_REG_RE = re.compile(r'\s*([xw][0-9]+)\s*=\s*((:?0x[0-9a-f]+)|([0-9]+))$')
def find_simple_tests() -> List[Tuple[str, str]]:
'''Find all test directories below ./simple (relative to this file)
Returns (asm, expected) pairs, with the paths to the assembly file and
expected.txt.
'''
root = os.path.join(os.path.dirname(__file__), 'simple')
ret = []
for subdir, _, files in os.walk(root):
if 'expected.txt' not in files:
continue
dirname = os.path.join(root, subdir)
asm_files = [name for name in files if name.endswith('.s')]
if len(asm_files) != 1:
raise RuntimeError('In the directory {!r}, there is an '
'expected.txt, but there are {} files ending '
'in .s (expected exactly one).'
.format(dirname, len(asm_files)))
ret.append((os.path.join(dirname, asm_files[0]),
os.path.join(dirname, 'expected.txt')))
return ret
def asm_and_link_one_file(asm_path: str, work_dir: str) -> str:
'''Assemble and link file at asm_path in work_dir.
Returns the path to the resulting ELF
'''
otbn_as = os.path.join(_UTIL_DIR, 'otbn-as')
otbn_ld = os.path.join(_UTIL_DIR, 'otbn-ld')
obj_path = os.path.join(work_dir, 'tst.o')
elf_path = os.path.join(work_dir, 'tst')
subprocess.run([otbn_as, '-o', obj_path, asm_path], check=True)
subprocess.run([otbn_ld, '-o', elf_path, obj_path], check=True)
return elf_path
def get_reg_dump(stdout: str) -> Dict[str, int]:
'''Parse the output from a simple test ending in print_regs
Returns a dictionary keyed by register name and with value equal to the
value that register ended up with.
'''
started = False
done = False
ret = {}
for line in stdout.split('\n'):
if line == 'PRINT_REGS':
if started:
raise RuntimeError('Multiple PRINT_REGS lines seen')
started = True
continue
if done or not started:
continue
if line == '.':
done = True
continue
m = _REG_RE.match(line)
if not m:
raise RuntimeError('Failed to parse line after PRINT_REGS ({!r}).'
.format(line))
ret[m.group(1)] = int(m.group(2), 0)
return ret
def get_reg_expected(exp_path: str) -> Dict[str, int]:
'''Read expected.txt
Returns same format as get_reg_dump, assuming any unspecified registers
should be zero.
'''
ret = {}
for idx in range(32):
ret['x{}'.format(idx)] = 0
ret['w{}'.format(idx)] = 0
with open(exp_path) as exp_file:
for idx, line in enumerate(exp_file):
# Comments with '#'; ignore blank lines
line = line.split('#', 1)[0].strip()
if not line:
continue
m = _REG_RE.match(line)
if m is None:
raise RuntimeError('{}:{}: Bad format for line in expected.txt.'
.format(exp_path, idx + 1))
ret[m.group(1)] = int(m.group(2), 0)
return ret
def test_count(tmpdir: str, asm_file: str, expected_file: str) -> None:
# Start by assembling and linking the input file
elf_file = asm_and_link_one_file(asm_file, tmpdir)
# Run the simulation. We can just pass a list of commands to stdin, and
# don't need to do anything clever to track what's going on.
stepped_py = os.path.join(_SIM_DIR, 'stepped.py')
commands = 'load_elf {}\nstart 0\nrun\nprint_regs\n'.format(elf_file)
sim_proc = subprocess.run([stepped_py], check=True, input=commands,
stdout=subprocess.PIPE, universal_newlines=True)
regs_seen = get_reg_dump(sim_proc.stdout)
regs_expected = get_reg_expected(expected_file)
assert regs_seen == regs_expected
def pytest_generate_tests(metafunc: Any) -> None:
if metafunc.function is test_count:
metafunc.parametrize("asm_file,expected_file", find_simple_tests())