test_server.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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. handler = receive_pack_handler_cls(
  51. dul_server.backend,
  52. [b"/"],
  53. Protocol(lambda x: b"", lambda x: None),
  54. )
  55. caps = handler.capabilities()
  56. self.assertNotIn(b"side-band-64k", caps)
  57. def _start_server(self, repo):
  58. backend = DictBackend({b"/": repo})
  59. dul_server = TCPGitServer(backend, b"localhost", 0, handlers=self._handlers())
  60. self._check_server(dul_server, repo)
  61. # Start server in a thread
  62. server_thread = threading.Thread(target=dul_server.serve)
  63. server_thread.daemon = True # Make thread daemon so it dies with main thread
  64. server_thread.start()
  65. # Add cleanup in the correct order - shutdown first, then close
  66. def cleanup_server():
  67. dul_server.shutdown()
  68. # Give thread a moment to exit cleanly before closing socket
  69. server_thread.join(timeout=1.0)
  70. dul_server.server_close()
  71. self.addCleanup(cleanup_server)
  72. self._server = dul_server
  73. _, port = self._server.socket.getsockname()
  74. return port
  75. @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
  76. class GitServerSideBand64kTestCase(GitServerTestCase):
  77. """Tests for client/server compatibility with side-band-64k support."""
  78. # side-band-64k in git-receive-pack was introduced in git 1.7.0.2
  79. min_git_version = (1, 7, 0, 2)
  80. def setUp(self) -> None:
  81. super().setUp()
  82. # side-band-64k is broken in the windows client.
  83. # https://github.com/msysgit/git/issues/101
  84. # Fix has landed for the 1.9.3 release.
  85. if os.name == "nt":
  86. require_git_version((1, 9, 3))
  87. def _handlers(self) -> None:
  88. return None # default handlers include side-band-64k
  89. def _check_server(self, server, repo) -> None:
  90. from dulwich.protocol import Protocol
  91. receive_pack_handler_cls = server.handlers[b"git-receive-pack"]
  92. # Create a handler instance to check capabilities
  93. handler = receive_pack_handler_cls(
  94. server.backend,
  95. [b"/"],
  96. Protocol(lambda x: b"", lambda x: None),
  97. )
  98. caps = handler.capabilities()
  99. self.assertIn(b"side-band-64k", caps)
  100. @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
  101. class GitServerSHA256TestCase(CompatTestCase):
  102. """Tests for SHA-256 repository server compatibility with git client."""
  103. protocol = "git"
  104. # SHA-256 support was introduced in git 2.29.0
  105. min_git_version = (2, 29, 0)
  106. def setUp(self) -> None:
  107. super().setUp()
  108. require_git_version(self.min_git_version)
  109. def _start_server(self, repo):
  110. backend = DictBackend({b"/": repo})
  111. dul_server = TCPGitServer(backend, b"localhost", 0)
  112. # Start server in a thread
  113. server_thread = threading.Thread(target=dul_server.serve)
  114. server_thread.daemon = True
  115. server_thread.start()
  116. # Add cleanup - shutdown first, then close
  117. def cleanup_server():
  118. dul_server.shutdown()
  119. # Give thread a moment to exit cleanly before closing socket
  120. server_thread.join(timeout=1.0)
  121. dul_server.server_close()
  122. self.addCleanup(cleanup_server)
  123. self._server = dul_server
  124. _, port = self._server.socket.getsockname()
  125. return port
  126. def url(self, port) -> str:
  127. return f"{self.protocol}://localhost:{port}/"
  128. def test_clone_sha256_repo_from_dulwich_server(self) -> None:
  129. """Test that git client can clone SHA-256 repo from dulwich server."""
  130. # Create SHA-256 repository with dulwich
  131. repo_path = tempfile.mkdtemp()
  132. self.addCleanup(shutil.rmtree, repo_path)
  133. source_repo = Repo.init(repo_path, mkdir=False, object_format="sha256")
  134. # Create test content
  135. blob = Blob.from_string(b"Test SHA-256 content from dulwich server")
  136. tree = Tree()
  137. tree.add(b"test.txt", 0o100644, blob.get_id(SHA256))
  138. commit = Commit()
  139. commit.tree = tree.get_id(SHA256)
  140. commit.author = commit.committer = b"Test User <test@example.com>"
  141. commit.commit_time = commit.author_time = 1234567890
  142. commit.commit_timezone = commit.author_timezone = 0
  143. commit.message = b"Test SHA-256 commit"
  144. # Add objects to repo
  145. source_repo.object_store.add_object(blob)
  146. source_repo.object_store.add_object(tree)
  147. source_repo.object_store.add_object(commit)
  148. # Set master ref
  149. source_repo.refs[b"refs/heads/master"] = commit.get_id(SHA256)
  150. # Start dulwich server
  151. port = self._start_server(source_repo)
  152. # Clone with git client
  153. clone_path = tempfile.mkdtemp()
  154. self.addCleanup(shutil.rmtree, clone_path)
  155. clone_dir = os.path.join(clone_path, "cloned_repo")
  156. run_git_or_fail(["clone", self.url(port), clone_dir], cwd=clone_path)
  157. # Verify cloned repo is SHA-256
  158. cloned_repo = Repo(clone_dir)
  159. self.addCleanup(cloned_repo.close)
  160. self.assertEqual(cloned_repo.object_format, SHA256)
  161. # Verify object format config
  162. output = run_git_or_fail(
  163. ["config", "--get", "extensions.objectformat"], cwd=clone_dir
  164. )
  165. self.assertEqual(output.strip(), b"sha256")
  166. # Verify commit was cloned
  167. cloned_head = cloned_repo.refs[b"refs/heads/master"]
  168. self.assertEqual(len(cloned_head), 64) # SHA-256 length
  169. self.assertEqual(cloned_head, commit.get_id(SHA256))
  170. # Verify git can read the commit
  171. log_output = run_git_or_fail(["log", "--format=%s", "-n", "1"], cwd=clone_dir)
  172. self.assertEqual(log_output.strip(), b"Test SHA-256 commit")
  173. source_repo.close()