test_common.py 8.97 KB
Newer Older
1
from textwrap import dedent
2 3
from collections.abc import Iterable
from glob import glob
Martin Jeřábek's avatar
Martin Jeřábek committed
4
from os.path import join, abspath
5 6
import logging
from pathlib import Path
7
from jinja2 import Environment, PackageLoader
Martin Jeřábek's avatar
Martin Jeřábek committed
8
from pprint import pprint
9
import random
10
from .gtkwave import tcl2gtkw
11 12 13
from typing import List, Tuple
import copy
import re
14

15 16 17
__all__ = ['add_sources', 'add_common_sources',
           'dict_merge', 'vhdl_serialize', 'dump_sim_options',
           'TestsBase', 'get_seed', 'OptionsDict']
18 19 20 21

d = Path(abspath(__file__)).parent
log = logging.getLogger(__name__)

22 23
jinja_env = Environment(loader=PackageLoader(__package__, 'data'), autoescape=False)

Martin Jeřábek's avatar
Martin Jeřábek committed
24

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
class OptionsDict(dict):
    # def __getattr__(self, key):
    #     return self[key]

    def __iadd__(self, upper: dict):
        self.__merge(self, upper)
        return self

    def __add__(self, upper: dict) -> 'OptionsDict':
        res = copy.deepcopy(self)
        res += upper
        return res

    def __radd__(self, lower: dict) -> 'OptionsDict':
        res = copy.deepcopy(lower)
        res += self
        return res

    @classmethod
    def __merge(cls, lower, upper) -> None:
        if isinstance(lower, OptionsDict):
            if not isinstance(upper, OptionsDict):
                raise TypeError('Cannot merge {} and {}'.format(type(lower), type('upper')))

            for k, v in upper.items():
                if k not in lower:
                    lower[k] = v
                else:
                    cls.__merge(lower[k], v)
        elif isinstance(lower, list):
            if not isinstance(upper, list):
                raise TypeError('Cannot merge {} and {}'.format(type(lower), type('upper')))
            lower.extend(upper)
        else:
            raise TypeError('Cannot merge {} and {}'.format(type(lower), type('upper')))


62
class TestsBase:
63
    def __init__(self, ui, lib, config, build, base, create_ghws: bool):
64 65 66 67 68
        self.ui = ui
        self.lib = lib
        self.config = config
        self.build = build
        self.base = base
69
        self.create_ghws = create_ghws
70

71 72 73 74
    @property
    def jinja_env(self):
        return jinja_env

75
    def add_sources(self) -> None:
Martin Jeřábek's avatar
Martin Jeřábek committed
76 77
        raise NotImplementedError()

78 79 80 81 82
    def configure(self) -> bool:
        """Configure the tests.

        Return False if there were unconfigured tests found."""

Martin Jeřábek's avatar
Martin Jeřábek committed
83
        raise NotImplementedError()
84

85 86 87 88 89 90 91 92 93 94 95 96
    def generate_init_tcl(self, fname: str, tcomp: str) -> OptionsDict:
        tcl = self.build / fname
        with tcl.open('wt', encoding='utf-8') as f:
            print(dedent('''\
                global TCOMP
                set TCOMP {}
                '''.format(tcomp)), file=f)
        return OptionsDict({"modelsim.init_files.after_load": [str(tcl)]})

    def add_modelsim_gui_file(self, tb, cfg, name, tcl_init_files: List[str]) -> OptionsDict:
        """Return sim_options to add to the testcase."""
        sim_options = OptionsDict({'ghdl.sim_flags': []})
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
        if 'wave' in cfg:
            tcl = self.base / cfg['wave']
            if not tcl.exists():
                log.warn('Wave file {} not found'.format(cfg['wave']))
        else:
            tcl = self.build / 'modelsim_gui_{}.tcl'.format(name)
            with tcl.open('wt', encoding='utf-8') as f:
                print(dedent('''\
                    start_CAN_simulation "dummy"
                    global IgnoreAddWaveErrors
                    puts "Automatically adding common waves. Failures are handled gracefully."
                    set IgnoreAddWaveErrors 1
                    add_test_status_waves
                    add_system_waves
                    set IgnoreAddWaveErrors 0
                    run_simulation
                    get_test_results
                    '''.format(name)), file=f)
115

116
        sim_options["modelsim.init_file.gui"] = str(tcl)
117 118 119 120 121 122 123
        if 'gtkw' in cfg:
            gtkw = self.base / cfg['gtkw']
            if not gtkw.exists():
                log.warn('GTKW wave file {} not found'.format(cfg['gtkw']))
        else:
            gtkw = tcl.with_suffix('.gtkw')
            tclfname = tcl.relative_to(self.base)
124
            ghw_file = self.build / (tb.name+'.elab.ghw')
125
            wave_opt_file = tcl.with_suffix('.wevaopt.txt')
126 127 128 129 130
            # We need the GHW file for TCL -> GTKW conversion. If we are
            # generating them, there is no sense in actually doing
            # the conversion now.
            if self.create_ghws:
                log.info('Will generate {}'.format(ghw_file))
131
                sim_options["ghdl.sim_flags"] += ['--wave=' + str(ghw_file)]
