bot_update_coverage_test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #!/usr/bin/env python
  2. # Copyright (c) 2015 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. import codecs
  6. import copy
  7. import json
  8. import os
  9. import sys
  10. import unittest
  11. #import test_env # pylint: disable=relative-import,unused-import
  12. sys.path.insert(0, os.path.join(
  13. os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
  14. 'recipes', 'recipe_modules', 'bot_update', 'resources'))
  15. import bot_update
  16. class MockedPopen(object):
  17. """A fake instance of a called subprocess.
  18. This is meant to be used in conjunction with MockedCall.
  19. """
  20. def __init__(self, args=None, kwargs=None):
  21. self.args = args or []
  22. self.kwargs = kwargs or {}
  23. self.return_value = None
  24. self.fails = False
  25. def returns(self, rv):
  26. """Set the return value when this popen is called.
  27. rv can be a string, or a callable (eg function).
  28. """
  29. self.return_value = rv
  30. return self
  31. def check(self, args, kwargs):
  32. """Check to see if the given args/kwargs call match this instance.
  33. This does a partial match, so that a call to "git clone foo" will match
  34. this instance if this instance was recorded as "git clone"
  35. """
  36. if any(input_arg != expected_arg
  37. for (input_arg, expected_arg) in zip(args, self.args)):
  38. return False
  39. return self.return_value
  40. def __call__(self, args, kwargs):
  41. """Actually call this popen instance."""
  42. if hasattr(self.return_value, '__call__'):
  43. return self.return_value(*args, **kwargs)
  44. return self.return_value
  45. class MockedCall(object):
  46. """A fake instance of bot_update.call().
  47. This object is pre-seeded with "answers" in self.expectations. The type
  48. is a MockedPopen object, or any object with a __call__() and check() method.
  49. The check() method is used to check to see if the correct popen object is
  50. chosen (can be a partial match, eg a "git clone" popen module would match
  51. a "git clone foo" call).
  52. By default, if no answers have been pre-seeded, the call() returns successful
  53. with an empty string.
  54. """
  55. def __init__(self, fake_filesystem):
  56. self.expectations = []
  57. self.records = []
  58. def expect(self, args=None, kwargs=None):
  59. args = args or []
  60. kwargs = kwargs or {}
  61. popen = MockedPopen(args, kwargs)
  62. self.expectations.append(popen)
  63. return popen
  64. def __call__(self, *args, **kwargs):
  65. self.records.append((args, kwargs))
  66. for popen in self.expectations:
  67. if popen.check(args, kwargs):
  68. self.expectations.remove(popen)
  69. return popen(args, kwargs)
  70. return ''
  71. class MockedGclientSync():
  72. """A class producing a callable instance of gclient sync.
  73. Because for bot_update, gclient sync also emits an output json file, we need
  74. a callable object that can understand where the output json file is going, and
  75. emit a (albite) fake file for bot_update to consume.
  76. """
  77. def __init__(self, fake_filesystem):
  78. self.output = {}
  79. self.fake_filesystem = fake_filesystem
  80. self.records = []
  81. def __call__(self, *args, **_):
  82. output_json_index = args.index('--output-json') + 1
  83. with self.fake_filesystem.open(args[output_json_index], 'w') as f:
  84. json.dump(self.output, f)
  85. self.records.append(args)
  86. class FakeFile():
  87. def __init__(self):
  88. self.contents = ''
  89. def write(self, buf):
  90. self.contents += buf
  91. def read(self):
  92. return self.contents
  93. def __enter__(self):
  94. return self
  95. def __exit__(self, _, __, ___):
  96. pass
  97. class FakeFilesystem():
  98. def __init__(self):
  99. self.files = {}
  100. def open(self, target, mode='r', encoding=None):
  101. if 'w' in mode:
  102. self.files[target] = FakeFile()
  103. return self.files[target]
  104. return self.files[target]
  105. def fake_git(*args, **kwargs):
  106. return bot_update.call('git', *args, **kwargs)
  107. class BotUpdateUnittests(unittest.TestCase):
  108. DEFAULT_PARAMS = {
  109. 'solutions': [{
  110. 'name': 'somename',
  111. 'url': 'https://fake.com'
  112. }],
  113. 'revisions': {},
  114. 'first_sln': 'somename',
  115. 'target_os': None,
  116. 'target_os_only': None,
  117. 'target_cpu': None,
  118. 'patch_root': None,
  119. 'patch_refs': [],
  120. 'gerrit_rebase_patch_ref': None,
  121. 'refs': [],
  122. 'git_cache_dir': '',
  123. 'cleanup_dir': None,
  124. 'gerrit_reset': None,
  125. 'disable_syntax_validation': False,
  126. }
  127. def setUp(self):
  128. sys.platform = 'linux2' # For consistency, ya know?
  129. self.filesystem = FakeFilesystem()
  130. self.call = MockedCall(self.filesystem)
  131. self.gclient = MockedGclientSync(self.filesystem)
  132. self.call.expect(
  133. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  134. ).returns(self.gclient)
  135. self.old_call = getattr(bot_update, 'call')
  136. self.params = copy.deepcopy(self.DEFAULT_PARAMS)
  137. setattr(bot_update, 'call', self.call)
  138. setattr(bot_update, 'git', fake_git)
  139. self.old_os_cwd = os.getcwd
  140. setattr(os, 'getcwd', lambda: '/b/build/slave/foo/build')
  141. setattr(bot_update, 'open', self.filesystem.open)
  142. self.old_codecs_open = codecs.open
  143. setattr(codecs, 'open', self.filesystem.open)
  144. def tearDown(self):
  145. setattr(bot_update, 'call', self.old_call)
  146. setattr(os, 'getcwd', self.old_os_cwd)
  147. delattr(bot_update, 'open')
  148. setattr(codecs, 'open', self.old_codecs_open)
  149. def overrideSetupForWindows(self):
  150. sys.platform = 'win'
  151. self.call.expect(
  152. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  153. ).returns(self.gclient)
  154. def testBasic(self):
  155. bot_update.ensure_checkout(**self.params)
  156. return self.call.records
  157. def testBasicRevision(self):
  158. self.params['revisions'] = {
  159. 'src': 'HEAD', 'src/v8': 'deadbeef', 'somename': 'DNE'}
  160. bot_update.ensure_checkout(**self.params)
  161. args = self.gclient.records[0]
  162. idx_first_revision = args.index('--revision')
  163. idx_second_revision = args.index(
  164. '--revision', idx_first_revision+1)
  165. idx_third_revision = args.index('--revision', idx_second_revision+1)
  166. self.assertEquals(args[idx_first_revision+1], 'somename@unmanaged')
  167. self.assertEquals(args[idx_second_revision+1], 'src@origin/master')
  168. self.assertEquals(args[idx_third_revision+1], 'src/v8@deadbeef')
  169. return self.call.records
  170. def testApplyPatchOnGclient(self):
  171. ref = 'refs/changes/12/345/6'
  172. repo = 'https://chromium.googlesource.com/v8/v8'
  173. self.params['patch_refs'] = ['%s@%s' % (repo, ref)]
  174. bot_update.ensure_checkout(**self.params)
  175. args = self.gclient.records[0]
  176. idx = args.index('--patch-ref')
  177. self.assertEqual(args[idx+1], self.params['patch_refs'][0])
  178. self.assertNotIn('--patch-ref', args[idx+1:])
  179. # Assert we're not patching in bot_update.py
  180. for record in self.call.records:
  181. self.assertNotIn('git fetch ' + repo,
  182. ' '.join(record[0]))
  183. def testPatchRefs(self):
  184. self.params['patch_refs'] = [
  185. 'https://chromium.googlesource.com/chromium/src@refs/changes/12/345/6',
  186. 'https://chromium.googlesource.com/v8/v8@refs/changes/1/234/56']
  187. bot_update.ensure_checkout(**self.params)
  188. args = self.gclient.records[0]
  189. patch_refs = set(
  190. args[i+1] for i in xrange(len(args))
  191. if args[i] == '--patch-ref' and i+1 < len(args))
  192. self.assertIn(self.params['patch_refs'][0], patch_refs)
  193. self.assertIn(self.params['patch_refs'][1], patch_refs)
  194. def testBreakLocks(self):
  195. self.overrideSetupForWindows()
  196. bot_update.ensure_checkout(**self.params)
  197. gclient_sync_cmd = None
  198. for record in self.call.records:
  199. args = record[0]
  200. if args[:4] == (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync'):
  201. gclient_sync_cmd = args
  202. self.assertTrue('--break_repo_locks' in gclient_sync_cmd)
  203. def testGitCheckoutBreaksLocks(self):
  204. self.overrideSetupForWindows()
  205. path = '/b/build/slave/foo/build/.git'
  206. lockfile = 'index.lock'
  207. removed = []
  208. old_os_walk = os.walk
  209. old_os_remove = os.remove
  210. setattr(os, 'walk', lambda _: [(path, None, [lockfile])])
  211. setattr(os, 'remove', removed.append)
  212. bot_update.ensure_checkout(**self.params)
  213. setattr(os, 'walk', old_os_walk)
  214. setattr(os, 'remove', old_os_remove)
  215. self.assertTrue(os.path.join(path, lockfile) in removed)
  216. def testGenerateManifestsBasic(self):
  217. gclient_output = {
  218. 'solutions': {
  219. 'breakpad/': {
  220. 'revision': None,
  221. 'scm': None,
  222. 'url': ('https://chromium.googlesource.com/breakpad.git' +
  223. '@5f638d532312685548d5033618c8a36f73302d0a')
  224. },
  225. "src/": {
  226. 'revision': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  227. 'scm': None,
  228. 'url': 'https://chromium.googlesource.com/a/chromium/src.git'
  229. },
  230. 'src/overriden': {
  231. 'revision': None,
  232. 'scm': 'git',
  233. 'url': None,
  234. },
  235. }
  236. }
  237. out = bot_update.create_manifest(gclient_output, None)
  238. self.assertEquals(len(out['directories']), 2)
  239. self.assertEquals(
  240. out['directories']['src']['git_checkout']['revision'],
  241. 'f671d3baeb64d9dba628ad582e867cf1aebc0207')
  242. self.assertEquals(
  243. out['directories']['src']['git_checkout']['repo_url'],
  244. 'https://chromium.googlesource.com/chromium/src')
  245. self.assertEquals(
  246. out['directories']['breakpad']['git_checkout']['revision'],
  247. '5f638d532312685548d5033618c8a36f73302d0a')
  248. self.assertEquals(
  249. out['directories']['breakpad']['git_checkout']['repo_url'],
  250. 'https://chromium.googlesource.com/breakpad')
  251. self.assertNotIn('src/overridden', out['directories'])
  252. def testParsesRevisions(self):
  253. revisions = [
  254. 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  255. 'src@deadbeef',
  256. 'https://foo.googlesource.com/bar@12345',
  257. 'bar@refs/experimental/test@example.com/test',
  258. ]
  259. expected_results = {
  260. 'root': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  261. 'src': 'deadbeef',
  262. 'https://foo.googlesource.com/bar.git': '12345',
  263. 'bar': 'refs/experimental/test@example.com/test',
  264. }
  265. actual_results = bot_update.parse_revisions(revisions, 'root')
  266. self.assertEqual(expected_results, actual_results)
  267. if __name__ == '__main__':
  268. unittest.main()