diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a32f07c..ed65ca0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,12 @@ repos: - repo: local hooks: + - id: run-tests + name: Run tests + entry: pipenv run test + language: system + pass_filenames: false + - id: commitlint name: Commitlint entry: python -m src.commitlint.cli --file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5793352..1378c37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Install Development Dependencies (Using Pipenv) -All the dependencies are managed by Pipenv. Please install Pipenv on your system first by following the instructions at [https://pipenv.pypa.io/en/latest/installation/](https://pipenv.pypa.io/en/latest/installation/). +All the dependencies are managed by Pipenv. Please install Pipenv on your system first by following the instructions at [https://pipenv.pypa.io/en/latest/installation.html](https://pipenv.pypa.io/en/latest/installation.html). Once Pipenv is installed, you can install the development dependencies by running the following command: @@ -17,3 +17,11 @@ To install pre-commit and commit-msg hook for this project, run the following co ```bash pipenv run install-hooks ``` + +## Run tests + +Run the tests using the below command: + +```bash +pipenv run test +``` diff --git a/Pipfile b/Pipfile index 7e9b042..5bc4ca0 100644 --- a/Pipfile +++ b/Pipfile @@ -13,4 +13,5 @@ pytest-cov = "*" [scripts] test = "pytest" coverage = "pytest --cov=src/ --no-cov-on-fail" +coverage-html = "pytest --cov=src/ --cov-report=html --no-cov-on-fail" install-hooks = "pre-commit install --hook-type pre-commit --hook-type commit-msg" diff --git a/src/commitlint/cli.py b/src/commitlint/cli.py index b865d71..f330959 100644 --- a/src/commitlint/cli.py +++ b/src/commitlint/cli.py @@ -82,10 +82,10 @@ def main() -> None: if success: sys.stdout.write(f"{COMMIT_SUCCESSFUL}\n") sys.exit(0) - - _show_errors(errors) - sys.exit(1) + else: + _show_errors(errors) + sys.exit(1) if __name__ == "__main__": - main() + main() # pragma: no cover diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..d884a46 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,98 @@ +# type: ignore +# pylint: disable=all + +from unittest.mock import MagicMock, call, mock_open, patch + +from src.commitlint.cli import get_args, main +from src.commitlint.messages import COMMIT_SUCCESSFUL, INCORRECT_FORMAT_ERROR + + +class TestCLI: + @patch( + "argparse.ArgumentParser.parse_args", + return_value=MagicMock(commit_message="commit message", file=None), + ) + def test__get_args__with_commit_message(self, *_): + args = get_args() + assert args.commit_message == "commit message" + assert args.file is None + + @patch( + "argparse.ArgumentParser.parse_args", + return_value=MagicMock(commit_message=None, file="path/to/file.txt"), + ) + def test__get_args__with_file(self, *_): + args = get_args() + assert args.file == "path/to/file.txt" + assert args.commit_message is None + + @patch("argparse.ArgumentParser.error") + def test__get_args__without_commit_message_and_file(self, mock_error): + get_args() + mock_error.assert_called_with( + "Please provide either a commit message or a file." + ) + + @patch( + "src.commitlint.cli.get_args", + return_value=MagicMock(commit_message="feat: valid commit message", file=None), + ) + @patch("sys.stdout.write") + @patch("sys.exit") + def test__main__valid_commit_message( + self, + mock_sys_exit, + mock_stdout_write, + *_, + ): + main() + mock_sys_exit.assert_called_with(0) + mock_stdout_write.assert_called_with(f"{COMMIT_SUCCESSFUL}\n") + + @patch( + "src.commitlint.cli.get_args", + return_value=MagicMock(commit_message="Invalid commit message", file=None), + ) + @patch("sys.stderr.write") + @patch("sys.exit") + def test__main__invalid_commit_message( + self, + mock_sys_exit, + mock_stderr_write, + *_, + ): + main() + mock_sys_exit.assert_called_with(1) + mock_stderr_write.assert_has_calls( + [call("✖ Found 1 errors.\n\n"), call(f"- {INCORRECT_FORMAT_ERROR}\n\n")] + ) + + @patch( + "src.commitlint.cli.get_args", + return_value=MagicMock(file="path/to/file.txt", commit_message=None), + ) + @patch("sys.stdout.write") + @patch("sys.exit") + @patch("builtins.open", mock_open(read_data="feat: valid commit message")) + def test__main__valid_commit_message_from_file( + self, mock_sys_exit, mock_stdout_write, *_ + ): + main() + mock_sys_exit.assert_called_with(0) + mock_stdout_write.assert_called_with(f"{COMMIT_SUCCESSFUL}\n") + + @patch( + "src.commitlint.cli.get_args", + return_value=MagicMock(file="path/to/file.txt", commit_message=None), + ) + @patch("sys.stderr.write") + @patch("sys.exit") + @patch("builtins.open", mock_open(read_data="Invalid commit message")) + def test__main__invalid_commit_message_from_file( + self, mock_sys_exit, mock_stderr_write, *_ + ): + main() + mock_sys_exit.assert_called_with(1) + mock_stderr_write.assert_has_calls( + [call("✖ Found 1 errors.\n\n"), call(f"- {INCORRECT_FORMAT_ERROR}\n\n")] + ) diff --git a/tests/test_commitlint/__init__.py b/tests/test_commitlint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_commitlint/test_check_commit_message.py b/tests/test_commitlint/test_check_commit_message.py new file mode 100644 index 0000000..6b00fd1 --- /dev/null +++ b/tests/test_commitlint/test_check_commit_message.py @@ -0,0 +1,94 @@ +# type: ignore +# pylint: disable=all + +from src.commitlint import check_commit_message +from src.commitlint.constants import COMMIT_MAX_LENGTH +from src.commitlint.messages import HEADER_LENGTH_ERROR, INCORRECT_FORMAT_ERROR + + +def test__check_commit_message__header_length_error(): + commit_message = "feat: " + "a" * (COMMIT_MAX_LENGTH + 1) + success, errors = check_commit_message(commit_message) + assert success is False + assert HEADER_LENGTH_ERROR in errors + + +def test__check_commit_message__header_length_valid(): + commit_message = "feat: " + "a" * (COMMIT_MAX_LENGTH - 1) + success, errors = check_commit_message(commit_message) + assert success is False + assert HEADER_LENGTH_ERROR in errors + + +def test__check_commit_message__incorrect_format_error(): + commit_message = "This is an invalid commit message" + success, errors = check_commit_message(commit_message) + assert success is False + assert INCORRECT_FORMAT_ERROR in errors + + +def test__check_commit_message__incorrect_format_error_and_health_length_invalid(): + commit_message = "Test " + "a" * (COMMIT_MAX_LENGTH + 1) + success, errors = check_commit_message(commit_message) + assert success is False + assert HEADER_LENGTH_ERROR in errors + assert INCORRECT_FORMAT_ERROR in errors + + +def test__check_commit_message__valid(): + commit_message = "feat: add new feature" + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] + + +def test__check_commit_message__valid_with_scope(): + commit_message = "feat(scope): add new feature" + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] + + +def test__check_commit_message__empty_scope_error(): + commit_message = "feat(): add new feature" + success, errors = check_commit_message(commit_message) + assert success is False + assert INCORRECT_FORMAT_ERROR in errors + + +def test__check_commit_message__valid_with_body(): + commit_message = "fix(scope): fix a bug\n\nThis is the body of the commit message." + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] + + +def test__check_commit_message__header_line_error(): + commit_message = "feat(): add new feature\ntest" + success, errors = check_commit_message(commit_message) + assert success is False + assert INCORRECT_FORMAT_ERROR in errors + + +def test__check_commit_message__with_comments(): + commit_message = "feat(scope): add new feature\n#this is a comment" + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] + + +def test__check_commit_message__with_diff(): + commit_message = ( + "fix: fixed a bug\n\nthis is body\n" + "# ------------------------ >8 ------------------------\nDiff message" + ) + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] + + +def test__check_commit_message__ignored(): + commit_message = "Merge pull request #123" + success, errors = check_commit_message(commit_message) + assert success is True + assert errors == [] diff --git a/tests/test_commitlint/test_is_ingored.py b/tests/test_commitlint/test_is_ingored.py new file mode 100644 index 0000000..1c2b135 --- /dev/null +++ b/tests/test_commitlint/test_is_ingored.py @@ -0,0 +1,34 @@ +# type: ignore +# pylint: disable=all + +import pytest + +from src.commitlint.commitlint import is_ignored + + +@pytest.mark.parametrize( + "commit_message, expected_result", + [ + ("Merge pull request #123", True), + ("Merge feature-branch into production", True), + ("Merge branch hotfix-123", True), + ("Merge tag release-v2.0.1", True), + ('Revert "Undo last commit"', True), + ("revert Fix-Typo", True), + ("Merged bugfix-789 in master", True), + ("Merged PR #987: Update documentation", True), + ("Merge remote-tracking branch upstream/develop", True), + ("Automatic merge from CI/CD", True), + ("Auto-merged feature-branch into staging", True), + ("Merge tag v3.5.0", True), + ("Merge pull request #456: Feature XYZ", True), + ('Revert "Apply security patch"', True), + ("Merged PR #321: Bugfix - Resolve issue with login", True), + ("Merge my feature", False), + ("Add new feature", False), + ("feat: this is conventional commit format", False), + ], +) +def test__is_ignored(commit_message, expected_result): + result = is_ignored(commit_message) + assert result == expected_result diff --git a/tests/test_commitlint/test_remove_comments.py b/tests/test_commitlint/test_remove_comments.py new file mode 100644 index 0000000..31ddef1 --- /dev/null +++ b/tests/test_commitlint/test_remove_comments.py @@ -0,0 +1,47 @@ +# type: ignore +# pylint: disable=all + +from src.commitlint.commitlint import remove_comments + + +def test__remove_comments__no_comments(): + input_msg = "Commit message without comments" + expected_output = "Commit message without comments" + result = remove_comments(input_msg) + assert result == expected_output + + +def test__remove_comments__with_comments(): + input_msg = "# Comment\nRegular text" + expected_output = "Regular text" + result = remove_comments(input_msg) + assert result == expected_output + + +def test__remove_comments__with_diff_message(): + input_msg = ( + "Fix a bug\n" + "# ------------------------ >8 ------------------------\n" + "Diff message" + ) + expected_output = "Fix a bug" + result = remove_comments(input_msg) + assert result == expected_output + + +def test__remove_comments__multiple_comments(): + input_msg = "New feature\n# Comment\n# Another comment" + expected_output = "New feature" + result = remove_comments(input_msg) + assert result == expected_output + + +def test__remove_comments__comments_before_diff(): + input_msg = ( + "#Comments\n" + "# ------------------------ >8 ------------------------\n" + "Diff message" + ) + expected_output = "" + result = remove_comments(input_msg) + assert result == expected_output