132 133 134 135 136 137 138 139
            else:
                if not ghw_file.exists():
                    log.warning("Cannot convert wave file {} to gtkw, because"
                                " GHW file is missing. Run test with "
                                "--create-ghws.".format(tclfname))
                    gtkw = None
                else:
                    log.info('Converting wave file {} to gtkw ...'.format(tclfname))
140 141 142 143 144
                    used_signals = tcl2gtkw(str(tcl), tcl_init_files, str(gtkw), ghw_file)
                    with wave_opt_file.open('wt') as f:
                        f.write('$ version 1.1\n')
                        f.writelines('\n'.join(used_signals))
                    sim_options['ghdl.sim_flags'] += ['--read-wave-opt='+str(wave_opt_file)]
145
        if gtkw:
146
            try:
147 148
                tb.set_sim_option("ghdl.gtkwave_flags", [])
                sim_options["ghdl.gtkwave_flags"] = ['--save='+str(gtkw)]
149
            except ValueError:
150
                try:
151 152
                    tb.set_sim_option("ghdl.gtkw_file", "")
                    sim_options["ghdl.gtkw_file"] = str(gtkw)
153 154
                except ValueError:
                    log.warning('Setting GTKW file per test is not supported in this VUnit version.')
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        return OptionsDict(sim_options)

    def get_default_sim_options(self) -> OptionsDict:
        c, s = get_default_compile_and_sim_options()
        return s

    def add_psl_cov(self, name) -> OptionsDict:
        name = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
        psl_path = self.build / "functional_coverage" / "coverage_data" \
                    / "psl_cov_{}.json".format(name)
        sim_flags = ["--psl-report={}".format(psl_path)]
        return OptionsDict({"ghdl.sim_flags": sim_flags})

    @staticmethod
    def set_sim_options(tb, options: OptionsDict) -> None:
        for k, v in options.items():
            tb.set_sim_option(k, v)

173

174
def add_sources(lib, patterns) -> None:
175 176 177 178 179 180 181
    for pattern in patterns:
        p = join(str(d.parent), pattern)
        log.debug('Adding sources matching {}'.format(p))
        for f in glob(p, recursive=True):
            if f != "tb_wrappers.vhd":
                lib.add_source_file(str(f))

182

183
def add_common_sources(lib, ui) -> None:
Ille, Ondrej, Ing.'s avatar
Ille, Ondrej, Ing. committed
184 185 186
    add_sources(lib, ['../src/**/*.vhd'])
    ui.enable_check_preprocessing()
    ui.enable_location_preprocessing() #(additional_subprograms=['log'])
187
    add_sources(lib, ['*.vhd', 'lib/*.vhd', 'models/*.vhd'])
188

Martin Jeřábek's avatar
Martin Jeřábek committed
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
def get_default_compile_and_sim_options() -> Tuple[OptionsDict, OptionsDict]:
    # TODO: move to config
    debug = True
    coverage = True
    psl = True

    compile_flags = []  # type: List[str]
    elab_flags = ["-Wl,-no-pie"]

    if debug:
        compile_flags += ['-g']
        elab_flags += ['-g']
    if coverage:
        compile_flags += ["-fprofile-arcs", "-ftest-coverage"]
        elab_flags += ["-Wl,-lgcov", "-Wl,--coverage"]
    if psl:
        compile_flags += ['-fpsl']
        elab_flags += ['-fpsl']

    compile_options = OptionsDict()
    compile_options["ghdl.flags"] = compile_flags

    cmif = ['../lib/test_lib.tcl', 'modelsim_init.tcl']
    common_modelsim_init_files = [str(d/x) for x in cmif]
    sim_options = OptionsDict({
        "ghdl.elab_flags": elab_flags,
        "modelsim.init_files.after_load": common_modelsim_init_files,
        "ghdl.sim_flags": ["--ieee-asserts=disable-at-0"],
    })
    return compile_options, sim_options


def get_compile_options() -> OptionsDict:
    c, s = get_default_compile_and_sim_options()
    return c
225

Martin Jeřábek's avatar
Martin Jeřábek committed
226

227
def get_seed(cfg) -> int:
228 229 230
    if 'seed' in cfg and 'randomize' in cfg:
        log.warning('Both "seed" and "randomize" are set - seed takes precedence')
    if 'seed' in cfg:
231
        seed = int(cfg['seed'], 0)
232 233 234 235 236 237 238 239
    elif cfg.get('randomize', False):
        # only 31 bits
        seed = int(random.random() * 2**31) & 0x7FFFFFFF
    else:
        seed = 0
    return seed


240
def dict_merge(up, *lowers) -> None:
241 242 243 244 245
    for lower in lowers:
        for k, v in lower.items():
            if k not in up:
                up[k] = v

Martin Jeřábek's avatar
Martin Jeřábek committed
246

247
def vhdl_serialize(o) -> str:
248 249 250 251 252 253 254 255
    if isinstance(o, Iterable):
        ss = []
        for x in o:
            ss.append(vhdl_serialize(x))
        return ''.join(['(', ', '.join(ss), ')'])
    else:
        return str(o)

Martin Jeřábek's avatar
Martin Jeřábek committed
256

257
def dump_sim_options(lib) -> None:
258 259 260 261 262 263
    for tb in lib.get_test_benches('*'):
        for cfgs in tb._test_bench.get_configuration_dicts():
            for name, cfg in cfgs.items():
                print('{}#{}:'.format(tb.name, name))
                #pprint(cfg.__dict__)
                pprint(cfg.sim_options)