Skip to content

Commit 0c393ce

Browse files
committed
Added support for running testbenches depending on a set of user-provided 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
1 parent ce51561 commit 0c393ce

File tree

3 files changed

+183
-18
lines changed

3 files changed

+183
-18
lines changed

tests/unit/test_ui.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,109 @@ 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+
12681371
def test_get_simulator_name(self):
12691372
ui = self._create_ui()
12701373
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: 74 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,20 @@ def from_args(
111111
"""
112112
return cls(args, vhdl_standard=vhdl_standard)
113113

114+
@staticmethod
115+
def _make_test_filter(args, test_patterns):
116+
def test_filter(name, attribute_names):
117+
keep = any(fnmatch(name, pattern) for pattern in test_patterns)
118+
119+
if args.with_attributes is not None:
120+
keep = keep and set(args.with_attributes).issubset(attribute_names)
121+
122+
if args.without_attributes is not None:
123+
keep = keep and set(args.without_attributes).isdisjoint(attribute_names)
124+
return keep
125+
126+
return test_filter
127+
114128
def __init__(
115129
self,
116130
args,
@@ -125,17 +139,7 @@ def __init__(
125139
else:
126140
self._printer = COLOR_PRINTER
127141

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
142+
self._test_filter = self._make_test_filter(args, args.test_patterns)
139143
self._vhdl_standard: VHDLStandard = select_vhdl_standard(vhdl_standard)
140144

141145
self._preprocessors = [] # type: ignore
@@ -162,6 +166,8 @@ def test_filter(name, attribute_names):
162166

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

169+
self._run_dependent_on: List[Union[str, Path]] = []
170+
165171
def _create_database(self):
166172
"""
167173
Create a persistent database to store expensive parse results
@@ -736,6 +742,8 @@ def _main(self, post_run):
736742
"""
737743
Base vunit main function without performing exit
738744
"""
745+
if self._run_dependent_on:
746+
self._update_test_filter(self._run_dependent_on)
739747

740748
if self._args.export_json is not None:
741749
return self._main_export_json(self._args.export_json)
@@ -752,6 +760,47 @@ def _main(self, post_run):
752760
all_ok = self._main_run(post_run)
753761
return all_ok
754762

763+
def _update_test_filter(self, dependency_files):
764+
"""
765+
Update test filter to match testbenches depending on user_files
766+
"""
767+
# Find source file objects corresponding to the provided files.
768+
# Non-existing files, directories and files not added to the
769+
# project are ignored.
770+
dependency_paths = []
771+
for dependency_file in dependency_files:
772+
if isinstance(dependency_file, str):
773+
dependency_file = Path(dependency_file)
774+
dependency_file = dependency_file.resolve()
775+
if dependency_file.exists() and not dependency_file.is_dir():
776+
dependency_paths.append(dependency_file)
777+
778+
project = self._project
779+
dependency_source_files = [
780+
source_file
781+
for source_file in project.get_source_files_in_order()
782+
if source_file.original_name in dependency_paths
783+
]
784+
785+
# Get dependent files, non-testbench files included
786+
dependency_graph = project.create_dependency_graph(True)
787+
dependent_files = project.get_affected_files(dependency_source_files, dependency_graph.get_dependent)
788+
789+
# Extract testbenches from dependent files and create corresponding test patterns:
790+
# lib_name.tb_name*
791+
test_patterns = []
792+
for dependent_file in dependent_files:
793+
library_name = dependent_file.library.name
794+
for tb in self._test_bench_list.get_test_benches_in_library(library_name):
795+
if tb.design_unit.source_file == dependent_file:
796+
test_patterns.append(f"{library_name}.{tb.name}*")
797+
798+
# Update test filter to match test patterns
799+
if isinstance(self._args.test_patterns, list):
800+
test_patterns += self._args.test_patterns
801+
802+
self._test_filter = self._make_test_filter(self._args, test_patterns)
803+
755804
def _create_simulator_if(self):
756805
"""
757806
Create new simulator instance
@@ -1032,6 +1081,19 @@ def add_json4vhdl(self):
10321081
"""
10331082
self._builtins.add("json4vhdl")
10341083

1084+
def run_dependent(self, source_files: List[Union[str, Path]]) -> None:
1085+
"""
1086+
Run testbenches depending on the provided source files.
1087+
1088+
Test patterns on the command line will add to the depending testbenches.
1089+
1090+
:param source_files: List of :class:`str` or :class:`pathlib.Path` items,
1091+
each representing the relative or absolute path to
1092+
the source file.
1093+
:returns: None
1094+
"""
1095+
self._run_dependent_on = source_files
1096+
10351097
def get_compile_order(self, source_files=None):
10361098
"""
10371099
Get the compile order of all or specific source files and

0 commit comments

Comments
 (0)