style: fix lint errors + reformat source with black

This commit is contained in:
Uri Shaked
2023-09-21 22:30:52 +03:00
parent 7d48aab542
commit aa8f760375
14 changed files with 1285 additions and 954 deletions
+439 -456
View File
@@ -1,17 +1,17 @@
'''
TruthTable testing
Copyright (C) 2023 Pat Deegan, https://psychogenic.com
"""
TruthTable testing
Copyright (C) 2023 Pat Deegan, https://psychogenic.com
A simple system to take truth table data from yosys, like
A simple system to take truth table data from yosys, like
ui_in | uo_out
---------- | -----------
8'11111000 | 8'xxxxxxx0
8'11111001 | 8'xxxxxxx1
8'11111010 | 8'xxxxxxx0
...
or manually crafted combi- or clocked sequences, encapsulated in
ui_in | uo_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 |
@@ -24,479 +24,462 @@
| --- --- tc | -- 11100 - | locked |
...
and run it through cocotb harness.
and run it through cocotb harness.
SAMPLE USAGE:
@cocotb.test()
async def truthTableCompare(dut):
i_bus = dut.ui_in
o_bus = dut.uo_out
tt = truthtable.loadSimpleTruthTable('test/truthtable.txt')
await tt.testAll(i_bus, o_bus, dut._log)
@cocotb.test()
async def truthTableCompare(dut):
i_bus = dut.ui_in
o_bus = dut.uo_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 logging
import re
import logging
from string import Template as StringTemplate
from cocotb.binary import BinaryValue
from cocotb.triggers import Timer
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
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}'
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]
"""
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 __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}"
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))
"""
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 validateAndClean(self, val: str, usingRegex):
if val is None or not len(val):
return None
def inputFrom(self, val:str):
return self.validateAndClean(val, self.inputValueValidatorRe)
mtch = usingRegex.match(val)
def outputFrom(self, val:str):
if val is None or not len(val):
return None
return self.validateAndClean(val, self.outputValueValidatorRe)
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))
"""
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)
"""
A specialization of the TestTableParser that handles the
particulars of extracting test inputs/outputs from a markdown
table.
"""
# 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 __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.
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
| 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)
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
self.columnSeperatorRe = columnSeparatorRegex
eol = r"(\r\n|\r|\n)"
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
# 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 loadMarkdownTruthTable(filepath:str, logger=None):
with open(filepath, 'r') as f:
contents = f.read()
return parseMarkdownTable(contents, logger)
return None
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))
TruthTableExample = '''
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 = """
\\ui_in | \\uo_out
---------- | -----------
8'11111000 | 8'xxxxxxx0
@@ -508,10 +491,10 @@ TruthTableExample = '''
8'11111110 | 8'xxxxxxx1
8'11111111 | 8'xxxxxxx0
'''
"""
# with comments and spacing
TruthTableMarkdownExample = '''
TruthTableMarkdownExample = """
|IN: CBA RC | output | comment |
|--------------|--------------|-----------|
| 000 000 00 | -- ----- - | init |
@@ -528,12 +511,12 @@ TruthTableMarkdownExample = '''
| --- 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}')
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}')
+10 -11
View File
@@ -1,28 +1,27 @@
'''
"""
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.
'''
"""
truthTableCompare -- loads markdown truth table and, if load succeeded, actually performs the tests.
"""
usermodule = parentDUT.dut
i_bus = parentDUT.ui_in
o_bus = parentDUT.uo_out
tt = truthtable.loadMarkdownTruthTable('truthtable.md', usermodule._log)
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.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)