1# Copyright 2023 Google Inc. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5# This is a copy of PRESUBMIT_test_mocks.py from the Chromium project. 6 7from collections import defaultdict 8import fnmatch 9import json 10import os 11import re 12import subprocess 13import sys 14 15 16def _ReportErrorFileAndLine(filename, line_num, dummy_line): 17 """Default error formatter for _FindNewViolationsOfRule.""" 18 return '%s:%s' % (filename, line_num) 19 20 21class MockCannedChecks(object): 22 def _FindNewViolationsOfRule(self, callable_rule, input_api, 23 source_file_filter=None, 24 error_formatter=_ReportErrorFileAndLine): 25 """Find all newly introduced violations of a per-line rule (a callable). 26 27 Arguments: 28 callable_rule: a callable taking a file extension and line of input and 29 returning True if the rule is satisfied and False if there was a 30 problem. 31 input_api: object to enumerate the affected files. 32 source_file_filter: a filter to be passed to the input api. 33 error_formatter: a callable taking (filename, line_number, line) and 34 returning a formatted error string. 35 36 Returns: 37 A list of the newly-introduced violations reported by the rule. 38 """ 39 errors = [] 40 for f in input_api.AffectedFiles(include_deletes=False, 41 file_filter=source_file_filter): 42 # For speed, we do two passes, checking first the full file. Shelling out 43 # to the SCM to determine the changed region can be quite expensive on 44 # Win32. Assuming that most files will be kept problem-free, we can 45 # skip the SCM operations most of the time. 46 extension = str(f.LocalPath()).rsplit('.', 1)[-1] 47 if all(callable_rule(extension, line) for line in f.NewContents()): 48 # No violation found in full text: can skip considering diff. 49 continue 50 51 for line_num, line in f.ChangedContents(): 52 if not callable_rule(extension, line): 53 errors.append(error_formatter( 54 f.LocalPath(), line_num, line)) 55 56 return errors 57 58 59class MockInputApi(object): 60 """Mock class for the InputApi class. 61 62 This class can be used for unittests for presubmit by initializing the files 63 attribute as the list of changed files. 64 """ 65 66 DEFAULT_FILES_TO_SKIP = () 67 68 def __init__(self): 69 self.canned_checks = MockCannedChecks() 70 self.fnmatch = fnmatch 71 self.json = json 72 self.re = re 73 self.os_path = os.path 74 self.platform = sys.platform 75 self.python_executable = sys.executable 76 self.python3_executable = sys.executable 77 self.platform = sys.platform 78 self.subprocess = subprocess 79 self.sys = sys 80 self.files = [] 81 self.is_committing = False 82 self.change = MockChange([]) 83 self.presubmit_local_path = os.path.dirname(__file__) 84 self.is_windows = sys.platform == 'win32' 85 self.no_diffs = False 86 # Although this makes assumptions about command line arguments used by test 87 # scripts that create mocks, it is a convenient way to set up the verbosity 88 # via the input api. 89 self.verbose = '--verbose' in sys.argv 90 91 def CreateMockFileInPath(self, f_list): 92 self.os_path.exists = lambda x: x in f_list 93 94 def AffectedFiles(self, file_filter=None, include_deletes=True): 95 for file in self.files: 96 if file_filter and not file_filter(file): 97 continue 98 if not include_deletes and file.Action() == 'D': 99 continue 100 yield file 101 102 def RightHandSideLines(self, source_file_filter=None): 103 affected_files = self.AffectedSourceFiles(source_file_filter) 104 for af in affected_files: 105 lines = af.ChangedContents() 106 for line in lines: 107 yield (af, line[0], line[1]) 108 109 def AffectedSourceFiles(self, file_filter=None): 110 return self.AffectedFiles(file_filter=file_filter) 111 112 def FilterSourceFile(self, file, 113 files_to_check=(), files_to_skip=()): 114 local_path = file.LocalPath() 115 found_in_files_to_check = not files_to_check 116 if files_to_check: 117 if type(files_to_check) is str: 118 raise TypeError( 119 'files_to_check should be an iterable of strings') 120 for pattern in files_to_check: 121 compiled_pattern = re.compile(pattern) 122 if compiled_pattern.match(local_path): 123 found_in_files_to_check = True 124 break 125 if files_to_skip: 126 if type(files_to_skip) is str: 127 raise TypeError( 128 'files_to_skip should be an iterable of strings') 129 for pattern in files_to_skip: 130 compiled_pattern = re.compile(pattern) 131 if compiled_pattern.match(local_path): 132 return False 133 return found_in_files_to_check 134 135 def LocalPaths(self): 136 return [file.LocalPath() for file in self.files] 137 138 def PresubmitLocalPath(self): 139 return self.presubmit_local_path 140 141 def ReadFile(self, filename, mode='r'): 142 if hasattr(filename, 'AbsoluteLocalPath'): 143 filename = filename.AbsoluteLocalPath() 144 for file_ in self.files: 145 if file_.LocalPath() == filename: 146 return '\n'.join(file_.NewContents()) 147 # Otherwise, file is not in our mock API. 148 raise IOError("No such file or directory: '%s'" % filename) 149 150 151class MockOutputApi(object): 152 """Mock class for the OutputApi class. 153 154 An instance of this class can be passed to presubmit unittests for outputting 155 various types of results. 156 """ 157 158 class PresubmitResult(object): 159 def __init__(self, message, items=None, long_text=''): 160 self.message = message 161 self.items = items 162 self.long_text = long_text 163 164 def __repr__(self): 165 return self.message 166 167 class PresubmitError(PresubmitResult): 168 def __init__(self, message, items=None, long_text=''): 169 MockOutputApi.PresubmitResult.__init__( 170 self, message, items, long_text) 171 self.type = 'error' 172 173 class PresubmitPromptWarning(PresubmitResult): 174 def __init__(self, message, items=None, long_text=''): 175 MockOutputApi.PresubmitResult.__init__( 176 self, message, items, long_text) 177 self.type = 'warning' 178 179 class PresubmitNotifyResult(PresubmitResult): 180 def __init__(self, message, items=None, long_text=''): 181 MockOutputApi.PresubmitResult.__init__( 182 self, message, items, long_text) 183 self.type = 'notify' 184 185 class PresubmitPromptOrNotify(PresubmitResult): 186 def __init__(self, message, items=None, long_text=''): 187 MockOutputApi.PresubmitResult.__init__( 188 self, message, items, long_text) 189 self.type = 'promptOrNotify' 190 191 def __init__(self): 192 self.more_cc = [] 193 194 def AppendCC(self, more_cc): 195 self.more_cc.append(more_cc) 196 197 198class MockFile(object): 199 """Mock class for the File class. 200 201 This class can be used to form the mock list of changed files in 202 MockInputApi for presubmit unittests. 203 """ 204 205 def __init__(self, local_path, new_contents, old_contents=None, action='A', 206 scm_diff=None): 207 self._local_path = local_path 208 self._new_contents = new_contents 209 self._changed_contents = [(i + 1, l) 210 for i, l in enumerate(new_contents)] 211 self._action = action 212 if scm_diff: 213 self._scm_diff = scm_diff 214 else: 215 self._scm_diff = ( 216 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % 217 (local_path, len(new_contents))) 218 for l in new_contents: 219 self._scm_diff += "+%s\n" % l 220 self._old_contents = old_contents 221 222 def Action(self): 223 return self._action 224 225 def ChangedContents(self): 226 return self._changed_contents 227 228 def NewContents(self, flush_cache=False): 229 return self._new_contents 230 231 def LocalPath(self): 232 return self._local_path 233 234 def AbsoluteLocalPath(self): 235 return self._local_path 236 237 def GenerateScmDiff(self): 238 return self._scm_diff 239 240 def OldContents(self): 241 return self._old_contents 242 243 def rfind(self, p): 244 """os.path.basename is called on MockFile so we need an rfind method.""" 245 return self._local_path.rfind(p) 246 247 def __getitem__(self, i): 248 """os.path.basename is called on MockFile so we need a get method.""" 249 return self._local_path[i] 250 251 def __len__(self): 252 """os.path.basename is called on MockFile so we need a len method.""" 253 return len(self._local_path) 254 255 def replace(self, altsep, sep): 256 """os.path.basename is called on MockFile so we need a replace method.""" 257 return self._local_path.replace(altsep, sep) 258 259 260class MockAffectedFile(MockFile): 261 def AbsoluteLocalPath(self): 262 return self._local_path 263 264 265class MockChange(object): 266 """Mock class for Change class. 267 268 This class can be used in presubmit unittests to mock the query of the 269 current change. 270 """ 271 272 def __init__(self, changed_files): 273 self._changed_files = changed_files 274 self.author_email = None 275 self.footers = defaultdict(list) 276 277 def LocalPaths(self): 278 return self._changed_files 279 280 def AffectedFiles(self, include_dirs=False, include_deletes=True, 281 file_filter=None): 282 return self._changed_files 283 284 def GitFootersFromDescription(self): 285 return self.footers 286