527 lines
17 KiB
Python
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")
|