|  | # Copyright 2023 Google Inc. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | # This is a copy of PRESUBMIT_test_mocks.py from the Chromium project. | 
|  |  | 
|  | from collections import defaultdict | 
|  | import fnmatch | 
|  | import json | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  |  | 
|  | def _ReportErrorFileAndLine(filename, line_num, dummy_line): | 
|  | """Default error formatter for _FindNewViolationsOfRule.""" | 
|  | return '%s:%s' % (filename, line_num) | 
|  |  | 
|  |  | 
|  | class MockCannedChecks(object): | 
|  | def _FindNewViolationsOfRule(self, callable_rule, input_api, | 
|  | source_file_filter=None, | 
|  | error_formatter=_ReportErrorFileAndLine): | 
|  | """Find all newly introduced violations of a per-line rule (a callable). | 
|  |  | 
|  | Arguments: | 
|  | callable_rule: a callable taking a file extension and line of input and | 
|  | returning True if the rule is satisfied and False if there was a | 
|  | problem. | 
|  | input_api: object to enumerate the affected files. | 
|  | source_file_filter: a filter to be passed to the input api. | 
|  | error_formatter: a callable taking (filename, line_number, line) and | 
|  | returning a formatted error string. | 
|  |  | 
|  | Returns: | 
|  | A list of the newly-introduced violations reported by the rule. | 
|  | """ | 
|  | errors = [] | 
|  | for f in input_api.AffectedFiles(include_deletes=False, | 
|  | file_filter=source_file_filter): | 
|  | # For speed, we do two passes, checking first the full file.  Shelling out | 
|  | # to the SCM to determine the changed region can be quite expensive on | 
|  | # Win32.  Assuming that most files will be kept problem-free, we can | 
|  | # skip the SCM operations most of the time. | 
|  | extension = str(f.LocalPath()).rsplit('.', 1)[-1] | 
|  | if all(callable_rule(extension, line) for line in f.NewContents()): | 
|  | # No violation found in full text: can skip considering diff. | 
|  | continue | 
|  |  | 
|  | for line_num, line in f.ChangedContents(): | 
|  | if not callable_rule(extension, line): | 
|  | errors.append(error_formatter( | 
|  | f.LocalPath(), line_num, line)) | 
|  |  | 
|  | return errors | 
|  |  | 
|  |  | 
|  | class MockInputApi(object): | 
|  | """Mock class for the InputApi class. | 
|  |  | 
|  | This class can be used for unittests for presubmit by initializing the files | 
|  | attribute as the list of changed files. | 
|  | """ | 
|  |  | 
|  | DEFAULT_FILES_TO_SKIP = () | 
|  |  | 
|  | def __init__(self): | 
|  | self.canned_checks = MockCannedChecks() | 
|  | self.fnmatch = fnmatch | 
|  | self.json = json | 
|  | self.re = re | 
|  | self.os_path = os.path | 
|  | self.platform = sys.platform | 
|  | self.python_executable = sys.executable | 
|  | self.python3_executable = sys.executable | 
|  | self.platform = sys.platform | 
|  | self.subprocess = subprocess | 
|  | self.sys = sys | 
|  | self.files = [] | 
|  | self.is_committing = False | 
|  | self.change = MockChange([]) | 
|  | self.presubmit_local_path = os.path.dirname(__file__) | 
|  | self.is_windows = sys.platform == 'win32' | 
|  | self.no_diffs = False | 
|  | # Although this makes assumptions about command line arguments used by test | 
|  | # scripts that create mocks, it is a convenient way to set up the verbosity | 
|  | # via the input api. | 
|  | self.verbose = '--verbose' in sys.argv | 
|  |  | 
|  | def CreateMockFileInPath(self, f_list): | 
|  | self.os_path.exists = lambda x: x in f_list | 
|  |  | 
|  | def AffectedFiles(self, file_filter=None, include_deletes=True): | 
|  | for file in self.files: | 
|  | if file_filter and not file_filter(file): | 
|  | continue | 
|  | if not include_deletes and file.Action() == 'D': | 
|  | continue | 
|  | yield file | 
|  |  | 
|  | def RightHandSideLines(self, source_file_filter=None): | 
|  | affected_files = self.AffectedSourceFiles(source_file_filter) | 
|  | for af in affected_files: | 
|  | lines = af.ChangedContents() | 
|  | for line in lines: | 
|  | yield (af, line[0], line[1]) | 
|  |  | 
|  | def AffectedSourceFiles(self, file_filter=None): | 
|  | return self.AffectedFiles(file_filter=file_filter) | 
|  |  | 
|  | def FilterSourceFile(self, file, | 
|  | files_to_check=(), files_to_skip=()): | 
|  | local_path = file.LocalPath() | 
|  | found_in_files_to_check = not files_to_check | 
|  | if files_to_check: | 
|  | if type(files_to_check) is str: | 
|  | raise TypeError( | 
|  | 'files_to_check should be an iterable of strings') | 
|  | for pattern in files_to_check: | 
|  | compiled_pattern = re.compile(pattern) | 
|  | if compiled_pattern.match(local_path): | 
|  | found_in_files_to_check = True | 
|  | break | 
|  | if files_to_skip: | 
|  | if type(files_to_skip) is str: | 
|  | raise TypeError( | 
|  | 'files_to_skip should be an iterable of strings') | 
|  | for pattern in files_to_skip: | 
|  | compiled_pattern = re.compile(pattern) | 
|  | if compiled_pattern.match(local_path): | 
|  | return False | 
|  | return found_in_files_to_check | 
|  |  | 
|  | def LocalPaths(self): | 
|  | return [file.LocalPath() for file in self.files] | 
|  |  | 
|  | def PresubmitLocalPath(self): | 
|  | return self.presubmit_local_path | 
|  |  | 
|  | def ReadFile(self, filename, mode='r'): | 
|  | if hasattr(filename, 'AbsoluteLocalPath'): | 
|  | filename = filename.AbsoluteLocalPath() | 
|  | for file_ in self.files: | 
|  | if file_.LocalPath() == filename: | 
|  | return '\n'.join(file_.NewContents()) | 
|  | # Otherwise, file is not in our mock API. | 
|  | raise IOError("No such file or directory: '%s'" % filename) | 
|  |  | 
|  |  | 
|  | class MockOutputApi(object): | 
|  | """Mock class for the OutputApi class. | 
|  |  | 
|  | An instance of this class can be passed to presubmit unittests for outputting | 
|  | various types of results. | 
|  | """ | 
|  |  | 
|  | class PresubmitResult(object): | 
|  | def __init__(self, message, items=None, long_text=''): | 
|  | self.message = message | 
|  | self.items = items | 
|  | self.long_text = long_text | 
|  |  | 
|  | def __repr__(self): | 
|  | return self.message | 
|  |  | 
|  | class PresubmitError(PresubmitResult): | 
|  | def __init__(self, message, items=None, long_text=''): | 
|  | MockOutputApi.PresubmitResult.__init__( | 
|  | self, message, items, long_text) | 
|  | self.type = 'error' | 
|  |  | 
|  | class PresubmitPromptWarning(PresubmitResult): | 
|  | def __init__(self, message, items=None, long_text=''): | 
|  | MockOutputApi.PresubmitResult.__init__( | 
|  | self, message, items, long_text) | 
|  | self.type = 'warning' | 
|  |  | 
|  | class PresubmitNotifyResult(PresubmitResult): | 
|  | def __init__(self, message, items=None, long_text=''): | 
|  | MockOutputApi.PresubmitResult.__init__( | 
|  | self, message, items, long_text) | 
|  | self.type = 'notify' | 
|  |  | 
|  | class PresubmitPromptOrNotify(PresubmitResult): | 
|  | def __init__(self, message, items=None, long_text=''): | 
|  | MockOutputApi.PresubmitResult.__init__( | 
|  | self, message, items, long_text) | 
|  | self.type = 'promptOrNotify' | 
|  |  | 
|  | def __init__(self): | 
|  | self.more_cc = [] | 
|  |  | 
|  | def AppendCC(self, more_cc): | 
|  | self.more_cc.append(more_cc) | 
|  |  | 
|  |  | 
|  | class MockFile(object): | 
|  | """Mock class for the File class. | 
|  |  | 
|  | This class can be used to form the mock list of changed files in | 
|  | MockInputApi for presubmit unittests. | 
|  | """ | 
|  |  | 
|  | def __init__(self, local_path, new_contents, old_contents=None, action='A', | 
|  | scm_diff=None): | 
|  | self._local_path = local_path | 
|  | self._new_contents = new_contents | 
|  | self._changed_contents = [(i + 1, l) | 
|  | for i, l in enumerate(new_contents)] | 
|  | self._action = action | 
|  | if scm_diff: | 
|  | self._scm_diff = scm_diff | 
|  | else: | 
|  | self._scm_diff = ( | 
|  | "--- /dev/null\n+++ %s\n@@ -0,0 +1,%d @@\n" % | 
|  | (local_path, len(new_contents))) | 
|  | for l in new_contents: | 
|  | self._scm_diff += "+%s\n" % l | 
|  | self._old_contents = old_contents | 
|  |  | 
|  | def Action(self): | 
|  | return self._action | 
|  |  | 
|  | def ChangedContents(self): | 
|  | return self._changed_contents | 
|  |  | 
|  | def NewContents(self): | 
|  | return self._new_contents | 
|  |  | 
|  | def LocalPath(self): | 
|  | return self._local_path | 
|  |  | 
|  | def AbsoluteLocalPath(self): | 
|  | return self._local_path | 
|  |  | 
|  | def GenerateScmDiff(self): | 
|  | return self._scm_diff | 
|  |  | 
|  | def OldContents(self): | 
|  | return self._old_contents | 
|  |  | 
|  | def rfind(self, p): | 
|  | """os.path.basename is called on MockFile so we need an rfind method.""" | 
|  | return self._local_path.rfind(p) | 
|  |  | 
|  | def __getitem__(self, i): | 
|  | """os.path.basename is called on MockFile so we need a get method.""" | 
|  | return self._local_path[i] | 
|  |  | 
|  | def __len__(self): | 
|  | """os.path.basename is called on MockFile so we need a len method.""" | 
|  | return len(self._local_path) | 
|  |  | 
|  | def replace(self, altsep, sep): | 
|  | """os.path.basename is called on MockFile so we need a replace method.""" | 
|  | return self._local_path.replace(altsep, sep) | 
|  |  | 
|  |  | 
|  | class MockAffectedFile(MockFile): | 
|  | def AbsoluteLocalPath(self): | 
|  | return self._local_path | 
|  |  | 
|  |  | 
|  | class MockChange(object): | 
|  | """Mock class for Change class. | 
|  |  | 
|  | This class can be used in presubmit unittests to mock the query of the | 
|  | current change. | 
|  | """ | 
|  |  | 
|  | def __init__(self, changed_files): | 
|  | self._changed_files = changed_files | 
|  | self.author_email = None | 
|  | self.footers = defaultdict(list) | 
|  |  | 
|  | def LocalPaths(self): | 
|  | return self._changed_files | 
|  |  | 
|  | def AffectedFiles(self, include_dirs=False, include_deletes=True, | 
|  | file_filter=None): | 
|  | return self._changed_files | 
|  |  | 
|  | def GitFootersFromDescription(self): | 
|  | return self.footers |