# Copyright 2014 The Chromium Authors # 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 os import re import subprocess import sys _REPO_ROOT = os.path.abspath(os.path.dirname(__file__)) # TODO(dcheng): It's kind of horrible that this is copy and pasted from # presubmit_canned_checks.py, but it's far easier than any of the alternatives. 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.basename = os.path.basename self.canned_checks = MockCannedChecks() self.fnmatch = fnmatch self.json = json self.re = re # We want os_path.exists() and os_path.isfile() to work for files # that are both in the filesystem and mock files we have added # via InitFiles(). # By setting os_path to a copy of os.path rather than directly we # can not only have os_path.exists() be a combined output for fake # files and real files in the filesystem. import importlib.util SPEC_OS_PATH = importlib.util.find_spec('os.path') os_path1 = importlib.util.module_from_spec(SPEC_OS_PATH) SPEC_OS_PATH.loader.exec_module(os_path1) sys.modules['os_path1'] = os_path1 self.os_path = os_path1 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( os.path.abspath(sys.argv[0])) 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 InitFiles(self, files): # Actual presubmit calls normpath, but too many tests break to do this # right in MockFile(). for f in files: f._local_path = os.path.normpath(f._local_path) self.files = files files_that_exist = { p.AbsoluteLocalPath() for p in files if p.Action() != 'D' } def mock_exists(path): if not os.path.isabs(path): path = os.path.join(self.presubmit_local_path, path) path = os.path.normpath(path) return path in files_that_exist or any( f.startswith(path) for f in files_that_exist) or os.path.exists(path) def mock_isfile(path): if not os.path.isabs(path): path = os.path.join(self.presubmit_local_path, path) path = os.path.normpath(path) return path in files_that_exist or os.path.isfile(path) def mock_glob(pattern, *args, **kwargs): return fnmatch.filter(files_that_exist, pattern) # Do not stub these in the constructor to not break existing tests. self.os_path.exists = mock_exists self.os_path.isfile = mock_isfile self.glob = mock_glob 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, include_deletes=False) def AffectedTestableFiles(self, file_filter=None): return self.AffectedFiles(file_filter=file_filter, include_deletes=False) 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() norm_filename = os.path.normpath(filename) for file_ in self.files: to_check = (file_.LocalPath(), file_.AbsoluteLocalPath()) if filename in to_check or norm_filename in to_check: 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 or [] def __str__(self): return self._local_path 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 os.path.join(_REPO_ROOT, self._local_path) def GenerateScmDiff(self): return self._scm_diff def OldContents(self): return self._old_contents def rfind(self, p): """Required when os.path.basename() is called on MockFile.""" return self._local_path.rfind(p) def __getitem__(self, i): """Required when os.path.basename() is called on MockFile.""" return self._local_path[i] def __len__(self): """Required when os.path.basename() is called on MockFile.""" return len(self._local_path) def replace(self, altsep, sep): """Required when os.path.basename() is called on MockFile.""" return self._local_path.replace(altsep, sep) class MockAffectedFile(MockFile): pass 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 def RepositoryRoot(self): return _REPO_ROOT