bot_update_coverage_test.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 testBasicRevision(self):
  160. self.params['revisions'] = {
  161. 'src': 'HEAD', 'src/v8': 'deadbeef', 'somename': 'DNE'}
  162. bot_update.ensure_checkout(**self.params)
  163. args = self.gclient.records[0]
  164. idx_first_revision = args.index('--revision')
  165. idx_second_revision = args.index(
  166. '--revision', idx_first_revision+1)
  167. idx_third_revision = args.index('--revision', idx_second_revision+1)
  168. self.assertEqual(args[idx_first_revision+1], 'somename@unmanaged')
  169. self.assertEqual(args[idx_second_revision+1], 'src@origin/master')
  170. self.assertEqual(args[idx_third_revision+1], 'src/v8@deadbeef')
  171. return self.call.records
  172. def testTagsByDefault(self):
  173. bot_update.ensure_checkout(**self.params)
  174. found = False
  175. for record in self.call.records:
  176. args = record[0]
  177. if args[:3] == ('git', 'cache', 'populate'):
  178. self.assertFalse('--no-fetch-tags' in args)
  179. found = True
  180. self.assertTrue(found)
  181. return self.call.records
  182. def testNoTags(self):
  183. params = self.params
  184. params['no_fetch_tags'] = True
  185. bot_update.ensure_checkout(**params)
  186. found = False
  187. for record in self.call.records:
  188. args = record[0]
  189. if args[:3] == ('git', 'cache', 'populate'):
  190. self.assertTrue('--no-fetch-tags' in args)
  191. found = True
  192. self.assertTrue(found)
  193. return self.call.records
  194. def testApplyPatchOnGclient(self):
  195. ref = 'refs/changes/12/345/6'
  196. repo = 'https://chromium.googlesource.com/v8/v8'
  197. self.params['patch_refs'] = ['%s@%s' % (repo, ref)]
  198. bot_update.ensure_checkout(**self.params)
  199. args = self.gclient.records[0]
  200. idx = args.index('--patch-ref')
  201. self.assertEqual(args[idx+1], self.params['patch_refs'][0])
  202. self.assertNotIn('--patch-ref', args[idx+1:])
  203. # Assert we're not patching in bot_update.py
  204. for record in self.call.records:
  205. self.assertNotIn('git fetch ' + repo,
  206. ' '.join(record[0]))
  207. def testPatchRefs(self):
  208. self.params['patch_refs'] = [
  209. 'https://chromium.googlesource.com/chromium/src@refs/changes/12/345/6',
  210. 'https://chromium.googlesource.com/v8/v8@refs/changes/1/234/56']
  211. bot_update.ensure_checkout(**self.params)
  212. args = self.gclient.records[0]
  213. patch_refs = set(
  214. args[i+1] for i in xrange(len(args))
  215. if args[i] == '--patch-ref' and i+1 < len(args))
  216. self.assertIn(self.params['patch_refs'][0], patch_refs)
  217. self.assertIn(self.params['patch_refs'][1], patch_refs)
  218. def testBreakLocks(self):
  219. self.overrideSetupForWindows()
  220. bot_update.ensure_checkout(**self.params)
  221. gclient_sync_cmd = None
  222. for record in self.call.records:
  223. args = record[0]
  224. if args[:4] == (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync'):
  225. gclient_sync_cmd = args
  226. self.assertTrue('--break_repo_locks' in gclient_sync_cmd)
  227. def testGitCheckoutBreaksLocks(self):
  228. self.overrideSetupForWindows()
  229. path = '/b/build/foo/build/.git'
  230. lockfile = 'index.lock'
  231. removed = []
  232. old_os_walk = os.walk
  233. old_os_remove = os.remove
  234. setattr(os, 'walk', lambda _: [(path, None, [lockfile])])
  235. setattr(os, 'remove', removed.append)
  236. bot_update.ensure_checkout(**self.params)
  237. setattr(os, 'walk', old_os_walk)
  238. setattr(os, 'remove', old_os_remove)
  239. self.assertTrue(os.path.join(path, lockfile) in removed)
  240. def testGenerateManifestsBasic(self):
  241. gclient_output = {
  242. 'solutions': {
  243. 'breakpad/': {
  244. 'revision': None,
  245. 'scm': None,
  246. 'url': ('https://chromium.googlesource.com/breakpad.git' +
  247. '@5f638d532312685548d5033618c8a36f73302d0a')
  248. },
  249. "src/": {
  250. 'revision': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  251. 'scm': None,
  252. 'url': 'https://chromium.googlesource.com/a/chromium/src.git'
  253. },
  254. 'src/overriden': {
  255. 'revision': None,
  256. 'scm': 'git',
  257. 'url': None,
  258. },
  259. }
  260. }
  261. out = bot_update.create_manifest(gclient_output, None)
  262. self.assertEqual(len(out['directories']), 2)
  263. self.assertEqual(
  264. out['directories']['src']['git_checkout']['revision'],
  265. 'f671d3baeb64d9dba628ad582e867cf1aebc0207')
  266. self.assertEqual(
  267. out['directories']['src']['git_checkout']['repo_url'],
  268. 'https://chromium.googlesource.com/chromium/src')
  269. self.assertEqual(
  270. out['directories']['breakpad']['git_checkout']['revision'],
  271. '5f638d532312685548d5033618c8a36f73302d0a')
  272. self.assertEqual(
  273. out['directories']['breakpad']['git_checkout']['repo_url'],
  274. 'https://chromium.googlesource.com/breakpad')
  275. self.assertNotIn('src/overridden', out['directories'])
  276. def testParsesRevisions(self):
  277. revisions = [
  278. 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  279. 'src@deadbeef',
  280. 'https://foo.googlesource.com/bar@12345',
  281. 'bar@refs/experimental/test@example.com/test',
  282. ]
  283. expected_results = {
  284. 'root': 'f671d3baeb64d9dba628ad582e867cf1aebc0207',
  285. 'src': 'deadbeef',
  286. 'https://foo.googlesource.com/bar.git': '12345',
  287. 'bar': 'refs/experimental/test@example.com/test',
  288. }
  289. actual_results = bot_update.parse_revisions(revisions, 'root')
  290. self.assertEqual(expected_results, actual_results)
  291. if __name__ == '__main__':
  292. unittest.main()