Skip to content

Commit

Permalink
Added support for running testbenches depending on a set of user-prov…
Browse files Browse the repository at this point in the history
…ided source files.

The intended use case is to feed VUnit with files that has been changed since some point in time.
The changes are typically provided by the version control system
  • Loading branch information
LarsAsplund committed Jan 26, 2025
1 parent ce51561 commit a79f309
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 18 deletions.
103 changes: 103 additions & 0 deletions tests/unit/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,109 @@ def test_get_testbench_files(self):
sorted(expected, key=lambda x: x.name),
)

@with_tempdir
def test_run_dependent(self, tempdir):
def setup(ui):
"Setup the project"
ui.add_vhdl_builtins()
lib1 = ui.add_library("lib1")
lib2 = ui.add_library("lib2")

rtl = []
for i in range(4):
rtl_source = f"""\
entity rtl{i} is
end entity;
architecture a of rtl{i} is
begin
end architecture;
"""
file_name = str(Path(tempdir) / f"rtl{i}.vhd")
self.create_file(file_name, rtl_source)
rtl.append(lib1.add_source_file(file_name))

tb = []
for i in range(2):
file_name = str(Path(tempdir) / f"tb{i}.vhd")
create_vhdl_test_bench_file(
f"tb{i}",
file_name,
tests=["Test 1"] if i == 0 else [],
)
if i == 0:
tb.append(lib1.add_source_file(file_name))
else:
tb.append(lib2.add_source_file(file_name))

rtl[1].add_dependency_on(rtl[0])
rtl[2].add_dependency_on(rtl[0])
tb[0].add_dependency_on(rtl[1])
tb[1].add_dependency_on(rtl[2])

return rtl, tb

def check_stdout(ui, expected):
"Check that stdout matches expected"
with mock.patch("sys.stdout", autospec=True) as stdout:
self._run_main(ui)
text = "".join([call[1][0] for call in stdout.write.mock_calls])
# @TODO not always in the same order in Python3 due to dependency graph
print(text)
self.assertEqual(set(text.splitlines()), set(expected.splitlines()))

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([rtl[0]._source_file.name])
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([rtl[1]._source_file.name])
check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([Path(rtl[2]._source_file.name)])
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([tb[0]._source_file.name])
check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([tb[1]._source_file.name])
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")

ui = self._create_ui("--list", "*tb0*")
rtl, tb = setup(ui)
ui.run_dependent([tb[1]._source_file.name])
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([tb[1]._source_file.name, rtl[1]._source_file.name])
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([tb[1]._source_file.name, "Missing file"])
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")

a_dir = Path(tempdir) / "a_dir"
a_dir.mkdir()
ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([tb[1]._source_file.name, a_dir])
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")

ui = self._create_ui("--list")
rtl, tb = setup(ui)
ui.run_dependent([rtl[3]._source_file.name])
check_stdout(ui, "Listed 0 tests")

