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_BLACK_LIST = ()
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, white_list=(), black_list=()):
96 local_path = file.LocalPath()
97 found_in_white_list = not white_list
99 if type(white_list) is str:
100 raise TypeError('white_list should be an iterable of strings')
101 for pattern in white_list:
102 compiled_pattern = re.compile(pattern)
103 if compiled_pattern.search(local_path):
104 found_in_white_list = True
107 if type(black_list) is str:
108 raise TypeError('black_list should be an iterable of strings')
109 for pattern in black_list:
110 compiled_pattern = re.compile(pattern)
111 if compiled_pattern.search(local_path):
113 return found_in_white_list
115 def LocalPaths(self):
116 return [file.LocalPath() for file in self.files]
118 def PresubmitLocalPath(self):
119 return self.presubmit_local_path
121 def ReadFile(self, filename, mode='rU'):
122 if hasattr(filename, 'AbsoluteLocalPath'):
123 filename = filename.AbsoluteLocalPath()
124 for file_ in self.files:
125 if file_.LocalPath() == filename:
126 return '\n'.join(file_.NewContents())
127 # Otherwise, file is not in our mock API.
128 raise IOError, "No such file or directory: '%s'" % filename
131 class MockOutputApi(object):
132 """Mock class for the OutputApi class.
134 An instance of this class can be passed to presubmit unittests for outputing
135 various types of results.
138 class PresubmitResult(object):
139 def __init__(self, message, items=None, long_text=''):
140 self.message = message
142 self.long_text = long_text
147 class PresubmitError(PresubmitResult):
148 def __init__(self, message, items=None, long_text=''):
149 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
152 class PresubmitPromptWarning(PresubmitResult):
153 def __init__(self, message, items=None, long_text=''):
154 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
155 self.type = 'warning'
157 class PresubmitNotifyResult(PresubmitResult):
158 def __init__(self, message, items=None, long_text=''):
159 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
162 class PresubmitPromptOrNotify(PresubmitResult):
163 def __init__(self, message, items=None, long_text=''):
164 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
165 self.type = 'promptOrNotify'
170 def AppendCC(self, more_cc):
171 self.more_cc.extend(more_cc)
174 class MockFile(object):
175 """Mock class for the File class.
177 This class can be used to form the mock list of changed files in
178 MockInputApi for presubmit unittests.
181 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
182 self._local_path = local_path
183 self._new_contents = new_contents
184 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
185 self._action = action
186 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
188 self._old_contents = old_contents
189 for l in new_contents:
190 self._scm_diff += "+%s\n" % l
195 def ChangedContents(self):
196 return self._changed_contents
198 def NewContents(self):
199 return self._new_contents
202 return self._local_path
204 def AbsoluteLocalPath(self):
205 return self._local_path
207 def GenerateScmDiff(self):
208 return self._scm_diff
210 def OldContents(self):
211 return self._old_contents
214 """os.path.basename is called on MockFile so we need an rfind method."""
215 return self._local_path.rfind(p)
217 def __getitem__(self, i):
218 """os.path.basename is called on MockFile so we need a get method."""
219 return self._local_path[i]
222 """os.path.basename is called on MockFile so we need a len method."""
223 return len(self._local_path)
225 def replace(self, altsep, sep):
226 """os.path.basename is called on MockFile so we need a replace method."""
227 return self._local_path.replace(altsep, sep)
230 class MockAffectedFile(MockFile):
231 def AbsoluteLocalPath(self):
232 return self._local_path
235 class MockChange(object):
236 """Mock class for Change class.
238 This class can be used in presubmit unittests to mock the query of the
242 def __init__(self, changed_files):
243 self._changed_files = changed_files
244 self.footers = defaultdict(list)
246 def LocalPaths(self):
247 return self._changed_files
249 def AffectedFiles(self, include_dirs=False, include_deletes=True,
251 return self._changed_files
253 def GitFootersFromDescription(self):