From 4ecc2812b4752766199d1c017f0e55e787573318 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 14 Dec 2025 17:24:43 +0200 Subject: [PATCH 1/5] feat: Add comprehensive tests --- tests/config_edge_test.py | 80 ------ tests/config_fallback_test.py | 65 ----- tests/config_import_test.py | 94 ------- tests/config_test.py | 229 +++++++++++++++ tests/engine_comprehensive_test.py | 322 --------------------- tests/engine_test.py | 437 +++++++++++++++++++++++++++++ tests/main_test.py | 59 ++++ tests/rule_builder_test.py | 116 ++++++++ tests/util_test.py | 173 ++++++++++++ 9 files changed, 1014 insertions(+), 561 deletions(-) delete mode 100644 tests/config_edge_test.py delete mode 100644 tests/config_fallback_test.py delete mode 100644 tests/config_import_test.py delete mode 100644 tests/engine_comprehensive_test.py diff --git a/tests/config_edge_test.py b/tests/config_edge_test.py deleted file mode 100644 index ccafb4df..00000000 --- a/tests/config_edge_test.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Test for TOML parsing errors and exception handling.""" - -import pytest -import tempfile -import os -from commit_check.config import load_config - - -@pytest.mark.benchmark -def test_load_config_invalid_toml(): - """Test handling of invalid TOML syntax.""" - invalid_toml = b""" -[incomplete -missing closing bracket -""" - with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f: - f.write(invalid_toml) - f.flush() - - try: - with pytest.raises(Exception): # Should raise a TOML parsing error - load_config(f.name) - finally: - os.unlink(f.name) - - -@pytest.mark.benchmark -def test_load_config_file_permission_error(): - """Test handling of file permission errors.""" - config_content = b""" -[checks] -test = true -""" - with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f: - f.write(config_content) - f.flush() - - try: - # Remove read permissions to simulate permission error - os.chmod(f.name, 0o000) - - with pytest.raises(PermissionError): - load_config(f.name) - finally: - # Restore permissions and clean up - os.chmod(f.name, 0o644) - os.unlink(f.name) - - -@pytest.mark.benchmark -def test_tomli_import_fallback(): - """Test the tomli import fallback when tomllib is not available.""" - # We need to test the import fallback behavior - # This is complex because the imports happen at module load time - - # Create a minimal test by importing the config module's behavior - config_content = b""" -[test] -fallback = true -""" - - with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f: - f.write(config_content) - f.flush() - - try: - # Test that we can load config regardless of which TOML library is used - config = load_config(f.name) - assert config["test"]["fallback"] is True - - # Since we can't easily test the import fallback on Python 3.11+, - # let's at least verify that the toml_load function works - from commit_check.config import toml_load - - with open(f.name, "rb") as config_file: - result = toml_load(config_file) - assert result["test"]["fallback"] is True - - finally: - os.unlink(f.name) diff --git a/tests/config_fallback_test.py b/tests/config_fallback_test.py deleted file mode 100644 index 0c6a8e24..00000000 --- a/tests/config_fallback_test.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Direct test of the config import fallback using module manipulation.""" - -import sys -import tempfile -import os -import pytest -from unittest.mock import patch - - -@pytest.mark.benchmark -def test_config_tomli_fallback_direct(): - """Test config.py fallback to tomli by manipulating imports.""" - - # Save original state - original_modules = sys.modules.copy() - - try: - # Remove config module if already imported - if "commit_check.config" in sys.modules: - del sys.modules["commit_check.config"] - - # Make tomllib unavailable by raising ImportError - original_import = __import__ - - def mock_import(name, globals=None, locals=None, fromlist=(), level=0): - if name == "tomllib": - raise ImportError("No module named 'tomllib'") - # For tomli, return a working mock - if name == "tomli": - - class MockTomli: - @staticmethod - def load(f): - content = f.read().decode("utf-8") - # Simple parser for test - if 'test_key = "test_value"' in content: - return {"test_key": "test_value"} - return {} - - return MockTomli() - return original_import(name, globals, locals, fromlist, level) - - with patch("builtins.__import__", side_effect=mock_import): - # Now import config - should use tomli fallback - import commit_check.config as config - - # Test that it works - config_content = b'test_key = "test_value"' - with tempfile.NamedTemporaryFile( - mode="wb", suffix=".toml", delete=False - ) as f: - f.write(config_content) - f.flush() - - try: - with open(f.name, "rb") as config_file: - result = config.toml_load(config_file) - assert result == {"test_key": "test_value"} - finally: - os.unlink(f.name) - - finally: - # Restore original modules - sys.modules.clear() - sys.modules.update(original_modules) diff --git a/tests/config_import_test.py b/tests/config_import_test.py deleted file mode 100644 index 083e84fb..00000000 --- a/tests/config_import_test.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Test import fallback by creating a test version of config.py.""" - -import tempfile -import os -from unittest.mock import patch -import pytest - - -@pytest.mark.benchmark -def test_tomli_import_fallback_simulation(): - """Test tomli import fallback by simulating the ImportError condition.""" - - # Create test code that simulates the config.py import logic - test_code = """ -try: - import tomllib - toml_load = tomllib.load - used_tomllib = True -except ImportError: - import tomli - toml_load = tomli.load - used_tomllib = False -""" - - # Test case 1: Normal case (tomllib available) - namespace1 = {} - exec(test_code, namespace1) - assert namespace1["used_tomllib"] is True - assert callable(namespace1["toml_load"]) - - # Test case 2: Simulate ImportError for tomllib - with patch.dict("sys.modules", {"tomllib": None}): - with patch( - "builtins.__import__", - side_effect=lambda name, *args, **kwargs: _mock_import_error( - name, *args, **kwargs - ), - ): - namespace2 = {} - exec(test_code, namespace2) - assert namespace2["used_tomllib"] is False - assert callable(namespace2["toml_load"]) - - -def _mock_import_error(name, *args, **kwargs): - """Mock import function that raises ImportError for tomllib.""" - if name == "tomllib": - raise ImportError("No module named 'tomllib'") - - # For tomli, we need to mock it since it might not be installed - if name == "tomli": - # Create a mock tomli module - class MockTomli: - @staticmethod - def load(f): - # Simple TOML parser for testing - content = f.read().decode("utf-8") - if "[test]" in content and 'value = "test"' in content: - return {"test": {"value": "test"}} - return {} - - return MockTomli() - - # For all other imports, use the real import - return __import__(name, *args, **kwargs) - - -@pytest.mark.benchmark -def test_import_paths_coverage(): - """Ensure both import paths are conceptually tested.""" - # This test verifies that both the tomllib and tomli code paths - # would work in their respective environments - - # Test the function signature matches expectation - config_content = b""" -[test] -value = "test" -""" - - with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f: - f.write(config_content) - f.flush() - - try: - # Test using the actual config module (uses tomllib on Python 3.11+) - from commit_check.config import toml_load - - with open(f.name, "rb") as config_file: - result = toml_load(config_file) - - assert result == {"test": {"value": "test"}} - - finally: - os.unlink(f.name) diff --git a/tests/config_test.py b/tests/config_test.py index fb2d5259..192912a6 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -1,5 +1,6 @@ """Tests for commit_check.config module.""" +import builtins import pytest import tempfile import os @@ -204,3 +205,231 @@ def load(f): sys.modules["tomllib"] = original_tomllib if original_config is not None: sys.modules["commit_check.config"] = original_config + + +# Tests from config_edge_test.py +class TestConfigEdgeCases: + """Test edge cases and error handling in config loading.""" + + def test_load_config_invalid_toml(self, tmp_path): + """Test loading config with invalid TOML syntax.""" + invalid_config = tmp_path / "invalid.toml" + invalid_config.write_text("invalid toml [[[") + + with pytest.raises(Exception): # Could be TOMLDecodeError or similar + load_config(str(invalid_config)) + + def test_load_config_file_permission_error(self, tmp_path, monkeypatch): + """Test load_config when file cannot be read due to permissions.""" + config_file = tmp_path / "no_perms.toml" + config_file.write_text("[commit-check]\nenabled = true") + + # Mock open to raise PermissionError + original_open = builtins.open + + def mock_open(*args, **kwargs): + if "no_perms.toml" in str(args[0]): + raise PermissionError("Permission denied") + return original_open(*args, **kwargs) + + monkeypatch.setattr(builtins, "open", mock_open) + + with pytest.raises(PermissionError): + load_config(str(config_file)) + + def test_tomli_import_fallback(self, monkeypatch): + """Test tomli import fallback when tomllib not available.""" + import sys + + # Save original modules + original_tomllib = sys.modules.get("tomllib") + original_tomli = sys.modules.get("tomli") + + try: + # Remove tomllib from sys.modules + if "tomllib" in sys.modules: + del sys.modules["tomllib"] + + # Force reimport of config module + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + # Mock tomllib to not exist + monkeypatch.setitem(sys.modules, "tomllib", None) + + # Import should fall back to tomli + from commit_check.config import toml_load + + assert toml_load is not None + finally: + # Restore original modules + if original_tomllib is not None: + sys.modules["tomllib"] = original_tomllib + elif "tomllib" in sys.modules: + del sys.modules["tomllib"] + + if original_tomli is not None: + sys.modules["tomli"] = original_tomli + elif "tomli" in sys.modules: + del sys.modules["tomli"] + + # Force reimport of config module to restore original state + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + import commit_check.config # noqa: F401 + + +# Tests from config_fallback_test.py +class TestConfigTomllibFallback: + """Test tomllib/tomli fallback mechanism.""" + + def test_config_tomli_fallback_direct(self, tmp_path): + """Test that config module can use tomli when tomllib is not available.""" + import sys + + # Save original modules + original_tomllib = sys.modules.get("tomllib") + original_tomli = sys.modules.get("tomli") + original_config = sys.modules.get("commit_check.config") + + try: + # Remove tomllib and config from sys.modules to force fresh import + if "tomllib" in sys.modules: + del sys.modules["tomllib"] + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + # Block tomllib import by setting it to None + sys.modules["tomllib"] = None + + # Now import config - should fall back to tomli + from commit_check.config import load_config, toml_load + + # Verify toml_load is available + assert toml_load is not None + + # Test that config loading works with tomli + config_file = tmp_path / "test.toml" + config_file.write_text("[commit-check]\nenabled = true") + + result = load_config(str(config_file)) + assert result is not None + assert "commit-check" in result + assert result["commit-check"]["enabled"] is True + + finally: + # Restore original modules + if original_tomllib is not None: + sys.modules["tomllib"] = original_tomllib + elif "tomllib" in sys.modules: + del sys.modules["tomllib"] + + if original_tomli is not None: + sys.modules["tomli"] = original_tomli + elif "tomli" in sys.modules: + del sys.modules["tomli"] + + if original_config is not None: + sys.modules["commit_check.config"] = original_config + elif "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + +# Tests from config_import_test.py +class TestConfigImportPaths: + """Test import path coverage in config module.""" + + def test_tomli_import_fallback_simulation(self, monkeypatch): + """Test tomli import fallback by simulating tomllib unavailability.""" + import sys + + # Save original modules + original_tomllib = sys.modules.get("tomllib") + original_config = sys.modules.get("commit_check.config") + + try: + # Remove modules to force fresh import + if "tomllib" in sys.modules: + del sys.modules["tomllib"] + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + # Simulate tomllib not being available by blocking its import + import builtins + + original_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == "tomllib": + raise ModuleNotFoundError("No module named 'tomllib'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", mock_import) + + # Import config - should trigger tomli fallback + from commit_check.config import toml_load + + # Verify toml_load is available (from tomli) + assert toml_load is not None + + finally: + # Restore original import + monkeypatch.undo() + + # Restore original modules + if original_tomllib is not None: + sys.modules["tomllib"] = original_tomllib + elif "tomllib" in sys.modules: + del sys.modules["tomllib"] + + if original_config is not None: + sys.modules["commit_check.config"] = original_config + elif "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + def test_import_paths_coverage(self): + """Test various import paths in config module.""" + import sys + + # Save original modules + original_tomllib = sys.modules.get("tomllib") + original_tomli = sys.modules.get("tomli") + original_config = sys.modules.get("commit_check.config") + + try: + # Test 1: Import with tomllib available + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + from commit_check.config import toml_load as toml_load_1 + + assert toml_load_1 is not None + + # Test 2: Force tomli fallback + if "tomllib" in sys.modules: + del sys.modules["tomllib"] + if "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] + + sys.modules["tomllib"] = None + + from commit_check.config import toml_load as toml_load_2 + + assert toml_load_2 is not None + + finally: + # Restore original modules + if original_tomllib is not None: + sys.modules["tomllib"] = original_tomllib + elif "tomllib" in sys.modules: + del sys.modules["tomllib"] + + if original_tomli is not None: + sys.modules["tomli"] = original_tomli + elif "tomli" in sys.modules: + del sys.modules["tomli"] + + if original_config is not None: + sys.modules["commit_check.config"] = original_config + elif "commit_check.config" in sys.modules: + del sys.modules["commit_check.config"] diff --git a/tests/engine_comprehensive_test.py b/tests/engine_comprehensive_test.py deleted file mode 100644 index 8ce16da3..00000000 --- a/tests/engine_comprehensive_test.py +++ /dev/null @@ -1,322 +0,0 @@ -"""Comprehensive tests for commit_check.engine module.""" - -from unittest.mock import patch -from commit_check.engine import ( - ValidationResult, - ValidationContext, - ValidationEngine, - CommitMessageValidator, - SubjectCapitalizationValidator, - SubjectImperativeValidator, - SubjectLengthValidator, - AuthorValidator, - BranchValidator, - MergeBaseValidator, - SignoffValidator, - BodyValidator, - CommitTypeValidator, -) -from commit_check.rule_builder import ValidationRule -import pytest - - -class TestValidationResult: - @pytest.mark.benchmark - def test_validation_result_values(self): - """Test ValidationResult enum values.""" - assert ValidationResult.PASS == 0 - assert ValidationResult.FAIL == 1 - - -class TestValidationContext: - @pytest.mark.benchmark - def test_validation_context_creation(self): - """Test ValidationContext creation.""" - context = ValidationContext() - assert context.stdin_text is None - assert context.commit_file is None - - context_with_data = ValidationContext( - stdin_text="test commit", commit_file="commit.txt" - ) - assert context_with_data.stdin_text == "test commit" - assert context_with_data.commit_file == "commit.txt" - - -class TestCommitMessageValidator: - @pytest.mark.benchmark - def test_commit_message_validator_creation(self): - """Test CommitMessageValidator creation.""" - rule = ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - validator = CommitMessageValidator(rule) - assert validator.rule == rule - - @patch("commit_check.engine.has_commits") - @pytest.mark.benchmark - def test_commit_message_validator_with_stdin(self, mock_has_commits): - """Test CommitMessageValidator with stdin text.""" - mock_has_commits.return_value = True - - rule = ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - validator = CommitMessageValidator(rule) - context = ValidationContext(stdin_text="feat: add new feature") - - result = validator.validate(context) - assert result == ValidationResult.PASS - - @patch("commit_check.engine.get_commit_info") - @patch("commit_check.engine.has_commits") - @pytest.mark.benchmark - def test_commit_message_validator_failure( - self, mock_has_commits, mock_get_commit_info - ): - """Test CommitMessageValidator failure case.""" - mock_has_commits.return_value = True - - rule = ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - validator = CommitMessageValidator(rule) - context = ValidationContext(stdin_text="bad commit message") - - with patch.object(validator, "_print_failure") as mock_print: - result = validator.validate(context) - assert result == ValidationResult.FAIL - mock_print.assert_called_once() - - @patch("commit_check.engine.has_commits") - @pytest.mark.benchmark - def test_commit_message_validator_skip_validation(self, mock_has_commits): - """Test CommitMessageValidator skips when no commits and no stdin.""" - mock_has_commits.return_value = False - - rule = ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - validator = CommitMessageValidator(rule) - context = ValidationContext() # No stdin_text - - result = validator.validate(context) - assert result == ValidationResult.PASS - - -class TestSubjectCapitalizationValidator: - @pytest.mark.benchmark - def test_subject_capitalization_pass(self): - """Test SubjectCapitalizationValidator pass case.""" - rule = ValidationRule( - check="subject_capitalized", - regex="^[A-Z]", - error="Subject must be capitalized", - suggest="Capitalize first letter", - ) - validator = SubjectCapitalizationValidator(rule) - # Use conventional commit format with capitalized description - context = ValidationContext(stdin_text="feat: Add new feature") - - result = validator.validate(context) - assert result == ValidationResult.PASS - - @pytest.mark.benchmark - def test_subject_capitalization_fail(self): - """Test SubjectCapitalizationValidator fail case.""" - rule = ValidationRule( - check="subject_capitalized", - regex="^[A-Z]", - error="Subject must be capitalized", - suggest="Capitalize first letter", - ) - validator = SubjectCapitalizationValidator(rule) - # Use conventional commit format with lowercase description - context = ValidationContext(stdin_text="feat: add new feature") - - with patch.object(validator, "_print_failure") as mock_print: - result = validator.validate(context) - assert result == ValidationResult.FAIL - mock_print.assert_called_once() - - -class TestSubjectImperativeValidator: - @pytest.mark.benchmark - def test_subject_imperative_pass(self): - """Test SubjectImperativeValidator pass case.""" - rule = ValidationRule( - check="imperative", - regex="", - error="Subject must be imperative", - suggest="Use imperative mood", - ) - validator = SubjectImperativeValidator(rule) - # Use conventional commit with imperative verb "add" - context = ValidationContext(stdin_text="feat: add new feature") - - result = validator.validate(context) - assert result == ValidationResult.PASS - - @pytest.mark.benchmark - def test_subject_imperative_fail(self): - """Test SubjectImperativeValidator fail case.""" - rule = ValidationRule( - check="imperative", - regex="", - error="Subject must be imperative", - suggest="Use imperative mood", - ) - validator = SubjectImperativeValidator(rule) - # Use past tense "added" which is not imperative - context = ValidationContext(stdin_text="feat: added new feature") - - with patch.object(validator, "_print_failure") as mock_print: - result = validator.validate(context) - assert result == ValidationResult.FAIL - mock_print.assert_called_once() - - -class TestSubjectLengthValidator: - @pytest.mark.benchmark - def test_subject_length_pass(self): - """Test SubjectLengthValidator pass case.""" - rule = ValidationRule( - check="subject_max_length", - regex="", - error="Subject too long", - suggest="Keep subject short", - value=50, - ) - validator = SubjectLengthValidator(rule) - context = ValidationContext(stdin_text="Add feature") - - result = validator.validate(context) - assert result == ValidationResult.PASS - - @pytest.mark.benchmark - def test_subject_length_fail(self): - """Test SubjectLengthValidator fail case.""" - rule = ValidationRule( - check="subject_max_length", - regex="", - error="Subject too long: max 10 characters", - suggest="Keep subject short", - value=10, - ) - validator = SubjectLengthValidator(rule) - context = ValidationContext(stdin_text="This is a very long subject line") - - with patch.object(validator, "_print_failure") as mock_print: - result = validator.validate(context) - assert result == ValidationResult.FAIL - mock_print.assert_called_once() - - -class TestValidationEngine: - @pytest.mark.benchmark - def test_validation_engine_creation(self): - """Test ValidationEngine creation.""" - rules = [ - ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - ] - engine = ValidationEngine(rules) - assert engine.rules == rules - - @pytest.mark.benchmark - def test_validation_engine_validator_map(self): - """Test ValidationEngine VALIDATOR_MAP contains expected mappings.""" - engine = ValidationEngine([]) - - expected_mappings = { - "message": CommitMessageValidator, - "subject_capitalized": SubjectCapitalizationValidator, - "imperative": SubjectImperativeValidator, - "subject_max_length": SubjectLengthValidator, - "subject_min_length": SubjectLengthValidator, - "author_name": AuthorValidator, - "author_email": AuthorValidator, - "branch": BranchValidator, - "merge_base": MergeBaseValidator, - "require_signed_off_by": SignoffValidator, - "require_body": BodyValidator, - "allow_merge_commits": CommitTypeValidator, - "allow_revert_commits": CommitTypeValidator, - "allow_empty_commits": CommitTypeValidator, - "allow_fixup_commits": CommitTypeValidator, - "allow_wip_commits": CommitTypeValidator, - "ignore_authors": CommitTypeValidator, - } - - for check, validator_class in expected_mappings.items(): - assert engine.VALIDATOR_MAP[check] == validator_class - - @pytest.mark.benchmark - def test_validation_engine_validate_all_pass(self): - """Test ValidationEngine validate_all with all passing rules.""" - rules = [ - ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - ] - engine = ValidationEngine(rules) - context = ValidationContext(stdin_text="feat: add new feature") - - with patch("commit_check.engine.has_commits", return_value=True): - result = engine.validate_all(context) - assert result == ValidationResult.PASS - - @pytest.mark.benchmark - def test_validation_engine_validate_all_fail(self): - """Test ValidationEngine validate_all with failing rule.""" - rules = [ - ValidationRule( - check="message", - regex="^(feat|fix):", - error="Invalid commit message", - suggest="Use conventional format", - ) - ] - engine = ValidationEngine(rules) - context = ValidationContext(stdin_text="bad commit message") - - with patch("commit_check.engine.has_commits", return_value=True): - result = engine.validate_all(context) - assert result == ValidationResult.FAIL - - @pytest.mark.benchmark - def test_validation_engine_unknown_validator(self): - """Test ValidationEngine with unknown validator type.""" - rules = [ - ValidationRule( - check="unknown_check", - regex="", - error="Unknown error", - suggest="Unknown suggest", - ) - ] - engine = ValidationEngine(rules) - context = ValidationContext(stdin_text="test") - - # Should skip unknown validators and continue - result = engine.validate_all(context) - assert result == ValidationResult.PASS diff --git a/tests/engine_test.py b/tests/engine_test.py index 8cdabf7d..1c49a32d 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -862,3 +862,440 @@ def test_validate_with_scoped_breaking_change(self): # "resolve" is a valid imperative word with scope and breaking change notation result = validator.validate(context) assert result == ValidationResult.PASS + + +# Additional comprehensive coverage tests +class TestCommitMessageValidatorEdgeCases: + """Test edge cases for CommitMessageValidator.""" + + def test_validate_with_valid_conventional_commit(self): + """Test validation with valid conventional commit message.""" + rule = ValidationRule( + check="message", + regex=r"^(feat|fix|chore):.*", + error="Invalid message", + ) + validator = CommitMessageValidator(rule) + context = ValidationContext(stdin_text="feat: add new feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + +class TestSubjectCapitalizationEdgeCases: + """Test additional subject capitalization validator edge cases.""" + + def test_validate_merge_commit_passes(self): + """Test that merge commits pass capitalization check.""" + rule = ValidationRule( + check="subject_capitalized", + error="Subject must be capitalized", + ) + validator = SubjectCapitalizationValidator(rule) + context = ValidationContext(stdin_text="merge: something") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + def test_validate_conventional_commit_capitalized(self): + """Test conventional commit with capitalized description.""" + rule = ValidationRule( + check="subject_capitalized", + error="Subject must be capitalized", + ) + validator = SubjectCapitalizationValidator(rule) + context = ValidationContext(stdin_text="feat: Add new feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + +class TestSubjectImperativeEdgeCases: + """Test additional imperative validator edge cases.""" + + def test_validate_non_match_fallback(self): + """Test that non-imperative verbs fail validation.""" + rule = ValidationRule( + check="imperative", + error="Use imperative mood", + ) + validator = SubjectImperativeValidator(rule) + context = ValidationContext(stdin_text="feat: Added something") + + result = validator.validate(context) + assert result == ValidationResult.FAIL + + +class TestSubjectLengthEdgeCases: + """Test additional subject length validator edge cases.""" + + def test_validate_unknown_check_type(self): + """Test with unknown check type.""" + rule = ValidationRule( + check="unknown_length_check", + value=50, + ) + validator = SubjectLengthValidator(rule) + context = ValidationContext(stdin_text="any subject") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + +class TestAuthorValidatorEdgeCases: + """Test additional author validator edge cases.""" + + def test_validate_regex_first_then_ignored(self): + """Test that regex is checked before ignored list.""" + rule = ValidationRule( + check="author_name", + regex=r"^[A-Z][a-z]+ [A-Z][a-z]+$", + ignored=["bot-user"], + ) + validator = AuthorValidator(rule) + context = ValidationContext(stdin_text="bot-user") + + result = validator.validate(context) + assert result == ValidationResult.FAIL + + +class TestMergeBaseEdgeCases: + """Test additional merge base validator edge cases.""" + + def test_find_target_branch_cleans_remote_prefix(self): + """Test that _find_target_branch cleans remote prefixes.""" + rule = ValidationRule(check="merge_base", regex="^main$") + validator = MergeBaseValidator(rule) + + with patch( + "subprocess.check_output", + return_value="* feature/test\n remotes/origin/main\n", + ): + target = validator._find_target_branch("^main$") + assert target == "main" + + +class TestCommitTypeEdgeCases: + """Test additional commit type validator edge cases.""" + + def test_validate_merge_commit_when_not_allowed(self): + """Test that merge commits are rejected when not allowed.""" + rule = ValidationRule( + check="allow_merge_commits", + value=False, + ) + validator = CommitTypeValidator(rule) + context = ValidationContext(stdin_text="Merge branch 'feature' into main") + + result = validator.validate(context) + assert result == ValidationResult.FAIL + + def test_validate_unknown_check_type(self): + """Test with unknown check type.""" + rule = ValidationRule( + check="unknown_type_check", + value=True, + ) + validator = CommitTypeValidator(rule) + context = ValidationContext(stdin_text="any message") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + +# Tests from engine_comprehensive_test.py +class TestValidationResultComprehensive: + @pytest.mark.benchmark + def test_validation_result_values(self): + """Test ValidationResult enum values.""" + assert ValidationResult.PASS == 0 + assert ValidationResult.FAIL == 1 + + +class TestValidationContextComprehensive: + @pytest.mark.benchmark + def test_validation_context_creation(self): + """Test ValidationContext creation.""" + context = ValidationContext() + assert context.stdin_text is None + assert context.commit_file is None + + context_with_data = ValidationContext( + stdin_text="test commit", commit_file="commit.txt" + ) + assert context_with_data.stdin_text == "test commit" + assert context_with_data.commit_file == "commit.txt" + + +class TestCommitMessageValidatorComprehensive: + @pytest.mark.benchmark + def test_commit_message_validator_creation(self): + """Test CommitMessageValidator creation.""" + rule = ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + validator = CommitMessageValidator(rule) + assert validator.rule == rule + + @patch("commit_check.engine.has_commits") + @pytest.mark.benchmark + def test_commit_message_validator_with_stdin(self, mock_has_commits): + """Test CommitMessageValidator with stdin text.""" + mock_has_commits.return_value = True + + rule = ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + validator = CommitMessageValidator(rule) + context = ValidationContext(stdin_text="feat: add new feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + @patch("commit_check.engine.get_commit_info") + @patch("commit_check.engine.has_commits") + @pytest.mark.benchmark + def test_commit_message_validator_failure( + self, mock_has_commits, mock_get_commit_info + ): + """Test CommitMessageValidator failure case.""" + mock_has_commits.return_value = True + + rule = ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + validator = CommitMessageValidator(rule) + context = ValidationContext(stdin_text="bad commit message") + + with patch.object(validator, "_print_failure") as mock_print: + result = validator.validate(context) + assert result == ValidationResult.FAIL + mock_print.assert_called_once() + + @patch("commit_check.engine.has_commits") + @pytest.mark.benchmark + def test_commit_message_validator_skip_validation(self, mock_has_commits): + """Test CommitMessageValidator skips when no commits and no stdin.""" + mock_has_commits.return_value = False + + rule = ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + validator = CommitMessageValidator(rule) + context = ValidationContext() # No stdin_text + + result = validator.validate(context) + assert result == ValidationResult.PASS + + +class TestSubjectCapitalizationComprehensive: + @pytest.mark.benchmark + def test_subject_capitalization_pass(self): + """Test SubjectCapitalizationValidator pass case.""" + rule = ValidationRule( + check="subject_capitalized", + regex="^[A-Z]", + error="Subject must be capitalized", + suggest="Capitalize first letter", + ) + validator = SubjectCapitalizationValidator(rule) + context = ValidationContext(stdin_text="feat: Add new feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + @pytest.mark.benchmark + def test_subject_capitalization_fail(self): + """Test SubjectCapitalizationValidator fail case.""" + rule = ValidationRule( + check="subject_capitalized", + regex="^[A-Z]", + error="Subject must be capitalized", + suggest="Capitalize first letter", + ) + validator = SubjectCapitalizationValidator(rule) + context = ValidationContext(stdin_text="feat: add new feature") + + with patch.object(validator, "_print_failure") as mock_print: + result = validator.validate(context) + assert result == ValidationResult.FAIL + mock_print.assert_called_once() + + +class TestSubjectImperativeComprehensive: + @pytest.mark.benchmark + def test_subject_imperative_pass(self): + """Test SubjectImperativeValidator pass case.""" + rule = ValidationRule( + check="imperative", + regex="", + error="Subject must be imperative", + suggest="Use imperative mood", + ) + validator = SubjectImperativeValidator(rule) + context = ValidationContext(stdin_text="feat: add new feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + @pytest.mark.benchmark + def test_subject_imperative_fail(self): + """Test SubjectImperativeValidator fail case.""" + rule = ValidationRule( + check="imperative", + regex="", + error="Subject must be imperative", + suggest="Use imperative mood", + ) + validator = SubjectImperativeValidator(rule) + context = ValidationContext(stdin_text="feat: added new feature") + + with patch.object(validator, "_print_failure") as mock_print: + result = validator.validate(context) + assert result == ValidationResult.FAIL + mock_print.assert_called_once() + + +class TestSubjectLengthComprehensive: + @pytest.mark.benchmark + def test_subject_length_pass(self): + """Test SubjectLengthValidator pass case.""" + rule = ValidationRule( + check="subject_max_length", + regex="", + error="Subject too long", + suggest="Keep subject short", + value=50, + ) + validator = SubjectLengthValidator(rule) + context = ValidationContext(stdin_text="Add feature") + + result = validator.validate(context) + assert result == ValidationResult.PASS + + @pytest.mark.benchmark + def test_subject_length_fail(self): + """Test SubjectLengthValidator fail case.""" + rule = ValidationRule( + check="subject_max_length", + regex="", + error="Subject too long: max 10 characters", + suggest="Keep subject short", + value=10, + ) + validator = SubjectLengthValidator(rule) + context = ValidationContext(stdin_text="This is a very long subject line") + + with patch.object(validator, "_print_failure") as mock_print: + result = validator.validate(context) + assert result == ValidationResult.FAIL + mock_print.assert_called_once() + + +class TestValidationEngineComprehensive: + @pytest.mark.benchmark + def test_validation_engine_creation(self): + """Test ValidationEngine creation.""" + rules = [ + ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + ] + engine = ValidationEngine(rules) + assert engine.rules == rules + + @pytest.mark.benchmark + def test_validation_engine_validator_map(self): + """Test ValidationEngine VALIDATOR_MAP contains expected mappings.""" + engine = ValidationEngine([]) + + expected_mappings = { + "message": CommitMessageValidator, + "subject_capitalized": SubjectCapitalizationValidator, + "imperative": SubjectImperativeValidator, + "subject_max_length": SubjectLengthValidator, + "subject_min_length": SubjectLengthValidator, + "author_name": AuthorValidator, + "author_email": AuthorValidator, + "branch": BranchValidator, + "merge_base": MergeBaseValidator, + "require_signed_off_by": SignoffValidator, + "require_body": BodyValidator, + "allow_merge_commits": CommitTypeValidator, + "allow_revert_commits": CommitTypeValidator, + "allow_empty_commits": CommitTypeValidator, + "allow_fixup_commits": CommitTypeValidator, + "allow_wip_commits": CommitTypeValidator, + "ignore_authors": CommitTypeValidator, + } + + for check, validator_class in expected_mappings.items(): + assert engine.VALIDATOR_MAP[check] == validator_class + + @pytest.mark.benchmark + def test_validation_engine_validate_all_pass(self): + """Test ValidationEngine validate_all with all passing rules.""" + rules = [ + ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + ] + engine = ValidationEngine(rules) + context = ValidationContext(stdin_text="feat: add new feature") + + with patch("commit_check.engine.has_commits", return_value=True): + result = engine.validate_all(context) + assert result == ValidationResult.PASS + + @pytest.mark.benchmark + def test_validation_engine_validate_all_fail(self): + """Test ValidationEngine validate_all with failing rule.""" + rules = [ + ValidationRule( + check="message", + regex="^(feat|fix):", + error="Invalid commit message", + suggest="Use conventional format", + ) + ] + engine = ValidationEngine(rules) + context = ValidationContext(stdin_text="bad commit message") + + with patch("commit_check.engine.has_commits", return_value=True): + result = engine.validate_all(context) + assert result == ValidationResult.FAIL + + @pytest.mark.benchmark + def test_validation_engine_unknown_validator(self): + """Test ValidationEngine with unknown validator type.""" + rules = [ + ValidationRule( + check="unknown_check", + regex="", + error="Unknown error", + suggest="Unknown suggest", + ) + ] + engine = ValidationEngine(rules) + context = ValidationContext(stdin_text="test") + + result = engine.validate_all(context) + assert result == ValidationResult.PASS diff --git a/tests/main_test.py b/tests/main_test.py index b03ab76c..e382d9a7 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -2,6 +2,7 @@ import pytest import tempfile import os +from unittest.mock import patch from commit_check.main import StdinReader, _get_message_content, main CMD = "commit-check" @@ -319,3 +320,61 @@ def test_nonexistent_config_file_error(self, capsys): "Error: Specified config file not found: /nonexistent/config.toml" in captured.err ) + + +# Additional coverage tests +class TestMainAdditionalCoverage: + """Additional tests for comprehensive main.py coverage.""" + + def test_main_message_validation_git_format_used(self): + """Test message validation uses git format B.""" + with patch("sys.argv", ["commit-check", "--message"]): + with patch.object(StdinReader, "read_piped_input", return_value=None): + with patch( + "commit_check.util.get_commit_info", return_value="feat: test" + ): + with patch("commit_check.util.has_commits", return_value=True): + result = main() + assert result == 0 + + def test_main_with_file_not_found_exception(self): + """Test main with FileNotFoundError.""" + with patch( + "sys.argv", + [ + "commit-check", + "--config", + "/nonexistent/config.toml", + "--message", + "test.txt", + ], + ): + with patch( + "commit_check.config.load_config", + side_effect=FileNotFoundError("config not found"), + ): + result = main() + assert result == 1 + + +class TestGetMessageContentAdditional: + """Additional tests for _get_message_content.""" + + def test_get_message_content_file_oserror(self): + """Test file reading with OSError.""" + stdin_reader = StdinReader() + + with patch("builtins.open", side_effect=OSError("permission denied")): + result = _get_message_content("/some/file.txt", stdin_reader) + assert result is None + + +class TestStdinReaderAdditional: + """Additional tests for StdinReader.""" + + def test_read_piped_input_strips_whitespace(self): + """Test reading piped input strips whitespace.""" + with patch("sys.stdin.isatty", return_value=False): + with patch("sys.stdin.read", return_value=" data \n"): + result = StdinReader.read_piped_input() + assert result == "data" diff --git a/tests/rule_builder_test.py b/tests/rule_builder_test.py index 09daa0e2..9bced5a0 100644 --- a/tests/rule_builder_test.py +++ b/tests/rule_builder_test.py @@ -155,3 +155,119 @@ def test_rule_builder_boolean_rule_subject_disabled(self): # This should return None when subject_capitalized is False (line 232) rule = builder._build_boolean_rule(catalog_entry, builder.commit_config) assert rule is None + + +# Additional coverage tests +class TestRuleBuilderAdditionalCoverage: + """Additional tests for comprehensive rule_builder.py coverage.""" + + def test_build_length_rule_with_non_integer(self): + """Test length rule building with non-integer config value.""" + config = {"commit": {"subject_max_length": "not an int"}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + length_rules = [r for r in rules if r.check == "subject_max_length"] + assert len(length_rules) == 0 + + def test_build_author_list_rule_with_non_list(self): + """Test author list rule building with non-list config value.""" + config = {"commit": {"ignore_authors": "not a list"}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + author_rules = [r for r in rules if r.check == "ignore_authors"] + assert len(author_rules) == 0 + + def test_build_merge_base_rule_with_non_string(self): + """Test merge base rule building with non-string config value.""" + config = {"branch": {"require_rebase_target": 123}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + merge_rules = [r for r in rules if r.check == "merge_base"] + assert len(merge_rules) == 0 + + def test_build_merge_base_rule_with_empty_string(self): + """Test merge base rule building with empty string.""" + config = {"branch": {"require_rebase_target": ""}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + merge_rules = [r for r in rules if r.check == "merge_base"] + assert len(merge_rules) == 0 + + def test_build_allow_merge_commits_enabled(self): + """Test building allow_merge_commits rule when enabled (default).""" + config = {"commit": {"allow_merge_commits": True}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + merge_rules = [r for r in rules if r.check == "allow_merge_commits"] + assert len(merge_rules) == 0 + + def test_build_length_rule_with_format(self): + """Test length rule with format placeholder.""" + config = {"commit": {"subject_max_length": 72}} + builder = RuleBuilder(config) + + from commit_check.rules_catalog import COMMIT_RULES + + catalog_entry = next( + (r for r in COMMIT_RULES if r.check == "subject_max_length"), None + ) + + if catalog_entry: + rule = builder._build_length_rule(catalog_entry, "subject_max_length") + assert rule is not None + assert "72" in rule.error + + def test_build_author_list_rule_with_empty_list(self): + """Test author list rule building with empty list.""" + config = {"commit": {"ignore_authors": []}} + builder = RuleBuilder(config) + rules = builder.build_all_rules() + + author_rules = [r for r in rules if r.check == "ignore_authors"] + assert len(author_rules) == 0 + + +class TestValidationRuleToDict: + """Test ValidationRule to_dict edge cases.""" + + def test_to_dict_with_all_fields(self): + """Test to_dict with all fields populated.""" + rule = ValidationRule( + check="author_name", + regex=r"^[A-Z].*", + error="Invalid author", + suggest="Use proper name", + value=True, + allowed=["Alice", "Bob"], + ignored=["bot"], + ) + + result = rule.to_dict() + + assert result["check"] == "author_name" + assert result["regex"] == r"^[A-Z].*" + assert result["error"] == "Invalid author" + assert result["suggest"] == "Use proper name" + assert result["value"] is True + assert result["allowed"] == ["Alice", "Bob"] + assert result["allowed_types"] == ["Alice", "Bob"] + assert result["ignored"] == ["bot"] + + def test_to_dict_with_minimal_fields(self): + """Test to_dict with minimal fields.""" + rule = ValidationRule(check="message") + + result = rule.to_dict() + + assert result["check"] == "message" + assert result["regex"] == "" + assert result["error"] == "" + assert result["suggest"] == "" + assert "value" not in result + assert "allowed" not in result + assert "ignored" not in result diff --git a/tests/util_test.py b/tests/util_test.py index 4e34dfc3..398552ae 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -8,8 +8,14 @@ from commit_check.util import print_error_header from commit_check.util import print_error_message from commit_check.util import print_suggestion +from commit_check.util import _find_check +from commit_check.util import _print_failure +from commit_check.util import _find_config_file +from commit_check.util import _load_toml +from commit_check.util import validate_config from subprocess import CalledProcessError, PIPE from unittest.mock import MagicMock +from unittest.mock import patch class TestUtil: @@ -339,3 +345,170 @@ def test_print_suggestion_exit1(self, capfd): assert e.value.code == 1 stdout, _ = capfd.readouterr() assert "commit-check does not support" in stdout + + +# Additional coverage tests +class TestFindCheck: + """Test the _find_check function.""" + + def test_find_check_with_matching_type(self): + """Test finding a check by type.""" + checks = [ + {"check": "message", "regex": ".*"}, + {"check": "branch", "regex": ".*"}, + ] + result = _find_check(checks, "branch") + assert result == {"check": "branch", "regex": ".*"} + + def test_find_check_with_no_match(self): + """Test when no check matches.""" + checks = [{"check": "message", "regex": ".*"}] + result = _find_check(checks, "author_name") + assert result is None + + +class TestPrintFailure: + """Test the _print_failure function.""" + + def test_print_failure_first_call_prints_header(self, capsys): + """Test that header is printed on first failure.""" + print_error_header.has_been_called = False + check = {"check": "message", "error": "Invalid format", "suggest": "Use feat:"} + _print_failure(check, "^feat:.*", "wrong message") + + captured = capsys.readouterr() + assert "Commit rejected" in captured.out + assert "check failed ==>" in captured.out + assert "Suggest:" in captured.out + + def test_print_failure_subsequent_call_no_header(self, capsys): + """Test that header is not printed on subsequent failures.""" + print_error_header.has_been_called = True + check = {"check": "branch", "error": "Invalid branch"} + _print_failure(check, "^feature/.*", "wrong-branch") + + captured = capsys.readouterr() + assert "CHECK" not in captured.out + assert "check failed ==>" in captured.out + + +class TestPrintSuggestionEdgeCases: + """Test print_suggestion error paths.""" + + def test_print_suggestion_with_none_raises_system_exit(self, capsys): + """Test that None suggestion raises SystemExit.""" + with pytest.raises(SystemExit) as exc_info: + print_suggestion(None) + assert exc_info.value.code == 1 + + captured = capsys.readouterr() + assert "commit-check does not support" in captured.out + + +class TestTomlLoading: + """Test TOML loading edge cases.""" + + def test_load_toml_with_nonexistent_file(self, tmp_path): + """Test loading TOML from nonexistent file.""" + nonexistent = tmp_path / "does_not_exist.toml" + result = _load_toml(nonexistent) + assert result == {} + + def test_load_toml_with_invalid_toml(self, tmp_path): + """Test loading invalid TOML file.""" + invalid_toml = tmp_path / "invalid.toml" + invalid_toml.write_text("this is not valid toml ][") + result = _load_toml(invalid_toml) + assert result == {} + + @patch("commit_check.util._toml", None) + def test_load_toml_without_toml_module(self, tmp_path): + """Test loading TOML when toml module is not available.""" + valid_toml = tmp_path / "valid.toml" + valid_toml.write_text("[commit]\nconventional_commits = true") + result = _load_toml(valid_toml) + assert result == {} + + +class TestFindConfigFile: + """Test config file finding logic.""" + + def test_find_config_file_in_directory_commit_check_toml(self, tmp_path): + """Test finding commit-check.toml in directory.""" + config_file = tmp_path / "commit-check.toml" + config_file.write_text("[commit]") + result = _find_config_file(str(tmp_path)) + assert result == config_file + + def test_find_config_file_in_directory_cchk_toml(self, tmp_path): + """Test finding cchk.toml when commit-check.toml doesn't exist.""" + config_file = tmp_path / "cchk.toml" + config_file.write_text("[commit]") + result = _find_config_file(str(tmp_path)) + assert result == config_file + + def test_find_config_file_priority(self, tmp_path): + """Test that commit-check.toml has priority over cchk.toml.""" + commit_check_file = tmp_path / "commit-check.toml" + cchk_file = tmp_path / "cchk.toml" + commit_check_file.write_text("[commit]") + cchk_file.write_text("[branch]") + result = _find_config_file(str(tmp_path)) + assert result == commit_check_file + + def test_find_config_file_explicit_toml_path(self, tmp_path): + """Test finding explicitly specified TOML file.""" + config_file = tmp_path / "custom.toml" + config_file.write_text("[commit]") + result = _find_config_file(str(config_file)) + assert result == config_file + + def test_find_config_file_nonexistent_explicit_path(self, tmp_path): + """Test with nonexistent explicit file path.""" + nonexistent = tmp_path / "nonexistent.toml" + result = _find_config_file(str(nonexistent)) + assert result is None + + def test_find_config_file_non_toml_extension(self, tmp_path): + """Test with non-TOML file extension.""" + yaml_file = tmp_path / "config.yaml" + yaml_file.write_text("commit: {}") + result = _find_config_file(str(yaml_file)) + assert result is None + + def test_find_config_file_empty_directory(self, tmp_path): + """Test finding config in empty directory.""" + result = _find_config_file(str(tmp_path)) + assert result is None + + +class TestValidateConfigYamlFallback: + """Test validate_config YAML fallback paths.""" + + def test_validate_config_with_yaml_fallback(self, tmp_path): + """Test YAML fallback when TOML not found.""" + yaml_file = tmp_path / "config.yml" + yaml_file.write_text("checks:\n - check: message\n regex: .*") + result = validate_config(str(yaml_file)) + assert "checks" in result + assert len(result["checks"]) > 0 + + def test_validate_config_yaml_not_found(self, tmp_path): + """Test YAML fallback with nonexistent file.""" + nonexistent = tmp_path / "nonexistent.yml" + result = validate_config(str(nonexistent)) + assert result == {} + + def test_validate_config_invalid_yaml(self, tmp_path): + """Test YAML fallback with invalid YAML.""" + invalid_yaml = tmp_path / "invalid.yml" + invalid_yaml.write_text("invalid: yaml: content: [") + result = validate_config(str(invalid_yaml)) + assert result == {} + + def test_validate_config_empty_yaml(self, tmp_path): + """Test YAML fallback with empty file.""" + empty_yaml = tmp_path / "empty.yml" + empty_yaml.write_text("") + result = validate_config(str(empty_yaml)) + assert result == {} From 74513f72fe2f1d1f4087c3c2ed3203d7b9b69358 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 14 Dec 2025 17:28:21 +0200 Subject: [PATCH 2/5] feat: Add 'tomli' to test dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dce7b4ea..70c8a0f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ tracker = "https://github.com/commit-check/commit-check/issues" [project.optional-dependencies] dev = ['nox'] -test = ['coverage', 'pytest', 'pytest-mock', 'pytest-codspeed'] +test = ['coverage', 'pytest', 'pytest-mock', 'pytest-codspeed', "tomli"] docs = ['sphinx<9', 'sphinx-immaterial', 'sphinx-autobuild', 'sphinx_issues'] [tool.setuptools] From d90f1d82a8d81ab13db36d96d4f672c22ff111af Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 14 Dec 2025 21:03:21 +0200 Subject: [PATCH 3/5] fix(tests): Update exception handling and clean up test cases --- tests/config_test.py | 3 +-- tests/engine_test.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/config_test.py b/tests/config_test.py index 192912a6..c3faa679 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -216,7 +216,7 @@ def test_load_config_invalid_toml(self, tmp_path): invalid_config = tmp_path / "invalid.toml" invalid_config.write_text("invalid toml [[[") - with pytest.raises(Exception): # Could be TOMLDecodeError or similar + with pytest.raises(Exception): # TOMLDecodeError varies by library load_config(str(invalid_config)) def test_load_config_file_permission_error(self, tmp_path, monkeypatch): @@ -276,7 +276,6 @@ def test_tomli_import_fallback(self, monkeypatch): # Force reimport of config module to restore original state if "commit_check.config" in sys.modules: del sys.modules["commit_check.config"] - import commit_check.config # noqa: F401 # Tests from config_fallback_test.py diff --git a/tests/engine_test.py b/tests/engine_test.py index 1c49a32d..c2fd7f79 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1061,9 +1061,7 @@ def test_commit_message_validator_with_stdin(self, mock_has_commits): @patch("commit_check.engine.get_commit_info") @patch("commit_check.engine.has_commits") @pytest.mark.benchmark - def test_commit_message_validator_failure( - self, mock_has_commits, mock_get_commit_info - ): + def test_commit_message_validator_failure(self, mock_has_commits): """Test CommitMessageValidator failure case.""" mock_has_commits.return_value = True From 7912080531f855a4a362f7b5a7a876b35d626b39 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:15:52 +0200 Subject: [PATCH 4/5] fix(tests): Add missing mock parameter to test method signature (#332) --- tests/engine_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/engine_test.py b/tests/engine_test.py index c2fd7f79..e9541d99 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1061,7 +1061,7 @@ def test_commit_message_validator_with_stdin(self, mock_has_commits): @patch("commit_check.engine.get_commit_info") @patch("commit_check.engine.has_commits") @pytest.mark.benchmark - def test_commit_message_validator_failure(self, mock_has_commits): + def test_commit_message_validator_failure(self, mock_has_commits, mock_get_commit_info): """Test CommitMessageValidator failure case.""" mock_has_commits.return_value = True From 65a259b0e5bb6f26399d3c8bbc705a051c5e32bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 06:16:47 +0000 Subject: [PATCH 5/5] ci: auto fixes from pre-commit.com hooks --- tests/engine_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/engine_test.py b/tests/engine_test.py index e9541d99..1c49a32d 100644 --- a/tests/engine_test.py +++ b/tests/engine_test.py @@ -1061,7 +1061,9 @@ def test_commit_message_validator_with_stdin(self, mock_has_commits): @patch("commit_check.engine.get_commit_info") @patch("commit_check.engine.has_commits") @pytest.mark.benchmark - def test_commit_message_validator_failure(self, mock_has_commits, mock_get_commit_info): + def test_commit_message_validator_failure( + self, mock_has_commits, mock_get_commit_info + ): """Test CommitMessageValidator failure case.""" mock_has_commits.return_value = True