def test_get_simulator_name(self):
ui = self._create_ui()
self.assertEqual(ui.get_simulator_name(), "mock")
Expand Down
12 changes: 6 additions & 6 deletions vunit/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ def get_files_in_compile_order(self, incremental=True, dependency_graph=None, fi
files_to_recompile = self._get_files_to_recompile(
files or self.get_source_files_in_order(), dependency_graph, incremental
)
return self._get_affected_files_in_compile_order(files_to_recompile, dependency_graph.get_dependent)
return self.get_affected_files_in_compile_order(files_to_recompile, dependency_graph.get_dependent)

def _get_files_to_recompile(self, files, dependency_graph, incremental):
"""
Expand All @@ -527,15 +527,15 @@ def get_dependencies_in_compile_order(self, target_files=None, implementation_de
target_files = self._source_files_in_order

dependency_graph = self.create_dependency_graph(implementation_dependencies)
return self._get_affected_files_in_compile_order(set(target_files), dependency_graph.get_dependencies)
return self.get_affected_files_in_compile_order(set(target_files), dependency_graph.get_dependencies)

def _get_affected_files_in_compile_order(self, target_files, get_depend_func):
def get_affected_files_in_compile_order(self, target_files, get_depend_func):
"""
Returns the affected files in compile order given a list of target files and a dependencie function
:param target_files: The files to compile
:param get_depend_func: one of DependencyGraph [get_dependencies, get_dependent, get_direct_dependencies]
"""
affected_files = self._get_affected_files(target_files, get_depend_func)
affected_files = self.get_affected_files(target_files, get_depend_func)
return self._get_compile_order(affected_files, get_depend_func.__self__)

def get_minimal_file_set_in_compile_order(self, target_files=None):
Expand All @@ -546,7 +546,7 @@ def get_minimal_file_set_in_compile_order(self, target_files=None):
###
# First get all files that are required to fullfill the dependencies for the target files
dependency_graph = self.create_dependency_graph(True)
dependency_files = self._get_affected_files(
dependency_files = self.get_affected_files(
target_files or self.get_source_files_in_order(),
dependency_graph.get_dependencies,
)
Expand All @@ -562,7 +562,7 @@ def get_minimal_file_set_in_compile_order(self, target_files=None):
min_file_set_to_be_compiled = [f for f in max_file_set_to_be_compiled if f in dependency_files]
return min_file_set_to_be_compiled

def _get_affected_files(self, target_files, get_depend_func):
def get_affected_files(self, target_files, get_depend_func):
"""
Get affected files given a list of type SourceFile, if the list is None
all files are taken into account
Expand Down
88 changes: 76 additions & 12 deletions vunit/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging
import json
import os
from typing import Optional, Set, Union
from typing import Optional, Set, Union, List
from pathlib import Path
from fnmatch import fnmatch

Expand Down Expand Up @@ -111,6 +111,22 @@ def from_args(
"""
return cls(args, vhdl_standard=vhdl_standard)

@staticmethod
def _make_test_filter(args, test_patterns):
"Create test filter function from test patterns."

def test_filter(name, attribute_names):
keep = any(fnmatch(name, pattern) for pattern in test_patterns)

if args.with_attributes is not None:
keep = keep and set(args.with_attributes).issubset(attribute_names)

if args.without_attributes is not None:
keep = keep and set(args.without_attributes).isdisjoint(attribute_names)
return keep

return test_filter

def __init__(
self,
args,
Expand All @@ -125,17 +141,7 @@ def __init__(
else:
self._printer = COLOR_PRINTER

def test_filter(name, attribute_names):
keep = any(fnmatch(name, pattern) for pattern in args.test_patterns)

if args.with_attributes is not None:
keep = keep and set(args.with_attributes).issubset(attribute_names)

if args.without_attributes is not None:
keep = keep and set(args.without_attributes).isdisjoint(attribute_names)
return keep

self._test_filter = test_filter
self._test_filter = self._make_test_filter(args, args.test_patterns)
self._vhdl_standard: VHDLStandard = select_vhdl_standard(vhdl_standard)

self._preprocessors = [] # type: ignore
Expand All @@ -162,6 +168,8 @@ def test_filter(name, attribute_names):

self._builtins = Builtins(self, self._vhdl_standard, simulator_class)

self._run_dependent_on: List[Union[str, Path]] = []

def _create_database(self):
"""
Create a persistent database to store expensive parse results
Expand Down Expand Up @@ -736,6 +744,8 @@ def _main(self, post_run):
"""
Base vunit main function without performing exit
"""
if self._run_dependent_on:
self._update_test_filter(self._run_dependent_on)

if self._args.export_json is not None:
return self._main_export_json(self._args.export_json)
Expand All @@ -752,6 +762,47 @@ def _main(self, post_run):
all_ok = self._main_run(post_run)
return all_ok

def _update_test_filter(self, dependency_files):
"""
Update test filter to match testbenches depending on user_files
"""
# Find source file objects corresponding to the provided files.
# Non-existing files, directories and files not added to the
# project are ignored.
dependency_paths = []
for dependency_file in dependency_files:
if isinstance(dependency_file, str):
dependency_file = Path(dependency_file)
dependency_file = dependency_file.resolve()
if dependency_file.exists() and not dependency_file.is_dir():
dependency_paths.append(dependency_file)

project = self._project
dependency_source_files = [
source_file
for source_file in project.get_source_files_in_order()
if source_file.original_name in dependency_paths
]

# Get dependent files, non-testbench files included
dependency_graph = project.create_dependency_graph(True)
dependent_files = project.get_affected_files(dependency_source_files, dependency_graph.get_dependent)

# Extract testbenches from dependent files and create corresponding test patterns:
# lib_name.tb_name*
test_patterns = []
for dependent_file in dependent_files:
library_name = dependent_file.library.name
for testbench in self._test_bench_list.get_test_benches_in_library(library_name):
if testbench.design_unit.source_file == dependent_file:
test_patterns.append(f"{library_name}.{testbench.name}*")

# Update test filter to match test patterns
if isinstance(self._args.test_patterns, list):
test_patterns += self._args.test_patterns

self._test_filter = self._make_test_filter(self._args, test_patterns)

def _create_simulator_if(self):
"""
Create new simulator instance
Expand Down Expand Up @@ -1032,6 +1083,19 @@ def add_json4vhdl(self):
"""
self._builtins.add("json4vhdl")

def run_dependent(self, source_files: List[Union[str, Path]]) -> None:
"""
Run testbenches depending on the provided source files.
Test patterns on the command line will add to the depending testbenches.
:param source_files: List of :class:`str` or :class:`pathlib.Path` items,
each representing the relative or absolute path to
the source file.
:returns: None
"""
self._run_dependent_on = source_files

def get_compile_order(self, source_files=None):
"""
Get the compile order of all or specific source files and
Expand Down

0 comments on commit a79f309

Please sign in to comment.