From 52160677cb574ec2d1ea2494f35e835f491c6bb6 Mon Sep 17 00:00:00 2001 From: Ajesh Sen Thapa Date: Thu, 16 May 2024 22:58:58 +0545 Subject: [PATCH] feat: add verbose option - Added verbose option on cli. - Added verbose option on github actions. - Updated doc for verbose and quiet. --- .github/workflows/test_action.yaml | 2 + README.md | 25 +++- action.yml | 29 +++-- github_actions/run.py | 21 ++-- src/commitlint/cli.py | 80 +++++++------ src/commitlint/config.py | 78 +++++++++++++ src/commitlint/git_helpers.py | 21 +++- src/commitlint/linter/_linter.py | 8 ++ src/commitlint/linter/validators.py | 5 +- src/commitlint/output.py | 51 +++++++++ tests/test_cli.py | 171 +++++++++++++++++++--------- tests/test_config.py | 35 ++++++ tests/test_output.py | 54 +++++++++ 13 files changed, 464 insertions(+), 116 deletions(-) create mode 100644 src/commitlint/config.py create mode 100644 src/commitlint/output.py create mode 100644 tests/test_config.py create mode 100644 tests/test_output.py diff --git a/.github/workflows/test_action.yaml b/.github/workflows/test_action.yaml index eb2ed63..f4ce9ca 100644 --- a/.github/workflows/test_action.yaml +++ b/.github/workflows/test_action.yaml @@ -16,3 +16,5 @@ jobs: uses: ./ # Uses an action in the root directory # or use a released GitHub Action # uses: opensource-nepal/commitlint@v0.2.1 + with: + verbose: true diff --git a/README.md b/README.md index 4ab3ede..95d414d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ jobs: | # | Name | Type | Default | Description | | --- | ----------------- | ------- | ------- | --------------------------------------------------------------------- | | 1 | **fail_on_error** | Boolean | true | Determines whether the GitHub Action should fail if commitlint fails. | +| 2 | **verbose** | Boolean | true | Verbose output. | #### GitHub Action Outputs @@ -121,23 +122,23 @@ pip install commitlint ### Usage ``` -$ commitlint --help -usage: commitlint [-h] [-V] [--file FILE] [--hash HASH] [--from-hash FROM_HASH] [--to-hash TO_HASH] [--skip-detail] [commit_message] - -Check if a commit message follows the conventional commit format. +commitlint [-h] [-V] [--file FILE] [--hash HASH] [--from-hash FROM_HASH] [--to-hash TO_HASH] [--skip-detail] [-q | -v] + [commit_message] positional arguments: - commit_message The commit message to be checked. + commit_message The commit message to be checked optional arguments: -h, --help show this help message and exit -V, --version show program's version number and exit - --file FILE Path to a file containing the commit message. + --file FILE Path to a file containing the commit message --hash HASH Commit hash --from-hash FROM_HASH From commit hash --to-hash TO_HASH To commit hash --skip-detail Skip the detailed error message check + -q, --quiet Ignore stdout and stderr + -v, --verbose Verbose output ``` ### Examples @@ -174,6 +175,18 @@ $ commitlint --skip-detail "chore: my commit message" $ commitlint --skip-detail --hash 9a8c08173 ``` +Run commitlint in quiet mode: + +```shell +$ commitlint --quiet "chore: my commit message" +``` + +Run commitlint in verbose mode: + +```shell +$ commitlint --verbose "chore: my commit message" +``` + Version check: ```shell diff --git a/action.yml b/action.yml index ce16dd4..c69c42b 100644 --- a/action.yml +++ b/action.yml @@ -1,27 +1,31 @@ -name: "Conventional Commitlint" -description: "A GitHub Action to check conventional commit message" +name: 'Conventional Commitlint' +description: 'A GitHub Action to check conventional commit message' inputs: fail_on_error: description: Whether to fail the workflow if commit messages don't follow conventions. default: 'true' required: false + verbose: + description: Verbose output. + default: 'false' + required: false outputs: - status: - description: Status - value: ${{ steps.commitlint.outputs.status }} - exit_code: - description: Exit Code - value: ${{ steps.commitlint.outputs.exit_code }} + status: + description: Status + value: ${{ steps.commitlint.outputs.status }} + exit_code: + description: Exit Code + value: ${{ steps.commitlint.outputs.exit_code }} branding: - color: "red" - icon: "git-commit" + color: 'red' + icon: 'git-commit' runs: - using: "composite" + using: 'composite' steps: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: '3.8' - name: Install Commitlint run: python -m pip install -e ${{ github.action_path }} @@ -59,3 +63,4 @@ runs: shell: bash env: INPUT_FAIL_ON_ERROR: ${{ inputs.fail_on_error }} + INPUT_VERBOSE: ${{ inputs.verbose }} diff --git a/github_actions/run.py b/github_actions/run.py index 4491b5e..1822bf7 100644 --- a/github_actions/run.py +++ b/github_actions/run.py @@ -16,6 +16,7 @@ # Inputs INPUT_FAIL_ON_ERROR = "INPUT_FAIL_ON_ERROR" +INPUT_VERBOSE = "INPUT_VERBOSE" # Status STATUS_SUCCESS = "success" @@ -136,14 +137,20 @@ def _check_commits(from_hash: str, to_hash: str) -> None: """ sys.stdout.write(f"Commit from {from_hash} to {to_hash}\n") try: + commands = [ + "commitlint", + "--from-hash", + from_hash, + "--to-hash", + to_hash, + ] + + verbose = _parse_boolean_input(_get_input(INPUT_VERBOSE)) + if verbose: + commands.append("--verbose") + output = subprocess.check_output( - [ - "commitlint", - "--from-hash", - from_hash, - "--to-hash", - to_hash, - ], + commands, text=True, ).strip() sys.stdout.write(f"{output}\n") diff --git a/src/commitlint/cli.py b/src/commitlint/cli.py index 9fe7cc9..2914c55 100644 --- a/src/commitlint/cli.py +++ b/src/commitlint/cli.py @@ -18,7 +18,9 @@ import sys from typing import List +from . import output from .__version__ import __version__ +from .config import config from .exceptions import CommitlintException from .git_helpers import get_commit_message_of_hash, get_commit_messages_of_hash_range from .linter import lint_commit_message @@ -48,10 +50,10 @@ def get_args() -> argparse.Namespace: # for commit message check group = parser.add_mutually_exclusive_group(required=True) group.add_argument( - "commit_message", nargs="?", type=str, help="The commit message to be checked." + "commit_message", nargs="?", type=str, help="The commit message to be checked" ) group.add_argument( - "--file", type=str, help="Path to a file containing the commit message." + "--file", type=str, help="Path to a file containing the commit message" ) group.add_argument("--hash", type=str, help="Commit hash") group.add_argument("--from-hash", type=str, help="From commit hash") @@ -64,14 +66,26 @@ def get_args() -> argparse.Namespace: action="store_true", help="Skip the detailed error message check", ) + + output_group = parser.add_mutually_exclusive_group(required=False) # --quiet option is optional - parser.add_argument( + output_group.add_argument( "-q", "--quiet", action="store_true", help="Ignore stdout and stderr", default=False, ) + + # --verbose option is optional + output_group.add_argument( + "-v", + "--verbose", + action="store_true", + help="Verbose output", + default=False, + ) + # parsing args args = parser.parse_args() @@ -95,15 +109,15 @@ def _show_errors( error_count = len(errors) commit_message = remove_comments(commit_message) - sys.stderr.write(f"⧗ Input:\n{commit_message}\n\n") + output.error(f"⧗ Input:\n{commit_message}\n") if skip_detail: - sys.stderr.write(f"{VALIDATION_FAILED}\n") + output.error(VALIDATION_FAILED) return - sys.stderr.write(f"✖ Found {error_count} error(s).\n") + output.error(f"✖ Found {error_count} error(s).") for error in errors: - sys.stderr.write(f"- {error}\n") + output.error(f"- {error}") def _get_commit_message_from_file(filepath: str) -> str: @@ -121,42 +135,35 @@ def _get_commit_message_from_file(filepath: str) -> str: IOError: If there is an issue reading the file. """ abs_filepath = os.path.abspath(filepath) + output.verbose(f"reading commit message from file {abs_filepath}") with open(abs_filepath, encoding="utf-8") as commit_message_file: commit_message = commit_message_file.read().strip() return commit_message -def _handle_commit_message( - commit_message: str, skip_detail: bool, quiet: bool = False -) -> None: +def _handle_commit_message(commit_message: str, skip_detail: bool) -> None: """ Handles a single commit message, checks its validity, and prints the result. Args: commit_message (str): The commit message to be handled. skip_detail (bool): Whether to skip the detailed error linting. - quiet (bool): Whether to ignore stout and stderr Raises: SystemExit: If the commit message is invalid. """ success, errors = lint_commit_message(commit_message, skip_detail=skip_detail) - if success and quiet: - return - if success: - sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n") + output.success(VALIDATION_SUCCESSFUL) return - if not quiet: - _show_errors(commit_message, errors, skip_detail=skip_detail) - + _show_errors(commit_message, errors, skip_detail=skip_detail) sys.exit(1) def _handle_multiple_commit_messages( - commit_messages: List[str], skip_detail: bool, quiet: bool = False + commit_messages: List[str], skip_detail: bool ) -> None: """ Handles multiple commit messages, checks their validity, and prints the result. @@ -164,7 +171,6 @@ def _handle_multiple_commit_messages( Args: commit_messages (List[str]): List of commit messages to be handled. skip_detail (bool): Whether to skip the detailed error linting. - quiet (bool): Whether to show the error and messages in console Raises: SystemExit: If any of the commit messages is invalid. """ @@ -173,18 +179,17 @@ def _handle_multiple_commit_messages( for commit_message in commit_messages: success, errors = lint_commit_message(commit_message, skip_detail=skip_detail) if success: + output.verbose("lint success") continue has_error = True - if not quiet: - _show_errors(commit_message, errors, skip_detail=skip_detail) - sys.stderr.write("\n") + _show_errors(commit_message, errors, skip_detail=skip_detail) + output.error("") if has_error: sys.exit(1) - if not quiet: - sys.stdout.write(f"{VALIDATION_SUCCESSFUL}\n") + output.success(VALIDATION_SUCCESSFUL) def main() -> None: @@ -193,31 +198,34 @@ def main() -> None: """ args = get_args() + # setting config based on args + config.quiet = args.quiet + config.verbose = args.verbose + + output.verbose("starting commitlint") try: if args.file: + output.verbose("checking commit from file") commit_message = _get_commit_message_from_file(args.file) - _handle_commit_message( - commit_message, skip_detail=args.skip_detail, quiet=args.quiet - ) + _handle_commit_message(commit_message, skip_detail=args.skip_detail) elif args.hash: + output.verbose("checking commit from hash") commit_message = get_commit_message_of_hash(args.hash) - _handle_commit_message( - commit_message, skip_detail=args.skip_detail, quiet=args.quiet - ) + _handle_commit_message(commit_message, skip_detail=args.skip_detail) elif args.from_hash: + output.verbose("checking commit from hash range") commit_messages = get_commit_messages_of_hash_range( args.from_hash, args.to_hash ) _handle_multiple_commit_messages( - commit_messages, skip_detail=args.skip_detail, quiet=args.quiet + commit_messages, skip_detail=args.skip_detail ) else: + output.verbose("checking commit message") commit_message = args.commit_message.strip() - _handle_commit_message( - commit_message, skip_detail=args.skip_detail, quiet=args.quiet - ) + _handle_commit_message(commit_message, skip_detail=args.skip_detail) except CommitlintException as ex: - sys.stderr.write(f"{ex}\n") + output.error(f"{ex}") sys.exit(1) diff --git a/src/commitlint/config.py b/src/commitlint/config.py new file mode 100644 index 0000000..f4ab9f4 --- /dev/null +++ b/src/commitlint/config.py @@ -0,0 +1,78 @@ +""" +Contains config for the commitlint. +""" + +from typing import Optional + + +class _CommitlintConfig: + """ + Singleton class for storing commitlint configs + """ + + _instance: Optional["_CommitlintConfig"] = None # for singleton property + + _verbose: bool = False + _quiet: bool = False + + def __new__(cls) -> "_CommitlintConfig": + """ + Return singleton instance. + """ + if cls._instance is None: + cls._instance = super().__new__(cls) + + return cls._instance + + @property + def verbose(self) -> bool: + """ + Get the current verbose setting. + + Returns: + bool: The current verbose setting. + """ + return self._verbose + + @verbose.setter + def verbose(self, value: bool) -> None: + """ + Set the verbose setting. + + Args: + value (bool): New value for verbose setting. + """ + if value: + self._quiet = False + + self._verbose = value + + @property + def quiet(self) -> bool: + """ + Get the current quiet setting. + + Returns: + bool: The current quiet setting. + """ + return self._quiet + + @quiet.setter + def quiet(self, value: bool) -> None: + """ + Set the quiet setting. + + Args: + value (bool): New value for quiet setting. + """ + if value: + self._verbose = False + + self._quiet = value + + +config = _CommitlintConfig() + +__all__ = [ + "config", +] diff --git a/src/commitlint/git_helpers.py b/src/commitlint/git_helpers.py index c99f5c3..daeed3a 100644 --- a/src/commitlint/git_helpers.py +++ b/src/commitlint/git_helpers.py @@ -5,6 +5,7 @@ import subprocess from typing import List +from . import output from .exceptions import GitCommitNotFoundException, GitInvalidCommitRangeException @@ -23,16 +24,21 @@ def get_commit_message_of_hash(commit_hash: str) -> str: GitCommitNotFoundException: If the specified commit hash is not found or if there is an error retrieving the commit message. """ + output.verbose(f"fetching commit message from hash {commit_hash}") try: # Run 'git show --format=%B -s' command to get the commit message + output.verbose(f"executing: git show --format=%B -s {commit_hash}") commit_message = subprocess.check_output( ["git", "show", "--format=%B", "-s", commit_hash], text=True, stderr=subprocess.PIPE, ).strip() + output.verbose(commit_message) return commit_message - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as ex: + output.verbose("unable to fetch commit message using git command") + output.verbose(f"{ex.__class__.__name__}: {ex}") raise GitCommitNotFoundException( f"Failed to retrieve commit message for hash {commit_hash}" ) from None @@ -64,6 +70,9 @@ def get_commit_messages_of_hash_range( """ # as the commit range doesn't support initial commit hash, # commit message of `from_hash` is taken separately + output.verbose( + f"fetching commit messages from hash range, from: {from_hash}, to: {to_hash}" + ) from_commit_message = get_commit_message_of_hash(from_hash) try: @@ -73,18 +82,26 @@ def get_commit_messages_of_hash_range( delimiter = "========commit-delimiter========" hash_range = f"{from_hash}..{to_hash}" + output.verbose( + f"executing: git log --format=%B{delimiter} --reverse {hash_range}" + ) commit_messages_output = subprocess.check_output( ["git", "log", f"--format=%B{delimiter}", "--reverse", hash_range], text=True, stderr=subprocess.PIPE, ) + output.verbose(commit_messages_output) + commit_messages = commit_messages_output.split(f"{delimiter}\n") return [from_commit_message] + [ commit_message.strip() for commit_message in commit_messages if commit_message.strip() ] - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as ex: + output.verbose("unable to fetch commit messages using git command") + output.verbose(f"{ex.__class__.__name__}: {ex}") + raise GitInvalidCommitRangeException( f"Failed to retrieve commit messages for the range {from_hash} to {to_hash}" ) from None diff --git a/src/commitlint/linter/_linter.py b/src/commitlint/linter/_linter.py index 0ccb554..108bf47 100644 --- a/src/commitlint/linter/_linter.py +++ b/src/commitlint/linter/_linter.py @@ -5,6 +5,7 @@ from typing import List, Tuple +from .. import output from .utils import is_ignored, remove_comments from .validators import ( HeaderLengthValidator, @@ -29,23 +30,30 @@ def lint_commit_message( Tuple[bool, List[str]]: Returns success as a first element and list of errors on the second elements. If success is true, errors will be empty. """ + output.verbose("linting commit message:") + output.verbose(f"----------\n{commit_message}\n----------") # perform processing and pre checks # removing unnecessary commit comments + output.verbose("removing comments from the commit message") commit_message = remove_comments(commit_message) # checking if commit message should be ignored + output.verbose("checking if the commit message is in ignored list") if is_ignored(commit_message): + output.verbose("commit message ignored, skipping lint") return True, [] # for skip_detail check if skip_detail: + output.verbose("running simple validators for linting") return run_validators( commit_message, validator_classes=[HeaderLengthValidator, SimplePatternValidator], fail_fast=True, ) + output.verbose("running detailed validators for linting") return run_validators( commit_message, validator_classes=[HeaderLengthValidator, PatternValidator] ) diff --git a/src/commitlint/linter/validators.py b/src/commitlint/linter/validators.py index cced7cd..07e76a4 100644 --- a/src/commitlint/linter/validators.py +++ b/src/commitlint/linter/validators.py @@ -8,6 +8,7 @@ from abc import ABC, abstractmethod from typing import List, Tuple, Type, Union +from .. import output from ..constants import COMMIT_HEADER_MAX_LENGTH, COMMIT_TYPES from ..messages import ( COMMIT_TYPE_INVALID_ERROR, @@ -306,11 +307,13 @@ def run_validators( success = True errors: List[str] = [] - # checking the length of header for validator_class in validator_classes: + output.verbose(f"running validator {validator_class.__name__}") validator = validator_class(commit_message) if not validator.is_valid(): + output.verbose(f"{validator_class.__name__}: validation failed") if fail_fast: + output.verbose(f"fail_fast: {fail_fast}, skipping further validations") # returning immediately if any error occurs. return False, validator.errors() diff --git a/src/commitlint/output.py b/src/commitlint/output.py new file mode 100644 index 0000000..f7a4975 --- /dev/null +++ b/src/commitlint/output.py @@ -0,0 +1,51 @@ +""" +This module provides functions for displaying outputs related to commitlint. + +NOTE: If any future changes are made to the output implementation, +they will be done from here. + +TODO: Add color on success and error (#5). +""" + +import sys + +from .config import config + + +def success(message: str) -> None: + """ + Print a success message. + + Args: + message (str): The success message to print. + """ + if config.quiet: + return + + sys.stdout.write(f"{message}\n") + + +def error(message: str) -> None: + """ + Print an error message. + + Args: + message (str): The error message to print. + """ + if config.quiet: + return + + sys.stderr.write(f"{message}\n") + + +def verbose(message: str) -> None: + """ + Print a verbose message if in verbose mode. + + Args: + message (str): The verbose message to print. + """ + if not config.verbose: + return + + sys.stdout.write(f"{message}\n") diff --git a/tests/test_cli.py b/tests/test_cli.py index 25feba4..0b98db7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,6 +6,7 @@ import pytest from commitlint.cli import get_args, main +from commitlint.config import config from commitlint.exceptions import CommitlintException from commitlint.messages import ( INCORRECT_FORMAT_ERROR, @@ -84,6 +85,8 @@ def test__get_args__with_skip_detail(self, *_): assert args.skip_detail is True +@patch("commitlint.output.success") +@patch("commitlint.output.error") class TestCLIMain: # main: commit_message @@ -96,16 +99,14 @@ class TestCLIMain: from_hash=None, skip_detail=False, quiet=False, + verbose=False, ), ) - @patch("sys.stdout.write") def test__main__valid_commit_message( - self, - mock_stdout_write, - *_, + self, _mock_get_args, _mock_output_error, mock_output_success ): main() - mock_stdout_write.assert_called_with(f"{VALIDATION_SUCCESSFUL}\n") + mock_output_success.assert_called_with(f"{VALIDATION_SUCCESSFUL}") @patch( "commitlint.cli.get_args", @@ -116,16 +117,14 @@ def test__main__valid_commit_message( from_hash=None, skip_detail=True, quiet=False, + verbose=False, ), ) - @patch("sys.stdout.write") def test__main__valid_commit_message_using_skip_detail( - self, - mock_stdout_write, - *_, + self, _mock_get_args, _mock_output_error, mock_output_success ): main() - mock_stdout_write.assert_called_once_with(f"{VALIDATION_SUCCESSFUL}\n") + mock_output_success.assert_called_once_with(f"{VALIDATION_SUCCESSFUL}") @patch( "commitlint.cli.get_args", @@ -136,22 +135,19 @@ def test__main__valid_commit_message_using_skip_detail( from_hash=None, skip_detail=False, quiet=False, + verbose=False, ), ) - @patch("sys.stderr.write") def test__main__invalid_commit_message( - self, - mock_stderr_write, - *_, + self, _mock_get_args, mock_output_error, _mock_output_success ): with pytest.raises(SystemExit): main() - - mock_stderr_write.assert_has_calls( + mock_output_error.assert_has_calls( [ - call("⧗ Input:\nInvalid commit message\n\n"), - call("✖ Found 1 error(s).\n"), - call(f"- {INCORRECT_FORMAT_ERROR}\n"), + call("⧗ Input:\nInvalid commit message\n"), + call("✖ Found 1 error(s)."), + call(f"- {INCORRECT_FORMAT_ERROR}"), ] ) @@ -164,21 +160,19 @@ def test__main__invalid_commit_message( from_hash=None, skip_detail=True, quiet=False, + verbose=False, ), ) - @patch("sys.stderr.write") def test__main__invalid_commit_message_using_skip_detail( - self, - mock_stderr_write, - *_, + self, _mock_get_args, mock_output_error, _mock_output_success ): with pytest.raises(SystemExit): main() - mock_stderr_write.assert_has_calls( + mock_output_error.assert_has_calls( [ - call("⧗ Input:\nInvalid commit message\n\n"), - call(f"{VALIDATION_FAILED}\n"), + call("⧗ Input:\nInvalid commit message\n"), + call(f"{VALIDATION_FAILED}"), ] ) @@ -188,27 +182,29 @@ def test__main__invalid_commit_message_using_skip_detail( "commitlint.cli.get_args", return_value=MagicMock(file="path/to/file.txt", skip_detail=False, quiet=False), ) - @patch("sys.stdout.write") @patch("builtins.open", mock_open(read_data="feat: valid commit message")) - def test__main__valid_commit_message_with_file(self, mock_stdout_write, *_): + def test__main__valid_commit_message_with_file( + self, _mock_get_args, _mock_output_error, mock_output_success + ): main() - mock_stdout_write.assert_called_with(f"{VALIDATION_SUCCESSFUL}\n") + mock_output_success.assert_called_with(f"{VALIDATION_SUCCESSFUL}") @patch( "commitlint.cli.get_args", return_value=MagicMock(file="path/to/file.txt", skip_detail=False, quiet=False), ) - @patch("sys.stderr.write") @patch("builtins.open", mock_open(read_data="Invalid commit message 2")) - def test__main__invalid_commit_message_with_file(self, mock_stderr_write, *_): + def test__main__invalid_commit_message_with_file( + self, _mock_get_args, mock_output_error, _mock_output_success + ): with pytest.raises(SystemExit): main() - mock_stderr_write.assert_has_calls( + mock_output_error.assert_has_calls( [ - call("⧗ Input:\nInvalid commit message 2\n\n"), - call("✖ Found 1 error(s).\n"), - call(f"- {INCORRECT_FORMAT_ERROR}\n"), + call("⧗ Input:\nInvalid commit message 2\n"), + call("✖ Found 1 error(s)."), + call(f"- {INCORRECT_FORMAT_ERROR}"), ] ) @@ -221,13 +217,16 @@ def test__main__invalid_commit_message_with_file(self, mock_stderr_write, *_): ), ) @patch("commitlint.cli.get_commit_message_of_hash") - @patch("sys.stdout.write") def test__main__valid_commit_message_with_hash( - self, mock_stdout_write, mock_get_commit_message_of_hash, *_ + self, + mock_get_commit_message_of_hash, + _mock_get_args, + _mock_output_error, + mock_output_success, ): mock_get_commit_message_of_hash.return_value = "feat: valid commit message" main() - mock_stdout_write.assert_called_with(f"{VALIDATION_SUCCESSFUL}\n") + mock_output_success.assert_called_with(f"{VALIDATION_SUCCESSFUL}") @patch( "commitlint.cli.get_args", @@ -236,20 +235,23 @@ def test__main__valid_commit_message_with_hash( ), ) @patch("commitlint.cli.get_commit_message_of_hash") - @patch("sys.stderr.write") def test__main__invalid_commit_message_with_hash( - self, mock_stderr_write, mock_get_commit_message_of_hash, *_ + self, + mock_get_commit_message_of_hash, + _mock_get_args, + mock_output_error, + _mock_output_success, ): mock_get_commit_message_of_hash.return_value = "Invalid commit message" with pytest.raises(SystemExit): main() - mock_stderr_write.assert_has_calls( + mock_output_error.assert_has_calls( [ - call("⧗ Input:\nInvalid commit message\n\n"), - call("✖ Found 1 error(s).\n"), - call(f"- {INCORRECT_FORMAT_ERROR}\n"), + call("⧗ Input:\nInvalid commit message\n"), + call("✖ Found 1 error(s)."), + call(f"- {INCORRECT_FORMAT_ERROR}"), ] ) @@ -264,19 +266,23 @@ def test__main__invalid_commit_message_with_hash( to_hash="end_commit_hash", skip_detail=False, quiet=False, + verbose=False, ), ) @patch("commitlint.cli.get_commit_messages_of_hash_range") - @patch("sys.stdout.write") def test__main__valid_commit_message_with_hash_range( - self, mock_stdout_write, mock_get_commit_messages, *_ + self, + mock_get_commit_messages, + _mock_get_args, + _mock_output_error, + mock_output_success, ): mock_get_commit_messages.return_value = [ "feat: commit message 1", "fix: commit message 2", ] main() - mock_stdout_write.assert_called_with(f"{VALIDATION_SUCCESSFUL}\n") + mock_output_success.assert_called_with(f"{VALIDATION_SUCCESSFUL}") @patch( "commitlint.cli.get_args", @@ -287,12 +293,16 @@ def test__main__valid_commit_message_with_hash_range( to_hash="end_commit_hash", skip_detail=False, quiet=False, + verbose=False, ), ) - @patch("sys.stderr.write") @patch("commitlint.cli.get_commit_messages_of_hash_range") def test__main__invalid_commit_message_with_hash_range( - self, mock_get_commit_messages, *_ + self, + mock_get_commit_messages, + _mock_get_args, + _mock_output_error, + _mock_output_success, ): mock_get_commit_messages.return_value = [ "Invalid commit message 1", @@ -313,16 +323,69 @@ def test__main__invalid_commit_message_with_hash_range( @patch( "commitlint.cli.lint_commit_message", ) - @patch("sys.stderr.write") def test__main__handle_exceptions( - self, mock_stderr_write, mock_lint_commit_message, *_ + self, + mock_lint_commit_message, + _mock_get_args, + mock_output_error, + _mock_output_success, ): mock_lint_commit_message.side_effect = CommitlintException("Test message") with pytest.raises(SystemExit): main() - mock_stderr_write.assert_called_with("Test message\n") + mock_output_error.assert_called_with("Test message") + + # main : quiet + + @patch( + "commitlint.cli.get_args", + return_value=MagicMock( + commit_message="feat: test commit", + file=None, + hash=None, + from_hash=None, + skip_detail=False, + quiet=True, + verbose=False, + ), + ) + def test__main__sets_config_for_quiet( + self, + _mock_get_args, + _mock_output_error, + _mock_output_success, + ): + main() + assert config.quiet is True + + # main : verbose + + @patch( + "commitlint.cli.get_args", + return_value=MagicMock( + commit_message="feat: test commit", + file=None, + hash=None, + from_hash=None, + skip_detail=False, + quiet=False, + verbose=True, + ), + ) + def test__main__sets_config_for_verbose( + self, + _mock_get_args, + _mock_output_error, + _mock_output_success, + ): + main() + assert config.verbose is True + + +class TestCLIMainQuiet: + # main : quiet (directly checking stdout and stderr) @patch( "commitlint.cli.get_args", @@ -333,6 +396,7 @@ def test__main__handle_exceptions( from_hash=None, skip_detail=False, quiet=True, + verbose=False, ), ) @patch("sys.stdout.write") @@ -355,6 +419,7 @@ def test__main__quiet_option_with_invalid_commit_message( from_hash=None, skip_detail=False, quiet=True, + verbose=False, ), ) @patch("sys.stdout.write") @@ -375,6 +440,7 @@ def test__main__quiet_option_with_valid_commit_message( to_hash="end_commit_hash", skip_detail=False, quiet=True, + verbose=False, ), ) @patch("commitlint.cli.get_commit_messages_of_hash_range") @@ -398,6 +464,7 @@ def test__valid_commit_message_with_hash_range_in_quiet( to_hash="end_commit_hash", skip_detail=False, quiet=True, + verbose=False, ), ) @patch("commitlint.cli.get_commit_messages_of_hash_range") diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..1b39df5 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,35 @@ +# type: ignore +# pylint: disable=all + +from typing import Generator + +import pytest + +from commitlint.config import _CommitlintConfig as CommitlintConfig + + +@pytest.fixture(scope="class") +def config_instance() -> Generator[CommitlintConfig, None, None]: + config = CommitlintConfig() + yield config + config.verbose = False + config.quiet = False + + +class TestCommitlintConfig: + def test_singleton_instance(self, config_instance: CommitlintConfig) -> None: + config1 = config_instance + config2 = config_instance + assert config1 is config2 + + def test_verbose_property(self, config_instance: CommitlintConfig) -> None: + config = config_instance + config.verbose = True + assert config.verbose + assert not config.quiet + + def test_quiet_property(self, config_instance: CommitlintConfig) -> None: + config = config_instance + config.quiet = True + assert config.quiet + assert not config.verbose diff --git a/tests/test_output.py b/tests/test_output.py new file mode 100644 index 0000000..2a4b190 --- /dev/null +++ b/tests/test_output.py @@ -0,0 +1,54 @@ +# type: ignore +# pylint: disable=all + +from unittest.mock import MagicMock, patch + +from commitlint.output import error, success, verbose + + +@patch("commitlint.output.config", quiet=False) +@patch("sys.stdout") +def test_success(mock_stdout: MagicMock, _mock_config: MagicMock): + message = "Success message" + success(message) + mock_stdout.write.assert_called_once_with(f"{message}\n") + + +@patch("commitlint.output.config", quiet=True) +@patch("sys.stdout") +def test_success_for_quiet(mock_stdout: MagicMock, _mock_config: MagicMock): + message = "Success message" + success(message) + mock_stdout.write.assert_not_called() + + +@patch("commitlint.output.config", quiet=False) +@patch("sys.stderr") +def test_error(mock_stderr: MagicMock, _mock_config: MagicMock): + message = "Error message" + error(message) + mock_stderr.write.assert_called_once_with(f"{message}\n") + + +@patch("commitlint.output.config", quiet=True) +@patch("sys.stderr") +def test_error_for_quiet(mock_stderr: MagicMock, _mock_config: MagicMock): + message = "Error message" + error(message) + mock_stderr.write.assert_not_called() + + +@patch("commitlint.output.config", verbose=True) +@patch("sys.stdout") +def test_verbose(mock_stdout: MagicMock, _mock_config: MagicMock): + message = "Verbose message" + verbose(message) + mock_stdout.write.assert_called_once_with(f"{message}\n") + + +@patch("commitlint.output.config", verbose=False) +@patch("sys.stdout") +def test_verbose_for_non_verbose(mock_stdout: MagicMock, _mock_config: MagicMock): + message = "Verbose message" + verbose(message) + mock_stdout.write.assert_not_called()