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")