File size: 3,446 Bytes
e8b8483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""The generated RTL carries the selected dims and computes the comparison.

The structural checks run everywhere. The simulation checks run when Icarus
Verilog is available, on PATH or via the IVERILOG and VVP environment variables.
"""
import os
import re
import shutil
import subprocess

import pytest

from conftest import REPO, load

RTL = REPO / 'rtl'
RULES = load('rules.json')['rules']
NAMES = sorted(RULES)
N_VECTORS = 256


def tool(name, env_var):
    return os.environ.get(env_var) or shutil.which(name)


IVERILOG, VVP = tool('iverilog', 'IVERILOG'), tool('vvp', 'VVP')
needs_sim = pytest.mark.skipif(not (IVERILOG and VVP),
                               reason='Icarus Verilog not found')


def source(name):
    return (RTL / f'person_{name}.v').read_text(encoding='utf-8')


@pytest.mark.parametrize('name', NAMES)
def test_module_exists(name):
    assert (RTL / f'person_{name}.v').exists()


@pytest.mark.parametrize('name', NAMES)
def test_ports_are_exactly_the_selected_dims(name):
    r = RULES[name]
    declared = re.findall(r'\bf(\d+)\b', source(name).split(');')[0])
    assert sorted(int(d) for d in set(declared)) == sorted(r['pos_dims'] + r['neg_dims'])


@pytest.mark.parametrize('name', NAMES)
def test_no_constant_is_baked_in(name):
    """A zero-parameter rule must not contain a fitted threshold."""
    body = source(name).split('\n')
    body = '\n'.join(l for l in body if not l.strip().startswith('//'))
    assert 'localparam' not in body, f'{name} declares a constant'
    for lit in re.findall(r"\d+'s?d(\d+)", body):
        assert int(lit) == 0, f'{name} compares against a non-zero constant'


def vectors(n, dims, seed=0):
    import random
    rng = random.Random(seed)
    return [{d: rng.randint(-128, 127) for d in dims} for _ in range(n)]


def reference(vec, pos, neg):
    return sum(vec[d] for d in pos) > sum(vec[d] for d in neg)


@needs_sim
@pytest.mark.parametrize('name', NAMES)
def test_rtl_matches_the_reference(tmp_path, name):
    r = RULES[name]
    dims = r['pos_dims'] + r['neg_dims']
    vecs = vectors(N_VECTORS, dims)
    # Half the vectors are pushed onto the boundary, where > must reject ties.
    for i in range(0, len(vecs), 2):
        v = vecs[i]
        v[r['pos_dims'][0]] = (sum(v[d] for d in r['neg_dims'])
                               - sum(v[d] for d in r['pos_dims'][1:]))
    vecs = [v for v in vecs if all(-128 <= x <= 127 for x in v.values())]

    top = f'person_{name}'
    conns = ',\n        '.join(f'.f{d}(f{d})' for d in dims)
    decls = ', '.join(f'f{d}' for d in dims)
    lines = []
    for v in vecs:
        lines.append('    ' + ' '.join(f'f{d} = {v[d]};' for d in dims) + ' #1;'
                     ' $display("%b", out);')
    tb = f'''`timescale 1ns/1ps
module tb;
  reg signed [7:0] {decls};
  wire out;
  {top} dut (
        {conns},
        .person_present(out));
  initial begin
{chr(10).join(lines)}
    $finish;
  end
endmodule
'''
    (tmp_path / 'tb.v').write_text(tb)
    subprocess.run([IVERILOG, '-g2005', '-o', 'tb.vvp',
                    str(RTL / f'{top}.v'), 'tb.v'],
                   cwd=tmp_path, check=True, capture_output=True)
    out = subprocess.run([VVP, 'tb.vvp'], cwd=tmp_path, check=True,
                         capture_output=True, text=True).stdout
    got = [l.strip() == '1' for l in out.splitlines() if l.strip() in ('0', '1')]
    assert got == [reference(v, r['pos_dims'], r['neg_dims']) for v in vecs]