Coverage for tsfpga/git_simulation_subset.py: 94%
68 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-08-29 20:51 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-08-29 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# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import re
12from pathlib import Path
13from typing import TYPE_CHECKING, Any
15from git.repo import Repo
17from .hdl_file import HdlFile
18from .system_utils import path_relative_to
20if TYPE_CHECKING:
21 from collections.abc import Iterable
23 from git.diff import DiffIndex
24 from vunit.ui import VUnit
26 from .module import BaseModule
27 from .module_list import ModuleList
29# VHDL, Verilog and SystemVerilog.
30_FILE_ENDINGS = HdlFile.file_endings
33class GitSimulationSubset:
34 """
35 Find a subset of testbenches to simulate based on the local git
36 tree compared to a reference branch.
37 """
39 # To match file name "regs_NAME.toml/json/yaml".
40 _re_register_data_filename = re.compile(r"^regs_(.+)\.[a-z]+$")
42 def __init__(
43 self, repo_root: Path, reference_branch: str, modules: ModuleList | None = None
44 ) -> None:
45 """
46 Arguments:
47 repo_root: Root directory where git commands will be run.
48 reference_branch: What git branch to compare against, when finding what files
49 have changed.
50 Typically "origin/main" or "origin/master".
51 modules: Provide this argument to make the class look for changes in the modules'
52 register data files also.
53 If any changes are found, the register HDL files, and any HDL files that depend
54 on them, will be added to the test pattern.
55 """
56 self._repo_root = repo_root
57 self._reference_branch = reference_branch
58 self._modules = modules
60 def update_test_pattern(self, vunit_proj: VUnit) -> set[Path]:
61 """
62 Update VUnit project test pattern to include testbenches depending on files
63 that have changes in the local git tree compared to the reference branch.
65 Arguments:
66 vunit_proj: The VUnit project that will be updated.
68 Returns:
69 The HDL files that were found to have a git diff.
70 """
71 hdl_files = self.get_hdl_file_diff()
72 vunit_proj.update_test_pattern(include_dependent_on=hdl_files)
74 return hdl_files
76 def get_hdl_file_diff(self) -> set[Path]:
77 """
78 Get the HDL files that have changes in the the local git tree compared to
79 the reference branch.
80 """
81 repo = Repo(self._repo_root)
83 head_commit = repo.head.commit
84 reference_commit = repo.commit(rev=self._reference_branch)
86 # Local uncommitted changed
87 working_tree_changes = head_commit.diff(None)
89 # Changes in the git log compared to the reference commit
90 history_changes = head_commit.diff(reference_commit)
92 all_changes: DiffIndex[Any] = working_tree_changes + history_changes
94 return self._get_hdl_files(diffs=all_changes)
96 def _get_hdl_files(self, diffs: DiffIndex[Any]) -> set[Path]:
97 """
98 Return HDL files that have been changed (added/renamed/modified/deleted) within any
99 of the ``diffs`` commits.
101 Will also try to find HDL files that depend on generated register artifacts that
102 have changed.
103 """
104 files = set()
106 def add_register_artifacts_if_match(
107 diff_path: Path, module_register_data_file: Path, module: BaseModule
108 ) -> None:
109 """
110 Note that Path.__eq__ does not do normalization of paths.
111 If one argument is relative and the other is absolute, they will not be equal.
112 Hence, it is important that both paths are resolved before comparison.
113 """
114 if diff_path != module_register_data_file:
115 return
117 re_match = self._re_register_data_filename.match(module_register_data_file.name)
118 if re_match is None:
119 raise ValueError("Register data file does not use the expected naming convention")
121 register_list_name = re_match.group(1)
122 regs_pkg_path = module.register_synthesis_folder / f"{register_list_name}_regs_pkg.vhd"
124 # It is okay to add only the base register package, since all other
125 # register artifacts depend on it.
126 # This file will typically not exist yet in a CI flow, so it doesn't make sense to
127 # assert for its existence.
128 files.add(regs_pkg_path)
130 for diff_path in self._get_diff_paths(diffs=diffs):
131 if diff_path.name.endswith(_FILE_ENDINGS):
132 files.add(diff_path)
134 elif self._modules is not None:
135 for module in self._modules:
136 module_register_data_file = module.register_data_file
138 if isinstance(module_register_data_file, list):
139 # In case users implement a sub-class of BaseModule that has multiple
140 # register lists.
141 # This is not a standard use case that we recommend or support in general,
142 # but we support it here for convenience.
143 for data_file in module_register_data_file:
144 add_register_artifacts_if_match(
145 diff_path=diff_path,
146 module_register_data_file=data_file,
147 module=module,
148 )
150 else:
151 add_register_artifacts_if_match(
152 diff_path=diff_path,
153 module_register_data_file=module_register_data_file,
154 module=module,
155 )
157 self._print_file_list(files=files)
158 return files
160 def _get_diff_paths(self, diffs: DiffIndex[Any]) -> Iterable[Path]:
161 """
162 * If a file is modified, ``a_path`` and ``b_path`` are set and point to the same file.
163 * If a file is added, ``a_path`` is None and ``b_path`` points to the newly added file.
164 * If a file is deleted, ``b_path`` is None and ``a_path`` points to the old deleted file.
165 We still include the 'a_path' in in this case, since we want to catch
166 if any files depend on the deleted file, which would be an error.
167 This mechanism probably does not work, since VUnit dependency scanner does not know what
168 was in that file.
169 But from the perspective of this method, returning the deleted file is still correct.
170 """
171 result = set()
173 for diff in diffs:
174 if diff.a_path is not None:
175 result.add(Path(diff.a_path).resolve())
177 if diff.b_path is not None:
178 result.add(Path(diff.b_path).resolve())
180 return result
182 @staticmethod
183 def _print_file_list(files: set[Path]) -> None:
184 if not files:
185 return
187 print("Found git diff related to the following files:")
189 sorted_files = sorted(files)
190 cwd = Path.cwd()
191 for path in sorted_files:
192 relative_path = path_relative_to(path=path, other=cwd)
193 print(f" {relative_path}")
194 print()