diff --git a/tests/unit/test_ui.py b/tests/unit/test_ui.py index 10f1b628c..db76f9ee3 100644 --- a/tests/unit/test_ui.py +++ b/tests/unit/test_ui.py @@ -14,6 +14,7 @@ from string import Template from pathlib import Path from os import chdir, getcwd +from os.path import relpath import json import re from re import MULTILINE @@ -1265,6 +1266,187 @@ def test_get_testbench_files(self): sorted(expected, key=lambda x: x.name), ) + @with_tempdir + def test_update_test_pattern(self, tempdir): + relative_tempdir = Path(relpath(str(tempdir.resolve()), str(Path().cwd()))) + + 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): + vhdl_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, vhdl_source) + rtl.append(lib1.add_source_file(file_name)) + + verilog_source = """\ +module rtl4; +endmodule +""" + file_name = str(Path(tempdir) / f"rtl4.v") + self.create_file(file_name, verilog_source) + rtl.append(lib2.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]) + rtl[2].add_dependency_on(rtl[4]) + 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())) + + def restore_test_pattern(): + ui.update_test_pattern() + + ui = self._create_ui("--list") + rtl, tb = setup(ui) + ui.update_test_pattern() + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern(["*"]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern(exclude_dependent_on=["*"]) + check_stdout(ui, "Listed 0 tests") + + restore_test_pattern() + ui.update_test_pattern(["*"], ["*"]) + check_stdout(ui, "Listed 0 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[0]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[1]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([Path(rtl[2]._source_file.name)]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[3]._source_file.name]) + check_stdout(ui, "Listed 0 tests") + + restore_test_pattern() + ui.update_test_pattern([Path(rtl[4]._source_file.name)]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([tb[0]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([tb[1]._source_file.name]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([tb[1]._source_file.name, rtl[1]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern([tb[1]._source_file.name, "Missing file"]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + a_dir = Path(tempdir) / "a_dir" + a_dir.mkdir() + ui.update_test_pattern([tb[1]._source_file.name, a_dir]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([relative_tempdir / "rtl1*"]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern(["./*rtl1*"]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + # Create a path that starts with .. + path = Path(rtl[0]._source_file.name).resolve() + path_relative_drive = path.relative_to(path.anchor) + relative_path_to_drive = Path("../" * len(Path(".").resolve().parents)) + test_path = relative_path_to_drive / path_relative_drive + ui.update_test_pattern([test_path]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern([tempdir / "rtl?.vhd"]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[0]._source_file.name], [rtl[1]._source_file.name]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[0]._source_file.name], [rtl[3]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + restore_test_pattern() + ui.update_test_pattern([rtl[0]._source_file.name], [rtl[3]._source_file.name, rtl[4]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern(exclude_dependent_on=[rtl[4]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern(["*.v"]) + check_stdout(ui, "lib2.tb1.all\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern(exclude_dependent_on=["*.v"]) + check_stdout(ui, "lib1.tb0.Test 1\nListed 1 tests") + + restore_test_pattern() + ui.update_test_pattern(["*.v"], ["*.v"]) + check_stdout(ui, "Listed 0 tests") + + restore_test_pattern() + ui.update_test_pattern(set([rtl[0]._source_file.name]), set([rtl[3]._source_file.name])) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + + ui = self._create_ui("--list", "*tb0*") + rtl, tb = setup(ui) + ui.update_test_pattern([tb[1]._source_file.name]) + check_stdout(ui, "lib1.tb0.Test 1\nlib2.tb1.all\nListed 2 tests") + def test_get_simulator_name(self): ui = self._create_ui() self.assertEqual(ui.get_simulator_name(), "mock") diff --git a/vunit/project.py b/vunit/project.py index e384e6019..689aa23e5 100644 --- a/vunit/project.py +++ b/vunit/project.py @@ -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): """ @@ -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): @@ -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, ) @@ -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 diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index dd9975e78..2e6645e85 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -16,9 +16,10 @@ 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 +from glob import glob from ..database import PickledDataBase, DataBase from .. import ostools @@ -111,6 +112,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, @@ -125,17 +142,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 @@ -162,6 +169,9 @@ def test_filter(name, attribute_names): self._builtins = Builtins(self, self._vhdl_standard, simulator_class) + self._include_in_test_pattern: Optional[List[Union[str, Path]]] = [] + self._exclude_from_test_pattern: Optional[List[Union[str, Path]]] = [] + def _create_database(self): """ Create a persistent database to store expensive parse results @@ -736,6 +746,8 @@ def _main(self, post_run): """ Base vunit main function without performing exit """ + if self._include_in_test_pattern or self._exclude_from_test_pattern: + self._update_test_filter(self._include_in_test_pattern, self._exclude_from_test_pattern) if self._args.export_json is not None: return self._main_export_json(self._args.export_json) @@ -752,6 +764,56 @@ def _main(self, post_run): all_ok = self._main_run(post_run) return all_ok + def _update_test_filter(self, include_dependencies=None, exclude_dependencies=None): + """ + Update test filter to reflect included and excluded testbenches + """ + # Default is to include all files and exclude none + include_dependencies = "*" if include_dependencies is None else include_dependencies + + project = self._project + project_source_files = project.get_source_files_in_order() + + def get_dependent_files(dependencies): + "Return all project files dependent on project files matching any of the dependencies." + if not dependencies: + return set() + + # Get project files matching any pattern + dependency_files = set() + for source_file in project_source_files: + for dependency in dependencies: + dependency_str = str(dependency) if isinstance(dependency, Path) else dependency + dependency_str = os.path.normpath(dependency_str) + if source_file.original_name.match(dependency_str): + dependency_files.add(source_file) + # This covers the case where the dependency is a relative path starting with ../ + elif source_file.original_name.match(os.path.abspath(dependency_str)): + dependency_files.add(source_file) + + # Get dependent files, non-testbench files included + dependency_graph = project.create_dependency_graph(True) + dependent_files = set(project.get_affected_files(dependency_files, dependency_graph.get_dependent)) + + return dependent_files + + dependent_files = get_dependent_files(include_dependencies) - get_dependent_files(exclude_dependencies) + + # 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 @@ -1032,6 +1094,32 @@ def add_json4vhdl(self): """ self._builtins.add("json4vhdl") + def update_test_pattern( + self, + include_dependent_on: Optional[List[Union[str, Path]]] = None, + exclude_dependent_on: Optional[List[Union[str, Path]]] = None, + ) -> None: + """ + Update test pattern to include testbenches depending on source files with a file path matching any of the + patterns given in `include_dependent_on_file_patterns` but exclude testbenches depending on source file + patterns in `exclude_dependent_on_file_patterns`. + + Test patterns given on the command line will add to the included test patterns. Excluded testbenches take + precedence over included testbenches. + + :param include_dependent_on: List of :class:`str` or :class:`pathlib.Path` items, + each representing a relative, an absolute file path + pattern. Applied recursively. Default is including + all project source files. + :param exclude_dependent_on: List of :class:`str` or :class:`pathlib.Path` items, + each representing a relative or an absolute file path + pattern. Applied recursively. Default is excluding no + project source file. + :returns: None + """ + self._include_in_test_pattern = include_dependent_on + self._exclude_from_test_pattern = exclude_dependent_on + def get_compile_order(self, source_files=None): """ Get the compile order of all or specific source files and