blob: b7d1b482c5f45447a232ef92eb31c6796c0028c5 [file] [log] [blame]
gayane3dff8c22014-12-04 17:09:511# Copyright 2014 The Chromium Authors. All rights reserved.
2# 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):
Daniel Cheng264a447d2017-09-28 22:17:5966 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2567 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5168 self.json = json
69 self.re = re
70 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0071 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5172 self.python_executable = sys.executable
Takuto Ikutadca10222022-04-13 02:51:2173 self.python3_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1374 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5175 self.subprocess = subprocess
Dan Beam35b10c12019-11-27 01:17:3476 self.sys = sys
gayane3dff8c22014-12-04 17:09:5177 self.files = []
78 self.is_committing = False
gayanee1702662014-12-13 03:48:0979 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3480 self.presubmit_local_path = os.path.dirname(__file__)
Bruce Dawson3740e0732022-04-07 16:17:2281 self.is_windows = sys.platform == 'win32'
gayane3dff8c22014-12-04 17:09:5182
Zhiling Huang45cabf32018-03-10 00:50:0383 def CreateMockFileInPath(self, f_list):
84 self.os_path.exists = lambda x: x in f_list
85
Giovanni Ortuño Urquidiab84da62021-12-10 00:53:2186 def AffectedFiles(self, file_filter=None, include_deletes=True):
Sylvain Defresnea8b73d252018-02-28 15:45:5487 for file in self.files:
88 if file_filter and not file_filter(file):
89 continue
90 if not include_deletes and file.Action() == 'D':
91 continue
92 yield file
gayane3dff8c22014-12-04 17:09:5193
Lukasz Anforowicz7016d05e2021-11-30 03:56:2794 def RightHandSideLines(self, source_file_filter=None):
95 affected_files = self.AffectedSourceFiles(source_file_filter)
96 for af in affected_files:
97 lines = af.ChangedContents()
98 for line in lines:
99 yield (af, line[0], line[1])
100
glidere61efad2015-02-18 17:39:43101 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:54102 return self.AffectedFiles(file_filter=file_filter)
103
Robert Ma0303a3ad2020-07-22 18:48:48104 def FilterSourceFile(self, file,
Josip Sokcevic8b6cc432020-08-05 17:45:33105 files_to_check=(), files_to_skip=()):
Sylvain Defresnea8b73d252018-02-28 15:45:54106 local_path = file.LocalPath()
Robert Ma0303a3ad2020-07-22 18:48:48107 found_in_files_to_check = not files_to_check
108 if files_to_check:
109 if type(files_to_check) is str:
110 raise TypeError('files_to_check should be an iterable of strings')
111 for pattern in files_to_check:
Sylvain Defresnea8b73d252018-02-28 15:45:54112 compiled_pattern = re.compile(pattern)
Henrique Ferreiro81d580022021-11-29 21:27:19113 if compiled_pattern.match(local_path):
Robert Ma0303a3ad2020-07-22 18:48:48114 found_in_files_to_check = True
Vaclav Brozekf01ed502018-03-16 19:38:24115 break
Robert Ma0303a3ad2020-07-22 18:48:48116 if files_to_skip:
117 if type(files_to_skip) is str:
118 raise TypeError('files_to_skip should be an iterable of strings')
119 for pattern in files_to_skip:
Sylvain Defresnea8b73d252018-02-28 15:45:54120 compiled_pattern = re.compile(pattern)
Henrique Ferreiro81d580022021-11-29 21:27:19121 if compiled_pattern.match(local_path):
Sylvain Defresnea8b73d252018-02-28 15:45:54122 return False
Robert Ma0303a3ad2020-07-22 18:48:48123 return found_in_files_to_check
glidere61efad2015-02-18 17:39:43124
davileene0426252015-03-02 21:10:41125 def LocalPaths(self):
Alexei Svitkine137d4c662019-07-17 21:28:24126 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41127
gayane3dff8c22014-12-04 17:09:51128 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34129 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51130
131 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43132 if hasattr(filename, 'AbsoluteLocalPath'):
133 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51134 for file_ in self.files:
135 if file_.LocalPath() == filename:
136 return '\n'.join(file_.NewContents())
137 # Otherwise, file is not in our mock API.
Dirk Prankee3c9c62d2021-05-18 18:35:59138 raise IOError("No such file or directory: '%s'" % filename)
gayane3dff8c22014-12-04 17:09:51139
140
141class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46142 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51143
144 An instance of this class can be passed to presubmit unittests for outputing
145 various types of results.
146 """
147
148 class PresubmitResult(object):
149 def __init__(self, message, items=None, long_text=''):
150 self.message = message
151 self.items = items
152 self.long_text = long_text
153
gayane940df072015-02-24 14:28:30154 def __repr__(self):
155 return self.message
156
gayane3dff8c22014-12-04 17:09:51157 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41158 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51159 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
160 self.type = 'error'
161
162 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41163 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51164 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
165 self.type = 'warning'
166
167 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41168 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51169 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
170 self.type = 'notify'
171
172 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41173 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51174 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
175 self.type = 'promptOrNotify'
176
Daniel Cheng7052cdf2017-11-21 19:23:29177 def __init__(self):
178 self.more_cc = []
179
180 def AppendCC(self, more_cc):
Kevin McNee967dd2d22021-11-15 16:09:29181 self.more_cc.append(more_cc)
Daniel Cheng7052cdf2017-11-21 19:23:29182
gayane3dff8c22014-12-04 17:09:51183
184class MockFile(object):
185 """Mock class for the File class.
186
187 This class can be used to form the mock list of changed files in
188 MockInputApi for presubmit unittests.
189 """
190
Dominic Battre645d42342020-12-04 16:14:10191 def __init__(self, local_path, new_contents, old_contents=None, action='A',
192 scm_diff=None):
gayane3dff8c22014-12-04 17:09:51193 self._local_path = local_path
194 self._new_contents = new_contents
195 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40196 self._action = action
Dominic Battre645d42342020-12-04 16:14:10197 if scm_diff:
198 self._scm_diff = scm_diff
199 else:
200 self._scm_diff = (
201 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
202 (local_path, len(new_contents)))
203 for l in new_contents:
204 self._scm_diff += "+%s\n" % l
Yoland Yanb92fa522017-08-28 17:37:06205 self._old_contents = old_contents
gayane3dff8c22014-12-04 17:09:51206
dbeam37e8e7402016-02-10 22:58:20207 def Action(self):
agrievef32bcc72016-04-04 14:57:40208 return self._action
dbeam37e8e7402016-02-10 22:58:20209
gayane3dff8c22014-12-04 17:09:51210 def ChangedContents(self):
211 return self._changed_contents
212
213 def NewContents(self):
214 return self._new_contents
215
216 def LocalPath(self):
217 return self._local_path
218
rdevlin.cronin9ab806c2016-02-26 23:17:13219 def AbsoluteLocalPath(self):
220 return self._local_path
221
jbriance9e12f162016-11-25 07:57:50222 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31223 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50224
Yoland Yanb92fa522017-08-28 17:37:06225 def OldContents(self):
226 return self._old_contents
227
davileene0426252015-03-02 21:10:41228 def rfind(self, p):
229 """os.path.basename is called on MockFile so we need an rfind method."""
230 return self._local_path.rfind(p)
231
232 def __getitem__(self, i):
233 """os.path.basename is called on MockFile so we need a get method."""
234 return self._local_path[i]
235
pastarmovj89f7ee12016-09-20 14:58:13236 def __len__(self):
237 """os.path.basename is called on MockFile so we need a len method."""
238 return len(self._local_path)
239
Julian Pastarmov4f7af532019-07-17 19:25:37240 def replace(self, altsep, sep):
241 """os.path.basename is called on MockFile so we need a replace method."""
242 return self._local_path.replace(altsep, sep)
243
gayane3dff8c22014-12-04 17:09:51244
glidere61efad2015-02-18 17:39:43245class MockAffectedFile(MockFile):
246 def AbsoluteLocalPath(self):
247 return self._local_path
248
249
gayane3dff8c22014-12-04 17:09:51250class MockChange(object):
251 """Mock class for Change class.
252
253 This class can be used in presubmit unittests to mock the query of the
254 current change.
255 """
256
257 def __init__(self, changed_files):
258 self._changed_files = changed_files
Chris Hall59f8d0c72020-05-01 07:31:19259 self.footers = defaultdict(list)
gayane3dff8c22014-12-04 17:09:51260
261 def LocalPaths(self):
262 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54263
264 def AffectedFiles(self, include_dirs=False, include_deletes=True,
265 file_filter=None):
266 return self._changed_files
Chris Hall59f8d0c72020-05-01 07:31:19267
268 def GitFootersFromDescription(self):
269 return self.footers