blob: 361eb4b18cd8b5478dbc836f58145e8fad37e5e7 [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
Daniel Cheng264a447d2017-09-28 22:17:5913# TODO(dcheng): It's kind of horrible that this is copy and pasted from
14# presubmit_canned_checks.py, but it's far easier than any of the alternatives.
15def _ReportErrorFileAndLine(filename, line_num, dummy_line):
16 """Default error formatter for _FindNewViolationsOfRule."""
17 return '%s:%s' % (filename, line_num)
18
19
20class MockCannedChecks(object):
21 def _FindNewViolationsOfRule(self, callable_rule, input_api,
22 source_file_filter=None,
23 error_formatter=_ReportErrorFileAndLine):
24 """Find all newly introduced violations of a per-line rule (a callable).
25
26 Arguments:
27 callable_rule: a callable taking a file extension and line of input and
28 returning True if the rule is satisfied and False if there was a
29 problem.
30 input_api: object to enumerate the affected files.
31 source_file_filter: a filter to be passed to the input api.
32 error_formatter: a callable taking (filename, line_number, line) and
33 returning a formatted error string.
34
35 Returns:
36 A list of the newly-introduced violations reported by the rule.
37 """
38 errors = []
39 for f in input_api.AffectedFiles(include_deletes=False,
40 file_filter=source_file_filter):
41 # For speed, we do two passes, checking first the full file. Shelling out
42 # to the SCM to determine the changed region can be quite expensive on
43 # Win32. Assuming that most files will be kept problem-free, we can
44 # skip the SCM operations most of the time.
45 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
46 if all(callable_rule(extension, line) for line in f.NewContents()):
47 continue # No violation found in full text: can skip considering diff.
48
49 for line_num, line in f.ChangedContents():
50 if not callable_rule(extension, line):
51 errors.append(error_formatter(f.LocalPath(), line_num, line))
52
53 return errors
gayane3dff8c22014-12-04 17:09:5154
Zhiling Huang45cabf32018-03-10 00:50:0355
gayane3dff8c22014-12-04 17:09:5156class MockInputApi(object):
57 """Mock class for the InputApi class.
58
59 This class can be used for unittests for presubmit by initializing the files
60 attribute as the list of changed files.
61 """
62
Robert Ma0303a3ad2020-07-22 18:48:4863 DEFAULT_FILES_TO_SKIP = ()
Sylvain Defresnea8b73d252018-02-28 15:45:5464
gayane3dff8c22014-12-04 17:09:5165 def __init__(self):
Georg Neis9080e0602024-08-23 01:50:2966 self.basename = os.path.basename
Daniel Cheng264a447d2017-09-28 22:17:5967 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2568 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5169 self.json = json
70 self.re = re
71 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0072 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5173 self.python_executable = sys.executable
Takuto Ikutadca10222022-04-13 02:51:2174 self.python3_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1375 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5176 self.subprocess = subprocess
Dan Beam35b10c12019-11-27 01:17:3477 self.sys = sys
gayane3dff8c22014-12-04 17:09:5178 self.files = []
79 self.is_committing = False
gayanee1702662014-12-13 03:48:0980 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3481 self.presubmit_local_path = os.path.dirname(__file__)
Bruce Dawson3740e0732022-04-07 16:17:2282 self.is_windows = sys.platform == 'win32'
Bruce Dawson344ab262022-06-04 11:35:1083 self.no_diffs = False
Ian Vollick9d42a072023-02-14 01:21:1484 # Although this makes assumptions about command line arguments used by test
85 # scripts that create mocks, it is a convenient way to set up the verbosity
86 # via the input api.
87 self.verbose = '--verbose' in sys.argv
gayane3dff8c22014-12-04 17:09:5188
Zhiling Huang45cabf32018-03-10 00:50:0389 def CreateMockFileInPath(self, f_list):
90 self.os_path.exists = lambda x: x in f_list
91
Giovanni Ortuño Urquidiab84da62021-12-10 00:53:2192 def AffectedFiles(self, file_filter=None, include_deletes=True):
Sylvain Defresnea8b73d252018-02-28 15:45:5493 for file in self.files:
94 if file_filter and not file_filter(file):
95 continue
96 if not include_deletes and file.Action() == 'D':
97 continue
98 yield file
gayane3dff8c22014-12-04 17:09:5199
Lukasz Anforowicz7016d05e2021-11-30 03:56:27100 def RightHandSideLines(self, source_file_filter=None):
101 affected_files = self.AffectedSourceFiles(source_file_filter)
102 for af in affected_files:
103 lines = af.ChangedContents()
104 for line in lines:
105 yield (af, line[0], line[1])
106
glidere61efad2015-02-18 17:39:43107 def AffectedSourceFiles(self, file_filter=None):
Arthur Sonzognic66e9c82024-04-23 07:53:04108 return self.AffectedFiles(file_filter=file_filter, include_deletes=False)
Sylvain Defresnea8b73d252018-02-28 15:45:54109
Georg Neis9080e0602024-08-23 01:50:29110 def AffectedTestableFiles(self, file_filter=None):
111 return self.AffectedFiles(file_filter=file_filter, include_deletes=False)
112
Robert Ma0303a3ad2020-07-22 18:48:48113 def FilterSourceFile(self, file,
Josip Sokcevic8b6cc432020-08-05 17:45:33114 files_to_check=(), files_to_skip=()):
Sylvain Defresnea8b73d252018-02-28 15:45:54115 local_path = file.LocalPath()
Robert Ma0303a3ad2020-07-22 18:48:48116 found_in_files_to_check = not files_to_check
117 if files_to_check:
118 if type(files_to_check) is str:
119 raise TypeError('files_to_check should be an iterable of strings')
120 for pattern in files_to_check:
Sylvain Defresnea8b73d252018-02-28 15:45:54121 compiled_pattern = re.compile(pattern)
Henrique Ferreiro81d580022021-11-29 21:27:19122 if compiled_pattern.match(local_path):
Robert Ma0303a3ad2020-07-22 18:48:48123 found_in_files_to_check = True
Vaclav Brozekf01ed502018-03-16 19:38:24124 break
Robert Ma0303a3ad2020-07-22 18:48:48125 if files_to_skip:
126 if type(files_to_skip) is str:
127 raise TypeError('files_to_skip should be an iterable of strings')
128 for pattern in files_to_skip:
Sylvain Defresnea8b73d252018-02-28 15:45:54129 compiled_pattern = re.compile(pattern)
Henrique Ferreiro81d580022021-11-29 21:27:19130 if compiled_pattern.match(local_path):
Sylvain Defresnea8b73d252018-02-28 15:45:54131 return False
Robert Ma0303a3ad2020-07-22 18:48:48132 return found_in_files_to_check
glidere61efad2015-02-18 17:39:43133
davileene0426252015-03-02 21:10:41134 def LocalPaths(self):
Alexei Svitkine137d4c662019-07-17 21:28:24135 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41136
gayane3dff8c22014-12-04 17:09:51137 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34138 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51139
Joanmarie Diggs0991fc62022-08-30 06:00:13140 def ReadFile(self, filename, mode='r'):
glidere61efad2015-02-18 17:39:43141 if hasattr(filename, 'AbsoluteLocalPath'):
142 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51143 for file_ in self.files:
144 if file_.LocalPath() == filename:
145 return '\n'.join(file_.NewContents())
146 # Otherwise, file is not in our mock API.
Dirk Prankee3c9c62d2021-05-18 18:35:59147 raise IOError("No such file or directory: '%s'" % filename)
gayane3dff8c22014-12-04 17:09:51148
149
150class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46151 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51152
Gao Shenga79ebd42022-08-08 17:25:59153 An instance of this class can be passed to presubmit unittests for outputting
gayane3dff8c22014-12-04 17:09:51154 various types of results.
155 """
156
157 class PresubmitResult(object):
158 def __init__(self, message, items=None, long_text=''):
159 self.message = message
160 self.items = items
161 self.long_text = long_text
162
gayane940df072015-02-24 14:28:30163 def __repr__(self):
164 return self.message
165
gayane3dff8c22014-12-04 17:09:51166 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41167 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51168 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
169 self.type = 'error'
170
171 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41172 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51173 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
174 self.type = 'warning'
175
176 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41177 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51178 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
179 self.type = 'notify'
180
181 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41182 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51183 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
184 self.type = 'promptOrNotify'
185
Daniel Cheng7052cdf2017-11-21 19:23:29186 def __init__(self):
187 self.more_cc = []
188
189 def AppendCC(self, more_cc):
Kevin McNee967dd2d22021-11-15 16:09:29190 self.more_cc.append(more_cc)
Daniel Cheng7052cdf2017-11-21 19:23:29191
gayane3dff8c22014-12-04 17:09:51192
193class MockFile(object):
194 """Mock class for the File class.
195
196 This class can be used to form the mock list of changed files in
197 MockInputApi for presubmit unittests.
198 """
199
Daniel Cheng99f90e6b2023-11-28 22:54:40200 def __init__(self, local_path, new_contents, old_contents=None, action='A',
Dominic Battre645d42342020-12-04 16:14:10201 scm_diff=None):
gayane3dff8c22014-12-04 17:09:51202 self._local_path = local_path
203 self._new_contents = new_contents
204 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40205 self._action = action
Dominic Battre645d42342020-12-04 16:14:10206 if scm_diff:
207 self._scm_diff = scm_diff
208 else:
209 self._scm_diff = (
210 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
211 (local_path, len(new_contents)))
212 for l in new_contents:
213 self._scm_diff += "+%s\n" % l
Georg Neis9080e0602024-08-23 01:50:29214 self._old_contents = old_contents or []
gayane3dff8c22014-12-04 17:09:51215
Luciano Pacheco23d752b02023-10-25 22:49:36216 def __str__(self):
217 return self._local_path
218
dbeam37e8e7402016-02-10 22:58:20219 def Action(self):
agrievef32bcc72016-04-04 14:57:40220 return self._action
dbeam37e8e7402016-02-10 22:58:20221
gayane3dff8c22014-12-04 17:09:51222 def ChangedContents(self):
223 return self._changed_contents
224
225 def NewContents(self):
226 return self._new_contents
227
228 def LocalPath(self):
229 return self._local_path
230
rdevlin.cronin9ab806c2016-02-26 23:17:13231 def AbsoluteLocalPath(self):
232 return self._local_path
233
jbriance9e12f162016-11-25 07:57:50234 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31235 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50236
Yoland Yanb92fa522017-08-28 17:37:06237 def OldContents(self):
238 return self._old_contents
239
davileene0426252015-03-02 21:10:41240 def rfind(self, p):
241 """os.path.basename is called on MockFile so we need an rfind method."""
242 return self._local_path.rfind(p)
243
244 def __getitem__(self, i):
245 """os.path.basename is called on MockFile so we need a get method."""
246 return self._local_path[i]
247
pastarmovj89f7ee12016-09-20 14:58:13248 def __len__(self):
249 """os.path.basename is called on MockFile so we need a len method."""
250 return len(self._local_path)
251
Julian Pastarmov4f7af532019-07-17 19:25:37252 def replace(self, altsep, sep):
253 """os.path.basename is called on MockFile so we need a replace method."""
254 return self._local_path.replace(altsep, sep)
255
gayane3dff8c22014-12-04 17:09:51256
glidere61efad2015-02-18 17:39:43257class MockAffectedFile(MockFile):
258 def AbsoluteLocalPath(self):
259 return self._local_path
260
261
gayane3dff8c22014-12-04 17:09:51262class MockChange(object):
263 """Mock class for Change class.
264
265 This class can be used in presubmit unittests to mock the query of the
266 current change.
267 """
268
269 def __init__(self, changed_files):
270 self._changed_files = changed_files
Thorben Troebst2d24c7062022-08-10 20:20:16271 self.author_email = None
Chris Hall59f8d0c72020-05-01 07:31:19272 self.footers = defaultdict(list)
gayane3dff8c22014-12-04 17:09:51273
274 def LocalPaths(self):
275 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54276
277 def AffectedFiles(self, include_dirs=False, include_deletes=True,
278 file_filter=None):
279 return self._changed_files
Chris Hall59f8d0c72020-05-01 07:31:19280
281 def GitFootersFromDescription(self):
282 return self.footers