PRESUBMIT_test_mocks.py 12 KB

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