test_trailers.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # test_trailers.py -- tests for git trailers
  2. # Copyright (C) 2025 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. """Tests for dulwich.trailers."""
  22. import unittest
  23. from dulwich.trailers import (
  24. Trailer,
  25. add_trailer_to_message,
  26. format_trailers,
  27. parse_trailers,
  28. )
  29. class TestTrailer(unittest.TestCase):
  30. """Tests for the Trailer class."""
  31. def test_init(self) -> None:
  32. """Test Trailer initialization."""
  33. trailer = Trailer("Signed-off-by", "Alice <alice@example.com>")
  34. self.assertEqual(trailer.key, "Signed-off-by")
  35. self.assertEqual(trailer.value, "Alice <alice@example.com>")
  36. self.assertEqual(trailer.separator, ":")
  37. def test_str(self) -> None:
  38. """Test Trailer string representation."""
  39. trailer = Trailer("Signed-off-by", "Alice <alice@example.com>")
  40. self.assertEqual(str(trailer), "Signed-off-by: Alice <alice@example.com>")
  41. def test_equality(self) -> None:
  42. """Test Trailer equality."""
  43. t1 = Trailer("Signed-off-by", "Alice")
  44. t2 = Trailer("Signed-off-by", "Alice")
  45. t3 = Trailer("Signed-off-by", "Bob")
  46. self.assertEqual(t1, t2)
  47. self.assertNotEqual(t1, t3)
  48. class TestParseTrailers(unittest.TestCase):
  49. """Tests for parse_trailers function."""
  50. def test_no_trailers(self) -> None:
  51. """Test parsing a message with no trailers."""
  52. message = b"Subject\n\nBody text\n"
  53. body, trailers = parse_trailers(message)
  54. self.assertEqual(body, b"Subject\n\nBody text\n")
  55. self.assertEqual(trailers, [])
  56. def test_simple_trailer(self) -> None:
  57. """Test parsing a message with a single trailer."""
  58. message = b"Subject\n\nBody text\n\nSigned-off-by: Alice <alice@example.com>\n"
  59. body, trailers = parse_trailers(message)
  60. self.assertEqual(body, b"Subject\n\nBody text\n")
  61. self.assertEqual(len(trailers), 1)
  62. self.assertEqual(trailers[0].key, "Signed-off-by")
  63. self.assertEqual(trailers[0].value, "Alice <alice@example.com>")
  64. def test_multiple_trailers(self) -> None:
  65. """Test parsing a message with multiple trailers."""
  66. message = b"Subject\n\nBody text\n\nSigned-off-by: Alice <alice@example.com>\nReviewed-by: Bob <bob@example.com>\n"
  67. body, trailers = parse_trailers(message)
  68. self.assertEqual(body, b"Subject\n\nBody text\n")
  69. self.assertEqual(len(trailers), 2)
  70. self.assertEqual(trailers[0].key, "Signed-off-by")
  71. self.assertEqual(trailers[0].value, "Alice <alice@example.com>")
  72. self.assertEqual(trailers[1].key, "Reviewed-by")
  73. self.assertEqual(trailers[1].value, "Bob <bob@example.com>")
  74. def test_trailer_with_multiline_value(self) -> None:
  75. """Test parsing a trailer with multiline value."""
  76. message = b"Subject\n\nBody\n\nTrailer: line1\n line2\n line3\n"
  77. _body, trailers = parse_trailers(message)
  78. self.assertEqual(len(trailers), 1)
  79. self.assertEqual(trailers[0].key, "Trailer")
  80. self.assertEqual(trailers[0].value, "line1 line2 line3")
  81. def test_no_blank_line_before_trailer(self) -> None:
  82. """Test that trailers without preceding blank line are not parsed."""
  83. message = b"Subject\nBody\nSigned-off-by: Alice\n"
  84. body, trailers = parse_trailers(message)
  85. self.assertEqual(body, message)
  86. self.assertEqual(trailers, [])
  87. def test_trailer_at_end_only(self) -> None:
  88. """Test that trailers must be at the end of the message."""
  89. message = b"Subject\n\nSigned-off-by: Alice\n\nMore body text\n"
  90. body, trailers = parse_trailers(message)
  91. # The "Signed-off-by" is not at the end, so it shouldn't be parsed as a trailer
  92. self.assertEqual(body, message)
  93. self.assertEqual(trailers, [])
  94. def test_different_separators(self) -> None:
  95. """Test parsing trailers with different separators."""
  96. message = b"Subject\n\nBody\n\nKey= value\n"
  97. _body, trailers = parse_trailers(message, separators="=")
  98. self.assertEqual(len(trailers), 1)
  99. self.assertEqual(trailers[0].key, "Key")
  100. self.assertEqual(trailers[0].value, "value")
  101. self.assertEqual(trailers[0].separator, "=")
  102. def test_empty_message(self) -> None:
  103. """Test parsing an empty message."""
  104. body, trailers = parse_trailers(b"")
  105. self.assertEqual(body, b"")
  106. self.assertEqual(trailers, [])
  107. class TestFormatTrailers(unittest.TestCase):
  108. """Tests for format_trailers function."""
  109. def test_empty_list(self) -> None:
  110. """Test formatting an empty list of trailers."""
  111. result = format_trailers([])
  112. self.assertEqual(result, b"")
  113. def test_single_trailer(self) -> None:
  114. """Test formatting a single trailer."""
  115. trailers = [Trailer("Signed-off-by", "Alice <alice@example.com>")]
  116. result = format_trailers(trailers)
  117. self.assertEqual(result, b"Signed-off-by: Alice <alice@example.com>\n")
  118. def test_multiple_trailers(self) -> None:
  119. """Test formatting multiple trailers."""
  120. trailers = [
  121. Trailer("Signed-off-by", "Alice <alice@example.com>"),
  122. Trailer("Reviewed-by", "Bob <bob@example.com>"),
  123. ]
  124. result = format_trailers(trailers)
  125. expected = b"Signed-off-by: Alice <alice@example.com>\nReviewed-by: Bob <bob@example.com>\n"
  126. self.assertEqual(result, expected)
  127. class TestAddTrailerToMessage(unittest.TestCase):
  128. """Tests for add_trailer_to_message function."""
  129. def test_add_to_empty_message(self) -> None:
  130. """Test adding a trailer to an empty message."""
  131. message = b""
  132. result = add_trailer_to_message(message, "Signed-off-by", "Alice")
  133. # Empty messages should get a trailer added
  134. self.assertIn(b"Signed-off-by: Alice", result)
  135. def test_add_to_message_without_trailers(self) -> None:
  136. """Test adding a trailer to a message without existing trailers."""
  137. message = b"Subject\n\nBody text\n"
  138. result = add_trailer_to_message(message, "Signed-off-by", "Alice")
  139. expected = b"Subject\n\nBody text\n\nSigned-off-by: Alice\n"
  140. self.assertEqual(result, expected)
  141. def test_add_to_message_with_existing_trailers(self) -> None:
  142. """Test adding a trailer to a message with existing trailers."""
  143. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\n"
  144. result = add_trailer_to_message(message, "Reviewed-by", "Bob")
  145. self.assertIn(b"Signed-off-by: Alice", result)
  146. self.assertIn(b"Reviewed-by: Bob", result)
  147. def test_add_duplicate_trailer_default(self) -> None:
  148. """Test adding a duplicate trailer with default if_exists."""
  149. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\n"
  150. result = add_trailer_to_message(
  151. message, "Signed-off-by", "Alice", if_exists="addIfDifferentNeighbor"
  152. )
  153. # Should not add duplicate
  154. self.assertEqual(result, message)
  155. def test_add_duplicate_trailer_add(self) -> None:
  156. """Test adding a duplicate trailer with if_exists=add."""
  157. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\n"
  158. result = add_trailer_to_message(
  159. message, "Signed-off-by", "Alice", if_exists="add"
  160. )
  161. # Should add duplicate
  162. self.assertEqual(result.count(b"Signed-off-by: Alice"), 2)
  163. def test_add_different_value(self) -> None:
  164. """Test adding a trailer with same key but different value."""
  165. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\n"
  166. result = add_trailer_to_message(message, "Signed-off-by", "Bob")
  167. self.assertIn(b"Signed-off-by: Alice", result)
  168. self.assertIn(b"Signed-off-by: Bob", result)
  169. def test_replace_existing(self) -> None:
  170. """Test replacing existing trailers with if_exists=replace."""
  171. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\nSigned-off-by: Bob\n"
  172. result = add_trailer_to_message(
  173. message, "Signed-off-by", "Charlie", if_exists="replace"
  174. )
  175. self.assertNotIn(b"Alice", result)
  176. self.assertNotIn(b"Bob", result)
  177. self.assertIn(b"Signed-off-by: Charlie", result)
  178. def test_do_nothing_if_exists(self) -> None:
  179. """Test if_exists=doNothing."""
  180. message = b"Subject\n\nBody\n\nSigned-off-by: Alice\n"
  181. result = add_trailer_to_message(
  182. message, "Signed-off-by", "Bob", if_exists="doNothing"
  183. )
  184. # Should not modify the message
  185. self.assertEqual(result, message)
  186. def test_if_missing_do_nothing(self) -> None:
  187. """Test if_missing=doNothing."""
  188. message = b"Subject\n\nBody\n"
  189. result = add_trailer_to_message(
  190. message, "Signed-off-by", "Alice", if_missing="doNothing"
  191. )
  192. # Should not add the trailer
  193. self.assertNotIn(b"Signed-off-by", result)
  194. def test_where_start(self) -> None:
  195. """Test adding trailer at start."""
  196. message = b"Subject\n\nBody\n\nReviewed-by: Bob\n"
  197. result = add_trailer_to_message(
  198. message, "Signed-off-by", "Alice", where="start"
  199. )
  200. # Parse to check order
  201. _, trailers = parse_trailers(result)
  202. self.assertEqual(len(trailers), 2)
  203. self.assertEqual(trailers[0].key, "Signed-off-by")
  204. self.assertEqual(trailers[1].key, "Reviewed-by")
  205. def test_custom_separator(self) -> None:
  206. """Test adding trailer with custom separator."""
  207. message = b"Subject\n\nBody\n"
  208. result = add_trailer_to_message(message, "Key", "value", separator="=")
  209. self.assertIn(b"Key= value", result)
  210. class TestIntegration(unittest.TestCase):
  211. """Integration tests for trailers."""
  212. def test_parse_and_format_roundtrip(self) -> None:
  213. """Test that parse and format are inverse operations."""
  214. original = b"Subject\n\nBody\n\nSigned-off-by: Alice\nReviewed-by: Bob\n"
  215. body, trailers = parse_trailers(original)
  216. formatted = body
  217. if body and not body.endswith(b"\n"):
  218. formatted += b"\n"
  219. if trailers:
  220. formatted += b"\n"
  221. formatted += format_trailers(trailers)
  222. self.assertEqual(formatted, original)
  223. def test_add_multiple_trailers(self) -> None:
  224. """Test adding multiple trailers in sequence."""
  225. message = b"Subject\n\nBody\n"
  226. message = add_trailer_to_message(message, "Signed-off-by", "Alice")
  227. message = add_trailer_to_message(message, "Reviewed-by", "Bob")
  228. message = add_trailer_to_message(message, "Tested-by", "Charlie")
  229. _, trailers = parse_trailers(message)
  230. self.assertEqual(len(trailers), 3)
  231. self.assertEqual(trailers[0].key, "Signed-off-by")
  232. self.assertEqual(trailers[1].key, "Reviewed-by")
  233. self.assertEqual(trailers[2].key, "Tested-by")