blob: ed6a750de3238a5db5a49d3571fafd022414391b [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
Zhiling Huang45cabf32018-03-10 00:50:0354
gayane3dff8c22014-12-04 17:09:5155class MockInputApi(object):
56 """Mock class for the InputApi class.
57
58 This class can be used for unittests for presubmit by initializing the files
59 attribute as the list of changed files.
60 """
61
Sylvain Defresnea8b73d252018-02-28 15:45:5462 DEFAULT_BLACK_LIST = ()
63
gayane3dff8c22014-12-04 17:09:5164 def __init__(self):
Daniel Cheng264a447d2017-09-28 22:17:5965 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2566 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5167 self.json = json
68 self.re = re
69 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0070 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5171 self.python_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1372 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5173 self.subprocess = subprocess
74 self.files = []
75 self.is_committing = False
gayanee1702662014-12-13 03:48:0976 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3477 self.presubmit_local_path = os.path.dirname(__file__)
gayane3dff8c22014-12-04 17:09:5178
Zhiling Huang45cabf32018-03-10 00:50:0379 def CreateMockFileInPath(self, f_list):
80 self.os_path.exists = lambda x: x in f_list
81
agrievef32bcc72016-04-04 14:57:4082 def AffectedFiles(self, file_filter=None, include_deletes=False):
Sylvain Defresnea8b73d252018-02-28 15:45:5483 for file in self.files:
84 if file_filter and not file_filter(file):
85 continue
86 if not include_deletes and file.Action() == 'D':
87 continue
88 yield file
gayane3dff8c22014-12-04 17:09:5189
glidere61efad2015-02-18 17:39:4390 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:5491 return self.AffectedFiles(file_filter=file_filter)
92
93 def FilterSourceFile(self, file, white_list=(), black_list=()):
94 local_path = file.LocalPath()
Vaclav Brozekf01ed502018-03-16 19:38:2495 found_in_white_list = not white_list
Sylvain Defresnea8b73d252018-02-28 15:45:5496 if white_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:0197 if type(white_list) is str:
98 raise TypeError('white_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:5499 for pattern in white_list:
100 compiled_pattern = re.compile(pattern)
101 if compiled_pattern.search(local_path):
Vaclav Brozekf01ed502018-03-16 19:38:24102 found_in_white_list = True
103 break
Sylvain Defresnea8b73d252018-02-28 15:45:54104 if black_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:01105 if type(black_list) is str:
106 raise TypeError('black_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:54107 for pattern in black_list:
108 compiled_pattern = re.compile(pattern)
109 if compiled_pattern.search(local_path):
110 return False
Vaclav Brozekf01ed502018-03-16 19:38:24111 return found_in_white_list
glidere61efad2015-02-18 17:39:43112
davileene0426252015-03-02 21:10:41113 def LocalPaths(self):
114 return self.files
115
gayane3dff8c22014-12-04 17:09:51116 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34117 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51118
119 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43120 if hasattr(filename, 'AbsoluteLocalPath'):
121 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51122 for file_ in self.files:
123 if file_.LocalPath() == filename:
124 return '\n'.join(file_.NewContents())
125 # Otherwise, file is not in our mock API.
126 raise IOError, "No such file or directory: '%s'" % filename
127
128
129class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46130 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51131
132 An instance of this class can be passed to presubmit unittests for outputing
133 various types of results.
134 """
135
136 class PresubmitResult(object):
137 def __init__(self, message, items=None, long_text=''):
138 self.message = message
139 self.items = items
140 self.long_text = long_text
141
gayane940df072015-02-24 14:28:30142 def __repr__(self):
143 return self.message
144
gayane3dff8c22014-12-04 17:09:51145 class PresubmitError(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 = 'error'
149
150 class PresubmitPromptWarning(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 = 'warning'
154
155 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41156 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51157 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
158 self.type = 'notify'
159
160 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41161 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51162 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
163 self.type = 'promptOrNotify'
164
Daniel Cheng7052cdf2017-11-21 19:23:29165 def __init__(self):
166 self.more_cc = []
167
168 def AppendCC(self, more_cc):
169 self.more_cc.extend(more_cc)
170
gayane3dff8c22014-12-04 17:09:51171
172class MockFile(object):
173 """Mock class for the File class.
174
175 This class can be used to form the mock list of changed files in
176 MockInputApi for presubmit unittests.
177 """
178
Yoland Yanb92fa522017-08-28 17:37:06179 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
gayane3dff8c22014-12-04 17:09:51180 self._local_path = local_path
181 self._new_contents = new_contents
182 self._changed_contents = [(i + 1, l) for i, l in enumerate(new_contents)]
agrievef32bcc72016-04-04 14:57:40183 self._action = action
jbriance9e12f162016-11-25 07:57:50184 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
185 len(new_contents))
Yoland Yanb92fa522017-08-28 17:37:06186 self._old_contents = old_contents
jbriance9e12f162016-11-25 07:57:50187 for l in new_contents:
188 self._scm_diff += "+%s\n" % l
gayane3dff8c22014-12-04 17:09:51189
dbeam37e8e7402016-02-10 22:58:20190 def Action(self):
agrievef32bcc72016-04-04 14:57:40191 return self._action
dbeam37e8e7402016-02-10 22:58:20192
gayane3dff8c22014-12-04 17:09:51193 def ChangedContents(self):
194 return self._changed_contents
195
196 def NewContents(self):
197 return self._new_contents
198
199 def LocalPath(self):
200 return self._local_path
201
rdevlin.cronin9ab806c2016-02-26 23:17:13202 def AbsoluteLocalPath(self):
203 return self._local_path
204
jbriance9e12f162016-11-25 07:57:50205 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31206 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50207
Yoland Yanb92fa522017-08-28 17:37:06208 def OldContents(self):
209 return self._old_contents
210
davileene0426252015-03-02 21:10:41211 def rfind(self, p):
212 """os.path.basename is called on MockFile so we need an rfind method."""
213 return self._local_path.rfind(p)
214
215 def __getitem__(self, i):
216 """os.path.basename is called on MockFile so we need a get method."""
217 return self._local_path[i]
218
pastarmovj89f7ee12016-09-20 14:58:13219 def __len__(self):
220 """os.path.basename is called on MockFile so we need a len method."""
221 return len(self._local_path)
222
gayane3dff8c22014-12-04 17:09:51223
glidere61efad2015-02-18 17:39:43224class MockAffectedFile(MockFile):
225 def AbsoluteLocalPath(self):
226 return self._local_path
227
228
gayane3dff8c22014-12-04 17:09:51229class MockChange(object):
230 """Mock class for Change class.
231
232 This class can be used in presubmit unittests to mock the query of the
233 current change.
234 """
235
236 def __init__(self, changed_files):
237 self._changed_files = changed_files
238
239 def LocalPaths(self):
240 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54241
242 def AffectedFiles(self, include_dirs=False, include_deletes=True,
243 file_filter=None):
244 return self._changed_files