2023-04-27 22:01:55 +03:00
|
|
|
# Copyright 2023 Google Inc.
|
2023-04-25 00:04:54 +03:00
|
|
|
# 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
|