bot_update_coverage_test.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. 'recipe_modules', 'bot_update', 'resources'))
  15. import bot_update
  16. DEFAULT_PARAMS = {
  17. 'solutions': [{
  18. 'name': 'somename',
  19. 'url': 'https://fake.com'
  20. }],
  21. 'revisions': [],
  22. 'first_sln': 'somename',
  23. 'target_os': None,
  24. 'target_os_only': None,
  25. 'patch_root': None,
  26. 'issue': None,
  27. 'patchset': None,
  28. 'rietveld_server': None,
  29. 'gerrit_repo': None,
  30. 'gerrit_ref': None,
  31. 'gerrit_rebase_patch_ref': None,
  32. 'revision_mapping': {},
  33. 'apply_issue_email_file': None,
  34. 'apply_issue_key_file': None,
  35. 'apply_issue_oauth2_file': None,
  36. 'shallow': False,
  37. 'refs': [],
  38. 'git_cache_dir': '',
  39. 'gerrit_reset': None,
  40. }
  41. class MockedPopen(object):
  42. """A fake instance of a called subprocess.
  43. This is meant to be used in conjunction with MockedCall.
  44. """
  45. def __init__(self, args=None, kwargs=None):
  46. self.args = args or []
  47. self.kwargs = kwargs or {}
  48. self.return_value = None
  49. self.fails = False
  50. def returns(self, rv):
  51. """Set the return value when this popen is called.
  52. rv can be a string, or a callable (eg function).
  53. """
  54. self.return_value = rv
  55. return self
  56. def check(self, args, kwargs):
  57. """Check to see if the given args/kwargs call match this instance.
  58. This does a partial match, so that a call to "git clone foo" will match
  59. this instance if this instance was recorded as "git clone"
  60. """
  61. if any(input_arg != expected_arg
  62. for (input_arg, expected_arg) in zip(args, self.args)):
  63. return False
  64. return self.return_value
  65. def __call__(self, args, kwargs):
  66. """Actually call this popen instance."""
  67. if hasattr(self.return_value, '__call__'):
  68. return self.return_value(*args, **kwargs)
  69. return self.return_value
  70. class MockedCall(object):
  71. """A fake instance of bot_update.call().
  72. This object is pre-seeded with "answers" in self.expectations. The type
  73. is a MockedPopen object, or any object with a __call__() and check() method.
  74. The check() method is used to check to see if the correct popen object is
  75. chosen (can be a partial match, eg a "git clone" popen module would match
  76. a "git clone foo" call).
  77. By default, if no answers have been pre-seeded, the call() returns successful
  78. with an empty string.
  79. """
  80. def __init__(self, fake_filesystem):
  81. self.expectations = []
  82. self.records = []
  83. def expect(self, args=None, kwargs=None):
  84. args = args or []
  85. kwargs = kwargs or {}
  86. popen = MockedPopen(args, kwargs)
  87. self.expectations.append(popen)
  88. return popen
  89. def __call__(self, *args, **kwargs):
  90. self.records.append((args, kwargs))
  91. for popen in self.expectations:
  92. if popen.check(args, kwargs):
  93. self.expectations.remove(popen)
  94. return popen(args, kwargs)
  95. return ''
  96. class MockedGclientSync():
  97. """A class producing a callable instance of gclient sync.
  98. Because for bot_update, gclient sync also emits an output json file, we need
  99. a callable object that can understand where the output json file is going, and
  100. emit a (albite) fake file for bot_update to consume.
  101. """
  102. def __init__(self, fake_filesystem):
  103. self.output = {}
  104. self.fake_filesystem = fake_filesystem
  105. def __call__(self, *args, **_):
  106. output_json_index = args.index('--output-json') + 1
  107. with self.fake_filesystem.open(args[output_json_index], 'w') as f:
  108. json.dump(self.output, f)
  109. class FakeFile():
  110. def __init__(self):
  111. self.contents = ''
  112. def write(self, buf):
  113. self.contents += buf
  114. def read(self):
  115. return self.contents
  116. def __enter__(self):
  117. return self
  118. def __exit__(self, _, __, ___):
  119. pass
  120. class FakeFilesystem():
  121. def __init__(self):
  122. self.files = {}
  123. def open(self, target, mode='r', encoding=None):
  124. if 'w' in mode:
  125. self.files[target] = FakeFile()
  126. return self.files[target]
  127. return self.files[target]
  128. def fake_git(*args, **kwargs):
  129. return bot_update.call('git', *args, **kwargs)
  130. class BotUpdateUnittests(unittest.TestCase):
  131. def setUp(self):
  132. sys.platform = 'linux2' # For consistency, ya know?
  133. self.filesystem = FakeFilesystem()
  134. self.call = MockedCall(self.filesystem)
  135. self.gclient = MockedGclientSync(self.filesystem)
  136. self.call.expect(
  137. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  138. ).returns(self.gclient)
  139. self.old_call = getattr(bot_update, 'call')
  140. self.params = copy.deepcopy(DEFAULT_PARAMS)
  141. setattr(bot_update, 'call', self.call)
  142. setattr(bot_update, 'git', fake_git)
  143. self.old_os_cwd = os.getcwd
  144. setattr(os, 'getcwd', lambda: '/b/build/slave/foo/build')
  145. setattr(bot_update, 'open', self.filesystem.open)
  146. self.old_codecs_open = codecs.open
  147. setattr(codecs, 'open', self.filesystem.open)
  148. def tearDown(self):
  149. setattr(bot_update, 'call', self.old_call)
  150. setattr(os, 'getcwd', self.old_os_cwd)
  151. delattr(bot_update, 'open')
  152. setattr(codecs, 'open', self.old_codecs_open)
  153. def overrideSetupForWindows(self):
  154. sys.platform = 'win'
  155. self.call.expect(
  156. (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync')
  157. ).returns(self.gclient)
  158. def testBasic(self):
  159. bot_update.ensure_checkout(**self.params)
  160. return self.call.records
  161. def testBasicShallow(self):
  162. self.params['shallow'] = True
  163. bot_update.ensure_checkout(**self.params)
  164. return self.call.records
  165. def testBreakLocks(self):
  166. self.overrideSetupForWindows()
  167. bot_update.ensure_checkout(**self.params)
  168. gclient_sync_cmd = None
  169. for record in self.call.records:
  170. args = record[0]
  171. if args[:4] == (sys.executable, '-u', bot_update.GCLIENT_PATH, 'sync'):
  172. gclient_sync_cmd = args
  173. self.assertTrue('--break_repo_locks' in gclient_sync_cmd)
  174. def testGitCheckoutBreaksLocks(self):
  175. self.overrideSetupForWindows()
  176. path = '/b/build/slave/foo/build/.git'
  177. lockfile = 'index.lock'
  178. removed = []
  179. old_os_walk = os.walk
  180. old_os_remove = os.remove
  181. setattr(os, 'walk', lambda _: [(path, None, [lockfile])])
  182. setattr(os, 'remove', removed.append)
  183. bot_update.ensure_checkout(**self.params)
  184. setattr(os, 'walk', old_os_walk)
  185. setattr(os, 'remove', old_os_remove)
  186. self.assertTrue(os.path.join(path, lockfile) in removed)
  187. if __name__ == '__main__':
  188. unittest.main()