test_server.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # test_server.py -- Compatibility tests for git server.
  2. # Copyright (C) 2010 Google, Inc.
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Compatibility tests between Dulwich and the cgit server.
  22. Warning: these tests should be fairly stable, but when writing/debugging new
  23. tests, deadlocks may freeze the test process such that it cannot be
  24. Ctrl-C'ed. On POSIX systems, you can kill the tests with Ctrl-Z, "kill %".
  25. """
  26. import os
  27. import shutil
  28. import sys
  29. import tempfile
  30. import threading
  31. from dulwich.object_format import SHA256
  32. from dulwich.objects import Blob, Commit, Tree
  33. from dulwich.repo import Repo
  34. from dulwich.server import DictBackend, TCPGitServer
  35. from .. import skipIf
  36. from .server_utils import NoSideBand64kReceivePackHandler, ServerTests
  37. from .utils import CompatTestCase, require_git_version, run_git_or_fail
  38. @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
  39. class GitServerTestCase(ServerTests, CompatTestCase):
  40. """Tests for client/server compatibility.
  41. This server test case does not use side-band-64k in git-receive-pack.
  42. """
  43. protocol = "git"
  44. def _handlers(self):
  45. return {b"git-receive-pack": NoSideBand64kReceivePackHandler}
  46. def _check_server(self, dul_server, repo) -> None:
  47. from dulwich.protocol import Protocol
  48. receive_pack_handler_cls = dul_server.handlers[b"git-receive-pack"]
  49. # Create a handler instance to check capabilities
  50. proto = Protocol(lambda x: b"", lambda x: None)
  51. try:
  52. handler = receive_pack_handler_cls(
  53. dul_server.backend,
  54. [b"/"],
  55. proto,
  56. )
  57. caps = handler.capabilities()
  58. self.assertNotIn(b"side-band-64k", caps)
  59. finally:
  60. proto.close()
  61. def _start_server(self, repo):
  62. backend = DictBackend({b"/": repo})
  63. dul_server = TCPGitServer(backend, b"localhost", 0, handlers=self._handlers())
  64. self._check_server(dul_server, repo)
  65. # Start server in a thread
  66. server_thread = threading.Thread(target=dul_server.serve)
  67. server_thread.daemon = True # Make thread daemon so it dies with main thread
  68. server_thread.start()
  69. # Add cleanup in the correct order - shutdown first, then close
  70. def cleanup_server():
  71. dul_server.shutdown()
  72. # Give thread a moment to exit cleanly before closing socket
  73. server_thread.join(timeout=1.0)
  74. dul_server.server_close()
  75. self.addCleanup(cleanup_server)
  76. self._server = dul_server
  77. _, port = self._server.socket.getsockname()
  78. return port
  79. @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
  80. class GitServerSideBand64kTestCase(GitServerTestCase):
  81. """Tests for client/server compatibility with side-band-64k support."""
  82. # side-band-64k in git-receive-pack was introduced in git 1.7.0.2
  83. min_git_version = (1, 7, 0, 2)
  84. def setUp(self) -> None:
  85. super().setUp()
  86. # side-band-64k is broken in the windows client.
  87. # https://github.com/msysgit/git/issues/101
  88. # Fix has landed for the 1.9.3 release.
  89. if os.name == "nt":
  90. require_git_version((1, 9, 3))
  91. def _handlers(self) -> None:
  92. return None # default handlers include side-band-64k
  93. def _check_server(self, server, repo) -> None:
  94. from dulwich.protocol import Protocol
  95. receive_pack_handler_cls = server.handlers[b"git-receive-pack"]
  96. # Create a handler instance to check capabilities
  97. proto = Protocol(lambda x: b"", lambda x: None)
  98. try:
  99. handler = receive_pack_handler_cls(
  100. server.backend,
  101. [b"/"],
  102. proto,
  103. )
  104. caps = handler.capabilities()
  105. self.assertIn(b"side-band-64k", caps)
  106. finally:
  107. proto.close()
  108. @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
  109. class GitServerSHA256TestCase(CompatTestCase):
  110. """Tests for SHA-256 repository server compatibility with git client."""
  111. protocol = "git"
  112. # SHA-256 support was introduced in git 2.29.0
  113. min_git_version = (2, 29, 0)
  114. def setUp(self) -> None:
  115. super().setUp()
  116. require_git_version(self.min_git_version)
  117. def _start_server(self, repo):
  118. backend = DictBackend({b"/": repo})
  119. dul_server = TCPGitServer(backend, b"localhost", 0)
  120. # Start server in a thread
  121. server_thread = threading.Thread(target=dul_server.serve)
  122. server_thread.daemon = True
  123. server_thread.start()
  124. # Add cleanup - shutdown first, then close
  125. def cleanup_server():
  126. dul_server.shutdown()
  127. # Give thread a moment to exit cleanly before closing socket
  128. server_thread.join(timeout=1.0)
  129. dul_server.server_close()
  130. self.addCleanup(cleanup_server)
  131. self._server = dul_server
  132. _, port = self._server.socket.getsockname()
  133. return port
  134. def url(self, port) -> str:
  135. return f"{self.protocol}://localhost:{port}/"
  136. def test_clone_sha256_repo_from_dulwich_server(self) -> None:
  137. """Test that git client can clone SHA-256 repo from dulwich server."""
  138. # Create SHA-256 repository with dulwich
  139. repo_path = tempfile.mkdtemp()
  140. self.addCleanup(shutil.rmtree, repo_path)
  141. source_repo = Repo.init(repo_path, mkdir=False, object_format="sha256")
  142. self.addCleanup(source_repo.close)
  143. # Create test content
  144. blob = Blob.from_string(b"Test SHA-256 content from dulwich server")
  145. tree = Tree()
  146. tree.add(b"test.txt", 0o100644, blob.get_id(SHA256))
  147. commit = Commit()
  148. commit.tree = tree.get_id(SHA256)
  149. commit.author = commit.committer = b"Test User <test@example.com>"
  150. commit.commit_time = commit.author_time = 1234567890
  151. commit.commit_timezone = commit.author_timezone = 0
  152. commit.message = b"Test SHA-256 commit"
  153. # Add objects to repo
  154. source_repo.object_store.add_object(blob)
  155. source_repo.object_store.add_object(tree)
  156. source_repo.object_store.add_object(commit)
  157. # Set master ref
  158. source_repo.refs[b"refs/heads/master"] = commit.get_id(SHA256)
  159. # Start dulwich server
  160. port = self._start_server(source_repo)
  161. # Clone with git client
  162. clone_path = tempfile.mkdtemp()
  163. self.addCleanup(shutil.rmtree, clone_path)
  164. clone_dir = os.path.join(clone_path, "cloned_repo")
  165. run_git_or_fail(["clone", self.url(port), clone_dir], cwd=clone_path)
  166. # Verify cloned repo is SHA-256
  167. cloned_repo = Repo(clone_dir)
  168. self.addCleanup(cloned_repo.close)
  169. self.assertEqual(cloned_repo.object_format, SHA256)
  170. # Verify object format config
  171. output = run_git_or_fail(
  172. ["config", "--get", "extensions.objectformat"], cwd=clone_dir
  173. )
  174. self.assertEqual(output.strip(), b"sha256")
  175. # Verify commit was cloned
  176. cloned_head = cloned_repo.refs[b"refs/heads/master"]
  177. self.assertEqual(len(cloned_head), 64) # SHA-256 length
  178. self.assertEqual(cloned_head, commit.get_id(SHA256))
  179. # Verify git can read the commit
  180. log_output = run_git_or_fail(["log", "--format=%s", "-n", "1"], cwd=clone_dir)
  181. self.assertEqual(log_output.strip(), b"Test SHA-256 commit")