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'
83 # Although this makes assumptions about command line arguments used by test
84 # scripts that create mocks, it is a convenient way to set up the verbosity
86 self.verbose = '--verbose' in sys.argv
88 def CreateMockFileInPath(self, f_list):
89 self.os_path.exists = lambda x: x in f_list
91 def AffectedFiles(self, file_filter=None, include_deletes=True):
92 for file in self.files:
93 if file_filter and not file_filter(file):
95 if not include_deletes and file.Action() == 'D':
99 def RightHandSideLines(self, source_file_filter=None):
100 affected_files = self.AffectedSourceFiles(source_file_filter)
101 for af in affected_files:
102 lines = af.ChangedContents()
104 yield (af, line[0], line[1])
106 def AffectedSourceFiles(self, file_filter=None):
107 return self.AffectedFiles(file_filter=file_filter)
109 def FilterSourceFile(self, file,
110 files_to_check=(), files_to_skip=()):
111 local_path = file.LocalPath()
112 found_in_files_to_check = not files_to_check
114 if type(files_to_check) is str:
115 raise TypeError('files_to_check should be an iterable of strings')
116 for pattern in files_to_check:
117 compiled_pattern = re.compile(pattern)
118 if compiled_pattern.match(local_path):
119 found_in_files_to_check = True
122 if type(files_to_skip) is str:
123 raise TypeError('files_to_skip should be an iterable of strings')
124 for pattern in files_to_skip:
125 compiled_pattern = re.compile(pattern)
126 if compiled_pattern.match(local_path):
128 return found_in_files_to_check
130 def LocalPaths(self):
131 return [file.LocalPath() for file in self.files]
133 def PresubmitLocalPath(self):
134 return self.presubmit_local_path
136 def ReadFile(self, filename, mode='r'):
137 if hasattr(filename, 'AbsoluteLocalPath'):
138 filename = filename.AbsoluteLocalPath()
139 for file_ in self.files:
140 if file_.LocalPath() == filename:
141 return '\n'.join(file_.NewContents())
142 # Otherwise, file is not in our mock API.
143 raise IOError("No such file or directory: '%s'" % filename)
146 class MockOutputApi(object):
147 """Mock class for the OutputApi class.
149 An instance of this class can be passed to presubmit unittests for outputting
150 various types of results.
153 class PresubmitResult(object):
154 def __init__(self, message, items=None, long_text=''):
155 self.message = message
157 self.long_text = long_text
162 class PresubmitError(PresubmitResult):
163 def __init__(self, message, items=None, long_text=''):
164 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
167 class PresubmitPromptWarning(PresubmitResult):
168 def __init__(self, message, items=None, long_text=''):
169 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
170 self.type = 'warning'
172 class PresubmitNotifyResult(PresubmitResult):
173 def __init__(self, message, items=None, long_text=''):
174 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
177 class PresubmitPromptOrNotify(PresubmitResult):
178 def __init__(self, message, items=None, long_text=''):
179 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
180 self.type = 'promptOrNotify'
185 def AppendCC(self, more_cc):
186 self.more_cc.append(more_cc)
189 class MockFile(object):
190 """Mock class for the File class.
192 This class can be used to form the mock list of changed files in
193 MockInputApi for presubmit unittests.
196 def __init__(self, local_path, new_contents, old_contents=None, action='A',
198 self._local_path = local_path
199 self._new_contents = new_contents
200 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
201 self._action = action
203 self._scm_diff = scm_diff
206 "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
207 (local_path, len(new_contents)))
208 for l in new_contents:
209 self._scm_diff += "+%s\n" % l
210 self._old_contents = old_contents
213 return self._local_path
218 def ChangedContents(self):
219 return self._changed_contents
221 def NewContents(self):
222 return self._new_contents
225 return self._local_path
227 def AbsoluteLocalPath(self):
228 return self._local_path
230 def GenerateScmDiff(self):
231 return self._scm_diff
233 def OldContents(self):
234 return self._old_contents
237 """os.path.basename is called on MockFile so we need an rfind method."""
238 return self._local_path.rfind(p)
240 def __getitem__(self, i):
241 """os.path.basename is called on MockFile so we need a get method."""
242 return self._local_path[i]
245 """os.path.basename is called on MockFile so we need a len method."""
246 return len(self._local_path)
248 def replace(self, altsep, sep):
249 """os.path.basename is called on MockFile so we need a replace method."""
250 return self._local_path.replace(altsep, sep)
253 class MockAffectedFile(MockFile):
254 def AbsoluteLocalPath(self):
255 return self._local_path
258 class MockChange(object):
259 """Mock class for Change class.
261 This class can be used in presubmit unittests to mock the query of the
265 def __init__(self, changed_files):
266 self._changed_files = changed_files
267 self.author_email = None
268 self.footers = defaultdict(list)
270 def LocalPaths(self):
271 return self._changed_files
273 def AffectedFiles(self, include_dirs=False, include_deletes=True,
275 return self._changed_files
277 def GitFootersFromDescription(self):