blob: f3e928cfcaaf310edd69f68e8c1c97e408318824 [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 = ()
64 # TODO(https://crbug.com/1098562): Remove once no longer used)
Sylvain Defresnea8b73d252018-02-28 15:45:5465 DEFAULT_BLACK_LIST = ()
66
gayane3dff8c22014-12-04 17:09:5167 def __init__(self):
Daniel Cheng264a447d2017-09-28 22:17:5968 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2569 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5170 self.json = json
71 self.re = re
72 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0073 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5174 self.python_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__)
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
agrievef32bcc72016-04-04 14:57:4086 def AffectedFiles(self, file_filter=None, include_deletes=False):
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
glidere61efad2015-02-18 17:39:4394 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:5495 return self.AffectedFiles(file_filter=file_filter)
96
Robert Ma0303a3ad2020-07-22 18:48:4897 def FilterSourceFile(self, file,
98 files_to_check=(), files_to_skip=(),
99 # TODO(https://crbug.com/1098562): Remove once no longer used
100 white_list=(), black_list=()):
101 files_to_check = files_to_check or white_list
102 files_to_skip = files_to_skip or black_list
Sylvain Defresnea8b73d252018-02-28 15:45:54103 local_path = file.LocalPath()
Robert Ma0303a3ad2020-07-22 18:48:48104 found_in_files_to_check = not files_to_check
105 if files_to_check:
106 if type(files_to_check) is str:
107 raise TypeError('files_to_check should be an iterable of strings')
108 for pattern in files_to_check:
Sylvain Defresnea8b73d252018-02-28 15:45:54109 compiled_pattern = re.compile(pattern)
110 if compiled_pattern.search(local_path):
Robert Ma0303a3ad2020-07-22 18:48:48111 found_in_files_to_check = True
Vaclav Brozekf01ed502018-03-16 19:38:24112 break
Robert Ma0303a3ad2020-07-22 18:48:48113 if files_to_skip:
114 if type(files_to_skip) is str:
115 raise TypeError('files_to_skip should be an iterable of strings')
116 for pattern in files_to_skip:
Sylvain Defresnea8b73d252018-02-28 15:45:54117 compiled_pattern = re.compile(pattern)
118 if compiled_pattern.search(local_path):
119 return False
Robert Ma0303a3ad2020-07-22 18:48:48120 return found_in_files_to_check
glidere61efad2015-02-18 17:39:43121
davileene0426252015-03-02 21:10:41122 def LocalPaths(self):
Alexei Svitkine137d4c662019-07-17 21:28:24123 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41124
gayane3dff8c22014-12-04 17:09:51125 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34126 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51127
128 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43129 if hasattr(filename, 'AbsoluteLocalPath'):
130 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51131 for file_ in self.files:
132 if file_.LocalPath() == filename:
133 return '\n'.join(file_.NewContents())
134 # Otherwise, file is not in our mock API.
135 raise IOError, "No such file or directory: '%s'" % filename
136
137
138class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46139 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51140
141 An instance of this class can be passed to presubmit unittests for outputing
142 various types of results.
143 """
144
145 class PresubmitResult(object):
146 def __init__(self, message, items=None, long_text=''):
147 self.message = message
148 self.items = items
149 self.long_text = long_text
150
gayane940df072015-02-24 14:28:30151 def __repr__(self):
152 return self.message
153
gayane3dff8c22014-12-04 17:09:51154 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41155 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51156 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
157 self.type = 'error'
158
159 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41160 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51161 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
162 self.type = 'warning'
163
164 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41165 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51166 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
167 self.type = 'notify'
168
169 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41170 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51171 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
172 self.type = 'promptOrNotify'
173
Daniel Cheng7052cdf2017-11-21 19:23:29174 def __init__(self):
175 self.more_cc = []
176
177 def AppendCC(self, more_cc):
178 self.more_cc.extend(more_cc)
179
gayane3dff8c22014-12-04 17:09:51180
181class MockFile(object):
182 """Mock class for the File class.
183
184 This class can be used to form the mock list of changed files in
185 MockInputApi for presubmit unittests.
186 """
187
Yoland Yanb92fa522017-08-28 17:37:06188 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
gayane3dff8c22014-12-04 17:09:51189 self._local_path = local_path
190 self._new_contents = new_contents
191 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40192 self._action = action
jbriance9e12f162016-11-25 07:57:50193 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
194 len(new_contents))
Yoland Yanb92fa522017-08-28 17:37:06195 self._old_contents = old_contents
jbriance9e12f162016-11-25 07:57:50196 for l in new_contents:
197 self._scm_diff += "+%s\n" % l
gayane3dff8c22014-12-04 17:09:51198
dbeam37e8e7402016-02-10 22:58:20199 def Action(self):
agrievef32bcc72016-04-04 14:57:40200 return self._action
dbeam37e8e7402016-02-10 22:58:20201
gayane3dff8c22014-12-04 17:09:51202 def ChangedContents(self):
203 return self._changed_contents
204
205 def NewContents(self):
206 return self._new_contents
207
208 def LocalPath(self):
209 return self._local_path
210
rdevlin.cronin9ab806c2016-02-26 23:17:13211 def AbsoluteLocalPath(self):
212 return self._local_path
213
jbriance9e12f162016-11-25 07:57:50214 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31215 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50216
Yoland Yanb92fa522017-08-28 17:37:06217 def OldContents(self):
218 return self._old_contents
219
davileene0426252015-03-02 21:10:41220 def rfind(self, p):
221 """os.path.basename is called on MockFile so we need an rfind method."""
222 return self._local_path.rfind(p)
223
224 def __getitem__(self, i):
225 """os.path.basename is called on MockFile so we need a get method."""
226 return self._local_path[i]
227
pastarmovj89f7ee12016-09-20 14:58:13228 def __len__(self):
229 """os.path.basename is called on MockFile so we need a len method."""
230 return len(self._local_path)
231
Julian Pastarmov4f7af532019-07-17 19:25:37232 def replace(self, altsep, sep):
233 """os.path.basename is called on MockFile so we need a replace method."""
234 return self._local_path.replace(altsep, sep)
235
gayane3dff8c22014-12-04 17:09:51236
glidere61efad2015-02-18 17:39:43237class MockAffectedFile(MockFile):
238 def AbsoluteLocalPath(self):
239 return self._local_path
240
241
gayane3dff8c22014-12-04 17:09:51242class MockChange(object):
243 """Mock class for Change class.
244
245 This class can be used in presubmit unittests to mock the query of the
246 current change.
247 """
248
249 def __init__(self, changed_files):
250 self._changed_files = changed_files
Chris Hall59f8d0c72020-05-01 07:31:19251 self.footers = defaultdict(list)
gayane3dff8c22014-12-04 17:09:51252
253 def LocalPaths(self):
254 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54255
256 def AffectedFiles(self, include_dirs=False, include_deletes=True,
257 file_filter=None):
258 return self._changed_files
Chris Hall59f8d0c72020-05-01 07:31:19259
260 def GitFootersFromDescription(self):
261 return self.footers
262