Coverage for tsfpga/git_simulation_subset.py: 79%

108 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 20:52 +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# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import re 

11from collections.abc import Iterable 

12from pathlib import Path 

13from typing import TYPE_CHECKING, Any, Optional 

14 

15# Third party libraries 

16from git.repo import Repo 

17 

18# Local folder libraries 

19from .hdl_file import HdlFile 

20 

21if TYPE_CHECKING: 

22 # Third party libraries 

23 from vunit.ui import VUnit 

24 

25 # Local folder libraries 

26 from .module import BaseModule 

27 from .module_list import ModuleList 

28 

29VHDL_FILE_ENDINGS = HdlFile.file_endings_mapping[HdlFile.Type.VHDL] 

30 

31 

32class GitSimulationSubset: 

33 """ 

34 Find a subset of testbenches to simulate based on git history. 

35 """ 

36 

37 # Should work with 

38 # * tb_<name>.vhd 

39 # * <name>_tb.vhd 

40 # and possibly .vhdl as file extension. 

41 _re_tb_filename = re.compile(r"^(tb_.+\.vhdl?)|(.+\_tb.vhdl?)$") 

42 # Should work with .toml, .json, .yaml, etc. 

43 _re_register_data_filename = re.compile(r"^regs_(.+)\.[a-z]+$") 

44 

45 def __init__( 

46 self, 

47 repo_root: Path, 

48 reference_branch: str, 

49 vunit_proj: "VUnit", 

50 modules: Optional["ModuleList"] = None, 

51 vunit_preprocessed_path: Optional[Path] = None, 

52 ) -> None: 

53 """ 

54 Arguments: 

55 repo_root: Root directory where git commands will be run. 

56 reference_branch: What git branch to compare against, when finding what files have 

57 changed. Typically "origin/main" or "origin/master". 

58 vunit_proj: A vunit project with all source files and testbenches added. Will be used 

59 for dependency scanning. 

60 modules: A list of modules that are included in the VUnit project. 

61 

62 When this argument is provided, this class will look for changes in the modules' 

63 register data files, and simulate the testbenches that depend on register artifacts 

64 in case of any changes. 

65 

66 This argument **must** be supplied if VUnit preprocessing is enabled. 

67 vunit_preprocessed_path: If location/check preprocessing is enabled 

68 in your VUnit project, supply the path to ``vunit_out/preprocessed``. 

69 """ 

70 self._repo_root = repo_root 

71 self._reference_branch = reference_branch 

72 self._vunit_proj = vunit_proj 

73 self._modules = modules 

74 self._vunit_preprocessed_path = vunit_preprocessed_path 

75 

76 def find_subset(self) -> list[tuple[str, str]]: 

77 """ 

78 Return all testbenches that have changes, or depend on files that have changes. 

79 

80 Return: 

81 The testbench names and their corresponding library names. 

82 A list of tuples ("testbench name", "library name"). 

83 """ 

84 diff_files = self._find_diff_vhd_files() 

85 

86 if self._vunit_preprocessed_path: 

87 # If preprocessing is enabled, VUnit's dependency graph is based on the files that 

88 # are in the vunit_out/preprocessed folder, not in the file's original location. 

89 # So manipulate the paths to point there. 

90 diff_files = self._get_preprocessed_file_locations(diff_files) 

91 self._print_file_list("Resolved diff file locations to be", diff_files) 

92 

93 # Find all testbench files that are available 

94 testbenches = self._find_testbenches() 

95 

96 # Gather the testbenches that depend on any files that have diffs 

97 testbenches_to_run = [] 

98 for testbench_source_file, library_name in testbenches: 

99 if self._source_file_depends_on_files( 

100 source_file=testbench_source_file, 

101 files=diff_files, 

102 ): 

103 testbench_file_name = Path(testbench_source_file.name).stem 

104 testbenches_to_run.append((testbench_file_name, library_name)) 

105 

106 return testbenches_to_run 

107 

108 def _find_diff_vhd_files(self) -> set[Path]: 

109 repo = Repo(self._repo_root) 

110 

111 head_commit = repo.head.commit 

112 reference_commit = repo.commit(self._reference_branch) 

113 

114 # Local uncommitted changed 

115 working_tree_changes = head_commit.diff(None) 

116 

117 # Changes in the git log compared to the reference commit 

118 history_changes = head_commit.diff(reference_commit) 

119 

120 return self._iterate_vhd_file_diffs(diffs=working_tree_changes + history_changes) 

121 

122 def _iterate_vhd_file_diffs(self, diffs: Any) -> set[Path]: 

123 """ 

124 Return the currently existing VHDL files that have been changed (added/renamed/modified) 

125 within any of the ``diffs`` commits. 

126 

127 Will also try to find VHDL files that depend on generated register artifacts that 

128 have changed. 

129 """ 

130 files = set() 

131 

132 def add_register_artifacts_if_match( 

133 diff_path: Path, module_register_data_file: Path, module: "BaseModule" 

134 ) -> None: 

135 """ 

136 Note that Path.__eq__ does not do normalization of paths. 

137 If one argument is a relative path and the other is an absolute path, they will not 

138 be considered equal. 

139 Hence, it is important that both paths are resolved before comparison. 

140 """ 

141 if diff_path != module_register_data_file: 

142 return 

143 

144 re_match = self._re_register_data_filename.match(module_register_data_file.name) 

145 assert ( 

146 re_match is not None 

147 ), "Register data file does not use the expected naming convention" 

