test_git.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Erik Johnson <erik@saltstack.com>
  4. """
  5. # Import Python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import copy
  8. import logging
  9. import os
  10. import subprocess
  11. import salt.modules.git as git_mod # Don't potentially shadow GitPython
  12. # Import Salt Libs
  13. from salt.utils.versions import LooseVersion
  14. # Import Salt Testing Libs
  15. from tests.support.mixins import LoaderModuleMockMixin
  16. from tests.support.mock import MagicMock, Mock, patch
  17. from tests.support.unit import TestCase
  18. log = logging.getLogger(__name__)
  19. WORKTREE_ROOT = "/tmp/salt-tests-tmpdir/main"
  20. WORKTREE_INFO = {
  21. WORKTREE_ROOT: {
  22. "HEAD": "119f025073875a938f2456f5ffd7d04e79e5a427",
  23. "branch": "refs/heads/master",
  24. "stale": False,
  25. },
  26. "/tmp/salt-tests-tmpdir/worktree1": {
  27. "HEAD": "d8d19cf75d7cc3bdc598dc2d472881d26b51a6bf",
  28. "branch": "refs/heads/worktree1",
  29. "stale": False,
  30. },
  31. "/tmp/salt-tests-tmpdir/worktree2": {
  32. "HEAD": "56332ca504aa8b37bb62b54272d52b1d6d832629",
  33. "branch": "refs/heads/worktree2",
  34. "stale": True,
  35. },
  36. "/tmp/salt-tests-tmpdir/worktree3": {
  37. "HEAD": "e148ea2d521313579f661373fbb93a48a5a6d40d",
  38. "branch": "detached",
  39. "tags": ["v1.1"],
  40. "stale": False,
  41. },
  42. "/tmp/salt-tests-tmpdir/worktree4": {
  43. "HEAD": "6bbac64d3ad5582b3147088a708952df185db020",
  44. "branch": "detached",
  45. "stale": True,
  46. },
  47. }
  48. def _git_version():
  49. git_version = subprocess.Popen(
  50. ["git", "--version"],
  51. shell=False,
  52. close_fds=True,
  53. stdout=subprocess.PIPE,
  54. stderr=subprocess.PIPE,
  55. ).communicate()[0]
  56. if not git_version:
  57. log.error("Git not installed")
  58. return False
  59. log.debug("Detected git version %s", git_version)
  60. return LooseVersion(git_version.split()[-1])
  61. class GitTestCase(TestCase, LoaderModuleMockMixin):
  62. """
  63. Test cases for salt.modules.git
  64. """
  65. def setup_loader_modules(self):
  66. return {
  67. git_mod: {"__utils__": {"ssh.key_is_encrypted": Mock(return_value=False)}}
  68. }
  69. def test_list_worktrees(self):
  70. """
  71. This tests git.list_worktrees
  72. """
  73. def _build_worktree_output(path):
  74. """
  75. Build 'git worktree list' output for a given path
  76. """
  77. return "worktree {0}\nHEAD {1}\n{2}\n".format(
  78. path,
  79. WORKTREE_INFO[path]["HEAD"],
  80. "branch {0}".format(WORKTREE_INFO[path]["branch"])
  81. if WORKTREE_INFO[path]["branch"] != "detached"
  82. else "detached",
  83. )
  84. # Build dict for _cmd_run_side_effect below. Start with the output from
  85. # 'git worktree list'.
  86. _cmd_run_values = {
  87. "git worktree list --porcelain": "\n".join(
  88. [_build_worktree_output(x) for x in WORKTREE_INFO]
  89. ),
  90. "git --version": "git version 2.7.0",
  91. }
  92. # Add 'git tag --points-at' output for detached HEAD worktrees with
  93. # tags pointing at HEAD.
  94. for path in WORKTREE_INFO:
  95. if WORKTREE_INFO[path]["branch"] != "detached":
  96. continue
  97. key = "git tag --points-at " + WORKTREE_INFO[path]["HEAD"]
  98. _cmd_run_values[key] = "\n".join(WORKTREE_INFO[path].get("tags", []))
  99. def _cmd_run_side_effect(key, **kwargs):
  100. # Not using dict.get() here because we want to know if
  101. # _cmd_run_values doesn't account for all uses of cmd.run_all.
  102. return {
  103. "stdout": _cmd_run_values[" ".join(key)],
  104. "stderr": "",
  105. "retcode": 0,
  106. "pid": 12345,
  107. }
  108. def _isdir_side_effect(key):
  109. # os.path.isdir() would return True on a non-stale worktree
  110. return not WORKTREE_INFO[key].get("stale", False)
  111. # Build return dict for comparison
  112. worktree_ret = copy.deepcopy(WORKTREE_INFO)
  113. for key in worktree_ret:
  114. ptr = worktree_ret.get(key)
  115. ptr["detached"] = ptr["branch"] == "detached"
  116. ptr["branch"] = (
  117. None if ptr["detached"] else ptr["branch"].replace("refs/heads/", "", 1)
  118. )
  119. cmd_run_mock = MagicMock(side_effect=_cmd_run_side_effect)
  120. isdir_mock = MagicMock(side_effect=_isdir_side_effect)
  121. with patch.dict(git_mod.__salt__, {"cmd.run_all": cmd_run_mock}):
  122. with patch.object(os.path, "isdir", isdir_mock):
  123. # Test all=True. Include all return data.
  124. self.maxDiff = None
  125. self.assertEqual(
  126. git_mod.list_worktrees(WORKTREE_ROOT, all=True, stale=False),
  127. worktree_ret,
  128. )
  129. # Test all=False and stale=False. Exclude stale worktrees from
  130. # return data.
  131. self.assertEqual(
  132. git_mod.list_worktrees(WORKTREE_ROOT, all=False, stale=False),
  133. dict(
  134. [
  135. (x, worktree_ret[x])
  136. for x in WORKTREE_INFO
  137. if not WORKTREE_INFO[x].get("stale", False)
  138. ]
  139. ),
  140. )
  141. # Test stale=True. Exclude non-stale worktrees from return
  142. # data.
  143. self.assertEqual(
  144. git_mod.list_worktrees(WORKTREE_ROOT, all=False, stale=True),
  145. dict(
  146. [
  147. (x, worktree_ret[x])
  148. for x in WORKTREE_INFO
  149. if WORKTREE_INFO[x].get("stale", False)
  150. ]
  151. ),
  152. )
  153. def test__git_run_tmp_wrapper(self):
  154. """
  155. When an identity file is specified, make sure we don't attempt to
  156. remove a temp wrapper that wasn't created. Windows doesn't use temp
  157. wrappers, and *NIX won't unless no username was specified and the path
  158. is not executable.
  159. """
  160. file_remove_mock = Mock()
  161. mock_true = Mock(return_value=True)
  162. mock_false = Mock(return_value=False)
  163. cmd_mock = MagicMock(return_value={"retcode": 0, "stdout": "", "stderr": ""})
  164. with patch.dict(
  165. git_mod.__salt__,
  166. {
  167. "file.file_exists": mock_true,
  168. "file.remove": file_remove_mock,
  169. "cmd.run_all": cmd_mock,
  170. "ssh.key_is_encrypted": mock_false,
  171. },
  172. ):
  173. # Non-windows
  174. with patch("salt.utils.platform.is_windows", mock_false), patch.object(
  175. git_mod, "_path_is_executable_others", mock_true
  176. ):
  177. # Command doesn't really matter here since we're mocking
  178. git_mod._git_run(
  179. ["git", "rev-parse", "HEAD"],
  180. cwd="/some/path",
  181. user=None,
  182. identity="/root/.ssh/id_rsa",
  183. )
  184. file_remove_mock.assert_not_called()
  185. file_remove_mock.reset_mock()
  186. with patch("salt.utils.platform.is_windows", mock_true), patch.object(
  187. git_mod, "_find_ssh_exe", MagicMock(return_value=r"C:\Git\ssh.exe")
  188. ):
  189. # Command doesn't really matter here since we're mocking
  190. git_mod._git_run(
  191. ["git", "rev-parse", "HEAD"],
  192. cwd=r"C:\some\path",
  193. user=None,
  194. identity=r"C:\ssh\id_rsa",
  195. )
  196. file_remove_mock.assert_not_called()