blob: 7549d3c85ce29180d38b4fb60fe59c2596eb6ec1 [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
Daniel Cheng13ca61a882017-08-25 15:11:255import fnmatch
gayane3dff8c22014-12-04 17:09:516import json
7import os
8import re
9import subprocess
10import sys
11
Daniel Cheng264a447d2017-09-28 22:17:5912# TODO(dcheng): It's kind of horrible that this is copy and pasted from
13# presubmit_canned_checks.py, but it's far easier than any of the alternatives.
14def _ReportErrorFileAndLine(filename, line_num, dummy_line):
15 """Default error formatter for _FindNewViolationsOfRule."""
16 return '%s:%s' % (filename, line_num)
17
18
19class MockCannedChecks(object):
20 def _FindNewViolationsOfRule(self, callable_rule, input_api,
21 source_file_filter=None,
22 error_formatter=_ReportErrorFileAndLine):
23 """Find all newly introduced violations of a per-line rule (a callable).
24
25 Arguments:
26 callable_rule: a callable taking a file extension and line of input and
27 returning True if the rule is satisfied and False if there was a
28 problem.
29 input_api: object to enumerate the affected files.
30 source_file_filter: a filter to be passed to the input api.
31 error_formatter: a callable taking (filename, line_number, line) and
32 returning a formatted error string.
33
34 Returns:
35 A list of the newly-introduced violations reported by the rule.
36 """
37 errors = []
38 for f in input_api.AffectedFiles(include_deletes=False,
39 file_filter=source_file_filter):
40 # For speed, we do two passes, checking first the full file. Shelling out
41 # to the SCM to determine the changed region can be quite expensive on
42 # Win32. Assuming that most files will be kept problem-free, we can
43 # skip the SCM operations most of the time.
44 extension = str(f.LocalPath()).rsplit('.', 1)[-1]
45 if all(callable_rule(extension, line) for line in f.NewContents()):
46 continue # No violation found in full text: can skip considering diff.
47
48 for line_num, line in f.ChangedContents():
49 if not callable_rule(extension, line):
50 errors.append(error_formatter(f.LocalPath(), line_num, line))
51
52 return errors
gayane3dff8c22014-12-04 17:09:5153
Zhiling Huang45cabf32018-03-10 00:50:0354
gayane3dff8c22014-12-04 17:09:5155class MockInputApi(object):
56 """Mock class for the InputApi class.
57
58 This class can be used for unittests for presubmit by initializing the files
59 attribute as the list of changed files.
60 """
61
Sylvain Defresnea8b73d252018-02-28 15:45:5462 DEFAULT_BLACK_LIST = ()
63
gayane3dff8c22014-12-04 17:09:5164 def __init__(self):
Daniel Cheng264a447d2017-09-28 22:17:5965 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2566 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5167 self.json = json
68 self.re = re
69 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0070 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5171 self.python_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1372 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5173 self.subprocess = subprocess
Dan Beam35b10c12019-11-27 01:17:3474 self.sys = sys
gayane3dff8c22014-12-04 17:09:5175 self.files = []
76 self.is_committing = False
gayanee1702662014-12-13 03:48:0977 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3478 self.presubmit_local_path = os.path.dirname(__file__)
gayane3dff8c22014-12-04 17:09:5179
Zhiling Huang45cabf32018-03-10 00:50:0380 def CreateMockFileInPath(self, f_list):
81 self.os_path.exists = lambda x: x in f_list
82
agrievef32bcc72016-04-04 14:57:4083 def AffectedFiles(self, file_filter=None, include_deletes=False):
Sylvain Defresnea8b73d252018-02-28 15:45:5484 for file in self.files:
85 if file_filter and not file_filter(file):
86 continue
87 if not include_deletes and file.Action() == 'D':
88 continue
89 yield file
gayane3dff8c22014-12-04 17:09:5190
glidere61efad2015-02-18 17:39:4391 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:5492 return self.AffectedFiles(file_filter=file_filter)
93
94 def FilterSourceFile(self, file, white_list=(), black_list=()):
95 local_path = file.LocalPath()
Vaclav Brozekf01ed502018-03-16 19:38:2496 found_in_white_list = not white_list
Sylvain Defresnea8b73d252018-02-28 15:45:5497 if white_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:0198 if type(white_list) is str:
99 raise TypeError('white_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:54100 for pattern in white_list:
101 compiled_pattern = re.compile(pattern)
102 if compiled_pattern.search(local_path):
Vaclav Brozekf01ed502018-03-16 19:38:24103 found_in_white_list = True
104 break
Sylvain Defresnea8b73d252018-02-28 15:45:54105 if black_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:01106 if type(black_list) is str:
107 raise TypeError('black_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:54108 for pattern in black_list:
109 compiled_pattern = re.compile(pattern)
110 if compiled_pattern.search(local_path):
111 return False
Vaclav Brozekf01ed502018-03-16 19:38:24112 return found_in_white_list
glidere61efad2015-02-18 17:39:43113
davileene0426252015-03-02 21:10:41114 def LocalPaths(self):
Alexei Svitkine137d4c662019-07-17 21:28:24115 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41116
gayane3dff8c22014-12-04 17:09:51117 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34118 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51119
120 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43121 if hasattr(filename, 'AbsoluteLocalPath'):
122 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51123 for file_ in self.files:
124 if file_.LocalPath() == filename:
125 return '\n'.join(file_.NewContents())
126 # Otherwise, file is not in our mock API.
127 raise IOError, "No such file or directory: '%s'" % filename
128
129
130class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46131 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51132
133 An instance of this class can be passed to presubmit unittests for outputing
134 various types of results.
135 """
136
137 class PresubmitResult(object):
138 def __init__(self, message, items=None, long_text=''):
139 self.message = message
140 self.items = items
141 self.long_text = long_text
142
gayane940df072015-02-24 14:28:30143 def __repr__(self):
144 return self.message
145
gayane3dff8c22014-12-04 17:09:51146 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41147 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51148 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
149 self.type = 'error'
150
151 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41152 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51153 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
154 self.type = 'warning'
155
156 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41157 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51158 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
159 self.type = 'notify'
160
161 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41162 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51163 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
164 self.type = 'promptOrNotify'
165
Daniel Cheng7052cdf2017-11-21 19:23:29166 def __init__(self):
167 self.more_cc = []
168
169 def AppendCC(self, more_cc):
170 self.more_cc.extend(more_cc)
171
gayane3dff8c22014-12-04 17:09:51172
173class MockFile(object):
174 """Mock class for the File class.
175
176 This class can be used to form the mock list of changed files in
177 MockInputApi for presubmit unittests.
178 """
179
Yoland Yanb92fa522017-08-28 17:37:06180 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
gayane3dff8c22014-12-04 17:09:51181 self._local_path = local_path
182 self._new_contents = new_contents
183 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40184 self._action = action
jbriance9e12f162016-11-25 07:57:50185 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
186 len(new_contents))
Yoland Yanb92fa522017-08-28 17:37:06187 self._old_contents = old_contents
jbriance9e12f162016-11-25 07:57:50188 for l in new_contents:
189 self._scm_diff += "+%s\n" % l
gayane3dff8c22014-12-04 17:09:51190
dbeam37e8e7402016-02-10 22:58:20191 def Action(self):
agrievef32bcc72016-04-04 14:57:40192 return self._action
dbeam37e8e7402016-02-10 22:58:20193
gayane3dff8c22014-12-04 17:09:51194 def ChangedContents(self):
195 return self._changed_contents
196
197 def NewContents(self):
198 return self._new_contents
199
200 def LocalPath(self):
201 return self._local_path
202
rdevlin.cronin9ab806c2016-02-26 23:17:13203 def AbsoluteLocalPath(self):
204 return self._local_path
205
jbriance9e12f162016-11-25 07:57:50206 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31207 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50208
Yoland Yanb92fa522017-08-28 17:37:06209 def OldContents(self):
210 return self._old_contents
211
davileene0426252015-03-02 21:10:41212 def rfind(self, p):
213 """os.path.basename is called on MockFile so we need an rfind method."""
214 return self._local_path.rfind(p)
215
216 def __getitem__(self, i):
217 """os.path.basename is called on MockFile so we need a get method."""
218 return self._local_path[i]
219
pastarmovj89f7ee12016-09-20 14:58:13220 def __len__(self):
221 """os.path.basename is called on MockFile so we need a len method."""
222 return len(self._local_path)
223
Julian Pastarmov4f7af532019-07-17 19:25:37224 def replace(self, altsep, sep):
225 """os.path.basename is called on MockFile so we need a replace method."""
226 return self._local_path.replace(altsep, sep)
227
gayane3dff8c22014-12-04 17:09:51228
glidere61efad2015-02-18 17:39:43229class MockAffectedFile(MockFile):
230 def AbsoluteLocalPath(self):
231 return self._local_path
232
233
gayane3dff8c22014-12-04 17:09:51234class MockChange(object):
235 """Mock class for Change class.
236
237 This class can be used in presubmit unittests to mock the query of the
238 current change.
239 """
240
241 def __init__(self, changed_files):
242 self._changed_files = changed_files
243
244 def LocalPaths(self):
245 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54246
247 def AffectedFiles(self, include_dirs=False, include_deletes=True,
248 file_filter=None):
249 return self._changed_files