148 

149 register_list_name = re_match.group(1) 

150 regs_pkg_path = module.register_synthesis_folder / f"{register_list_name}_regs_pkg.vhd" 

151 

152 # It is okay to add only the base register package, since all other 

153 # register artifacts depend on it. 

154 # This file will typically not exist yet in a CI flow, so it doesn't make sense to 

155 # assert for its existence. 

156 files.add(regs_pkg_path) 

157 

158 for diff_path in self._iterate_diff_paths(diffs=diffs): 

159 if diff_path.name.endswith(VHDL_FILE_ENDINGS): 

160 files.add(diff_path) 

161 

162 elif self._modules is not None: 

163 for module in self._modules: 

164 module_register_data_file = module.register_data_file 

165 

166 if isinstance(module_register_data_file, list): 

167 # In users implement a sub-class of BaseModule that has multiple register 

168 # lists. This is not a standard use case, but we support it here. 

169 for data_file in module_register_data_file: 

170 add_register_artifacts_if_match( 

171 diff_path=diff_path, 

172 module_register_data_file=data_file, 

173 module=module, 

174 ) 

175 

176 else: 

177 add_register_artifacts_if_match( 

178 diff_path=diff_path, 

179 module_register_data_file=module_register_data_file, 

180 module=module, 

181 ) 

182 

183 self._print_file_list("Found git diff in the following files", files) 

184 return files 

185 

186 def _iterate_diff_paths(self, diffs: Any) -> Iterable[Path]: 

187 for diff in diffs: 

188 # The diff contains "a" -> "b" changes information. In case of file deletion, a_path 

189 # will be set but not b_path. Removed files are not included by this method. 

190 if diff.b_path is not None: 

191 b_path = Path(diff.b_path) 

192 

193 # A file can be changed in an early commit, but then removed/renamed in a 

194 # later commit. Include only files that are currently existing. 

195 if b_path.exists(): 

196 yield b_path.resolve() 

197 

198 def _get_preprocessed_file_locations(self, vhd_files: set[Path]) -> set[Path]: 

199 """ 

200 Find the location of a VUnit preprocessed file, based on the path in the modules tree. 

201 Not all VHDL files are included in the simulation projects (e.g. often files that depend 

202 on IP cores are excluded), hence files that can not be found in any module's simulation 

203 files are ignored. 

204 """ 

205 assert ( 

206 self._modules is not None 

207 ), "Modules must be supplied when VUnit preprocessing is enabled" 

208 

209 result = set() 

210 for vhd_file in vhd_files: 

211 library_name = self._get_library_name_from_path(vhd_file) 

212 

213 if library_name is not None: 

214 # Ignore that '_vunit_preprocessed_path' is type 'Path | None', since we only come 

215 # if it is not 'None'. 

216 preprocessed_file = ( 

217 self._vunit_preprocessed_path # type: ignore[operator] 

218 / library_name 

219 / vhd_file.name 

220 ) 

221 assert preprocessed_file.exists(), preprocessed_file 

222 

223 result.add(preprocessed_file) 

224 

225 return result 

226 

227 def _get_library_name_from_path(self, vhd_file: Path) -> Optional[str]: 

228 """ 

229 Returns: Library name for the given file path. 

230 Will return None if no library can be found. 

231 """ 

232 # Ignore that '_modules' is type 'Path | None', since we only come 

233 # if it is not 'None'. 

234 for module in self._modules: # type: ignore[union-attr] 

235 for module_hdl_file in module.get_simulation_files(include_ip_cores=True): 

236 if module_hdl_file.path.name == vhd_file.name: 

237 return module.library_name 

238 

239 print(f"Could not find library for file {vhd_file}. It will be skipped.") 

240 return None 

241 

242 def _find_testbenches(self) -> list[tuple[Any, str]]: 

243 """ 

244 Find all testbench files that are available in the VUnit project. 

245 

246 Return: 

247 The VUnit ``SourceFile`` objects and library names for the files. 

248 """ 

249 result = [] 

250 for source_file in self._vunit_proj.get_source_files(): 

251 source_file_path = Path(source_file.name) 

252 assert source_file_path.exists(), source_file_path 

253 

254 # The file is considered a testbench if it follows the tb naming pattern 

255 if self._re_tb_filename.match(source_file_path.name) is not None: 

256 result.append((source_file, source_file.library.name)) 

257 

258 return result 

259 

260 def _source_file_depends_on_files(self, source_file: Any, files: set[Path]) -> bool: 

261 """ 

262 Return True if the source_file depends on any of the files. 

263 """ 

264 # Note that this includes the source_file itself. Is a list of SourceFile objects. 

265 implementation_subset = self._vunit_proj.get_implementation_subset([source_file]) 

266 

267 # Convert to a set of absolute Paths, for comparison with "files" which is of that type. 

268 source_file_dependencies = { 

269 Path(implementation_file.name).resolve() 

270 for implementation_file in implementation_subset 

271 } 

272 

273 intersection = source_file_dependencies & files 

274 if not intersection: 

275 return False 

276 

277 self._print_file_list( 

278 f"Testbench {source_file.name} depends on the following files which have a diff", 

279 intersection, 

280 ) 

281 return True 

282 

283 @staticmethod 

284 def _print_file_list(title: str, files: set[Path]) -> None: 

285 if not files: 

286 return 

287 

288 print(f"{title}:") 

289 for file_path in files: 

290 print(f" {file_path}") 

291 print()