diff --git a/.github/workflows/builddocs.yml b/.github/workflows/builddocs.yml index 8d6c318..5b7e5e7 100644 --- a/.github/workflows/builddocs.yml +++ b/.github/workflows/builddocs.yml @@ -1,4 +1,4 @@ -name: Build docs +name: Build and deploy docs on: workflow_dispatch: @@ -23,9 +23,9 @@ jobs: name: Build documentation steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3" - name: Install dependencies diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index d166489..ea7a52a 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -12,7 +12,7 @@ jobs: name: Builds VPI library steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Install dependencies @@ -24,12 +24,12 @@ jobs: - name: Build and run cunit test run: cd test;make;cd .. - name: Upload build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: libvpi path: build/verisocks.vpi - name: Archive cunit test artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cunit-test-results path: | @@ -43,9 +43,9 @@ jobs: name: Run pytest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.7" - name: Install dependencies @@ -55,7 +55,7 @@ jobs: pip install -e ./python pip install pytest pytest-cov - name: Download build artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: libvpi - name: Check verisocks.vpi @@ -67,7 +67,7 @@ jobs: run: pytest -x --log-cli-level=INFO --cov=verisocks - name: Archive test logs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-logs path: | diff --git a/.github/workflows/testdocs.yml b/.github/workflows/testdocs.yml new file mode 100644 index 0000000..d06836a --- /dev/null +++ b/.github/workflows/testdocs.yml @@ -0,0 +1,32 @@ +name: Test building docs + +on: + push: + branches-ignore: + - 'main' + paths: + - 'docs/**' + +jobs: + build: + runs-on: ubuntu-latest + name: Build documentation + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ./python + pip install sphinx sphinx-rtd-theme myst-parser + - name: Render + run: | + sphinx-build -M html ./docs ./docs/_build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./docs/_build/html" diff --git a/docs/conf.py b/docs/conf.py index 5df6134..cf2e362 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,8 +9,8 @@ project = 'Verisocks' copyright = '2023, Jérémie Chabloz' author = 'Jérémie Chabloz' -version = '1.0.0' -release = '1.0.0' +version = '1.1.0' +release = '1.1.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/src/python_client.rst b/docs/src/python_client.rst index 4cdc8df..188192b 100644 --- a/docs/src/python_client.rst +++ b/docs/src/python_client.rst @@ -42,3 +42,13 @@ Python client API documentation .. automodule:: verisocks.verisocks :members: + +Miscellaneous utilitaries +************************* + +The module :py:mod:`verisocks.utils` is a collection of miscellaneous +utilitaries and helper functions to ease setting up simulations using +Verisocks. + +.. automodule:: verisocks.utils + :members: diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 9fbbc3f..3a4345e 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -9,6 +9,19 @@ version numbering system follows the `semantic versioning `_ principles. +1.1.0 - Ongoing +*************** + +* Added :py:mod:`verisocks.utils` Python utilitary functions, including + documentation. +* Added :py:meth:`Verisocks.info() ` method + as a shortcut to implement the TCP protocol :keyword:`info + ` command. +* Corrected *SPI master* example for standalone execution. +* Added a minimalistic *Hello world* example, working both for standalone + execution or with pytest. + + 1.0.0 - 2024-01-04 ****************** diff --git a/docs/src/tcp_protocol.rst b/docs/src/tcp_protocol.rst index 1f8c94e..57de1d2 100644 --- a/docs/src/tcp_protocol.rst +++ b/docs/src/tcp_protocol.rst @@ -149,6 +149,9 @@ arbitrary text is then printed out to the VPI standard output. * :json:`"type": "ack"` (acknowledgement) * :json:`"value": "command info received"` +With the provided Python client reference implementation, the method +:py:meth:`Verisocks.info() ` +corresponds to this command. .. _sec_tcp_cmd_finish: diff --git a/examples/hello_world/README.md b/examples/hello_world/README.md new file mode 100644 index 0000000..3e759a7 --- /dev/null +++ b/examples/hello_world/README.md @@ -0,0 +1,50 @@ +# Verisocks example - Hello World + +## Introduction + +This example shows how to use Verisocks with a minimalistic test case. The test +does nothing except asking Verisocks for the simulator name and version and +then send an "Hello World!" string to the simulator using the `info` command of +the TCP protocol. + +This example uses the helper function `setup_sim()` in order to easily set up +the simulation. + +## Files + +The example folder contains the following files: + +* [hello_world_tb.v](hello_world_tb.v): Top verilog testbench +* [test_hello_world.py](test_hello_world.py): Verisocks Python testbench file + +## Running the example + +This example can be run by directly executing the Python file or by using +[`pytest`](https://docs.pytest.org). + +### Standalone execution + +Simply run the test script: +```sh +python test_hello_world.py +``` + +The results of the test script execution can be checked from the content of the +`vvp.log` file, which should look like this: + +```log +INFO [Verisocks]: Server address: 127.0.0.1 +INFO [Verisocks]: Port: 44041 +INFO [Verisocks]: Connected to localhost +INFO [Verisocks]: Command "get(sel=sim_info)" received. +INFO [Verisocks]: Command "info" received. +INFO [Verisocks]: Hello World! +INFO [Verisocks]: Command "finish" received. Terminating simulation... +INFO [Verisocks]: Returning control to simulator +``` + +### Using pytest + +If you already have it installed, simply run `pytest` from within the SPI +master example directory or from a parent directory. +Otherwise, follow [installation instruction](https://docs.pytest.org/en/latest/getting-started.html#install-pytest). diff --git a/examples/hello_world/hello_world_tb.v b/examples/hello_world/hello_world_tb.v new file mode 100644 index 0000000..1c130f8 --- /dev/null +++ b/examples/hello_world/hello_world_tb.v @@ -0,0 +1,35 @@ +/****************************************************************************** +File: hello_world_tb.v +Description: Hello world example testbench for Verisocks +******************************************************************************/ + +/******************************************************************************* +Includes and misc definitions +*******************************************************************************/ +`timescale 1us/10ps //Time scale definitions + +`ifndef VS_NUM_PORT +`define VS_NUM_PORT 5100 +`endif + +`ifndef VS_TIMEOUT +`define VS_TIMEOUT 120 +`endif + +/******************************************************************************* +Testbench +*******************************************************************************/ +module hello_world_tb(); + + initial begin + + /* Launch Verisocks server after other initialization */ + $verisocks_init(`VS_NUM_PORT, `VS_TIMEOUT); + + /* Make sure that the simulation finishes after a while... */ + #1000 + $finish(0); + + end + +endmodule diff --git a/examples/hello_world/pytest.ini b/examples/hello_world/pytest.ini new file mode 100644 index 0000000..fce50b3 --- /dev/null +++ b/examples/hello_world/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_file = pytest.log +log_file_level = DEBUG +log_file_format = [%(levelname)s][%(module)s] %(asctime)s - %(message)s +log_file_date_format = %Y-%m-%d %H:%M:%S diff --git a/examples/hello_world/test_hello_world.py b/examples/hello_world/test_hello_world.py new file mode 100644 index 0000000..e1c0a01 --- /dev/null +++ b/examples/hello_world/test_hello_world.py @@ -0,0 +1,60 @@ +from verisocks.verisocks import Verisocks +from verisocks.utils import setup_sim, find_free_port +import socket +import time +import pytest +import logging +import os.path + + +HOST = socket.gethostbyname("localhost") +PORT = find_free_port() +VS_TIMEOUT = 10 +LIBVPI = "../../build/verisocks.vpi" +CONNECT_DELAY = 0.1 + + +def setup_test(): + setup_sim( + LIBVPI, + "hello_world_tb.v", + cwd=os.path.dirname(__file__), + ivl_args=[ + f"-DVS_NUM_PORT={PORT}", + f"-DVS_TIMEOUT={VS_TIMEOUT}" + ] + ) + time.sleep(CONNECT_DELAY) + + +@pytest.fixture +def vs(): + setup_test() + _vs = Verisocks(HOST, PORT) + _vs.connect() + yield _vs + # Teardown + try: + _vs.finish() + except ConnectionError: + logging.warning("Connection error - Finish command not possible") + _vs.close() + + +def test_hello_world(vs): + + assert vs._connected + answer = vs.get("sim_info") + assert answer['type'] == "result" + print(f"Simulator: {answer['product']}") + print(f"Version: {answer['version']}") + + answer = vs.info("Hello World!") + assert answer['type'] == "ack" + + +if __name__ == "__main__": + + setup_test() + with Verisocks(HOST, PORT) as vs_cli: + test_hello_world(vs_cli) diff --git a/examples/spi_master/README.md b/examples/spi_master/README.md index 23b5ca1..19f5388 100644 --- a/examples/spi_master/README.md +++ b/examples/spi_master/README.md @@ -66,3 +66,21 @@ def send_spi(vs, tx_buffer): rx_buffer = answer['value'] return rx_buffer, counter ``` + +## Running the example + +This example can be run by directly executing the Python file or by using +[`pytest`](https://docs.pytest.org). + +### Standalone execution + +Simply run the test script: +```sh +python test_spi_master.py +``` + +### Using pytest + +If you already have it installed, simply run `pytest` from within the SPI +master example directory or from a parent directory. +Otherwise, follow [installation instruction](https://docs.pytest.org/en/latest/getting-started.html#install-pytest). diff --git a/examples/spi_master/spi_master_tb.v b/examples/spi_master/spi_master_tb.v index 0f88be8..0d19aa7 100644 --- a/examples/spi_master/spi_master_tb.v +++ b/examples/spi_master/spi_master_tb.v @@ -41,8 +41,10 @@ module spi_master_tb(); /* Initial loop */ initial begin - $dumpfile("spi_master_tb.fst"); + `ifdef DUMP_FILE + $dumpfile(`DUMP_FILE); $dumpvars(0, spi_master_tb); + `endif /* Launch Verisocks server after other initialization */ $verisocks_init(`VS_NUM_PORT, `VS_TIMEOUT); diff --git a/examples/spi_master/test_spi_master.py b/examples/spi_master/test_spi_master.py index 2e834db..3bc5c5c 100644 --- a/examples/spi_master/test_spi_master.py +++ b/examples/spi_master/test_spi_master.py @@ -1,82 +1,36 @@ from verisocks.verisocks import Verisocks -import subprocess +from verisocks.utils import setup_sim, find_free_port import os.path import time -import shutil import logging import pytest import socket import random -def find_free_port(): - with socket.socket() as s: - s.bind(('', 0)) - return s.getsockname()[1] - - # Parameters HOST = socket.gethostbyname("localhost") PORT = find_free_port() LIBVPI = "../../build/verisocks.vpi" # Relative path to this file! CONNECT_DELAY = 0.01 VS_TIMEOUT = 10 - - -def get_abspath(relpath): - """Builds an absolute path from a path which is relative to the current - file - - Args: - * relpath (str): Relative path - - Returns: - * abspath (str): Absolute path - """ - return os.path.join(os.path.dirname(__file__), relpath) - - -def setup_iverilog(vvp_name, *src_files): - """Elaborate and run the verilog testbench file provided as an argument - - Args: - * src_file (str): Path to source file - - Returns: - * pop: Popen instance for spawned process - """ - src_file_paths = [] - for src_file in src_files: - src_file_path = get_abspath(src_file) - if not os.path.isfile(src_file_path): - raise FileNotFoundError - src_file_paths.append(src_file_path) - vvp_file_path = get_abspath(vvp_name) - cmd = [ - shutil.which("iverilog"), - "-o", vvp_file_path, - "-Wall", - f"-DVS_NUM_PORT={PORT}", - f"-DVS_TIMEOUT={VS_TIMEOUT}", - *src_file_paths, - ] - subprocess.check_call(cmd) - libvpi_path = get_abspath(LIBVPI) - cmd = [shutil.which("vvp"), "-lvvp.log", "-m", libvpi_path, - vvp_file_path, "-fst"] - pop = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL +SRC = ["spi_master.v", "spi_slave.v", "spi_master_tb.v"] + + +def setup_test(): + setup_sim( + LIBVPI, + *SRC, + cwd=os.path.dirname(__file__), + vvp_filepath="spi_master_tb", + ivl_args=[ + f"-DVS_NUM_PORT={PORT}", + f"-DVS_TIMEOUT={VS_TIMEOUT}", + "-DDUMP_FILE=\"spi_master_tb.fst\"" + ] ) - print(f"Launched Icarus with PID {pop.pid}") - - # Some delay is required for Icarus to launch the Verisocks server before - # being able to connect - Please adjust CONNECT_DELAY if required. time.sleep(CONNECT_DELAY) - return pop - def send_spi(vs, tx_buffer): """Triggers an SPI transaction @@ -128,10 +82,7 @@ def send_spi(vs, tx_buffer): @pytest.fixture def vs(): # Set up Icarus simulation and launch it as a separate process - pop = setup_iverilog("spi_master_tb", - "spi_master.v", - "spi_slave.v", - "spi_master_tb.v") + setup_test() _vs = Verisocks(HOST, PORT) _vs.connect() yield _vs @@ -141,7 +92,6 @@ def vs(): except ConnectionError: logging.warning("Connection error - Finish command not possible") _vs.close() - pop.communicate(timeout=10) def get_random_tx_buffer(): @@ -175,9 +125,6 @@ def test_spi_master_simple(vs): if __name__ == "__main__": - str_port = input("Port number: ") - - with Verisocks(HOST, int(str_port)) as vs_cli: - vs_cli.connect() + setup_test() + with Verisocks(HOST, PORT) as vs_cli: test_spi_master_simple(vs_cli) - vs_cli.finish() diff --git a/python/test/test_verisocks.py b/python/test/test_verisocks.py index e06bc5c..f048d62 100644 --- a/python/test/test_verisocks.py +++ b/python/test/test_verisocks.py @@ -111,6 +111,12 @@ def test_connect_error(): vs.close() +def test_info(vs): + """Tests the info command""" + answer = vs.info("This is a test") + assert answer["type"] == "ack" + + def test_get_sim_info(vs): answer = vs.get("sim_info") assert answer["type"] == "result" diff --git a/python/verisocks/__init__.py b/python/verisocks/__init__.py index ef1a229..18199b9 100644 --- a/python/verisocks/__init__.py +++ b/python/verisocks/__init__.py @@ -1,4 +1,4 @@ """Verisocks Python client API """ -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/python/verisocks/utils.py b/python/verisocks/utils.py new file mode 100644 index 0000000..8da9826 --- /dev/null +++ b/python/verisocks/utils.py @@ -0,0 +1,134 @@ +import subprocess +import os.path +import shutil +import socket +import logging + + +def find_free_port(): + """Find a free port on localhost + + The implementation for this function is not very elegant, but it does the + job... It uses the property of :py:meth:`socket.socket.bind` method to bind + the socket to a randomly-assigned free port when provided a :code:`('', 0)` + address argument. Using :py:meth:`socket.socket.getsockname` is then used + to retrieve the corresponding port number. Since the bound socket is closed + within the function, it is assumed that the same port number should also be + free again; this is where the weakness of this method lies, since race + conditions cannot be fully excluded. + """ + with socket.socket() as s: + s.bind(('', 0)) + retval = s.getsockname()[1] + return retval + + +def _format_path(cwd, path): + if os.path.isabs(path): + return path + return os.path.abspath(os.path.join(cwd, path)) + + +def setup_sim(vpi_libpath, *src_files, cwd=".", vvp_filepath=None, + vvp_logpath="vvp.log", ivl_exec=None, ivl_args=None, + vvp_exec=None, vvp_args=None, vvp_postargs=None, + capture_output=True): + """Set up Icarus simulation by elaborating the design with :code:`iverilog` + and launching the simulation with :code:`vvp`. + + Args: + cwd (str): Reference path to be used for all paths provided as relative + paths. + vpi_libpath (str): Path to the compiled Verisocks VPI library. + src_files (str): Paths to all (verilog) source files to use for the + simulation. All files have to be added as separate arguments. + vvp_filepath (str): Path to the elaborated VVP file (iverilog output). + If None (default), "sim.vvp" will be used. + vvp_logpath (str): Path to a simulation logfile. Default="vvp.log". If + None, no logfile shall be created. + ivl_exec (str): Path to :code:`iverilog` executable (absolute path). If + None (default), it is assumed to be defined in the system path. + ivl_args (str, list(str)): Arguments to :code:`iverilog` executable. + vvp_exec (str): Path to :code:`vvp` executable (absolute path). If None + (default), it is assumed to be defined in the system path. + vvp_args (list(str)): Arguments to :code:`vvp` executable. + vvp_postargs (str, list(str)): (Post-)arguments to :code:`vvp` + executable. In order to dump waveforms to an FST file, this should + be configured as "-fst". + capture_output (bool): Defines if stdout and stderr output + are "captured" (i.e. not visible). + + Returns: + subprocess.Popen + """ + + vpi_libpath = _format_path(cwd, vpi_libpath) + if not os.path.isfile(vpi_libpath): + raise FileNotFoundError(f"Could not find {vpi_libpath}") + + src_file_paths = [] + for src_file in src_files: + src_file_path = _format_path(cwd, src_file) + if not os.path.isfile(src_file_path): + raise FileNotFoundError(f"File {src_file_path} not found") + src_file_paths.append(src_file_path) + + if vvp_filepath: + vvp_outfile = _format_path(cwd, vvp_filepath) + else: + vvp_outfile = _format_path(cwd, "sim.vvp") + + # Elaboration with iverilog + if ivl_exec: + ivl_cmd = [_format_path(cwd, ivl_exec)] + else: + ivl_cmd = [shutil.which("iverilog")] + + if ivl_args is None: + ivl_args = [] + elif isinstance(ivl_args, str): + ivl_args = [ivl_args] + + ivl_cmd += [ + "-o", vvp_outfile, + "-Wall", + *ivl_args, + *src_file_paths + ] + subprocess.check_call(ivl_cmd) + + # Simulation with vvp + if vvp_exec: + vvp_cmd = [_format_path(cwd, vvp_exec)] + else: + vvp_cmd = [shutil.which("vvp")] + + if vvp_logpath: + vvp_cmd += ["-l" + _format_path(cwd, vvp_logpath)] + if vvp_args is None: + vvp_args = [] + elif isinstance(vvp_args, str): + vvp_args = [vvp_args] + if vvp_postargs is None: + vvp_postargs = [] + elif isinstance(vvp_postargs, str): + vvp_postargs = [vvp_postargs] + + vvp_cmd += [ + "-m", vpi_libpath, + *vvp_args, + vvp_outfile, + *vvp_postargs + ] + + if capture_output: + pop = subprocess.Popen( + vvp_cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + pop = subprocess.Popen(vvp_cmd) + + logging.info(f"Launched Icarus with PID {pop.pid}") + return pop diff --git a/python/verisocks/verisocks.py b/python/verisocks/verisocks.py index 3e54eb7..6c042d3 100644 --- a/python/verisocks/verisocks.py +++ b/python/verisocks/verisocks.py @@ -361,7 +361,8 @@ def send_cmd(self, command, **kwargs): return self.send(command=command, **kwargs) def run(self, cb, **kwargs): - """Sends a "run" command request to the Verisocks server. + """Sends a :keyword:`run ` command request to the + Verisocks server. Equivalent to :code:`send_cmd("run", cb=cb, ...)`. This command gives the focus back to the simulator and lets it run until the specified @@ -403,7 +404,8 @@ def run(self, cb, **kwargs): return self.send(command="run", cb=cb, **kwargs) def set(self, path, **kwargs): - """Sends a "set" command request to the Verisocks server. + """Sends a :keyword:`set ` command request to the + Verisocks server. Equivalent to :code:`send_cmd("set", path=path, **kwargs)`. This commands sets the value of a verilog object as defined by its path. @@ -422,8 +424,26 @@ def set(self, path, **kwargs): """ return self.send(command="set", path=path, **kwargs) + def info(self, value): + """Sends an :keyword:`info ` command to the Verisocks + server. + + This is a shortcut function, which is equivalent to + :code:`send_cmd("info", ...)`. This command is used to send any text to + the Verisocks server, which will then be streamed out to the VPI + standard output. + + Args: + value (str): Text to be sent to the VPI stdout + + Returns: + JSON object: Content of returned message + """ + return self.send(command="info", value=value) + def get(self, sel, path=""): - """Sends a :code:`"get"` command request to the Verisocks server. + """Sends a :keyword:`get ` command request to the + Verisocks server. Equivalent to :code:`send_cmd("get", ...)`. This commands can be used to obtain different pieces of information from the Verisocks @@ -452,16 +472,18 @@ def get(self, sel, path=""): return self.send(command="get", sel=sel, path=path) def finish(self, timeout=None): - """Sends a ``"finish"`` command to the Verisocks server that terminates - the simulation (and therefore also closes the Verisocks server itself). - The connection is closed as well by the function as a clean-up. + """Sends a :keyword:`finish ` command to the + Verisocks server that terminates the simulation (and therefore also + closes the Verisocks server itself). The connection is closed as well + by the function as a clean-up. """ retval = self.send(command="finish", timeout=timeout) self.close() return retval def stop(self, timeout=None): - """Sends a ``"stop"`` command to the Verisocks server. + """Sends a :keyword:`stop ` command to the Verisocks + server. The ``"stop"`` command stops the simulation. The Verisocks server socket is not closed, but the simulation has to be restarted for any @@ -474,11 +496,11 @@ def stop(self, timeout=None): return self.send(command="stop", timeout=timeout) def exit(self): - """Sends an ``"exit"`` command to the Verisocks server that gives back - control to the simulator and closes the Verisocks server socket. The - simulation runs to its end without having the possibility to take the - control back from the simulator anymore. The connection is closed - as well by the function.""" + """Sends an :keyword:`exit ` command to the Verisocks + server that gives back control to the simulator and closes the + Verisocks server socket. The simulation runs to its end without having + the possibility to take the control back from the simulator anymore. + The connection is closed as well by the function.""" retval = self.send(command="exit") self.close() return retval