🌐 AI搜索 & 代理 主页
blob: a701e2af27e9dcd872efc62ed6dae3d43e93f3f3 [file] [log] [blame]
Avi Drissman24976592022-09-12 15:24:311# Copyright 2014 The Chromium Authors
gayane3dff8c22014-12-04 17:09:512# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Chris Hall59f8d0c72020-05-01 07:31:195from collections import defaultdict
Daniel Cheng13ca61a882017-08-25 15:11:256import fnmatch
gayane3dff8c22014-12-04 17:09:517import json
8import os
9import re
10import subprocess
11import sys
12
Andrew Grieve713b89b2024-10-15 20:20:0813_REPO_ROOT = os.path.abspath(os.path.dirname(__file__))
14
Kalvin Leecfd19ddc2025-10-28 06:17:3115
Daniel Cheng264a447d2017-09-28 22:17:5916# TODO(dcheng): It's kind of horrible that this is copy and pasted from
17# presubmit_canned_checks.py, but it's far easier than any of the alternatives.
18def _ReportErrorFileAndLine(filename, line_num, dummy_line):
Andrew Grieve66a2bc42024-10-04 21:20:2019 """Default error formatter for _FindNewViolationsOfRule."""
20 return '%s:%s' % (filename, line_num)
Daniel Cheng264a447d2017-09-28 22:17:5921
22
23class MockCannedChecks(object):
Kalvin Leecfd19ddc2025-10-28 06:17:3124
25 def _FindNewViolationsOfRule(self,
26 callable_rule,
27 input_api,
Andrew Grieve66a2bc42024-10-04 21:20:2028 source_file_filter=None,
29 error_formatter=_ReportErrorFileAndLine):
30 """Find all newly introduced violations of a per-line rule (a callable).
Daniel Cheng264a447d2017-09-28 22:17:5931
32 Arguments:
33 callable_rule: a callable taking a file extension and line of input and
34 returning True if the rule is satisfied and False if there was a
35 problem.
36 input_api: object to enumerate the affected files.
37 source_file_filter: a filter to be passed to the input api.
38 error_formatter: a callable taking (filename, line_number, line) and
39 returning a formatted error string.
40
41 Returns:
42 A list of the newly-introduced violations reported by the rule.
43 """
Andrew Grieve66a2bc42024-10-04 21:20:2044 errors = []
45 for f in input_api.AffectedFiles(include_deletes=False,
46 file_filter=source_file_filter):
47 # For speed, we do two passes, checking first the full file.
48 # Shelling out to the SCM to determine the changed region can be
49 # quite expensive on Win32. Assuming that most files will be kept
50 # problem-free, we can skip the SCM operations most of the time.
Anton Bershanskyi4253349482025-02-11 21:01:2751 extension = str(f.UnixLocalPath()).rsplit('.', 1)[-1]
Andrew Grieve66a2bc42024-10-04 21:20:2052 if all(callable_rule(extension, line) for line in f.NewContents()):
53 # No violation found in full text: can skip considering diff.
54 continue
Daniel Cheng264a447d2017-09-28 22:17:5955
Andrew Grieve66a2bc42024-10-04 21:20:2056 for line_num, line in f.ChangedContents():
57 if not callable_rule(extension, line):
58 errors.append(
59 error_formatter(f.LocalPath(), line_num, line))
Daniel Cheng264a447d2017-09-28 22:17:5960
Andrew Grieve66a2bc42024-10-04 21:20:2061 return errors
gayane3dff8c22014-12-04 17:09:5162
Zhiling Huang45cabf32018-03-10 00:50:0363
gayane3dff8c22014-12-04 17:09:5164class MockInputApi(object):
Andrew Grieve66a2bc42024-10-04 21:20:2065 """Mock class for the InputApi class.
gayane3dff8c22014-12-04 17:09:5166
67 This class can be used for unittests for presubmit by initializing the files
68 attribute as the list of changed files.
69 """
70
Andrew Grieve66a2bc42024-10-04 21:20:2071 DEFAULT_FILES_TO_SKIP = ()
Sylvain Defresnea8b73d252018-02-28 15:45:5472
Andrew Grieve66a2bc42024-10-04 21:20:2073 def __init__(self):
74 self.basename = os.path.basename
75 self.canned_checks = MockCannedChecks()
76 self.fnmatch = fnmatch
77 self.json = json
78 self.re = re
Joanna Wang130e7bdd2024-12-10 17:39:0379
80 # We want os_path.exists() and os_path.isfile() to work for files
81 # that are both in the filesystem and mock files we have added
82 # via InitFiles().
83 # By setting os_path to a copy of os.path rather than directly we
84 # can not only have os_path.exists() be a combined output for fake
85 # files and real files in the filesystem.
86 import importlib.util
87 SPEC_OS_PATH = importlib.util.find_spec('os.path')
88 os_path1 = importlib.util.module_from_spec(SPEC_OS_PATH)
89 SPEC_OS_PATH.loader.exec_module(os_path1)
90 sys.modules['os_path1'] = os_path1
91 self.os_path = os_path1
92
Andrew Grieve66a2bc42024-10-04 21:20:2093 self.platform = sys.platform
94 self.python_executable = sys.executable
95 self.python3_executable = sys.executable
96 self.platform = sys.platform
97 self.subprocess = subprocess
98 self.sys = sys
99 self.files = []
100 self.is_committing = False
101 self.change = MockChange([])
Andrew Grieve713b89b2024-10-15 20:20:08102 self.presubmit_local_path = os.path.dirname(
103 os.path.abspath(sys.argv[0]))
Andrew Grieve66a2bc42024-10-04 21:20:20104 self.is_windows = sys.platform == 'win32'
105 self.no_diffs = False
106 # Although this makes assumptions about command line arguments used by
107 # test scripts that create mocks, it is a convenient way to set up the
108 # verbosity via the input api.
109 self.verbose = '--verbose' in sys.argv
gayane3dff8c22014-12-04 17:09:51110
Andrew Grieve713b89b2024-10-15 20:20:08111 def InitFiles(self, files):
112 # Actual presubmit calls normpath, but too many tests break to do this
113 # right in MockFile().
114 for f in files:
115 f._local_path = os.path.normpath(f._local_path)
116 self.files = files
117 files_that_exist = {
118 p.AbsoluteLocalPath()
119 for p in files if p.Action() != 'D'
120 }
121
122 def mock_exists(path):
123 if not os.path.isabs(path):
124 path = os.path.join(self.presubmit_local_path, path)
Andrew Grieveb77ac76d2024-11-29 15:01:48125 path = os.path.normpath(path)
Joanna Wang130e7bdd2024-12-10 17:39:03126 return path in files_that_exist or any(
127 f.startswith(path)
128 for f in files_that_exist) or os.path.exists(path)
129
130 def mock_isfile(path):
131 if not os.path.isabs(path):
132 path = os.path.join(self.presubmit_local_path, path)
133 path = os.path.normpath(path)
134 return path in files_that_exist or os.path.isfile(path)
Andrew Grieve713b89b2024-10-15 20:20:08135
136 def mock_glob(pattern, *args, **kwargs):
Joanna Wang130e7bdd2024-12-10 17:39:03137 return fnmatch.filter(files_that_exist, pattern)
Andrew Grieve713b89b2024-10-15 20:20:08138
139 # Do not stub these in the constructor to not break existing tests.
140 self.os_path.exists = mock_exists
Joanna Wang130e7bdd2024-12-10 17:39:03141 self.os_path.isfile = mock_isfile
Andrew Grieve713b89b2024-10-15 20:20:08142 self.glob = mock_glob
Zhiling Huang45cabf32018-03-10 00:50:03143
Andrew Grieve66a2bc42024-10-04 21:20:20144 def AffectedFiles(self, file_filter=None, include_deletes=True):
145 for file in self.files:
146 if file_filter and not file_filter(file):
147 continue
148 if not include_deletes and file.Action() == 'D':
149 continue
150 yield file
gayane3dff8c22014-12-04 17:09:51151
Andrew Grieve66a2bc42024-10-04 21:20:20152 def RightHandSideLines(self, source_file_filter=None):
153 affected_files = self.AffectedSourceFiles(source_file_filter)
154 for af in affected_files:
155 lines = af.ChangedContents()
156 for line in lines:
157 yield (af, line[0], line[1])
Lukasz Anforowicz7016d05e2021-11-30 03:56:27158
Andrew Grieve66a2bc42024-10-04 21:20:20159 def AffectedSourceFiles(self, file_filter=None):
160 return self.AffectedFiles(file_filter=file_filter,
161 include_deletes=False)
Sylvain Defresnea8b73d252018-02-28 15:45:54162
Andrew Grieve66a2bc42024-10-04 21:20:20163 def AffectedTestableFiles(self, file_filter=None):
164 return self.AffectedFiles(file_filter=file_filter,
165 include_deletes=False)
Georg Neis9080e0602024-08-23 01:50:29166
Andrew Grieve66a2bc42024-10-04 21:20:20167 def FilterSourceFile(self, file, files_to_check=(), files_to_skip=()):
Anton Bershanskyi4253349482025-02-11 21:01:27168 local_path = file.UnixLocalPath()
Andrew Grieve66a2bc42024-10-04 21:20:20169 found_in_files_to_check = not files_to_check
170 if files_to_check:
171 if type(files_to_check) is str:
172 raise TypeError(
173 'files_to_check should be an iterable of strings')
174 for pattern in files_to_check:
175 compiled_pattern = re.compile(pattern)
176 if compiled_pattern.match(local_path):
177 found_in_files_to_check = True
178 break
179 if files_to_skip:
180 if type(files_to_skip) is str:
181 raise TypeError(
182 'files_to_skip should be an iterable of strings')
183 for pattern in files_to_skip:
184 compiled_pattern = re.compile(pattern)
185 if compiled_pattern.match(local_path):
186 return False
187 return found_in_files_to_check
glidere61efad2015-02-18 17:39:43188
Andrew Grieve66a2bc42024-10-04 21:20:20189 def LocalPaths(self):
190 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41191
Andrew Grieve66a2bc42024-10-04 21:20:20192 def PresubmitLocalPath(self):
193 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51194
Andrew Grieve66a2bc42024-10-04 21:20:20195 def ReadFile(self, filename, mode='r'):
196 if hasattr(filename, 'AbsoluteLocalPath'):
197 filename = filename.AbsoluteLocalPath()
Andrew Grieveb77ac76d2024-11-29 15:01:48198 norm_filename = os.path.normpath(filename)
Andrew Grieve66a2bc42024-10-04 21:20:20199 for file_ in self.files:
Andrew Grieveb77ac76d2024-11-29 15:01:48200 to_check = (file_.LocalPath(), file_.AbsoluteLocalPath())
201 if filename in to_check or norm_filename in to_check:
Andrew Grieve66a2bc42024-10-04 21:20:20202 return '\n'.join(file_.NewContents())
203 # Otherwise, file is not in our mock API.
204 raise IOError("No such file or directory: '%s'" % filename)
gayane3dff8c22014-12-04 17:09:51205
206
207class MockOutputApi(object):
Andrew Grieve66a2bc42024-10-04 21:20:20208 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51209
Gao Shenga79ebd42022-08-08 17:25:59210 An instance of this class can be passed to presubmit unittests for outputting
gayane3dff8c22014-12-04 17:09:51211 various types of results.
212 """
213
Andrew Grieve66a2bc42024-10-04 21:20:20214 class PresubmitResult(object):
gayane3dff8c22014-12-04 17:09:51215
Ben Pastenee79d66112025-04-23 19:46:15216 def __init__(self, message, items=None, long_text='', locations=[]):
Andrew Grieve66a2bc42024-10-04 21:20:20217 self.message = message
218 self.items = items
219 self.long_text = long_text
Ben Pastenee79d66112025-04-23 19:46:15220 self.locations = locations
gayane940df072015-02-24 14:28:30221
Andrew Grieve66a2bc42024-10-04 21:20:20222 def __repr__(self):
223 return self.message
gayane3dff8c22014-12-04 17:09:51224
Andrew Grieve66a2bc42024-10-04 21:20:20225 class PresubmitError(PresubmitResult):
gayane3dff8c22014-12-04 17:09:51226
Ben Pastenee79d66112025-04-23 19:46:15227 def __init__(self, *args, **kwargs):
228 MockOutputApi.PresubmitResult.__init__(self, *args, **kwargs)
Andrew Grieve66a2bc42024-10-04 21:20:20229 self.type = 'error'
gayane3dff8c22014-12-04 17:09:51230
Andrew Grieve66a2bc42024-10-04 21:20:20231 class PresubmitPromptWarning(PresubmitResult):
gayane3dff8c22014-12-04 17:09:51232
Ben Pastenee79d66112025-04-23 19:46:15233 def __init__(self, *args, **kwargs):
234 MockOutputApi.PresubmitResult.__init__(self, *args, **kwargs)
Andrew Grieve66a2bc42024-10-04 21:20:20235 self.type = 'warning'
Daniel Cheng7052cdf2017-11-21 19:23:29236
Andrew Grieve66a2bc42024-10-04 21:20:20237 class PresubmitNotifyResult(PresubmitResult):
238
Ben Pastenee79d66112025-04-23 19:46:15239 def __init__(self, *args, **kwargs):
240 MockOutputApi.PresubmitResult.__init__(self, *args, **kwargs)
Andrew Grieve66a2bc42024-10-04 21:20:20241 self.type = 'notify'
242
243 class PresubmitPromptOrNotify(PresubmitResult):
244
Ben Pastenee79d66112025-04-23 19:46:15245 def __init__(self, *args, **kwargs):
Kalvin Leecfd19ddc2025-10-28 06:17:31246 MockOutputApi.PresubmitResult.__init__(self, *args, **kwargs)
Andrew Grieve66a2bc42024-10-04 21:20:20247 self.type = 'promptOrNotify'
248
Ben Pastenee79d66112025-04-23 19:46:15249 class PresubmitResultLocation(object):
250
251 def __init__(self, file_path, start_line, end_line):
252 self.file_path = file_path
253 self.start_line = start_line
254 self.end_line = end_line
255
Andrew Grieve66a2bc42024-10-04 21:20:20256 def __init__(self):
257 self.more_cc = []
258
259 def AppendCC(self, more_cc):
260 self.more_cc.append(more_cc)
Daniel Cheng7052cdf2017-11-21 19:23:29261
gayane3dff8c22014-12-04 17:09:51262
263class MockFile(object):
Andrew Grieve66a2bc42024-10-04 21:20:20264 """Mock class for the File class.
gayane3dff8c22014-12-04 17:09:51265
266 This class can be used to form the mock list of changed files in
267 MockInputApi for presubmit unittests.
268 """
269
Andrew Grieve66a2bc42024-10-04 21:20:20270 def __init__(self,
271 local_path,
272 new_contents,
273 old_contents=None,
274 action='A',
275 scm_diff=None):
276 self._local_path = local_path
277 self._new_contents = new_contents
278 self._changed_contents = [(i + 1, l)
279 for i, l in enumerate(new_contents)]
280 self._action = action
281 if scm_diff:
282 self._scm_diff = scm_diff
283 else:
284 self._scm_diff = ("--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
285 (local_path, len(new_contents)))
286 for l in new_contents:
287 self._scm_diff += "+%s\n" % l
288 self._old_contents = old_contents or []
gayane3dff8c22014-12-04 17:09:51289
Andrew Grieve66a2bc42024-10-04 21:20:20290 def __str__(self):
291 return self._local_path
Luciano Pacheco23d752b02023-10-25 22:49:36292
Andrew Grieve66a2bc42024-10-04 21:20:20293 def Action(self):
294 return self._action
dbeam37e8e7402016-02-10 22:58:20295
Andrew Grieve66a2bc42024-10-04 21:20:20296 def ChangedContents(self):
297 return self._changed_contents
gayane3dff8c22014-12-04 17:09:51298
Andrew Grieve66a2bc42024-10-04 21:20:20299 def NewContents(self):
300 return self._new_contents
gayane3dff8c22014-12-04 17:09:51301
Andrew Grieve66a2bc42024-10-04 21:20:20302 def LocalPath(self):
303 return self._local_path
gayane3dff8c22014-12-04 17:09:51304
Andrew Grieve66a2bc42024-10-04 21:20:20305 def AbsoluteLocalPath(self):
Andrew Grieve713b89b2024-10-15 20:20:08306 return os.path.join(_REPO_ROOT, self._local_path)
rdevlin.cronin9ab806c2016-02-26 23:17:13307
Anton Bershanskyi4253349482025-02-11 21:01:27308 # This method must be functionally identical to
309 # AffectedFile.UnixLocalPath(), but must normalize Windows-style
310 # paths even on non-Windows platforms because tests contain them
311 def UnixLocalPath(self):
312 return self._local_path.replace('\\', '/')
313
Andrew Grieve66a2bc42024-10-04 21:20:20314 def GenerateScmDiff(self):
315 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50316
Andrew Grieve66a2bc42024-10-04 21:20:20317 def OldContents(self):
318 return self._old_contents
Yoland Yanb92fa522017-08-28 17:37:06319
Jonathan Lee7c3cc082025-08-07 01:29:17320 def Extension(self):
321 _, ext = os.path.splitext(self._local_path)
322 return ext
323
Andrew Grieve66a2bc42024-10-04 21:20:20324 def rfind(self, p):
325 """Required when os.path.basename() is called on MockFile."""
326 return self._local_path.rfind(p)
davileene0426252015-03-02 21:10:41327
Andrew Grieve66a2bc42024-10-04 21:20:20328 def __getitem__(self, i):
329 """Required when os.path.basename() is called on MockFile."""
330 return self._local_path[i]
davileene0426252015-03-02 21:10:41331
Andrew Grieve66a2bc42024-10-04 21:20:20332 def __len__(self):
333 """Required when os.path.basename() is called on MockFile."""
334 return len(self._local_path)
pastarmovj89f7ee12016-09-20 14:58:13335
Andrew Grieve66a2bc42024-10-04 21:20:20336 def replace(self, altsep, sep):
337 """Required when os.path.basename() is called on MockFile."""
338 return self._local_path.replace(altsep, sep)
Julian Pastarmov4f7af532019-07-17 19:25:37339
gayane3dff8c22014-12-04 17:09:51340
glidere61efad2015-02-18 17:39:43341class MockAffectedFile(MockFile):
Andrew Grieve713b89b2024-10-15 20:20:08342 pass
glidere61efad2015-02-18 17:39:43343
344
gayane3dff8c22014-12-04 17:09:51345class MockChange(object):
Andrew Grieve66a2bc42024-10-04 21:20:20346 """Mock class for Change class.
gayane3dff8c22014-12-04 17:09:51347
348 This class can be used in presubmit unittests to mock the query of the
349 current change.
350 """
351
Andrew Grieve66a2bc42024-10-04 21:20:20352 def __init__(self, changed_files):
353 self._changed_files = changed_files
354 self.author_email = None
355 self.footers = defaultdict(list)
gayane3dff8c22014-12-04 17:09:51356
Andrew Grieve66a2bc42024-10-04 21:20:20357 def LocalPaths(self):
358 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54359
Andrew Grieve66a2bc42024-10-04 21:20:20360 def AffectedFiles(self,
361 include_dirs=False,
362 include_deletes=True,
363 file_filter=None):
364 return self._changed_files
Chris Hall59f8d0c72020-05-01 07:31:19365
Andrew Grieve66a2bc42024-10-04 21:20:20366 def GitFootersFromDescription(self):
367 return self.footers
Andrew Grieve713b89b2024-10-15 20:20:08368
369 def RepositoryRoot(self):
370 return _REPO_ROOT