Skip to content

Commit 2b276d3

Browse files
committed
Added support for running testbenches depending on a set of user-provided source file patterns.
1 parent ce51561 commit 2b276d3

File tree

3 files changed

+212
-18
lines changed

3 files changed

+212
-18
lines changed

tests/unit/test_ui.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,119 @@ def test_get_testbench_files(self):
12651265
sorted(expected, key=lambda x: x.name),
12661266
)
12671267

1268+
@with_tempdir
1269+
def test_run_dependent(self, tempdir):
1270+
def setup(ui):
1271+
"Setup the project"
1272+
ui.add_vhdl_builtins()
1273+
lib1 = ui.add_library("lib1")
1274+
lib2 = ui.add_library("lib2")
1275+
1276+
rtl = []
1277+
for i in range(4):
1278+
rtl_source = f"""\
1279+
entity rtl{i} is
1280+
end entity;
1281+
1282+
architecture a of rtl{i} is
1283+
begin
1284+
end architecture;
1285+
"""
1286+
file_name = str(Path(tempdir) / f"rtl{i}.vhd")
1287+
self.create_file(file_name, rtl_source)
1288+
rtl.append(lib1.add_source_file(file_name))
1289+
1290+
tb = []
1291+
for i in range(2):
1292+
file_name = str(Path(tempdir) / f"tb{i}.vhd")
1293+
create_vhdl_test_bench_file(
1294+
f"tb{i}",
1295+
file_name,
1296+
tests=["Test 1"] if i == 0 else [],
1297+
)
1298+
if i == 0:
1299+
tb.append(lib1.add_source_file(file_name))
1300+
else:
1301+
tb.append(lib2.add_source_file(file_name))
1302+
1303+
rtl[1].add_dependency_on(rtl[0])
1304+
rtl[2].add_dependency_on(rtl[0])
1305+
tb[0].add_dependency_on(rtl[1])
1306+
tb[1].add_dependency_on(rtl[2])
1307+
1308+
return rtl, tb
1309+
1310+
def check_stdout(ui, expected):
1311+
"Check that stdout matches expected"
1312+
with mock.patch("sys.stdout", autospec=True) as stdout:
1313+
self._run_main(ui)
1314+
text = "".join([call[1][0] for call in stdout.write.mock_calls])
1315+
# @TODO not always in the same order in Python3 due to dependency graph
1316+
print(text)
1317+
self.assertEqual(set(text.splitlines()), set(expected.splitlines()))
1318+
1319+
ui = self._create_ui("--list")
1320+
rtl, tb = setup(ui)
1321+
ui.run_dependent([rtl[0]._source_file.name])
1322+
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")
1323+
1324+
ui = self._create_ui("--list")
1325+
rtl, tb = setup(ui)
1326+
ui.run_dependent([rtl[1]._source_file.name])
1327+
check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests")
1328+
1329+
ui = self._create_ui("--list")
1330+
rtl, tb = setup(ui)
1331+
ui.run_dependent([Path(rtl[2]._source_file.name)])
1332+
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")
1333+
1334+
ui = self._create_ui("--list")
1335+
rtl, tb = setup(ui)
1336+
ui.run_dependent([tb[0]._source_file.name])
1337+
check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests")
1338+
1339+
ui = self._create_ui("--list")
1340+
rtl, tb = setup(ui)
1341+
ui.run_dependent([tb[1]._source_file.name])
1342+
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")
1343+
1344+
ui = self._create_ui("--list", "*tb0*")
1345+
rtl, tb = setup(ui)
1346+
ui.run_dependent([tb[1]._source_file.name])
1347+
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")
1348+
1349+
ui = self._create_ui("--list")
1350+
rtl, tb = setup(ui)
1351+
ui.run_dependent([tb[1]._source_file.name, rtl[1]._source_file.name])
1352+
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")
1353+
1354+
ui = self._create_ui("--list")
1355+
rtl, tb = setup(ui)
1356+
ui.run_dependent([tb[1]._source_file.name, "Missing file"])
1357+
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")
1358+
1359+
a_dir = Path(tempdir) / "a_dir"
1360+
a_dir.mkdir()
1361+
ui = self._create_ui("--list")
1362+
rtl, tb = setup(ui)
1363+
ui.run_dependent([tb[1]._source_file.name, a_dir])
1364+
check_stdout(ui, "lib2.tb1.all\nListed 1 tests")
1365+
1366+
ui = self._create_ui("--list")
1367+
rtl, tb = setup(ui)
1368+
ui.run_dependent([rtl[3]._source_file.name])
1369+
check_stdout(ui, "Listed 0 tests")
1370+
1371+
ui = self._create_ui("--list")
1372+
rtl, tb = setup(ui)
1373+
ui.run_dependent(["*rtl1*"])
1374+
check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests")
1375+
1376+
ui = self._create_ui("--list")
1377+
rtl, tb = setup(ui)
1378+
ui.run_dependent(["*rtl?.vhd"])
1379+
check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests")
1380+
12681381
def test_get_simulator_name(self):
12691382
ui = self._create_ui()
12701383
self.assertEqual(ui.get_simulator_name(), "mock")

