Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project. 

5# https://tsfpga.com 

6# https://gitlab.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9import re 

10from pathlib import Path 

11 

12from git import Repo 

13 

14 

15class GitSimulationSubset: 

16 """ 

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

18 """ 

19 

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

21 

22 def __init__( 

23 self, repo_root, reference_branch, vunit_proj, vunit_preprocessed_path=None, modules=None 

24 ): 

25 """ 

26 Arguments: 

27 repo_root (``pathlib.Path``): Root directory where git commands will be run. 

28 reference_branch (str): What git branch to compare against, when finding what files have 

29 changed. Typically "origin/master". 

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

31 for dependency scanning. 

32 vunit_preprocessed_path (``pathlib.Path``): If location/check preprocessing is enabled 

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

34 modules (.ModuleList): A list of modules that are included in the VUnit project. Must be 

35 supplied only if preprocessing is enabled. 

36 """ 

37 self._repo_root = repo_root 

38 self._reference_branch = reference_branch 

39 self._vunit_proj = vunit_proj 

40 self._vunit_preprocessed_path = vunit_preprocessed_path 

41 self._modules = modules 

42 

43 if (vunit_preprocessed_path is not None) != (modules is not None): 

44 raise ValueError("Can not supply only one of vunit_preprocessed_path and modules") 

45 

46 def find_subset(self): 

47 """ 

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

49 

50 Return: 

51 list(tuple(str, str)): The testbench names and their corresponding library names. A list 

52 of tuples ("testbench name", "library name"). 

53 """ 

54 diff_files = self._find_diff_vhd_files() 

55 

56 if self._vunit_preprocessed_path: 

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

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

59 # So manipulate the paths to point there. 

60 diff_files = self._get_preprocessed_file_locations(diff_files) 

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

62 

63 # Find all testbench files that are available 

64 testbenches = self._find_testbenches() 

65 

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

67 testbenches_to_run = [] 

68 for testbench_source_file, library_name in testbenches: 

69 if self._source_file_depends_on_files( 

70 source_file=testbench_source_file, 

71 files=diff_files, 

72 ): 

73 testbench_file_name = Path(testbench_source_file.name).stem 

74 testbenches_to_run.append((testbench_file_name, library_name)) 

75 

76 return testbenches_to_run 

77 

78 def _find_diff_vhd_files(self): 

79 repo = Repo(self._repo_root) 

80 

81 head_commit = repo.head.commit 

82 reference_commit = repo.commit(self._reference_branch) 

83 

84 # Local uncommitted changed 

85 working_tree_changes = head_commit.diff(None) 

86 

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

88 history_changes = head_commit.diff(reference_commit) 

89 

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

91 

92 def _iterate_vhd_file_diffs(self, diffs): 

93 """ 

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

95 within any of the diffs commits. 

96 

97 Returns a set of Paths. 

98 """ 

99 files = set() 

100 

101 for diff in diffs: 

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

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

104 if diff.b_path is not None: 

105 b_path = Path(diff.b_path) 

106 

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

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

109 if b_path.exists(): 

110 if b_path.name.endswith(".vhd"): 

111 files.add(b_path) 

112 

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

114 return files 

115 

116 def _get_preprocessed_file_locations(self, vhd_files): 

117 """ 

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

119 """ 

120 result = set() 

121 for vhd_file in vhd_files: 

122 library_name = self._get_library_name_from_path(vhd_file) 

123 preprocessed_file = self._vunit_preprocessed_path / library_name / vhd_file.name 

124 assert preprocessed_file.exists(), preprocessed_file 

125 

126 result.add(preprocessed_file) 

127 

128 return result 

129 

130 def _get_library_name_from_path(self, vhd_file): 

131 """ 

132 Returns (str): Library name for the given file path. 

133 """ 

134 for module in self._modules: 

135 for module_hdl_file in module.get_simulation_files(): 

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

137 return module.library_name 

138 

139 assert False, f"Could not find library for file {vhd_file}" 

140 return None 

141 

142 def _find_testbenches(self): 

143 """ 

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

145 

146 Return: 

147 list(tuple(``SourceFile``, str)): The VUnit SourceFile objects and library names 

148 for the files. 

149 """ 

150 result = [] 

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

152 source_file_path = Path(source_file.name) 

153 assert source_file_path.exists(), source_file_path 

154 

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

156 if re.fullmatch(self._re_tb_filename, source_file_path.name): 

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

158 

159 return result 

160 

161 def _source_file_depends_on_files(self, source_file, files): 

162 """ 

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

164 """ 

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

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

167 

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

169 source_file_dependencies = { 

170 Path(implementation_file.name).resolve() 

171 for implementation_file in implementation_subset 

172 } 

173 

174 intersection = source_file_dependencies & files 

175 if not intersection: 

176 return False 

177 

178 self._print_file_list( 

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

180 intersection, 

181 ) 

182 return True 

183 

184 @staticmethod 

185 def _print_file_list(title, files): 

186 print(f"{title}:") 

187 for file_path in files: 

188 print(f" {file_path}") 

189 print()