1616import logging
1717import json
1818import os
19- from typing import Optional , Set , Union
19+ from typing import Optional , Set , Union , List
2020from pathlib import Path
2121from 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