xref: /aosp_15_r20/external/skia/PRESUBMIT_test_mocks.py (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1# Copyright 2023 Google Inc.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# This is a copy of PRESUBMIT_test_mocks.py from the Chromium project.
6
7from collections import defaultdict
8import fnmatch
9import json
10import os
11import re
12import subprocess
13import sys
14
15
16def _ReportErrorFileAndLine(filename, line_num, dummy_line):
17    """Default error formatter for _FindNewViolationsOfRule."""
18    return '%s:%s' % (filename, line_num)
19
20
21class MockCannedChecks(object):
22    def _FindNewViolationsOfRule(self, callable_rule, input_api,
23                                 source_file_filter=None,
24                                 error_formatter=_ReportErrorFileAndLine):
25        """Find all newly introduced violations of a per-line rule (a callable).
26
27        Arguments:
28          callable_rule: a callable taking a file extension and line of input and
29            returning True if the rule is satisfied and False if there was a
30            problem.
31          input_api: object to enumerate the affected files.
32          source_file_filter: a filter to be passed to the input api.
33          error_formatter: a callable taking (filename, line_number, line) and
34            returning a formatted error string.
35
36        Returns:
37          A list of the newly-introduced violations reported by the rule.
38        """
39        errors = []
40        for f in input_api.AffectedFiles(include_deletes=False,
41                                         file_filter=source_file_filter):
42            # For speed, we do two passes, checking first the full file.  Shelling out
43            # to the SCM to determine the changed region can be quite expensive on
44            # Win32.  Assuming that most files will be kept problem-free, we can
45            # skip the SCM operations most of the time.
46            extension = str(f.LocalPath()).rsplit('.', 1)[-1]
47            if all(callable_rule(extension, line) for line in f.NewContents()):
48                # No violation found in full text: can skip considering diff.
49                continue
50
51            for line_num, line in f.ChangedContents():
52                if not callable_rule(extension, line):
53                    errors.append(error_formatter(
54                        f.LocalPath(), line_num, line))
55
56        return errors
57
58
59class MockInputApi(object):
60    """Mock class for the InputApi class.
61
62    This class can be used for unittests for presubmit by initializing the files
63    attribute as the list of changed files.
64    """
65
66    DEFAULT_FILES_TO_SKIP = ()
67
68    def __init__(self):
69        self.canned_checks = MockCannedChecks()
70        self.fnmatch = fnmatch
71        self.json = json
72        self.re = re
73        self.os_path = os.path
74        self.platform = sys.platform
75        self.python_executable = sys.executable
76        self.python3_executable = sys.executable
77        self.platform = sys.platform
78        self.subprocess = subprocess
79        self.sys = sys
80        self.files = []
81        self.is_committing = False
82        self.change = MockChange([])
83        self.presubmit_local_path = os.path.dirname(__file__)
84        self.is_windows = sys.platform == 'win32'
85        self.no_diffs = False
86        # Although this makes assumptions about command line arguments used by test
87        # scripts that create mocks, it is a convenient way to set up the verbosity
88        # via the input api.
89        self.verbose = '--verbose' in sys.argv
90
91    def CreateMockFileInPath(self, f_list):
92        self.os_path.exists = lambda x: x in f_list
93
94    def AffectedFiles(self, file_filter=None, include_deletes=True):
95        for file in self.files:
96            if file_filter and not file_filter(file):
97                continue
98            if not include_deletes and file.Action() == 'D':
99                continue
100            yield file
101
102    def RightHandSideLines(self, source_file_filter=None):
103        affected_files = self.AffectedSourceFiles(source_file_filter)
104        for af in affected_files:
105            lines = af.ChangedContents()
106            for line in lines:
107                yield (af, line[0], line[1])
108
109    def AffectedSourceFiles(self, file_filter=None):
110        return self.AffectedFiles(file_filter=file_filter)
111
112    def FilterSourceFile(self, file,
113                         files_to_check=(), files_to_skip=()):
114        local_path = file.LocalPath()
115        found_in_files_to_check = not files_to_check
116        if files_to_check:
117            if type(files_to_check) is str:
118                raise TypeError(
119                    'files_to_check should be an iterable of strings')
120            for pattern in files_to_check:
121                compiled_pattern = re.compile(pattern)
122                if compiled_pattern.match(local_path):
123                    found_in_files_to_check = True
124                    break
125        if files_to_skip:
126            if type(files_to_skip) is str:
127                raise TypeError(
128                    'files_to_skip should be an iterable of strings')
129            for pattern in files_to_skip:
130                compiled_pattern = re.compile(pattern)
131                if compiled_pattern.match(local_path):
132                    return False
133        return found_in_files_to_check
134
135    def LocalPaths(self):
136        return [file.LocalPath() for file in self.files]
137
138    def PresubmitLocalPath(self):
139        return self.presubmit_local_path
140
141    def ReadFile(self, filename, mode='r'):
142        if hasattr(filename, 'AbsoluteLocalPath'):
143            filename = filename.AbsoluteLocalPath()
144        for file_ in self.files:
145            if file_.LocalPath() == filename:
146                return '\n'.join(file_.NewContents())
147        # Otherwise, file is not in our mock API.
148        raise IOError("No such file or directory: '%s'" % filename)
149
150
151class MockOutputApi(object):
152    """Mock class for the OutputApi class.
153
154    An instance of this class can be passed to presubmit unittests for outputting
155    various types of results.
156    """
157
158    class PresubmitResult(object):
159        def __init__(self, message, items=None, long_text=''):
160            self.message = message
161            self.items = items
162            self.long_text = long_text
163
164        def __repr__(self):
165            return self.message
166
167    class PresubmitError(PresubmitResult):
168        def __init__(self, message, items=None, long_text=''):
169            MockOutputApi.PresubmitResult.__init__(
170                self, message, items, long_text)
171            self.type = 'error'
172
173    class PresubmitPromptWarning(PresubmitResult):
174        def __init__(self, message, items=None, long_text=''):
175            MockOutputApi.PresubmitResult.__init__(
176                self, message, items, long_text)
177            self.type = 'warning'
178
179    class PresubmitNotifyResult(PresubmitResult):
180        def __init__(self, message, items=None, long_text=''):
181            MockOutputApi.PresubmitResult.__init__(
182                self, message, items, long_text)
183            self.type = 'notify'
184
185    class PresubmitPromptOrNotify(PresubmitResult):
186        def __init__(self, message, items=None, long_text=''):
187            MockOutputApi.PresubmitResult.__init__(
188                self, message, items, long_text)
189            self.type = 'promptOrNotify'
190
191    def __init__(self):
192        self.more_cc = []
193
194    def AppendCC(self, more_cc):
195        self.more_cc.append(more_cc)
196
197
198class MockFile(object):
199    """Mock class for the File class.
200
201    This class can be used to form the mock list of changed files in
202    MockInputApi for presubmit unittests.
203    """
204
205    def __init__(self, local_path, new_contents, old_contents=None, action='A',
206                 scm_diff=None):
207        self._local_path = local_path
208        self._new_contents = new_contents
209        self._changed_contents = [(i + 1, l)
210                                  for i, l in enumerate(new_contents)]
211        self._action = action
212        if scm_diff:
213            self._scm_diff = scm_diff
214        else:
215            self._scm_diff = (
216                "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" %
217                (local_path, len(new_contents)))
218            for l in new_contents:
219                self._scm_diff += "+%s\n" % l
220        self._old_contents = old_contents
221
222    def Action(self):
223        return self._action
224
225    def ChangedContents(self):
226        return self._changed_contents
227
228    def NewContents(self, flush_cache=False):
229        return self._new_contents
230
231    def LocalPath(self):
232        return self._local_path
233
234    def AbsoluteLocalPath(self):
235        return self._local_path
236
237    def GenerateScmDiff(self):
238        return self._scm_diff
239
240    def OldContents(self):
241        return self._old_contents
242
243    def rfind(self, p):
244        """os.path.basename is called on MockFile so we need an rfind method."""
245        return self._local_path.rfind(p)
246
247    def __getitem__(self, i):
248        """os.path.basename is called on MockFile so we need a get method."""
249        return self._local_path[i]
250
251    def __len__(self):
252        """os.path.basename is called on MockFile so we need a len method."""
253        return len(self._local_path)
254
255    def replace(self, altsep, sep):
256        """os.path.basename is called on MockFile so we need a replace method."""
257        return self._local_path.replace(altsep, sep)
258
259
260class MockAffectedFile(MockFile):
261    def AbsoluteLocalPath(self):
262        return self._local_path
263
264
265class MockChange(object):
266    """Mock class for Change class.
267
268    This class can be used in presubmit unittests to mock the query of the
269    current change.
270    """
271
272    def __init__(self, changed_files):
273        self._changed_files = changed_files
274        self.author_email = None
275        self.footers = defaultdict(list)
276
277    def LocalPaths(self):
278        return self._changed_files
279
280    def AffectedFiles(self, include_dirs=False, include_deletes=True,
281                      file_filter=None):
282        return self._changed_files
283
284    def GitFootersFromDescription(self):
285        return self.footers
286