Merge pull request #8 from psychogenic/tt03

test framework
This commit is contained in:
matt venn
2023-04-11 18:11:55 +02:00
committed by GitHub
7 changed files with 847 additions and 8 deletions
+85 -8
View File
@@ -3,6 +3,9 @@ import logging
import requests
import os
import subprocess
import shutil
import glob
import re
def load_yaml(args):
@@ -39,13 +42,75 @@ def fetch_file(url, filename):
r = requests.get(url)
if r.status_code != 200:
logging.warning("couldn't download {}".format(url))
exit(1)
return False
with open(filename, 'wb') as fh:
logging.info("written to {}".format(filename))
fh.write(r.content)
return True
def fetch_wokwi_file(wokwi_id:int, file_requested:str,
destination_name:str = None):
'''
fetch_wokwi_file -- perform a request on the wokwi api to
retrieve a file from a given project.
@param wokwi_id: project id (integer)
@param file_requested: the specific file requested for the
project, e.g. verilog or diagram.json
@param destination_name: file to write out to with result
'''
if destination_name is None or not len(destination_name):
destination_name = file_requested
url = f'https://wokwi.com/api/projects/{wokwi_id}/{file_requested}'
return fetch_file(url, destination_name)
def install_wokwi_testing(wokwi_id:int, destination_dir:str='src',
resource_dir:str=None):
wokwi_id_str = str(wokwi_id)
#wokwi_user_module = f'user_module_{wokwi_'
if resource_dir is None or not len(resource_dir):
resource_dir = os.path.join(os.path.dirname(__file__), 'testing')
# directories in testing/lib to copy over
pyLibsDir = os.path.join(resource_dir, 'lib')
# src template dir
srcTplDir = os.path.join(resource_dir, 'src-tpl')
for libfullpath in glob.glob(os.path.join(pyLibsDir, '*')):
logging.info(f'Copying {libfullpath} to {destination_dir}')
shutil.copytree(
libfullpath,
os.path.join(destination_dir, os.path.basename(libfullpath)),
dirs_exist_ok=True)
for srcTemplate in glob.glob(os.path.join(srcTplDir, '*')):
with open(srcTemplate, 'r') as f:
contents = f.read()
customizedContents = re.sub('WOKWI_ID', wokwi_id_str, contents)
outputFname = os.path.basename(srcTemplate)
with open(os.path.join(destination_dir, outputFname), 'w') as outf:
logging.info(f'writing src tpl to {outputFname}')
outf.write(customizedContents)
outf.close()
def get_project_source(yaml):
# wokwi_id must be an int or 0
try:
@@ -56,15 +121,27 @@ def get_project_source(yaml):
# it's a wokwi project
if wokwi_id != 0:
url = "https://wokwi.com/api/projects/{}/verilog".format(wokwi_id)
src_file = "user_module_{}.v".format(wokwi_id)
fetch_file(url, os.path.join("src", src_file))
src_file = f'user_module_{wokwi_id}.v'
if not fetch_wokwi_file(wokwi_id, 'verilog',
os.path.join("src", src_file) ):
logging.error(f'Could not fetch verilog file for wokwi project {wokwi_id}')
exit(1)
# also fetch the wokwi diagram
url = "https://wokwi.com/api/projects/{}/diagram.json".format(wokwi_id)
diagram_file = "wokwi_diagram.json"
fetch_file(url, os.path.join("src", diagram_file))
if not fetch_wokwi_file(wokwi_id, 'diagram.json',
os.path.join("src", 'wokwi_diagram.json')):
logging.error(f'Could not fetch diagram.json file for wokwi project {wokwi_id}')
exit(1)
# attempt to download the *optional* truthtable for the project
if fetch_wokwi_file(wokwi_id, 'truthtable.md',
os.path.join('src', 'truthtable.md')):
logging.info(f'Wokwi project {wokwi_id} has a truthtable included, will test!')
install_wokwi_testing(wokwi_id)
return [src_file, 'cells.v']
# else it's HDL, so check source files
+136
View File
@@ -0,0 +1,136 @@
### Wokwi/TinyTapeout testing system ###
A system has been devised to allow for simplified verification and testing of wokwi projects.
In this initial iteration, the process basically involves creating a 'truth table' type description of desired inputs and expected outputs, which allows for testing of both combinatorial and sequential/clocked circuits.
The system is designed to allow for simple extensions to this base functionality: the testing framework itself will be described below to assist in expanding it to encompass additional tests and validation of wokwi or other TT projects.
## Truth-table system ##
In its simplest incarnation, the truth-table defines a set of inputs provided to the circuit and describes the expected outputs.
The table itself is written as markdown, consisting of
* a header
* a separator
* the table rows
using markdown formatting. Eg
| inputs | outputs |
|-----------|-----------|
| 1111 0000 | xxxx 1100 |
etc
Inputs are in the left-side column, expected outputs on the right, specified
as MSB (i.e. bit 7 to 0, from left to right).
Any bits specified as 'x' or '-' are treated as "don't change" for
inputs and "don't care" for outputs.
The header is ignored, as are whitespaces, so you may format as appropriate.
Each row must have a least 8 non-whitespace bit specifications per column. E.g.
for a 2 bit adder, a table might consist of
| Cin B A | Cout S |
|---------------------|-------------|
| ----- 0 0 0 |-- 0 ---- 0 |
| ----- 0 0 1 |-- 0 ---- 1 |
| ----- 0 1 0 |-- 0 ---- 1 |
| ----- 0 1 1 |-- 1 ---- 0 |
| ----- 1 0 0 |-- 0 ---- 1 |
| ----- 1 0 1 |-- 1 ---- 0 |
| ----- 1 1 0 |-- 1 ---- 0 |
| ----- 1 1 1 |-- 1 ---- 1 |
A ‘t’ or ‘T’ bit will be toggled from whatever it was before.
An empty output column will apply the input, but not perform any test (i.e. behave the same as 8 ‘x’/’-’).
### Clocked designs ####
In addition to maintaining state for toggling the clock, one or more ‘c’ bits in an input column will cause the system to:
1) Set any bits specified, including toggling an “t” bits, without changing the ‘c’ bits (setup);
2) It will then toggle all ‘c’ bits, inverting them, but not changing anything else from previous step; and finally
3) It will toggle the ‘c’ bits again, returning them to their original state.
This means setup and hold times are respected and signals may be clocked in with a single row in the table, rather than having 3.
Any entry with a bit set to 'c' will:
* setup any specified bits (0 or 1) and toggle any bits as required
* then toggle (i.e. invert) the clock bit(s), without changing anything else
* then re-toggle the same clock bit(s), returning them to their original state
|IN: CBA RC | output | comment |
|--------------|--------------|-----------|
| 000 000 00 | -- ----- - | init |
| --- --- 1c | -- ----- - | reset |
| --- --- 0c | -- ----- - | |
| --- 111 -c | -- 11100 - | |
| --- 110 -c | -- 11111 - | success |
| --- 000 tc | -- ----- - | reset |
| --- --- tc | -- 11100 - | locked |
| --- 111 -c | -- 11100 - | bad combo |
| --- 100 -c | -- 11100 - | bad combo |
| --- 101 -c | -- 11100 - | bad combo |
| --- 100 -c | -- 11100 - | bad combo |
| --- 110 -c | -- 11111 - | success |
| --- 000 -c | -- 11111 - | still open|
| --- --- 1c | -- 11100 - | reset |
=============================
Note that using ‘c’ is optional. A row like
|IN: CBA RC |   output   | comment   |
|--------------|--------------|-----------|
| --- 110 -c | -- 11111 - | success |
Could be done manually, with
|IN: CBA RC |   output   | comment   |
|--------------|--------------|-----------|
| --- 110 -- | -- ----- - | set combo |
| --- --- -1 | -- ----- - | clock |
| --- --- -0 | -- 11111 - | success |
### Test framework function ###
The tt-tools system, in addition to downloading verilog and JSON for wokwi projects, now attempts to download a file name truthtable.md.
If this file is found, it assumes that te truthtable test must be run. To do this, the system now:
1) copies any directories found under testing/lib to the src/ directory, assuming these are python support packages; and
2) for each file in testing/src-tpl it will read in the file, replace any instance of WOKWI_ID with the appropriate id, and then write out a corresponding file into the actual src/ directory.
From this point, all that should be required is to run a make from within the src directory to launch cocotb with the test necessary to put the table through its paces.
2023-04-06 Pat Deegan
View File
+539
View File
@@ -0,0 +1,539 @@
'''
TruthTable testing
Copyright (C) 2023 Pat Deegan, https://psychogenic.com
A simple system to take truth table data from yosys, like
io_in | io_out
---------- | -----------
8'11111000 | 8'xxxxxxx0
8'11111001 | 8'xxxxxxx1
8'11111010 | 8'xxxxxxx0
...
or manually crafted combi- or clocked sequences, encapsulated in
markdown tables such as
|IN: CBA RC | output | comment |
|--------------|--------------|-----------|
| --- --- 1c | -- ----- - | reset |
| --- --- 0c | -- ----- - | |
| --- 111 -c | -- 11100 - | |
| --- 110 -c | -- 11111 - | success |
| --- 000 tc | -- ----- - | reset |
| --- --- tc | -- 11100 - | locked |
...
and run it through cocotb harness.
SAMPLE USAGE:
@cocotb.test()
async def truthTableCompare(dut):
i_bus = dut.io_in
o_bus = dut.io_out
tt = truthtable.loadSimpleTruthTable('test/truthtable.txt')
await tt.testAll(i_bus, o_bus, dut._log)
SPDX-FileCopyrightText: © 2023 Pat Deegan, https://psychogenic.com
SPDX-License-Identifier: Apache2.0
'''
from string import Template as StringTemplate
from cocotb.triggers import Timer
from cocotb.binary import BinaryValue
import re
import logging
log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class SaneBinaryValue(BinaryValue):
def __init__(self, vstr:str):
super().__init__(vstr, n_bits=len(vstr), bigEndian=False)
numbits = len(vstr)
self.hardBit = [True]*numbits
for i in range(numbits):
c = vstr[i]
if c != '1' and c != '0':
self.hardBit[(numbits - 1) - i] = False
class TruthMapping:
def __init__(self, resultingValue:str):
self.result = SaneBinaryValue(resultingValue)
def __str__(self):
return f'Expecting {self.result.binstr}'
class TruthTable:
'''
Holds the sequence/expected mapping of inputs -> outputs
and provides an async testAll() to perform the testing.
'''
def __init__(self, interStepDelay:int=10, interStepTimeUnits:str='ns'):
self.mappings = []
self.stepDelayTime = interStepDelay
self.stepDelayUnits = interStepTimeUnits
def addMapping(self, relationship:TruthMapping):
self.mappings.append(relationship)
def getMapping(self, idx:int) -> TruthMapping:
if idx >= len(self.mappings):
raise IndexError('out of bounds on mapping')
return self.mappings[idx]
def numMappings(self):
return len(self.mappings)
def __len__(self):
return self.numMappings()
def __getitem__(self, idx:int):
return self.getMapping(idx)
def __str__(self):
numsteps = len(self)
retStrs = [f"Truthtable with {numsteps} mappings"]
for i in range(numsteps):
step = self[i]
retStrs.append(f'{i + 1}\t{str(step)}')
return '\n'.join(retStrs)
def dump(self):
print(str(self))
async def testAll(self, i_bus, o_bus, logger=None):
for i in range(len(self)):
i_bus.value = self[i].state
await Timer(self.stepDelayTime, units=self.stepDelayUnits) # wait a tad
logger.debug(f'Set i_bus to {i_bus.value} vs {self[i].state}')
for idx in range(8):
logger.debug(f'{i}-{idx}: i_bus[{idx}] == {i_bus[idx]} == {self[i].state[idx]}')
expectedResult = self[i].result
if logger is not None:
logger.info(f'State {i}, setting input to {self[i].state}, expecting {expectedResult} (got {o_bus.value})')
# this is so stupid that it's probably wrong... there _must_ be
# a good way to do this, otherwise _what_ is the point of having
# unknown and don't care... but anyway,
# will die if o_bus[n] is some defined value
# but we expect a don't know (x) or even a don't care (-), ugh
# manual style
for bit in range(len(o_bus)):
if expectedResult.hardBit[bit]:
# ok, safe to compare, duh
# logger.info(f'Doing bit {bit}: {o_bus[bit]} == {expectedResult[bit]}')
assert o_bus[bit] == expectedResult[bit]
class OneToOneTruthMapping(TruthMapping):
def __init__(self, state:str, output:str):
super().__init__(output)
self.state = SaneBinaryValue(state)
def __str__(self):
return f'Set {self.state.binstr}, expect {self.result.binstr}'
class TestTableEntryParser:
'''
A little class that validates and cleans up
input and output specifications.
'''
def __init__(self, logger):
self.log = logger
self.optionalPrefix = r"(\d'|0b)?"
self.validInputValues = r'(\s*[01tTcCxX-]\s*){8}' # 0/1 hard-coded, t/T toggle, c/C clocking, x or - unchanged
self.validOutputValues = r'(\s*[01xX-]\s*){8}' # 0/1 hard-coded,x or - don't care
valueMatchReTpl = StringTemplate(r'\s*$prefix($valmatch)')
self.inputValueValidatorRe = re.compile(
valueMatchReTpl.substitute(
prefix=self.optionalPrefix, valmatch=self.validInputValues))
self.outputValueValidatorRe = re.compile(
valueMatchReTpl.substitute(
prefix=self.optionalPrefix, valmatch=self.validOutputValues))
def validateAndClean(self, val:str, usingRegex):
if val is None or not len(val):
return None
mtch = usingRegex.match(val)
if mtch is None:
vcln = val.strip()
if not len(vcln):
# just whitespace
return None
if vcln.find('#') >= 0 or vcln.find('//') >=0:
# just a comment
return None
if vcln.find('1') >=0 or vcln.find('0') >=0 or vcln.find('x') >= 0:
self.log.error(f"Bad value passed ? '{val}'")
return None
return re.sub(r'\s+', '', mtch.group(0)).lower()
def inputFrom(self, val:str):
return self.validateAndClean(val, self.inputValueValidatorRe)
def outputFrom(self, val:str):
if val is None or not len(val):
return None
return self.validateAndClean(val, self.outputValueValidatorRe)
class TestTableParser:
'''
Base class for tabular configuration values for input sequence and
expected outputs.
This class handles maintaining the truth table and the state between
steps in a sequence.
Basically does everything except handle the particularities of a
give table format (e.g. markdown).
'''
def __init__(self, logger=None, numIOBits:int=8):
if logger is None:
logger = logging.getLogger(__name__)
self.log = logger
self.numIOBits = numIOBits
self.valueParser = TestTableEntryParser(self.log)
self._truthTable = None
self._textTable = []
self._currentInput = SaneBinaryValue('0'*numIOBits)
self._ignoreOutput = SaneBinaryValue('-'*numIOBits)
def reset(self):
self._currentInput = SaneBinaryValue('00000000')
self._truthTable = None
self._textTable = []
@property
def truthTable(self) -> TruthTable:
if self._truthTable is None:
self._truthTable = TruthTable()
return self._truthTable
def _inputStateFromBits(self, inputBits):
newBits = ['0'] * self.numIOBits
for i in range(self.numIOBits):
if inputBits[i] == '0' or inputBits[i] == '1':
newBits[i] = inputBits[i]
self._currentInput[i] = int(inputBits[i])
elif inputBits[i] == 't':
# toggled
self._currentInput[i] = ~self._currentInput[i]
newBits[i] = str(self._currentInput[i])
else:
# unchanged
newBits[i] = str(self._currentInput[i])
return ''.join(newBits)
def addStep(self, inputValue:str, outputValue:str=''):
inputBits = self.valueParser.inputFrom(inputValue)
if inputBits is None or not len(inputBits):
self.log.info(f'Unable to parse input bits: {inputValue}')
return False
if inputBits.find('c') >= 0:
# clocking...
setupInputs = inputBits.replace('c', 'x')
self.log.debug(f'Have clocking in inputs {inputBits}, adding setup step {setupInputs}')
self.addStep(setupInputs) # setup
# clock edge 1
clockEdges = inputBits.replace('t', 'x') # don't toggle twice!
clockInAndHold = clockEdges.replace('c', 't')
self.log.debug(f'Now clock {clockEdges} x2')
self.addStep(clockInAndHold) # clock in
# clock edge 2
self.addStep(clockInAndHold) # clock out
inputBinVal = SaneBinaryValue(self._inputStateFromBits(inputBits))
outputBinVal = self._ignoreOutput
outputBits = ''
if outputValue is not None and len(outputValue):
self.log.info(f'outval is "{outputValue}"')
outputBits = self.valueParser.outputFrom(outputValue)
if outputBits is None or not len(outputBits):
self.log.info(f'Unable to parse output bits: {outputBits}')
return False
outputBinVal = SaneBinaryValue(outputBits)
self.log.info(f'Mapped {inputBits} -> {outputBits} ({inputBinVal} -> {outputBinVal})')
self.truthTable.addMapping(OneToOneTruthMapping(inputBinVal.binstr, outputBinVal.binstr))
self._textTable.append((inputBits, outputBits))
return True
def generateFrom(self, contents:str):
self.reset() # reset the table
self.log.error("Override generateFrom() in subclass")
return None
def __str__(self):
retStrs = [f'Truth table parser with {len(self._textTable)} steps added.']
for i in range(len(self._textTable)):
step = self._textTable[i]
if step[1] is not None and len(step[1]):
retStrs.append(f'{i+1}\t{step[0]} -> {step[1]}')
else:
retStrs.append(f'{i+1}\t{step[0]}')
return '\n'.join(retStrs)
def dump(self):
print(str(self))
class MarkdownTestTableParser(TestTableParser):
'''
A specialization of the TestTableParser that handles the
particulars of extracting test inputs/outputs from a markdown
table.
'''
def __init__(self, columnSeparatorRegex:str=r'\|', logger=None):
'''
We want to support markdown tables.
These may have some header row, followed by a separator (which may
or may not include aligment info, i.e. ':'), finally followed
by our content of interest. E.g.
| some name | another | <- column headers
|-----------|---------:| <- header sep
| CONTENT | CONTENT | <- actual content of interest
CONTENT is 8 bits, MSB
[7 ... 0]
A B C D E F G H
Where each bit is either:
0
1
X (or x) don't change/don't care
T (or t) toggle
C (or c) clock
...
'''
super().__init__(logger)
self.columnSeperatorRe = columnSeparatorRegex
eol = r'(\r\n|\r|\n)'
# the |-------|----- ... line
tableHeaderSepTpl = StringTemplate(r'\s*($sep\s*:?-{1,}:?\s*){2,}$sep$eol')
self.headerSepRe = re.compile(tableHeaderSepTpl.substitute(sep=self.columnSeperatorRe, eol=eol), re.M)
# each content line thereafter
# | CONTENT | CONTENT |
contentRowTpl = StringTemplate(r'^\s*(($sep\s*[^$sep\r\n]+){1,})$sep$eol')
self.contentRowRe = re.compile(contentRowTpl.substitute(sep=self.columnSeperatorRe, eol=eol), re.M)
self.rowItemSplitterRe = re.compile(self.columnSeperatorRe)
def parseMarkdownTable(self, contents:str):
headerSepSearch = self.headerSepRe.search(contents)
if headerSepSearch is not None:
# did have a header
# dump it, and everything before it
headerSpan = headerSepSearch.span()
headerEndPos = headerSpan[1]
self.log.debug(f"Dumping header to {headerEndPos}\n{contents[:headerEndPos]}")
contents = contents[headerEndPos:]
# extract valid rows in first pass
self.log.debug(f"Content row RE is {self.contentRowRe}")
contentRowMatches = self.contentRowRe.findall(contents)
if contentRowMatches is None or not len(contentRowMatches):
self.log.error('No valid rows found in markdown table')
return False
numSteps = 0
for aRow in contentRowMatches:
self.log.debug(f'Matched row {aRow}')
entries = self.rowItemSplitterRe.split(aRow[0])
# note, we have entries like
# SEP AAA SEP BBB SEP
# so splitting on SEP gives us an empty entry at front of list
# hence the requirement for len == 2 for at least 1 entry, len == 3
# for input and output.
if len(entries) < 2:
self.log.info(f'Row has insufficient entries ? ("{aRow[0]}")')
continue
inVal = entries[1]
outVal = None
if len(entries) > 2 and len(entries[2]):
outVal = entries[2].strip()
if self.addStep(inVal, outVal):
numSteps += 1
return numSteps
def generateFrom(self, contents:str):
self.reset()
tableParsed = self.parseMarkdownTable(contents)
if tableParsed is None or not tableParsed:
self.log.error("Could not extract truthtable from contents")
return None
return self.truthTable
def parseSimpleTable(contents:str):
tt = TruthTable()
m = re.compile(r'''^\s*\d'(\d+)\s*\|\s*\d'([zZxX\d]+)''', re.M)
for match in m.findall(contents):
if match[1] != 'x':
# we skip anything where all results are don't care
# result = match[1].replace('x', '-') # now using the BinaryValue override
result = match[1]
tt.addMapping(OneToOneTruthMapping(match[0],result))
return tt
def parseMarkdownTable(contents:str, logger=None):
m = MarkdownTestTableParser(logger=logger)
table = m.generateFrom(contents)
return table
def loadSimpleTruthTable(filepath:str):
with open(filepath, 'r') as f:
contents = f.read()
return parseSimpleTable(contents)
return None
def loadMarkdownTruthTable(filepath:str, logger=None):
with open(filepath, 'r') as f:
contents = f.read()
return parseMarkdownTable(contents, logger)
return None
TruthTableExample = '''
\\io_in | \\io_out
---------- | -----------
8'11111000 | 8'xxxxxxx0
8'11111001 | 8'xxxxxxx1
8'11111010 | 8'xxxxxxx0
8'11111011 | 8'xxxxxxx1
8'11111100 | 8'xxxxxxx0
8'11111101 | 8'xxxxxxx1
8'11111110 | 8'xxxxxxx1
8'11111111 | 8'xxxxxxx0
'''
# with comments and spacing
TruthTableMarkdownExample = '''
|IN: CBA RC | output | comment |
|--------------|--------------|-----------|
| 000 000 00 | -- ----- - | init |
| --- --- 1c | -- ----- - | reset |
| --- --- 0c | -- ----- - | |
| --- 111 -c | -- 11100 - | |
| --- 110 -c | -- 11111 - | success |
| --- 000 tc | -- ----- - | reset |
| --- --- tc | -- 11100 - | locked |
| --- 111 -c | -- 11100 - | bad combo |
| --- 100 -c | -- 11100 - | bad combo |
| --- 101 -c | -- 11100 - | bad combo |
| --- 100 -c | -- 11100 - | bad combo |
| --- 110 -c | -- 11111 - | success |
| --- 000 -c | -- 11111 - | still open|
| --- --- 1c | -- 11100 - | reset |
'''
if __name__ == "__main__":
m = MarkdownTestTableParser()
table = m.generateFrom(TruthTableMarkdownExample)
m.dump()
table.dump()
#tt = parseSimpleTable(TruthTableExample)
#print(f'with state {tt[0].state} you get {tt[0].result}')
+37
View File
@@ -0,0 +1,37 @@
# Makefile
# See https://docs.cocotb.org/en/stable/quickstart.html for more info
# defaults
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
# normal simulation
ifneq ($(GATES),yes)
# this is the only part you should need to modify:
VERILOG_SOURCES += $(PWD)/cells.v $(PWD)/user_module_WOKWI_ID.v $(PWD)/test_wokwi.v
else
# gate level simulation requires some extra setup, you shouldn't need to touch this
COMPILE_ARGS += -DGL_TEST
COMPILE_ARGS += -DFUNCTIONAL
COMPILE_ARGS += -DUSE_POWER_PINS
COMPILE_ARGS += -DSIM
COMPILE_ARGS += -DUNIT_DELAY=#1
VERILOG_SOURCES += $(PDK_ROOT)/sky130A/libs.ref/sky130_fd_sc_hd/verilog/primitives.v
VERILOG_SOURCES += $(PDK_ROOT)/sky130A/libs.ref/sky130_fd_sc_hd/verilog/sky130_fd_sc_hd.v
# this gets copied in by the GDS action workflow
VERILOG_SOURCES += $(PWD)/test_wokwi.v $(PWD)/gate_level_netlist.v
endif
# TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file
TOPLEVEL = test_wokwi
# MODULE is the basename of the Python test file
MODULE = test
# include cocotb's make rules to take care of the simulator setup
include $(shell cocotb-config --makefiles)/Makefile.sim
+28
View File
@@ -0,0 +1,28 @@
'''
Wokwi TinyTapeout truth-table based auto test sequence.
SPDX-FileCopyrightText: © 2023 Pat Deegan, https://psychogenic.com
SPDX-License-Identifier: Apache2.0
'''
import os
import cocotb
import testutils.truthtable as truthtable
@cocotb.test()
async def truthTableCompare(parentDUT):
'''
truthTableCompare -- loads markdown truth table and, if load succeeded, actually performs the tests.
'''
usermodule = parentDUT.dut
i_bus = parentDUT.io_in
o_bus = parentDUT.io_out
tt = truthtable.loadMarkdownTruthTable('truthtable.md', usermodule._log)
if tt is None:
usermodule._log.info('No truth table loaded, no table compare test to run')
return
usermodule._log.debug(str(tt))
await tt.testAll(i_bus, o_bus, usermodule._log)
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache2.0
`timescale 1ns / 1ps
`default_nettype none
module test_wokwi ();
wire [7:0] io_out;
wire [7:0] io_in;
user_module_WOKWI_ID dut (
`ifdef GL_TEST
.vccd1( 1'b1),
.vssd1( 1'b0),
`endif
.io_in (io_in),
.io_out(io_out)
);
initial begin
$dumpfile("wokwi_tb_WOKWI_ID.vcd");
$dumpvars(0, test_wokwi);
end
endmodule