From ddee08c8052dabf132801c7522264f745391b3d4 Mon Sep 17 00:00:00 2001 From: psychogenic Date: Tue, 11 Apr 2023 02:25:33 -0400 Subject: [PATCH] test framework --- project.py | 93 ++++- testing/README.md | 129 +++++++ testing/lib/testutils/__init__.py | 0 testing/lib/testutils/truthtable.py | 539 ++++++++++++++++++++++++++++ testing/src-tpl/Makefile | 37 ++ testing/src-tpl/test.py | 28 ++ testing/src-tpl/test_wokwi.v | 22 ++ 7 files changed, 840 insertions(+), 8 deletions(-) create mode 100644 testing/README.md create mode 100644 testing/lib/testutils/__init__.py create mode 100644 testing/lib/testutils/truthtable.py create mode 100644 testing/src-tpl/Makefile create mode 100644 testing/src-tpl/test.py create mode 100644 testing/src-tpl/test_wokwi.v diff --git a/project.py b/project.py index d252111..f9be4c2 100644 --- a/project.py +++ b/project.py @@ -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 diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..cdd12cd --- /dev/null +++ b/testing/README.md @@ -0,0 +1,129 @@ +### 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 + + +| --- 110 -c | -- 11111 - | success | + +Could be done manually, with + + +| --- 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 + + diff --git a/testing/lib/testutils/__init__.py b/testing/lib/testutils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/lib/testutils/truthtable.py b/testing/lib/testutils/truthtable.py new file mode 100644 index 0000000..3181ff0 --- /dev/null +++ b/testing/lib/testutils/truthtable.py @@ -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}') diff --git a/testing/src-tpl/Makefile b/testing/src-tpl/Makefile new file mode 100644 index 0000000..96e31df --- /dev/null +++ b/testing/src-tpl/Makefile @@ -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 diff --git a/testing/src-tpl/test.py b/testing/src-tpl/test.py new file mode 100644 index 0000000..74b8124 --- /dev/null +++ b/testing/src-tpl/test.py @@ -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) + diff --git a/testing/src-tpl/test_wokwi.v b/testing/src-tpl/test_wokwi.v new file mode 100644 index 0000000..0077464 --- /dev/null +++ b/testing/src-tpl/test_wokwi.v @@ -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