1
0
Fork 0
commitizen/tests/commands/test_commit_command.py
Daniel Baumann 167a3f8553
Adding upstream version 4.6.0+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-04-21 11:40:48 +02:00

527 lines
17 KiB
Python

import os
import sys
from unittest.mock import ANY
import pytest
from pytest_mock import MockFixture
from commitizen import cli, cmd, commands
from commitizen.cz.exceptions import CzException
from commitizen.cz.utils import get_backup_file_path
from commitizen.exceptions import (
CommitError,
CommitMessageLengthExceededError,
CustomError,
DryRunExit,
NoAnswersError,
NoCommitBackupError,
NotAGitProjectError,
NotAllowed,
NothingToCommitError,
)
from tests.utils import skip_below_py_3_13
@pytest.fixture
def staging_is_clean(mocker: MockFixture, tmp_git_project):
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
is_staging_clean_mock.return_value = False
return tmp_git_project
@pytest.fixture
def backup_file(tmp_git_project):
with open(get_backup_file_path(), "w") as backup_file:
backup_file.write("backup commit")
@pytest.mark.usefixtures("staging_is_clean")
def test_commit(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {})()
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_backup_on_failure(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #21",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("", "error", b"", b"", 9)
error_mock = mocker.patch("commitizen.out.error")
with pytest.raises(CommitError):
commit_cmd = commands.Commit(config, {})
temp_file = commit_cmd.temp_file
commit_cmd()
prompt_mock.assert_called_once()
error_mock.assert_called_once()
assert os.path.isfile(temp_file)
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_retry_fails_no_backup(config, mocker: MockFixture):
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
with pytest.raises(NoCommitBackupError) as excinfo:
commands.Commit(config, {"retry": True})()
assert NoCommitBackupError.message in str(excinfo.value)
@pytest.mark.usefixtures("staging_is_clean", "backup_file")
def test_commit_retry_works(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commit_cmd = commands.Commit(config, {"retry": True})
temp_file = commit_cmd.temp_file
commit_cmd()
commit_mock.assert_called_with("backup commit", args="")
prompt_mock.assert_not_called()
success_mock.assert_called_once()
assert not os.path.isfile(temp_file)
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_retry_after_failure_no_backup(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #21",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["retry_after_failure"] = True
commands.Commit(config, {})()
commit_mock.assert_called_with("feat: user created\n\ncloses #21", args="")
prompt_mock.assert_called_once()
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean", "backup_file")
def test_commit_retry_after_failure_works(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["retry_after_failure"] = True
commit_cmd = commands.Commit(config, {})
temp_file = commit_cmd.temp_file
commit_cmd()
commit_mock.assert_called_with("backup commit", args="")
prompt_mock.assert_not_called()
success_mock.assert_called_once()
assert not os.path.isfile(temp_file)
@pytest.mark.usefixtures("staging_is_clean", "backup_file")
def test_commit_retry_after_failure_with_no_retry_works(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #21",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["retry_after_failure"] = True
commit_cmd = commands.Commit(config, {"no_retry": True})
temp_file = commit_cmd.temp_file
commit_cmd()
commit_mock.assert_called_with("feat: user created\n\ncloses #21", args="")
prompt_mock.assert_called_once()
success_mock.assert_called_once()
assert not os.path.isfile(temp_file)
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_dry_run_option(config, mocker: MockFixture):
prompt_mock = mocker = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #57",
"footer": "",
}
with pytest.raises(DryRunExit):
commit_cmd = commands.Commit(config, {"dry_run": True})
commit_cmd()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_write_message_to_file_option(
config, tmp_path, mocker: MockFixture
):
tmp_file = tmp_path / "message"
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {"write_message_to_file": tmp_file})()
success_mock.assert_called_once()
assert tmp_file.exists()
assert tmp_file.read_text() == "feat: user created"
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_invalid_write_message_to_file_option(
config, tmp_path, mocker: MockFixture
):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
with pytest.raises(NotAllowed):
commit_cmd = commands.Commit(config, {"write_message_to_file": tmp_path})
commit_cmd()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_signoff_option(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {"signoff": True})()
commit_mock.assert_called_once_with(ANY, args="-s")
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_always_signoff_enabled(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["always_signoff"] = True
commands.Commit(config, {})()
commit_mock.assert_called_once_with(ANY, args="-s")
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_gpgsign_and_always_signoff_enabled(
config, mocker: MockFixture
):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["always_signoff"] = True
commands.Commit(config, {"extra_cli_args": "-S"})()
commit_mock.assert_called_once_with(ANY, args="-S -s")
success_mock.assert_called_once()
@pytest.mark.usefixtures("tmp_git_project")
def test_commit_when_nothing_to_commit(config, mocker: MockFixture):
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
is_staging_clean_mock.return_value = True
with pytest.raises(NothingToCommitError) as excinfo:
commit_cmd = commands.Commit(config, {})
commit_cmd()
assert "No files added to staging!" in str(excinfo.value)
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_with_allow_empty(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #21",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {"extra_cli_args": "--allow-empty"})()
commit_mock.assert_called_with(
"feat: user created\n\ncloses #21", args="--allow-empty"
)
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_with_signoff_and_allow_empty(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "closes #21",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
config.settings["always_signoff"] = True
commands.Commit(config, {"extra_cli_args": "--allow-empty"})()
commit_mock.assert_called_with(
"feat: user created\n\ncloses #21", args="--allow-empty -s"
)
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_when_customized_expected_raised(config, mocker: MockFixture, capsys):
_err = ValueError()
_err.__context__ = CzException("This is the root custom err")
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.side_effect = _err
with pytest.raises(CustomError) as excinfo:
commit_cmd = commands.Commit(config, {})
commit_cmd()
# Assert only the content in the formatted text
assert "This is the root custom err" in str(excinfo.value)
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_when_non_customized_expected_raised(
config, mocker: MockFixture, capsys
):
_err = ValueError()
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.side_effect = _err
with pytest.raises(ValueError):
commit_cmd = commands.Commit(config, {})
commit_cmd()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_when_no_user_answer(config, mocker: MockFixture, capsys):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = None
with pytest.raises(NoAnswersError):
commit_cmd = commands.Commit(config, {})
commit_cmd()
def test_commit_in_non_git_project(tmpdir, config):
with tmpdir.as_cwd():
with pytest.raises(NotAGitProjectError):
commands.Commit(config, {})
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_all_option(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
add_mock = mocker.patch("commitizen.git.add")
commands.Commit(config, {"all": True})()
add_mock.assert_called()
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_extra_args(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"prefix": "feat",
"subject": "user created",
"scope": "",
"is_breaking_change": False,
"body": "",
"footer": "",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {"extra_cli_args": "-- -extra-args1 -extra-arg2"})()
commit_mock.assert_called_once_with(ANY, args="-- -extra-args1 -extra-arg2")
success_mock.assert_called_once()
@pytest.mark.usefixtures("staging_is_clean")
def test_commit_command_with_message_length_limit(config, mocker: MockFixture):
prompt_mock = mocker.patch("questionary.prompt")
prefix = "feat"
subject = "random subject"
message_length = len(prefix) + len(": ") + len(subject)
prompt_mock.return_value = {
"prefix": prefix,
"subject": subject,
"scope": "",
"is_breaking_change": False,
"body": "random body",
"footer": "random footer",
}
commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
success_mock = mocker.patch("commitizen.out.success")
commands.Commit(config, {"message_length_limit": message_length})()
success_mock.assert_called_once()
with pytest.raises(CommitMessageLengthExceededError):
commands.Commit(config, {"message_length_limit": message_length - 1})()
@pytest.mark.usefixtures("staging_is_clean")
@pytest.mark.parametrize("editor", ["vim", None])
def test_manual_edit(editor, config, mocker: MockFixture, tmp_path):
mocker.patch("commitizen.git.get_core_editor", return_value=editor)
subprocess_mock = mocker.patch("subprocess.call")
mocker.patch("shutil.which", return_value=editor)
test_message = "Initial commit message"
temp_file = tmp_path / "temp_commit_message"
temp_file.write_text(test_message)
mock_temp_file = mocker.patch("tempfile.NamedTemporaryFile")
mock_temp_file.return_value.__enter__.return_value.name = str(temp_file)
commit_cmd = commands.Commit(config, {"edit": True})
if editor is None:
with pytest.raises(RuntimeError):
commit_cmd.manual_edit(test_message)
else:
edited_message = commit_cmd.manual_edit(test_message)
subprocess_mock.assert_called_once_with(["vim", str(temp_file)])
assert edited_message == test_message.strip()
temp_file.unlink()
@skip_below_py_3_13
def test_commit_command_shows_description_when_use_help_option(
mocker: MockFixture, capsys, file_regression
):
testargs = ["cz", "commit", "--help"]
mocker.patch.object(sys, "argv", testargs)
with pytest.raises(SystemExit):
cli.main()
out, _ = capsys.readouterr()
file_regression.check(out, extension=".txt")