1 # 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.
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.platform = sys.platform
74 self.subprocess = subprocess
77 self.is_committing = False
78 self.change = MockChange([])
79 self.presubmit_local_path = os.path.dirname(__file__)
81 def CreateMockFileInPath(self, f_list):
82 self.os_path.exists = lambda x: x in f_list
84 def AffectedFiles(self, file_filter=None, include_deletes=False):
85 for file in self.files:
86 if file_filter and not file_filter(file):
88 if not include_deletes and file.Action() == 'D':
92 def AffectedSourceFiles(self, file_filter=None):
93 return self.AffectedFiles(file_filter=file_filter)
95 def FilterSourceFile(self, file,
96 files_to_check=(), files_to_skip=()):
97 local_path = file.LocalPath()
98 found_in_files_to_check = not files_to_check
100 if type(files_to_check) is str:
101 raise TypeError('files_to_check should be an iterable of strings')
102 for pattern in files_to_check:
103 compiled_pattern = re.compile(pattern)
104 if compiled_pattern.search(local_path):
105 found_in_files_to_check = True
108 if type(files_to_skip) is str:
109 raise TypeError('files_to_skip should be an iterable of strings')
110 for pattern in files_to_skip:
111 compiled_pattern = re.compile(pattern)
112 if compiled_pattern.search(local_path):
114 return found_in_files_to_check
116 def LocalPaths(self):
117 return [file.LocalPath() for file in self.files]
119 def PresubmitLocalPath(self):
120 return self.presubmit_local_path
122 def ReadFile(self, filename, mode='rU'):
123 if hasattr(filename, 'AbsoluteLocalPath'):
124 filename = filename.AbsoluteLocalPath()
125 for file_ in self.files:
126 if file_.LocalPath() == filename:
127 return '\n'.join(file_.NewContents())
128 # Otherwise, file is not in our mock API.
129 raise IOError("No such file or directory: '%s'" % filename)
132 class MockOutputApi(object):
133 """Mock class for the OutputApi class.
135 An instance of this class can be passed to presubmit unittests for outputing
136 various types of results.
139 class PresubmitResult(object):
140 def __init__(self, message, items=None, long_text=''):
141 self.message = message
143 self.long_text = long_text
148 class PresubmitError(PresubmitResult):
149 def __init__(self, message, items=None, long_text=''):
150 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
153 class PresubmitPromptWarning(PresubmitResult):
154 def __init__(self, message, items=None, long_text=''):
155 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
156 self.type = 'warning'
158 class PresubmitNotifyResult(PresubmitResult):
159 def __init__(self, message, items=None, long_text=''):
160 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
163 class PresubmitPromptOrNotify(PresubmitResult):
164 def __init__(self, message, items=None, long_text=''):
165 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
166 self.type = 'promptOrNotify'
171 def AppendCC(self, more_cc):
172 self.more_cc.extend(more_cc)
175 class MockFile(object):
176 """Mock class for the File class.
178 This class can be used to form the mock list of changed files in
179 MockInputApi for presubmit unittests.
182 def __init__(self, local_path, new_contents, old_contents=None, action='A',
184 self._local_path = local_path
185 self._new_contents = new_contents
186 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
187 self._action = action
189 self._scm_diff = scm_diff
192 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
193 (local_path, len(new_contents)))
194 for l in new_contents:
195 self._scm_diff += "+%s\n" % l
196 self._old_contents = old_contents
201 def ChangedContents(self):
202 return self._changed_contents
204 def NewContents(self):
205 return self._new_contents
208 return self._local_path
210 def AbsoluteLocalPath(self):
211 return self._local_path
213 def GenerateScmDiff(self):
214 return self._scm_diff
216 def OldContents(self):
217 return self._old_contents
220 """os.path.basename is called on MockFile so we need an rfind method."""
221 return self._local_path.rfind(p)
223 def __getitem__(self, i):
224 """os.path.basename is called on MockFile so we need a get method."""
225 return self._local_path[i]
228 """os.path.basename is called on MockFile so we need a len method."""
229 return len(self._local_path)
231 def replace(self, altsep, sep):
232 """os.path.basename is called on MockFile so we need a replace method."""
233 return self._local_path.replace(altsep, sep)
236 class MockAffectedFile(MockFile):
237 def AbsoluteLocalPath(self):
238 return self._local_path
241 class MockChange(object):
242 """Mock class for Change class.
244 This class can be used in presubmit unittests to mock the query of the
248 def __init__(self, changed_files):
249 self._changed_files = changed_files
250 self.footers = defaultdict(list)
252 def LocalPaths(self):
253 return self._changed_files
255 def AffectedFiles(self, include_dirs=False, include_deletes=True,
257 return self._changed_files
259 def GitFootersFromDescription(self):