You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
282 lines
9.6 KiB
Python
282 lines
9.6 KiB
Python
# Copyright 2021 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
from collections import defaultdict
|
|
import fnmatch
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
from presubmit_canned_checks import _ReportErrorFileAndLine
|
|
|
|
|
|
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.platform = sys.platform
|
|
self.subprocess = subprocess
|
|
self.sys = sys
|
|
self.files = []
|
|
self.is_committing = False
|
|
self.no_diffs = False
|
|
self.change = MockChange([])
|
|
self.presubmit_local_path = os.path.dirname(__file__)
|
|
self.logging = logging.getLogger('PRESUBMIT')
|
|
|
|
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: # pylint: disable=redefined-builtin
|
|
if file_filter and not file_filter(file):
|
|
continue
|
|
if not include_deletes and file.Action() == 'D':
|
|
continue
|
|
yield file
|
|
|
|
def AffectedSourceFiles(self, file_filter=None):
|
|
return self.AffectedFiles(file_filter=file_filter)
|
|
|
|
def FilterSourceFile(
|
|
self,
|
|
file, # pylint: disable=redefined-builtin
|
|
files_to_check=(),
|
|
files_to_skip=()):
|
|
local_path = file.LocalPath()
|
|
found_in_files_to_check = not files_to_check
|
|
if files_to_check:
|
|
if isinstance(files_to_check, 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.search(local_path):
|
|
found_in_files_to_check = True
|
|
break
|
|
if files_to_skip:
|
|
if isinstance(files_to_skip, 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.search(local_path):
|
|
return False
|
|
return found_in_files_to_check
|
|
|
|
def LocalPaths(self):
|
|
return [file.LocalPath() for file in self.files] # pylint: disable=redefined-builtin
|
|
|
|
def PresubmitLocalPath(self):
|
|
return self.presubmit_local_path
|
|
|
|
def ReadFile(self, filename, mode='rU'):
|
|
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 outputing
|
|
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.extend(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 used on MockFile so we need an rfind method."""
|
|
return self._local_path.rfind(p)
|
|
|
|
def __getitem__(self, i):
|
|
"""os.path.basename is used on MockFile so we need a get method."""
|
|
return self._local_path[i]
|
|
|
|
def __len__(self):
|
|
"""os.path.basename is used on MockFile so we need a len method."""
|
|
return len(self._local_path)
|
|
|
|
def replace(self, altsep, sep):
|
|
"""os.path.basename is used 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, description='', issue=0):
|
|
self._changed_files = changed_files
|
|
self.footers = defaultdict(list)
|
|
self._description = description
|
|
self.issue = issue
|
|
|
|
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
|
|
|
|
def DescriptionText(self):
|
|
return self._description
|