# test_whitespace.py -- Tests for whitespace error detection # Copyright (C) 2025 Dulwich contributors # # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as published by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Tests for whitespace error detection.""" from dulwich.whitespace import ( DEFAULT_WHITESPACE_ERRORS, WhitespaceChecker, fix_whitespace_errors, parse_whitespace_config, ) from . import TestCase class WhitespaceConfigTests(TestCase): """Test core.whitespace configuration parsing.""" def test_parse_default(self) -> None: """Test default whitespace configuration.""" errors, tab_width = parse_whitespace_config(None) self.assertEqual(errors, DEFAULT_WHITESPACE_ERRORS) self.assertEqual(tab_width, 8) def test_parse_empty(self) -> None: """Test empty whitespace configuration.""" errors, tab_width = parse_whitespace_config("") self.assertEqual(errors, set()) self.assertEqual(tab_width, 8) def test_parse_single_error(self) -> None: """Test single error type.""" errors, tab_width = parse_whitespace_config("blank-at-eol") self.assertEqual(errors, {"blank-at-eol"}) self.assertEqual(tab_width, 8) def test_parse_multiple_errors(self) -> None: """Test multiple error types.""" errors, tab_width = parse_whitespace_config( "blank-at-eol,space-before-tab,tab-in-indent" ) self.assertEqual(errors, {"blank-at-eol", "space-before-tab", "tab-in-indent"}) self.assertEqual(tab_width, 8) def test_parse_with_negation(self) -> None: """Test negation of default errors.""" errors, tab_width = parse_whitespace_config("-blank-at-eol") # Should have defaults minus blank-at-eol expected = DEFAULT_WHITESPACE_ERRORS - {"blank-at-eol"} self.assertEqual(errors, expected) self.assertEqual(tab_width, 8) def test_parse_trailing_space_alias(self) -> None: """Test that trailing-space is an alias for blank-at-eol.""" errors, tab_width = parse_whitespace_config("trailing-space") self.assertEqual(errors, {"blank-at-eol"}) self.assertEqual(tab_width, 8) def test_parse_tabwidth(self) -> None: """Test tabwidth setting.""" errors, tab_width = parse_whitespace_config("blank-at-eol,tabwidth=4") self.assertEqual(errors, {"blank-at-eol"}) self.assertEqual(tab_width, 4) def test_parse_invalid_tabwidth(self) -> None: """Test invalid tabwidth defaults to 8.""" errors, tab_width = parse_whitespace_config("tabwidth=invalid") self.assertEqual(tab_width, 8) errors, tab_width = parse_whitespace_config("tabwidth=0") self.assertEqual(tab_width, 8) class WhitespaceCheckerTests(TestCase): """Test WhitespaceChecker functionality.""" def test_blank_at_eol(self) -> None: """Test detection of trailing whitespace.""" checker = WhitespaceChecker({"blank-at-eol"}) # No trailing whitespace errors = checker.check_line(b"normal line", 1) self.assertEqual(errors, []) # Trailing space errors = checker.check_line(b"trailing space ", 1) self.assertEqual(errors, [("blank-at-eol", 1)]) # Trailing tab errors = checker.check_line(b"trailing tab\t", 1) self.assertEqual(errors, [("blank-at-eol", 1)]) # Multiple trailing whitespace errors = checker.check_line(b"multiple \t ", 1) self.assertEqual(errors, [("blank-at-eol", 1)]) def test_space_before_tab(self) -> None: """Test detection of space before tab in indentation.""" checker = WhitespaceChecker({"space-before-tab"}) # No space before tab errors = checker.check_line(b"\tindented", 1) self.assertEqual(errors, []) # Space before tab in indentation errors = checker.check_line(b" \tindented", 1) self.assertEqual(errors, [("space-before-tab", 1)]) # Space before tab not in indentation (should not trigger) errors = checker.check_line(b"code \t comment", 1) self.assertEqual(errors, []) def test_indent_with_non_tab(self) -> None: """Test detection of 8+ spaces at start of line.""" checker = WhitespaceChecker({"indent-with-non-tab"}, tab_width=8) # Less than 8 spaces errors = checker.check_line(b" code", 1) self.assertEqual(errors, []) # Exactly 8 spaces errors = checker.check_line(b" code", 1) self.assertEqual(errors, [("indent-with-non-tab", 1)]) # More than 8 spaces errors = checker.check_line(b" code", 1) self.assertEqual(errors, [("indent-with-non-tab", 1)]) # Tab after spaces resets count errors = checker.check_line(b" \t code", 1) self.assertEqual(errors, []) # Custom tab width checker = WhitespaceChecker({"indent-with-non-tab"}, tab_width=4) errors = checker.check_line(b" code", 1) self.assertEqual(errors, [("indent-with-non-tab", 1)]) def test_tab_in_indent(self) -> None: """Test detection of tabs in indentation.""" checker = WhitespaceChecker({"tab-in-indent"}) # No tabs errors = checker.check_line(b" code", 1) self.assertEqual(errors, []) # Tab in indentation errors = checker.check_line(b"\tcode", 1) self.assertEqual(errors, [("tab-in-indent", 1)]) # Tab after non-whitespace (should not trigger) errors = checker.check_line(b"code\tcomment", 1) self.assertEqual(errors, []) def test_cr_at_eol(self) -> None: """Test detection of carriage return at end of line.""" checker = WhitespaceChecker({"cr-at-eol"}) # No CR errors = checker.check_line(b"normal line", 1) self.assertEqual(errors, []) # CR at end errors = checker.check_line(b"line\r", 1) self.assertEqual(errors, [("cr-at-eol", 1)]) def test_blank_at_eof(self) -> None: """Test detection of blank lines at end of file.""" checker = WhitespaceChecker({"blank-at-eof"}) # No trailing blank lines content = b"line1\nline2\nline3" errors = checker.check_content(content) self.assertEqual(errors, []) # One trailing blank line (normal for files ending with newline) content = b"line1\nline2\nline3\n" errors = checker.check_content(content) self.assertEqual(errors, []) # Multiple trailing blank lines content = b"line1\nline2\n\n\n" errors = checker.check_content(content) self.assertEqual(errors, [("blank-at-eof", 5)]) # Only blank lines content = b"\n\n\n" errors = checker.check_content(content) self.assertEqual(errors, [("blank-at-eof", 4)]) def test_multiple_errors(self) -> None: """Test detection of multiple error types.""" checker = WhitespaceChecker( {"blank-at-eol", "space-before-tab", "tab-in-indent"} ) # Line with multiple errors errors = checker.check_line(b" \tcode ", 1) error_types = {e[0] for e in errors} self.assertEqual( error_types, {"blank-at-eol", "space-before-tab", "tab-in-indent"} ) def test_check_content_crlf(self) -> None: """Test content checking with CRLF line endings.""" checker = WhitespaceChecker({"blank-at-eol", "cr-at-eol"}) # CRLF line endings content = b"line1\r\nline2 \r\nline3\r\n" errors = checker.check_content(content) # Should detect trailing space on line 2 but not CR (since CRLF is handled) self.assertEqual(errors, [("blank-at-eol", 2)]) class WhitespaceFixTests(TestCase): """Test whitespace error fixing.""" def test_fix_blank_at_eol(self) -> None: """Test fixing trailing whitespace.""" content = b"line1 \nline2\t\nline3" errors = [("blank-at-eol", 1), ("blank-at-eol", 2)] fixed = fix_whitespace_errors(content, errors) self.assertEqual(fixed, b"line1\nline2\nline3") def test_fix_blank_at_eof(self) -> None: """Test fixing blank lines at end of file.""" content = b"line1\nline2\n\n\n" errors = [("blank-at-eof", 4)] fixed = fix_whitespace_errors(content, errors) self.assertEqual(fixed, b"line1\nline2\n") def test_fix_cr_at_eol(self) -> None: """Test fixing carriage returns.""" content = b"line1\r\nline2\r\nline3\r" errors = [("cr-at-eol", 1), ("cr-at-eol", 2), ("cr-at-eol", 3)] fixed = fix_whitespace_errors(content, errors) # Our fix function removes all CRs when cr-at-eol errors are fixed self.assertEqual(fixed, b"line1\nline2\nline3") def test_fix_specific_types(self) -> None: """Test fixing only specific error types.""" content = b"line1 \nline2\n\n\n" errors = [("blank-at-eol", 1), ("blank-at-eof", 4)] # Fix only blank-at-eol fixed = fix_whitespace_errors(content, errors, fix_types={"blank-at-eol"}) self.assertEqual(fixed, b"line1\nline2\n\n\n") # Fix only blank-at-eof fixed = fix_whitespace_errors(content, errors, fix_types={"blank-at-eof"}) self.assertEqual(fixed, b"line1 \nline2\n") def test_fix_no_errors(self) -> None: """Test fixing with no errors returns original content.""" content = b"line1\nline2\nline3" fixed = fix_whitespace_errors(content, []) self.assertEqual(fixed, content)