1 # Copyright 2014 The Chromium Authors
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 from collections import defaultdict
13 # 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.
15 def _ReportErrorFileAndLine(filename, line_num, dummy_line):
16 """Default error formatter for _FindNewViolationsOfRule."""
17 return '%s:%s' % (filename, line_num)
20 class 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).
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
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.
36 A list of the newly-introduced violations reported by the rule.
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.
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))
56 class MockInputApi(object):
57 """Mock class for the InputApi class.
59 This class can be used for unittests for presubmit by initializing the files
60 attribute as the list of changed files.
63 DEFAULT_FILES_TO_SKIP = ()
66 self.canned_checks = MockCannedChecks()
67 self.fnmatch = fnmatch
70 self.os_path = os.path
71 self.platform = sys.platform
72 self.python_executable = sys.executable
73 self.python3_executable = sys.executable
74 self.platform = sys.platform
75 self.subprocess = subprocess
78 self.is_committing = False
79 self.change = MockChange([])
80 self.presubmit_local_path = os.path.dirname(__file__)
81 self.is_windows = sys.platform == 'win32'
84 def CreateMockFileInPath(self, f_list):
85 self.os_path.exists = lambda x: x in f_list
87 def AffectedFiles(self, file_filter=None, include_deletes=True):
88 for file in self.files:
89 if file_filter and not file_filter(file):
91 if not include_deletes and file.Action() == 'D':
95 def RightHandSideLines(self, source_file_filter=None):
96 affected_files = self.AffectedSourceFiles(source_file_filter)
97 for af in affected_files:
98 lines = af.ChangedContents()
100 yield (af, line[0], line[1])
102 def AffectedSourceFiles(self, file_filter=None):
103 return self.AffectedFiles(file_filter=file_filter)
105 def FilterSourceFile(self, file,
106 files_to_check=(), files_to_skip=()):
107 local_path = file.LocalPath()
108 found_in_files_to_check = not files_to_check
110 if type(files_to_check) is str:
111 raise TypeError('files_to_check should be an iterable of strings')
112 for pattern in files_to_check:
113 compiled_pattern = re.compile(pattern)
114 if compiled_pattern.match(local_path):
115 found_in_files_to_check = True
118 if type(files_to_skip) is str:
119 raise TypeError('files_to_skip should be an iterable of strings')
120 for pattern in files_to_skip:
121 compiled_pattern = re.compile(pattern)
122 if compiled_pattern.match(local_path):
124 return found_in_files_to_check
126 def LocalPaths(self):
127 return [file.LocalPath() for file in self.files]
129 def PresubmitLocalPath(self):
130 return self.presubmit_local_path
132 def ReadFile(self, filename, mode='r'):
133 if hasattr(filename, 'AbsoluteLocalPath'):
134 filename = filename.AbsoluteLocalPath()
135 for file_ in self.files:
136 if file_.LocalPath() == filename:
137 return '\n'.join(file_.NewContents())
138 # Otherwise, file is not in our mock API.
139 raise IOError("No such file or directory: '%s'" % filename)
142 class MockOutputApi(object):
143 """Mock class for the OutputApi class.
145 An instance of this class can be passed to presubmit unittests for outputting
146 various types of results.
149 class PresubmitResult(object):
150 def __init__(self, message, items=None, long_text=''):
151 self.message = message
153 self.long_text = long_text
158 class PresubmitError(PresubmitResult):
159 def __init__(self, message, items=None, long_text=''):
160 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
163 class PresubmitPromptWarning(PresubmitResult):
164 def __init__(self, message, items=None, long_text=''):
165 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
166 self.type = 'warning'
168 class PresubmitNotifyResult(PresubmitResult):
169 def __init__(self, message, items=None, long_text=''):
170 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
173 class PresubmitPromptOrNotify(PresubmitResult):
174 def __init__(self, message, items=None, long_text=''):
175 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
176 self.type = 'promptOrNotify'
181 def AppendCC(self, more_cc):
182 self.more_cc.append(more_cc)
185 class MockFile(object):
186 """Mock class for the File class.
188 This class can be used to form the mock list of changed files in
189 MockInputApi for presubmit unittests.
192 def __init__(self, local_path, new_contents, old_contents=None, action='A',
194 self._local_path = local_path
195 self._new_contents = new_contents
196 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
197 self._action = action
199 self._scm_diff = scm_diff
202 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
203 (local_path, len(new_contents)))
204 for l in new_contents:
205 self._scm_diff += "+%s\n" % l
206 self._old_contents = old_contents
211 def ChangedContents(self):
212 return self._changed_contents
214 def NewContents(self):
215 return self._new_contents
218 return self._local_path
220 def AbsoluteLocalPath(self):
221 return self._local_path
223 def GenerateScmDiff(self):
224 return self._scm_diff
226 def OldContents(self):
227 return self._old_contents
230 """os.path.basename is called on MockFile so we need an rfind method."""
231 return self._local_path.rfind(p)
233 def __getitem__(self, i):
234 """os.path.basename is called on MockFile so we need a get method."""
235 return self._local_path[i]
238 """os.path.basename is called on MockFile so we need a len method."""
239 return len(self._local_path)
241 def replace(self, altsep, sep):
242 """os.path.basename is called on MockFile so we need a replace method."""
243 return self._local_path.replace(altsep, sep)
246 class MockAffectedFile(MockFile):
247 def AbsoluteLocalPath(self):
248 return self._local_path
251 class MockChange(object):
252 """Mock class for Change class.
254 This class can be used in presubmit unittests to mock the query of the
258 def __init__(self, changed_files):
259 self._changed_files = changed_files
260 self.author_email = None
261 self.footers = defaultdict(list)
263 def LocalPaths(self):
264 return self._changed_files
266 def AffectedFiles(self, include_dirs=False, include_deletes=True,
268 return self._changed_files
270 def GitFootersFromDescription(self):