blob: 732da0ea38c71ad405ea94f726759cbc4202e610 [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
54class MockInputApi(object):
55 """Mock class for the InputApi class.
56
57 This class can be used for unittests for presubmit by initializing the files
58 attribute as the list of changed files.
59 """
60
Sylvain Defresnea8b73d252018-02-28 15:45:5461 DEFAULT_BLACK_LIST = ()
62
gayane3dff8c22014-12-04 17:09:5163 def __init__(self):
Daniel Cheng264a447d2017-09-28 22:17:5964 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2565 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5166 self.json = json
67 self.re = re
68 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0069 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5170 self.python_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1371 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5172 self.subprocess = subprocess
73 self.files = []
74 self.is_committing = False
gayanee1702662014-12-13 03:48:0975 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3476 self.presubmit_local_path = os.path.dirname(__file__)
gayane3dff8c22014-12-04 17:09:5177
agrievef32bcc72016-04-04 14:57:4078 def AffectedFiles(self, file_filter=None, include_deletes=False):
Sylvain Defresnea8b73d252018-02-28 15:45:5479 for file in self.files:
80 if file_filter and not file_filter(file):
81 continue
82 if not include_deletes and file.Action() == 'D':
83 continue
84 yield file
gayane3dff8c22014-12-04 17:09:5185
glidere61efad2015-02-18 17:39:4386 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:5487 return self.AffectedFiles(file_filter=file_filter)
88
89 def FilterSourceFile(self, file, white_list=(), black_list=()):
90 local_path = file.LocalPath()
91 if white_list:
92 for pattern in white_list:
93 compiled_pattern = re.compile(pattern)
94 if compiled_pattern.search(local_path):
95 return True
96 if black_list:
97 for pattern in black_list:
98 compiled_pattern = re.compile(pattern)
99 if compiled_pattern.search(local_path):
100 return False
101 return True
glidere61efad2015-02-18 17:39:43102
davileene0426252015-03-02 21:10:41103 def LocalPaths(self):
104 return self.files
105
gayane3dff8c22014-12-04 17:09:51106 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34107 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51108
109 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43110 if hasattr(filename, 'AbsoluteLocalPath'):
111 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51112 for file_ in self.files:
113 if file_.LocalPath() == filename:
114 return '\n'.join(file_.NewContents())
115 # Otherwise, file is not in our mock API.
116 raise IOError, "No such file or directory: '%s'" % filename
117
118
119class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46120 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51121
122 An instance of this class can be passed to presubmit unittests for outputing
123 various types of results.
124 """
125
126 class PresubmitResult(object):
127 def __init__(self, message, items=None, long_text=''):
128 self.message = message
129 self.items = items
130 self.long_text = long_text
131
gayane940df072015-02-24 14:28:30132 def __repr__(self):
133 return self.message
134
gayane3dff8c22014-12-04 17:09:51135 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41136 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51137 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
138 self.type = 'error'
139
140 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41141 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51142 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
143 self.type = 'warning'
144
145 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41146 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51147 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
148 self.type = 'notify'
149
150 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41151 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51152 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
153 self.type = 'promptOrNotify'
154
Daniel Cheng7052cdf2017-11-21 19:23:29155 def __init__(self):
156 self.more_cc = []
157
158 def AppendCC(self, more_cc):
159 self.more_cc.extend(more_cc)
160
gayane3dff8c22014-12-04 17:09:51161
162class MockFile(object):
163 """Mock class for the File class.
164
165 This class can be used to form the mock list of changed files in
166 MockInputApi for presubmit unittests.
167 """
168
Yoland Yanb92fa522017-08-28 17:37:06169 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
gayane3dff8c22014-12-04 17:09:51170 self._local_path = local_path
171 self._new_contents = new_contents
172 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40173 self._action = action
jbriance9e12f162016-11-25 07:57:50174 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
175 len(new_contents))
Yoland Yanb92fa522017-08-28 17:37:06176 self._old_contents = old_contents
jbriance9e12f162016-11-25 07:57:50177 for l in new_contents:
178 self._scm_diff += "+%s\n" % l
gayane3dff8c22014-12-04 17:09:51179
dbeam37e8e7402016-02-10 22:58:20180 def Action(self):
agrievef32bcc72016-04-04 14:57:40181 return self._action
dbeam37e8e7402016-02-10 22:58:20182
gayane3dff8c22014-12-04 17:09:51183 def ChangedContents(self):
184 return self._changed_contents
185
186 def NewContents(self):
187 return self._new_contents
188
189 def LocalPath(self):
190 return self._local_path
191
rdevlin.cronin9ab806c2016-02-26 23:17:13192 def AbsoluteLocalPath(self):
193 return self._local_path
194
jbriance9e12f162016-11-25 07:57:50195 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31196 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50197
Yoland Yanb92fa522017-08-28 17:37:06198 def OldContents(self):
199 return self._old_contents
200
davileene0426252015-03-02 21:10:41201 def rfind(self, p):
202 """os.path.basename is called on MockFile so we need an rfind method."""
203 return self._local_path.rfind(p)
204
205 def __getitem__(self, i):
206 """os.path.basename is called on MockFile so we need a get method."""
207 return self._local_path[i]
208
pastarmovj89f7ee12016-09-20 14:58:13209 def __len__(self):
210 """os.path.basename is called on MockFile so we need a len method."""
211 return len(self._local_path)
212
gayane3dff8c22014-12-04 17:09:51213
glidere61efad2015-02-18 17:39:43214class MockAffectedFile(MockFile):
215 def AbsoluteLocalPath(self):
216 return self._local_path
217
218
gayane3dff8c22014-12-04 17:09:51219class MockChange(object):
220 """Mock class for Change class.
221
222 This class can be used in presubmit unittests to mock the query of the
223 current change.
224 """
225
226 def __init__(self, changed_files):
227 self._changed_files = changed_files
228
229 def LocalPaths(self):
230 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54231
232 def AffectedFiles(self, include_dirs=False, include_deletes=True,
233 file_filter=None):
234 return self._changed_files