test_lfs_integration.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. # test_lfs_integration.py -- Integration tests for LFS with filters
  2. # Copyright (C) 2024 Jelmer Vernooij <jelmer@jelmer.uk>
  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. """Integration tests for LFS with the filter system."""
  22. import shutil
  23. import tempfile
  24. from dulwich.config import ConfigDict
  25. from dulwich.filters import FilterBlobNormalizer, FilterRegistry
  26. from dulwich.lfs import LFSFilterDriver, LFSStore
  27. from dulwich.objects import Blob
  28. from . import TestCase
  29. class LFSFilterIntegrationTests(TestCase):
  30. def setUp(self) -> None:
  31. super().setUp()
  32. # Suppress LFS warnings during these integration tests
  33. import logging
  34. self._old_level = logging.getLogger("dulwich.lfs").level
  35. logging.getLogger("dulwich.lfs").setLevel(logging.ERROR)
  36. # Create temporary directory for LFS store
  37. self.test_dir = tempfile.mkdtemp()
  38. self.addCleanup(shutil.rmtree, self.test_dir)
  39. # Set up LFS store and filter
  40. self.lfs_store = LFSStore.create(self.test_dir)
  41. self.lfs_filter = LFSFilterDriver(self.lfs_store)
  42. # Set up filter registry and normalizer
  43. self.config = ConfigDict()
  44. self.registry = FilterRegistry(self.config)
  45. self.registry.register_driver("lfs", self.lfs_filter)
  46. # Set up gitattributes to use LFS for .bin files
  47. from dulwich.attrs import GitAttributes, Pattern
  48. patterns = [
  49. (Pattern(b"*.bin"), {b"filter": b"lfs"}),
  50. ]
  51. self.gitattributes = GitAttributes(patterns)
  52. from dulwich.filters import FilterContext
  53. filter_context = FilterContext(self.registry)
  54. self.normalizer = FilterBlobNormalizer(
  55. self.config, self.gitattributes, filter_context=filter_context
  56. )
  57. def tearDown(self) -> None:
  58. # Restore original logging level
  59. import logging
  60. logging.getLogger("dulwich.lfs").setLevel(self._old_level)
  61. super().tearDown()
  62. def test_lfs_round_trip(self) -> None:
  63. """Test complete LFS round trip through filter normalizer."""
  64. # Create a blob with binary content
  65. original_content = b"This is a large binary file content" * 100
  66. blob = Blob()
  67. blob.data = original_content
  68. # Checkin: should convert to LFS pointer
  69. checked_in = self.normalizer.checkin_normalize(blob, b"large.bin")
  70. # Verify it's an LFS pointer
  71. self.assertLess(len(checked_in.data), len(original_content))
  72. self.assertTrue(
  73. checked_in.data.startswith(b"version https://git-lfs.github.com/spec/v1")
  74. )
  75. # Checkout: should restore original content
  76. checked_out = self.normalizer.checkout_normalize(checked_in, b"large.bin")
  77. # Verify we got back the original content
  78. self.assertEqual(checked_out.data, original_content)
  79. def test_non_lfs_file(self) -> None:
  80. """Test that non-LFS files pass through unchanged."""
  81. # Create a text file (not matching *.bin pattern)
  82. content = b"This is a regular text file"
  83. blob = Blob()
  84. blob.data = content
  85. # Both operations should return the original blob
  86. checked_in = self.normalizer.checkin_normalize(blob, b"file.txt")
  87. self.assertIs(checked_in, blob)
  88. checked_out = self.normalizer.checkout_normalize(blob, b"file.txt")
  89. self.assertIs(checked_out, blob)
  90. def test_lfs_pointer_file(self) -> None:
  91. """Test handling of files that are already LFS pointers."""
  92. # Create an LFS pointer manually
  93. from dulwich.lfs import LFSPointer
  94. # First store some content
  95. content = b"Content to be stored in LFS"
  96. sha = self.lfs_store.write_object([content])
  97. # Create pointer
  98. pointer = LFSPointer(sha, len(content))
  99. blob = Blob()
  100. blob.data = pointer.to_bytes()
  101. # Checkin should recognize it's already a pointer and not change it
  102. checked_in = self.normalizer.checkin_normalize(blob, b"data.bin")
  103. self.assertIs(checked_in, blob)
  104. # Checkout should expand it
  105. checked_out = self.normalizer.checkout_normalize(blob, b"data.bin")
  106. self.assertEqual(checked_out.data, content)
  107. def test_missing_lfs_object(self) -> None:
  108. """Test handling of LFS pointer with missing object."""
  109. from dulwich.lfs import LFSPointer
  110. # Create pointer to non-existent object
  111. pointer = LFSPointer(
  112. "0000000000000000000000000000000000000000000000000000000000000000", 1234
  113. )
  114. blob = Blob()
  115. blob.data = pointer.to_bytes()
  116. # Checkout should return the pointer as-is when object is missing
  117. checked_out = self.normalizer.checkout_normalize(blob, b"missing.bin")
  118. self.assertEqual(checked_out.data, blob.data)