test_whitespace.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. # test_whitespace.py -- Tests for whitespace error detection
  2. # Copyright (C) 2025 Dulwich contributors
  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 whitespace error detection."""
  22. from dulwich.whitespace import (
  23. DEFAULT_WHITESPACE_ERRORS,
  24. WhitespaceChecker,
  25. fix_whitespace_errors,
  26. parse_whitespace_config,
  27. )
  28. from . import TestCase
  29. class WhitespaceConfigTests(TestCase):
  30. """Test core.whitespace configuration parsing."""
  31. def test_parse_default(self) -> None:
  32. """Test default whitespace configuration."""
  33. errors, tab_width = parse_whitespace_config(None)
  34. self.assertEqual(errors, DEFAULT_WHITESPACE_ERRORS)
  35. self.assertEqual(tab_width, 8)
  36. def test_parse_empty(self) -> None:
  37. """Test empty whitespace configuration."""
  38. errors, tab_width = parse_whitespace_config("")
  39. self.assertEqual(errors, set())
  40. self.assertEqual(tab_width, 8)
  41. def test_parse_single_error(self) -> None:
  42. """Test single error type."""
  43. errors, tab_width = parse_whitespace_config("blank-at-eol")
  44. self.assertEqual(errors, {"blank-at-eol"})
  45. self.assertEqual(tab_width, 8)
  46. def test_parse_multiple_errors(self) -> None:
  47. """Test multiple error types."""
  48. errors, tab_width = parse_whitespace_config(
  49. "blank-at-eol,space-before-tab,tab-in-indent"
  50. )
  51. self.assertEqual(errors, {"blank-at-eol", "space-before-tab", "tab-in-indent"})
  52. self.assertEqual(tab_width, 8)
  53. def test_parse_with_negation(self) -> None:
  54. """Test negation of default errors."""
  55. errors, tab_width = parse_whitespace_config("-blank-at-eol")
  56. # Should have defaults minus blank-at-eol
  57. expected = DEFAULT_WHITESPACE_ERRORS - {"blank-at-eol"}
  58. self.assertEqual(errors, expected)
  59. self.assertEqual(tab_width, 8)
  60. def test_parse_trailing_space_alias(self) -> None:
  61. """Test that trailing-space is an alias for blank-at-eol."""
  62. errors, tab_width = parse_whitespace_config("trailing-space")
  63. self.assertEqual(errors, {"blank-at-eol"})
  64. self.assertEqual(tab_width, 8)
  65. def test_parse_tabwidth(self) -> None:
  66. """Test tabwidth setting."""
  67. errors, tab_width = parse_whitespace_config("blank-at-eol,tabwidth=4")
  68. self.assertEqual(errors, {"blank-at-eol"})
  69. self.assertEqual(tab_width, 4)
  70. def test_parse_invalid_tabwidth(self) -> None:
  71. """Test invalid tabwidth defaults to 8."""
  72. errors, tab_width = parse_whitespace_config("tabwidth=invalid")
  73. self.assertEqual(tab_width, 8)
  74. errors, tab_width = parse_whitespace_config("tabwidth=0")
  75. self.assertEqual(tab_width, 8)
  76. class WhitespaceCheckerTests(TestCase):
  77. """Test WhitespaceChecker functionality."""
  78. def test_blank_at_eol(self) -> None:
  79. """Test detection of trailing whitespace."""
  80. checker = WhitespaceChecker({"blank-at-eol"})
  81. # No trailing whitespace
  82. errors = checker.check_line(b"normal line", 1)
  83. self.assertEqual(errors, [])
  84. # Trailing space
  85. errors = checker.check_line(b"trailing space ", 1)
  86. self.assertEqual(errors, [("blank-at-eol", 1)])
  87. # Trailing tab
  88. errors = checker.check_line(b"trailing tab\t", 1)
  89. self.assertEqual(errors, [("blank-at-eol", 1)])
  90. # Multiple trailing whitespace
  91. errors = checker.check_line(b"multiple \t ", 1)
  92. self.assertEqual(errors, [("blank-at-eol", 1)])
  93. def test_space_before_tab(self) -> None:
  94. """Test detection of space before tab in indentation."""
  95. checker = WhitespaceChecker({"space-before-tab"})
  96. # No space before tab
  97. errors = checker.check_line(b"\tindented", 1)
  98. self.assertEqual(errors, [])
  99. # Space before tab in indentation
  100. errors = checker.check_line(b" \tindented", 1)
  101. self.assertEqual(errors, [("space-before-tab", 1)])
  102. # Space before tab not in indentation (should not trigger)
  103. errors = checker.check_line(b"code \t comment", 1)
  104. self.assertEqual(errors, [])
  105. def test_indent_with_non_tab(self) -> None:
  106. """Test detection of 8+ spaces at start of line."""
  107. checker = WhitespaceChecker({"indent-with-non-tab"}, tab_width=8)
  108. # Less than 8 spaces
  109. errors = checker.check_line(b" code", 1)
  110. self.assertEqual(errors, [])
  111. # Exactly 8 spaces
  112. errors = checker.check_line(b" code", 1)
  113. self.assertEqual(errors, [("indent-with-non-tab", 1)])
  114. # More than 8 spaces
  115. errors = checker.check_line(b" code", 1)
  116. self.assertEqual(errors, [("indent-with-non-tab", 1)])
  117. # Tab after spaces resets count
  118. errors = checker.check_line(b" \t code", 1)
  119. self.assertEqual(errors, [])
  120. # Custom tab width
  121. checker = WhitespaceChecker({"indent-with-non-tab"}, tab_width=4)
  122. errors = checker.check_line(b" code", 1)
  123. self.assertEqual(errors, [("indent-with-non-tab", 1)])
  124. def test_tab_in_indent(self) -> None:
  125. """Test detection of tabs in indentation."""
  126. checker = WhitespaceChecker({"tab-in-indent"})
  127. # No tabs
  128. errors = checker.check_line(b" code", 1)
  129. self.assertEqual(errors, [])
  130. # Tab in indentation
  131. errors = checker.check_line(b"\tcode", 1)
  132. self.assertEqual(errors, [("tab-in-indent", 1)])
  133. # Tab after non-whitespace (should not trigger)
  134. errors = checker.check_line(b"code\tcomment", 1)
  135. self.assertEqual(errors, [])
  136. def test_cr_at_eol(self) -> None:
  137. """Test detection of carriage return at end of line."""
  138. checker = WhitespaceChecker({"cr-at-eol"})
  139. # No CR
  140. errors = checker.check_line(b"normal line", 1)
  141. self.assertEqual(errors, [])
  142. # CR at end
  143. errors = checker.check_line(b"line\r", 1)
  144. self.assertEqual(errors, [("cr-at-eol", 1)])
  145. def test_blank_at_eof(self) -> None:
  146. """Test detection of blank lines at end of file."""
  147. checker = WhitespaceChecker({"blank-at-eof"})
  148. # No trailing blank lines
  149. content = b"line1\nline2\nline3"
  150. errors = checker.check_content(content)
  151. self.assertEqual(errors, [])
  152. # One trailing blank line (normal for files ending with newline)
  153. content = b"line1\nline2\nline3\n"
  154. errors = checker.check_content(content)
  155. self.assertEqual(errors, [])
  156. # Multiple trailing blank lines
  157. content = b"line1\nline2\n\n\n"
  158. errors = checker.check_content(content)
  159. self.assertEqual(errors, [("blank-at-eof", 5)])
  160. # Only blank lines
  161. content = b"\n\n\n"
  162. errors = checker.check_content(content)
  163. self.assertEqual(errors, [("blank-at-eof", 4)])
  164. def test_multiple_errors(self) -> None:
  165. """Test detection of multiple error types."""
  166. checker = WhitespaceChecker(
  167. {"blank-at-eol", "space-before-tab", "tab-in-indent"}
  168. )
  169. # Line with multiple errors
  170. errors = checker.check_line(b" \tcode ", 1)
  171. error_types = {e[0] for e in errors}
  172. self.assertEqual(
  173. error_types, {"blank-at-eol", "space-before-tab", "tab-in-indent"}
  174. )
  175. def test_check_content_crlf(self) -> None:
  176. """Test content checking with CRLF line endings."""
  177. checker = WhitespaceChecker({"blank-at-eol", "cr-at-eol"})
  178. # CRLF line endings
  179. content = b"line1\r\nline2 \r\nline3\r\n"
  180. errors = checker.check_content(content)
  181. # Should detect trailing space on line 2 but not CR (since CRLF is handled)
  182. self.assertEqual(errors, [("blank-at-eol", 2)])
  183. class WhitespaceFixTests(TestCase):
  184. """Test whitespace error fixing."""
  185. def test_fix_blank_at_eol(self) -> None:
  186. """Test fixing trailing whitespace."""
  187. content = b"line1 \nline2\t\nline3"
  188. errors = [("blank-at-eol", 1), ("blank-at-eol", 2)]
  189. fixed = fix_whitespace_errors(content, errors)
  190. self.assertEqual(fixed, b"line1\nline2\nline3")
  191. def test_fix_blank_at_eof(self) -> None:
  192. """Test fixing blank lines at end of file."""
  193. content = b"line1\nline2\n\n\n"
  194. errors = [("blank-at-eof", 4)]
  195. fixed = fix_whitespace_errors(content, errors)
  196. self.assertEqual(fixed, b"line1\nline2\n")
  197. def test_fix_cr_at_eol(self) -> None:
  198. """Test fixing carriage returns."""
  199. content = b"line1\r\nline2\r\nline3\r"
  200. errors = [("cr-at-eol", 1), ("cr-at-eol", 2), ("cr-at-eol", 3)]
  201. fixed = fix_whitespace_errors(content, errors)
  202. # Our fix function removes all CRs when cr-at-eol errors are fixed
  203. self.assertEqual(fixed, b"line1\nline2\nline3")
  204. def test_fix_specific_types(self) -> None:
  205. """Test fixing only specific error types."""
  206. content = b"line1 \nline2\n\n\n"
  207. errors = [("blank-at-eol", 1), ("blank-at-eof", 4)]
  208. # Fix only blank-at-eol
  209. fixed = fix_whitespace_errors(content, errors, fix_types={"blank-at-eol"})
  210. self.assertEqual(fixed, b"line1\nline2\n\n\n")
  211. # Fix only blank-at-eof
  212. fixed = fix_whitespace_errors(content, errors, fix_types={"blank-at-eof"})
  213. self.assertEqual(fixed, b"line1 \nline2\n")
  214. def test_fix_no_errors(self) -> None:
  215. """Test fixing with no errors returns original content."""
  216. content = b"line1\nline2\nline3"
  217. fixed = fix_whitespace_errors(content, [])
  218. self.assertEqual(fixed, content)