Coverage for tsfpga/git_simulation_subset.py: 78%

83 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-18 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 pathlib import Path 

12from typing import TYPE_CHECKING, Any, Optional 

13 

14# Third party libraries 

15from git.repo import Repo 

16 

17if TYPE_CHECKING: 

18 # Local folder libraries 

19 from .module_list import ModuleList 

20 

21 

22class GitSimulationSubset: 

23 """ 

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

25 """ 

26 

27 _re_tb_filename = re.compile(r"(tb_.+\.vhd)|(.+\_tb.vhd)") 

28 

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 

54 

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") 

57 

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

59 """ 

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

61 

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() 

67 

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) 

74 

75 # Find all testbench files that are available 

76 testbenches = self._find_testbenches() 

77 

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)) 

87 

88 return testbenches_to_run 

89 

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

91 repo = Repo(self._repo_root) 

92 

93 head_commit = repo.head.commit 

94 reference_commit = repo.commit(self._reference_branch) 

95 

96 # Local uncommitted changed 

97 working_tree_changes = head_commit.diff(None) 

98 

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

100 history_changes = head_commit.diff(reference_commit) 

101 

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

103 

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() 

110 

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) 

116 

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()) 

122 

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

124 return files 

125 

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) 

136 

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 

146 

147 result.add(preprocessed_file) 

148 

149 return result 

150 

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 

162 

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

164 return None 

165 

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

167 """ 

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

169 

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 

177 

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)) 

181 

182 return result 

183 

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]) 

190 

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 } 

196 

197 intersection = source_file_dependencies & files 

198 if not intersection: 

199 return False 

200 

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 

206 

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()