Coverage for tsfpga/git_simulation_subset.py: 78%
83 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-10 20:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-10 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import re
11from pathlib import Path
12from typing import TYPE_CHECKING, Any, Optional
14# Third party libraries
15from git.repo import Repo
17if TYPE_CHECKING:
18 # Local folder libraries
19 from .module_list import ModuleList
22class GitSimulationSubset:
23 """
24 Find a subset of testbenches to simulate based on git history.
25 """
27 _re_tb_filename = re.compile(r"(tb_.+\.vhd)|(.+\_tb.vhd)")
29 def __init__(
30 self,
31 repo_root: Path,
32 reference_branch: str,
33 vunit_proj: Any,
34 vunit_preprocessed_path: Optional[Path] = None,
35 modules: Optional["ModuleList"] = None,
36 ) -> None:
37 """
38 Arguments:
39 repo_root: Root directory where git commands will be run.
40 reference_branch: What git branch to compare against, when finding what files have
41 changed. Typically "origin/main" or "origin/master".
42 vunit_proj: A vunit project with all source files and testbenches added. Will be used
43 for dependency scanning.
44 vunit_preprocessed_path: If location/check preprocessing is enabled
45 in your VUnit project, supply the path to vunit_out/preprocessed.
46 modules: A list of modules that are included in the VUnit project.
47 Must be supplied only if preprocessing is enabled.
48 """
49 self._repo_root = repo_root
50 self._reference_branch = reference_branch
51 self._vunit_proj = vunit_proj
52 self._vunit_preprocessed_path = vunit_preprocessed_path
53 self._modules = modules
55 if (vunit_preprocessed_path is not None) != (modules is not None):
56 raise ValueError("Can not supply only one of vunit_preprocessed_path and modules")
58 def find_subset(self) -> list[tuple[str, str]]:
59 """
60 Return all testbenches that have changes, or depend on files that have changes.
62 Return:
63 The testbench names and their corresponding library names.
64 A list of tuples ("testbench name", "library name").
65 """
66 diff_files = self._find_diff_vhd_files()
68 if self._vunit_preprocessed_path:
69 # If preprocessing is enabled, VUnit's dependency graph is based on the files that
70 # are in the vunit_out/preprocessed folder, not in the file's original location.
71 # So manipulate the paths to point there.
72 diff_files = self._get_preprocessed_file_locations(diff_files)
73 self._print_file_list("Resolved diff file locations to be", diff_files)
75 # Find all testbench files that are available
76 testbenches = self._find_testbenches()
78 # Gather the testbenches that depend on any files that have diffs
79 testbenches_to_run = []
80 for testbench_source_file, library_name in testbenches:
81 if self._source_file_depends_on_files(
82 source_file=testbench_source_file,
83 files=diff_files,
84 ):
85 testbench_file_name = Path(testbench_source_file.name).stem
86 testbenches_to_run.append((testbench_file_name, library_name))
88 return testbenches_to_run
90 def _find_diff_vhd_files(self) -> set[Path]:
91 repo = Repo(self._repo_root)
93 head_commit = repo.head.commit
94 reference_commit = repo.commit(self._reference_branch)
96 # Local uncommitted changed
97 working_tree_changes = head_commit.diff(None)
99 # Changes in the git log compared to the reference commit
100 history_changes = head_commit.diff(reference_commit)
102 return self._iterate_vhd_file_diffs(diffs=working_tree_changes + history_changes)
104 def _iterate_vhd_file_diffs(self, diffs: Any) -> set[Path]:
105 """
106 Return the currently existing files that have been changed (added/renamed/modified)
107 within any of the diffs commits.
108 """
109 files = set()
111 for diff in diffs:
112 # The diff contains "a" -> "b" changes information. In case of file deletion, a_path
113 # will be set but not b_path. Removed files are not included by this method.
114 if diff.b_path is not None:
115 b_path = Path(diff.b_path)
117 # A file can be changed in an early commit, but then removed/renamed in a
118 # later commit. Include only files that are currently existing.
119 if b_path.exists():
120 if b_path.name.endswith(".vhd"):
121 files.add(b_path.resolve())
123 self._print_file_list("Found git diff in the following files", files)
124 return files
126 def _get_preprocessed_file_locations(self, vhd_files: set[Path]) -> set[Path]:
127 """
128 Find the location of a VUnit preprocessed file, based on the path in the modules tree.
129 Not all VHDL files are included in the simulation projects (e.g. often files that depend
130 on IP cores are excluded), hence files that can not be found in any module's simulation
131 files are ignored.
132 """
133 result = set()
134 for vhd_file in vhd_files:
135 library_name = self._get_library_name_from_path(vhd_file)
137 if library_name is not None:
138 # Ignore that '_vunit_preprocessed_path' is type 'Path | None', since we only come
139 # if it is not 'None'.
140 preprocessed_file = (
141 self._vunit_preprocessed_path # type: ignore[operator]
142 / library_name
143 / vhd_file.name
144 )
145 assert preprocessed_file.exists(), preprocessed_file
147 result.add(preprocessed_file)
149 return result
151 def _get_library_name_from_path(self, vhd_file: Path) -> Optional[str]:
152 """
153 Returns (str): Library name for the given file path.
154 Will return None if no library can be found.
155 """
156 # Ignore that '_modules' is type 'Path | None', since we only come
157 # if it is not 'None'.
158 for module in self._modules: # type: ignore[union-attr]
159 for module_hdl_file in module.get_simulation_files(include_ip_cores=True):
160 if module_hdl_file.path.name == vhd_file.name:
161 return module.library_name
163 print(f"Could not find library for file {vhd_file}. It will be skipped.")
164 return None
166 def _find_testbenches(self) -> list[tuple[Any, str]]:
167 """
168 Find all testbench files that are available in the VUnit project.
170 Return:
171 The VUnit ``SourceFile`` objects and library names for the files.
172 """
173 result = []
174 for source_file in self._vunit_proj.get_source_files():
175 source_file_path = Path(source_file.name)
176 assert source_file_path.exists(), source_file_path
178 # The file is considered a testbench if it follows the tb naming pattern
179 if re.fullmatch(self._re_tb_filename, source_file_path.name):
180 result.append((source_file, source_file.library.name))
182 return result
184 def _source_file_depends_on_files(self, source_file: Any, files: set[Path]) -> bool:
185 """
186 Return True if the source_file depends on any of the files.
187 """
188 # Note that this includes the source_file itself. Is a list of SourceFile objects.
189 implementation_subset = self._vunit_proj.get_implementation_subset([source_file])
191 # Convert to a set of absolute Paths, for comparison with "files" which is of that type.
192 source_file_dependencies = {
193 Path(implementation_file.name).resolve()
194 for implementation_file in implementation_subset
195 }
197 intersection = source_file_dependencies & files
198 if not intersection:
199 return False
201 self._print_file_list(
202 f"Testbench {source_file.name} depends on the following files which have a diff",
203 intersection,
204 )
205 return True
207 @staticmethod
208 def _print_file_list(title: str, files: set[Path]) -> None:
209 print(f"{title}:")
210 for file_path in files:
211 print(f" {file_path}")
212 print()