mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
style: fix lint errors + reformat source with black
This commit is contained in:
+439
-456
@@ -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}')
|
||||
|
||||
Reference in New Issue
Block a user