blob: 8318028e87c7c2ab88d7097aa4becce46bfb797a [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
Chris Hall59f8d0c72020-05-01 07:31:195from collections import defaultdict
Daniel Cheng13ca61a882017-08-25 15:11:256import fnmatch
gayane3dff8c22014-12-04 17:09:517import json
8import os
9import re
10import subprocess
11import sys
12
Daniel Cheng264a447d2017-09-28 22:17:5913# 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.
15def _ReportErrorFileAndLine(filename, line_num, dummy_line):
16 """Default error formatter for _FindNewViolationsOfRule."""
17 return '%s:%s' % (filename, line_num)
18
19
20class 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).
25
26 Arguments:
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
29 problem.
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.
34
35 Returns:
36 A list of the newly-introduced violations reported by the rule.
37 """
38 errors = []
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.
48
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))
52
53 return errors
gayane3dff8c22014-12-04 17:09:5154
Zhiling Huang45cabf32018-03-10 00:50:0355
gayane3dff8c22014-12-04 17:09:5156class MockInputApi(object):
57 """Mock class for the InputApi class.
58
59 This class can be used for unittests for presubmit by initializing the files
60 attribute as the list of changed files.
61 """
62
Sylvain Defresnea8b73d252018-02-28 15:45:5463 DEFAULT_BLACK_LIST = ()
64
gayane3dff8c22014-12-04 17:09:5165 def __init__(self):
Daniel Cheng264a447d2017-09-28 22:17:5966 self.canned_checks = MockCannedChecks()
Daniel Cheng13ca61a882017-08-25 15:11:2567 self.fnmatch = fnmatch
gayane3dff8c22014-12-04 17:09:5168 self.json = json
69 self.re = re
70 self.os_path = os.path
agrievebb9c5b472016-04-22 15:13:0071 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5172 self.python_executable = sys.executable
pastarmovj89f7ee12016-09-20 14:58:1373 self.platform = sys.platform
gayane3dff8c22014-12-04 17:09:5174 self.subprocess = subprocess
Dan Beam35b10c12019-11-27 01:17:3475 self.sys = sys
gayane3dff8c22014-12-04 17:09:5176 self.files = []
77 self.is_committing = False
gayanee1702662014-12-13 03:48:0978 self.change = MockChange([])
dpapad5c9c24e2017-05-31 20:51:3479 self.presubmit_local_path = os.path.dirname(__file__)
gayane3dff8c22014-12-04 17:09:5180
Zhiling Huang45cabf32018-03-10 00:50:0381 def CreateMockFileInPath(self, f_list):
82 self.os_path.exists = lambda x: x in f_list
83
agrievef32bcc72016-04-04 14:57:4084 def AffectedFiles(self, file_filter=None, include_deletes=False):
Sylvain Defresnea8b73d252018-02-28 15:45:5485 for file in self.files:
86 if file_filter and not file_filter(file):
87 continue
88 if not include_deletes and file.Action() == 'D':
89 continue
90 yield file
gayane3dff8c22014-12-04 17:09:5191
glidere61efad2015-02-18 17:39:4392 def AffectedSourceFiles(self, file_filter=None):
Sylvain Defresnea8b73d252018-02-28 15:45:5493 return self.AffectedFiles(file_filter=file_filter)
94
95 def FilterSourceFile(self, file, white_list=(), black_list=()):
96 local_path = file.LocalPath()
Vaclav Brozekf01ed502018-03-16 19:38:2497 found_in_white_list = not white_list
Sylvain Defresnea8b73d252018-02-28 15:45:5498 if white_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:0199 if type(white_list) is str:
100 raise TypeError('white_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:54101 for pattern in white_list:
102 compiled_pattern = re.compile(pattern)
103 if compiled_pattern.search(local_path):
Vaclav Brozekf01ed502018-03-16 19:38:24104 found_in_white_list = True
105 break
Sylvain Defresnea8b73d252018-02-28 15:45:54106 if black_list:
Wei-Yin Chen (陳威尹)b1ce35492018-07-31 02:37:01107 if type(black_list) is str:
108 raise TypeError('black_list should be an iterable of strings')
Sylvain Defresnea8b73d252018-02-28 15:45:54109 for pattern in black_list:
110 compiled_pattern = re.compile(pattern)
111 if compiled_pattern.search(local_path):
112 return False
Vaclav Brozekf01ed502018-03-16 19:38:24113 return found_in_white_list
glidere61efad2015-02-18 17:39:43114
davileene0426252015-03-02 21:10:41115 def LocalPaths(self):
Alexei Svitkine137d4c662019-07-17 21:28:24116 return [file.LocalPath() for file in self.files]
davileene0426252015-03-02 21:10:41117
gayane3dff8c22014-12-04 17:09:51118 def PresubmitLocalPath(self):
dpapad5c9c24e2017-05-31 20:51:34119 return self.presubmit_local_path
gayane3dff8c22014-12-04 17:09:51120
121 def ReadFile(self, filename, mode='rU'):
glidere61efad2015-02-18 17:39:43122 if hasattr(filename, 'AbsoluteLocalPath'):
123 filename = filename.AbsoluteLocalPath()
gayane3dff8c22014-12-04 17:09:51124 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
129
130
131class MockOutputApi(object):
gayane860db5c32014-12-05 16:16:46132 """Mock class for the OutputApi class.
gayane3dff8c22014-12-04 17:09:51133
134 An instance of this class can be passed to presubmit unittests for outputing
135 various types of results.
136 """
137
138 class PresubmitResult(object):
139 def __init__(self, message, items=None, long_text=''):
140 self.message = message
141 self.items = items
142 self.long_text = long_text
143
gayane940df072015-02-24 14:28:30144 def __repr__(self):
145 return self.message
146
gayane3dff8c22014-12-04 17:09:51147 class PresubmitError(PresubmitResult):
davileene0426252015-03-02 21:10:41148 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51149 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
150 self.type = 'error'
151
152 class PresubmitPromptWarning(PresubmitResult):
davileene0426252015-03-02 21:10:41153 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51154 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
155 self.type = 'warning'
156
157 class PresubmitNotifyResult(PresubmitResult):
davileene0426252015-03-02 21:10:41158 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51159 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
160 self.type = 'notify'
161
162 class PresubmitPromptOrNotify(PresubmitResult):
davileene0426252015-03-02 21:10:41163 def __init__(self, message, items=None, long_text=''):
gayane3dff8c22014-12-04 17:09:51164 MockOutputApi.PresubmitResult.__init__(self, message, items, long_text)
165 self.type = 'promptOrNotify'
166
Daniel Cheng7052cdf2017-11-21 19:23:29167 def __init__(self):
168 self.more_cc = []
169
170 def AppendCC(self, more_cc):
171 self.more_cc.extend(more_cc)
172
gayane3dff8c22014-12-04 17:09:51173
174class MockFile(object):
175 """Mock class for the File class.
176
177 This class can be used to form the mock list of changed files in
178 MockInputApi for presubmit unittests.
179 """
180
Yoland Yanb92fa522017-08-28 17:37:06181 def __init__(self, local_path, new_contents, old_contents=None, action='A'):
gayane3dff8c22014-12-04 17:09:51182 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)]
agrievef32bcc72016-04-04 14:57:40185 self._action = action
jbriance9e12f162016-11-25 07:57:50186 self._scm_diff = "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % (local_path,
187 len(new_contents))
Yoland Yanb92fa522017-08-28 17:37:06188 self._old_contents = old_contents
jbriance9e12f162016-11-25 07:57:50189 for l in new_contents:
190 self._scm_diff += "+%s\n" % l
gayane3dff8c22014-12-04 17:09:51191
dbeam37e8e7402016-02-10 22:58:20192 def Action(self):
agrievef32bcc72016-04-04 14:57:40193 return self._action
dbeam37e8e7402016-02-10 22:58:20194
gayane3dff8c22014-12-04 17:09:51195 def ChangedContents(self):
196 return self._changed_contents
197
198 def NewContents(self):
199 return self._new_contents
200
201 def LocalPath(self):
202 return self._local_path
203
rdevlin.cronin9ab806c2016-02-26 23:17:13204 def AbsoluteLocalPath(self):
205 return self._local_path
206
jbriance9e12f162016-11-25 07:57:50207 def GenerateScmDiff(self):
jbriance2c51e821a2016-12-12 08:24:31208 return self._scm_diff
jbriance9e12f162016-11-25 07:57:50209
Yoland Yanb92fa522017-08-28 17:37:06210 def OldContents(self):
211 return self._old_contents
212
davileene0426252015-03-02 21:10:41213 def rfind(self, p):
214 """os.path.basename is called on MockFile so we need an rfind method."""
215 return self._local_path.rfind(p)
216
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]
220
pastarmovj89f7ee12016-09-20 14:58:13221 def __len__(self):
222 """os.path.basename is called on MockFile so we need a len method."""
223 return len(self._local_path)
224
Julian Pastarmov4f7af532019-07-17 19:25:37225 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)
228
gayane3dff8c22014-12-04 17:09:51229
glidere61efad2015-02-18 17:39:43230class MockAffectedFile(MockFile):
231 def AbsoluteLocalPath(self):
232 return self._local_path
233
234
gayane3dff8c22014-12-04 17:09:51235class MockChange(object):
236 """Mock class for Change class.
237
238 This class can be used in presubmit unittests to mock the query of the
239 current change.
240 """
241
242 def __init__(self, changed_files):
243 self._changed_files = changed_files
Chris Hall59f8d0c72020-05-01 07:31:19244 self.footers = defaultdict(list)
gayane3dff8c22014-12-04 17:09:51245
246 def LocalPaths(self):
247 return self._changed_files
rdevlin.cronin113668252016-05-02 17:05:54248
249 def AffectedFiles(self, include_dirs=False, include_deletes=True,
250 file_filter=None):
251 return self._changed_files
Chris Hall59f8d0c72020-05-01 07:31:19252
253 def GitFootersFromDescription(self):
254 return self.footers
255