bot_update_coverage_test.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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. 'no_fetch_tags': False,
  122. 'refs': [],
  123. 'git_cache_dir': '',
  124. 'cleanup_dir': None,
  125. 'gerrit_reset': None,
  126. 'disable_syntax_validation': False,
  127. 'enforce_fetch': False,
  128. }
  129. def setUp(self):
  130. sys.platform = 'linux2' # For consistency, ya know?
  131. self.filesystem = FakeFilesystem()
  132. self.call = MockedCall(self.filesystem)
  133. self.gclient = MockedGclientSync(self.filesystem)
  134. self.call.expect(
  135. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  136. ).returns(self.gclient)
  137. self.old_call = getattr(bot_update, 'call')
  138. self.params = copy.deepcopy(self.DEFAULT_PARAMS)
  139. setattr(bot_update, 'call', self.call)
  140. setattr(bot_update, 'git', fake_git)
  141. self.old_os_cwd = os.getcwd
  142. setattr(os, 'getcwd', lambda: '/b/build/foo/build')
  143. setattr(bot_update, 'open', self.filesystem.open)
  144. self.old_codecs_open = codecs.open
  145. setattr(codecs, 'open', self.filesystem.open)
  146. def tearDown(self):
  147. setattr(bot_update, 'call', self.old_call)
  148. setattr(os, 'getcwd', self.old_os_cwd)
  149. delattr(bot_update, 'open')
  150. setattr(codecs, 'open', self.old_codecs_open)
  151. def overrideSetupForWindows(self):
  152. sys.platform = 'win'
  153. self.call.expect(
  154. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  155. ).returns(self.gclient)
  156. def testBasic(self):
  157. bot_update.ensure_checkout(**self.params)
  158. return self.call.records
  159. def testBasicCachepackOffloading(self):
  160. os.environ['PACKFILE_OFFLOADING'] = '1'
  161. bot_update.ensure_checkout(**self.params)
  162. os.environ.pop('PACKFILE_OFFLOADING')
  163. return self.call.records
  164. def testBasicRevision(self):
  165. self.params['revisions'] = {
  166. 'src': 'HEAD', 'src/v8': 'deadbeef', 'somename': 'DNE'}
  167. bot_update.ensure_checkout(**self.params)
  168. args = self.gclient.records[0]
  169. idx_first_revision = args.index('--revision')
  170. idx_second_revision = args.index(
  171. '--revision', idx_first_revision+1)
  172. idx_third_revision = args.index('--revision', idx_second_revision+1)
  173. self.assertEqual(args[idx_first_revision+1], 'somename@unmanaged')
  174. self.assertEqual(args[idx_second_revision+1], 'src@origin/master')
  175. self.assertEqual(args[idx_third_revision+1], 'src/v8@deadbeef')
  176. return self.call.records
  177. def testTagsByDefault(self):
  178. bot_update.ensure_checkout(**self.params)
  179. found = False
  180. for record in self.call.records:
  181. args = record[0]
  182. if args[:3] == ('git', 'cache', 'populate'):
  183. self.assertFalse('--no-fetch-tags' in args)
  184. found = True
  185. self.assertTrue(found)
  186. return self.call.records
  187. def testNoTags(self):
  188. params = self.params
  189. params['no_fetch_tags'] = True
  190. bot_update.ensure_checkout(**params)
  191. found = False
  192. for record in self.call.records:
  193. args = record[0]
  194. if args[:3] == ('git', 'cache', 'populate'):
  195. self.assertTrue('--no-fetch-tags' in args)
  196. found = True
  197. self.assertTrue(found)
  198. return self.call.records
  199. def testApplyPatchOnGclient(self):
  200. ref = 'refs/changes/12/345/6'
  201. repo = 'https://chromium.googlesource.com/v8/v8'
  202. self.params['patch_refs'] = ['%s@%s' % (repo, ref)]
  203. bot_update.ensure_checkout(**self.params)
  204. args = self.gclient.records[0]
  205. idx = args.index('--patch-ref')
  206. self.assertEqual(args[idx+1], self.params['patch_refs'][0])
  207. self.assertNotIn('--patch-ref', args[idx+1:])
  208. # Assert we're not patching in bot_update.py
  209. for record in self.call.records:
  210. self.assertNotIn('git fetch ' + repo,
  211. ' '.join(record[0]))
  212. def testPatchRefs(self):
  213. self.params['patch_refs'] = [
  214. 'https://chromium.googlesource.com/chromium/src@refs/changes/12/345/6',
  215. 'https://chromium.googlesource.com/v8/v8@refs/changes/1/234/56']
  216. bot_update.ensure_checkout(**self.params)
  217. args = self.gclient.records[0]
  218. patch_refs = set(
  219. args[i+1] for i in xrange(len(args))
  220. if args[i] == '--patch-ref' and i+1 < len(args))
  221. self.assertIn(self.params['patch_refs'][0], patch_refs)
  222. self.assertIn(self.params['patch_refs'][1], patch_refs)
  223. def testBreakLocks(self):
  224. self.overrideSetupForWindows()
  225. bot_update.ensure_checkout(**self.params)
  226. gclient_sync_cmd = None
  227. for record in self.call.records:
  228. args = record[0]
  229. if args[:4] == (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync'):
  230. gclient_sync_cmd = args
  231. self.assertTrue('--break_repo_locks' in gclient_sync_cmd)
  232. def testGitCheckoutBreaksLocks(self):
  233. self.overrideSetupForWindows()
  234. path = '/b/build/foo/build/.git'
  235. lockfile = 'index.lock'
  236. removed = []
  237. old_os_walk = os.walk
  238. old_os_remove = os.remove
  239. setattr(os, 'walk', lambda _: [(path, None, [lockfile])])
  240. setattr(os, 'remove', removed.append)
  241. bot_update.ensure_checkout(**self.params)
  242. setattr(os, 'walk', old_os_walk)
  243. setattr(os, 'remove', old_os_remove)
  244. self.assertTrue(os.path.join(path, lockfile) in removed)
  245. def testGenerateManifestsBasic(self):
  246. gclient_output = {
  247. 'solutions': {
  248. 'breakpad/': {
  249. 'revision': None,
  250. 'scm': None,
  251. 'url': ('https://chromium.googlesource.com/breakpad.git' +
  252. '@5f638d532312685548d5033618c8a36f73302d0a')
  253. },
  254. "src/": {
  255. 'revision': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  256. 'scm': None,
  257. 'url': 'https://chromium.googlesource.com/a/chromium/src.git'
  258. },
  259. 'src/overriden': {
  260. 'revision': None,
  261. 'scm': 'git',
  262. 'url': None,
  263. },
  264. }
  265. }
  266. out = bot_update.create_manifest(gclient_output, None)
  267. self.assertEqual(len(out['directories']), 2)
  268. self.assertEqual(
  269. out['directories']['src']['git_checkout']['revision'],
  270. 'f671d3baeb64d9dba628ad582e867cf1aebc0207')
  271. self.assertEqual(
  272. out['directories']['src']['git_checkout']['repo_url'],
  273. 'https://chromium.googlesource.com/chromium/src')
  274. self.assertEqual(
  275. out['directories']['breakpad']['git_checkout']['revision'],
  276. '5f638d532312685548d5033618c8a36f73302d0a')
  277. self.assertEqual(
  278. out['directories']['breakpad']['git_checkout']['repo_url'],
  279. 'https://chromium.googlesource.com/breakpad')
  280. self.assertNotIn('src/overridden', out['directories'])
  281. def testParsesRevisions(self):
  282. revisions = [
  283. 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  284. 'src@deadbeef',
  285. 'https://foo.googlesource.com/bar@12345',
  286. 'bar@refs/experimental/test@example.com/test',
  287. ]
  288. expected_results = {
  289. 'root': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  290. 'src': 'deadbeef',
  291. 'https://foo.googlesource.com/bar.git': '12345',
  292. 'bar': 'refs/experimental/test@example.com/test',
  293. }
  294. actual_results = bot_update.parse_revisions(revisions, 'root')
  295. self.assertEqual(expected_results, actual_results)
  296. if __name__ == '__main__':
  297. unittest.main()