vunit/project.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ def get_files_in_compile_order(self, incremental=True, dependency_graph=None, fi
500500
files_to_recompile = self._get_files_to_recompile(
501501
files or self.get_source_files_in_order(), dependency_graph, incremental
502502
)
503-
return self._get_affected_files_in_compile_order(files_to_recompile, dependency_graph.get_dependent)
503+
return self.get_affected_files_in_compile_order(files_to_recompile, dependency_graph.get_dependent)
504504

505505
def _get_files_to_recompile(self, files, dependency_graph, incremental):
506506
"""
@@ -527,15 +527,15 @@ def get_dependencies_in_compile_order(self, target_files=None, implementation_de
527527
target_files = self._source_files_in_order
528528

529529
dependency_graph = self.create_dependency_graph(implementation_dependencies)
530-
return self._get_affected_files_in_compile_order(set(target_files), dependency_graph.get_dependencies)
530+
return self.get_affected_files_in_compile_order(set(target_files), dependency_graph.get_dependencies)
531531

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

541541
def get_minimal_file_set_in_compile_order(self, target_files=None):
@@ -546,7 +546,7 @@ def get_minimal_file_set_in_compile_order(self, target_files=None):
546546
###
547547
# First get all files that are required to fullfill the dependencies for the target files
548548
dependency_graph = self.create_dependency_graph(True)
549-
dependency_files = self._get_affected_files(
549+
dependency_files = self.get_affected_files(
550550
target_files or self.get_source_files_in_order(),
551551
dependency_graph.get_dependencies,
552552
)
@@ -562,7 +562,7 @@ def get_minimal_file_set_in_compile_order(self, target_files=None):
562562
min_file_set_to_be_compiled = [f for f in max_file_set_to_be_compiled if f in dependency_files]
563563
return min_file_set_to_be_compiled
564564

565-
def _get_affected_files(self, target_files, get_depend_func):
565+
def get_affected_files(self, target_files, get_depend_func):
566566
"""
567567
Get affected files given a list of type SourceFile, if the list is None
568568
all files are taken into account

vunit/ui/__init__.py

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import json
1818
import os
19-
from typing import Optional, Set, Union
19+
from typing import Optional, Set, Union, List
2020
from pathlib import Path
2121
from fnmatch import fnmatch
2222

@@ -111,6 +111,22 @@ def from_args(
111111
"""
112112
return cls(args, vhdl_standard=vhdl_standard)
113113

114+
@staticmethod
115+
def _make_test_filter(args, test_patterns):
116+
"Create test filter function from test patterns."
117+
118+
def test_filter(name, attribute_names):
119+
keep = any(fnmatch(name, pattern) for pattern in test_patterns)
120+
121+
if args.with_attributes is not None:
122+
keep = keep and set(args.with_attributes).issubset(attribute_names)
123+
124+
if args.without_attributes is not None:
125+
keep = keep and set(args.without_attributes).isdisjoint(attribute_names)
126+
return keep
127+
128+
return test_filter
129+
114130
def __init__(
115131
self,
116132
args,
@@ -125,17 +141,7 @@ def __init__(
125141
else:
126142
self._printer = COLOR_PRINTER
127143

128-
def test_filter(name, attribute_names):
129-
keep = any(fnmatch(name, pattern) for pattern in args.test_patterns)
130-
131-
if args.with_attributes is not None:
132-
keep = keep and set(args.with_attributes).issubset(attribute_names)
133-
134-
if args.without_attributes is not None:
135-
keep = keep and set(args.without_attributes).isdisjoint(attribute_names)
136-
return keep
137-
138-
self._test_filter = test_filter
144+
self._test_filter = self._make_test_filter(args, args.test_patterns)
139145
self._vhdl_standard: VHDLStandard = select_vhdl_standard(vhdl_standard)
140146

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

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

171+
self._run_dependent_on: List[Union[str, Path]] = []
172+
165173
def _create_database(self):
166174
"""
167175
Create a persistent database to store expensive parse results
@@ -736,6 +744,8 @@ def _main(self, post_run):
736744
"""
737745
Base vunit main function without performing exit
738746
"""
747+
if self._run_dependent_on:
748+
self._update_test_filter(self._run_dependent_on)
739749

740750
if self._args.export_json is not None:
741751
return self._main_export_json(self._args.export_json)
@@ -752,6 +762,64 @@ def _main(self, post_run):
752762
all_ok = self._main_run(post_run)
753763
return all_ok
754764

765+
def _update_test_filter(self, dependencies):
766+
"""
767+
Update test filter to match testbenches depending on provided dependencies
768+
"""
769+
# Find source file objects corresponding to the provided dependencies.
770+
# Separate file patterns from file paths to speed-up matching
771+
dependency_paths = []
772+
dependency_patterns = []
773+
for dependency in dependencies:
774+
# Assume input is a file path
775+
is_str = isinstance(dependency, str)
776+
dependency_path = Path(dependency) if is_str else dependency
777+
778+
if dependency_path.is_file():
779+
dependency_paths.append(dependency_path.resolve())
780+
# If a string input doesn't point to a file it is assumed
781+
# to be a pattern. There is a small but acceptable risk that
782+
# it leads to invalid matches but the risk is small. For example,
783+
# a directory named like a test: lib.tb.test. Note that directories
784+
# can't have wildcards such as * and ?
785+
elif is_str:
786+
dependency_patterns.append(dependency)
787+
788+
project = self._project
789+
dependency_source_files = []
790+
if dependency_paths:
791+
dependency_source_files = [
792+
source_file
793+
for source_file in project.get_source_files_in_order()
794+
if source_file.original_name in dependency_paths
795+
]
796+
797+
if dependency_patterns:
798+
dependency_source_files += [
799+
source_file
800+
for source_file in project.get_source_files_in_order()
801+
if any(fnmatch(source_file.original_name, pattern) for pattern in dependency_patterns)
802+
]
803+
804+
# Get dependent files, non-testbench files included
805+
dependency_graph = project.create_dependency_graph(True)
806+
dependent_files = project.get_affected_files(dependency_source_files, dependency_graph.get_dependent)
807+
808+
# Extract testbenches from dependent files and create corresponding test patterns:
809+
# lib_name.tb_name*
810+
test_patterns = []
811+
for dependent_file in dependent_files:
812+
library_name = dependent_file.library.name
813+
for testbench in self._test_bench_list.get_test_benches_in_library(library_name):
814+
if testbench.design_unit.source_file == dependent_file:
815+
test_patterns.append(f"{library_name}.{testbench.name}*")
816+
817+
# Update test filter to match test patterns
818+
if isinstance(self._args.test_patterns, list):
819+
test_patterns += self._args.test_patterns
820+
821+
self._test_filter = self._make_test_filter(self._args, test_patterns)
822+
755823
def _create_simulator_if(self):
756824
"""
757825
Create new simulator instance
@@ -1032,6 +1100,19 @@ def add_json4vhdl(self):
10321100
"""
10331101
self._builtins.add("json4vhdl")
10341102

1103+
def run_dependent(self, source_files: List[Union[str, Path]]) -> None:
1104+
"""
1105+
Run testbenches depending on the provided source files.
1106+
1107+
Test patterns on the command line will add to the depending testbenches.
1108+
1109+
:param source_files: List of :class:`str` or :class:`pathlib.Path` items,
1110+
each representing the relative or absolute path to
1111+
the source file.
1112+
:returns: None
1113+
"""
1114+
self._run_dependent_on = source_files
1115+
10351116
def get_compile_order(self, source_files=None):
10361117
"""
10371118
Get the compile order of all or specific source files and

0 commit comments

Comments
 (0)