from __future__ import annotations import json import os import sys from typing import Any import pytest import yaml from pytest_mock import MockFixture from commitizen import cli, commands from commitizen.__version__ import __version__ from commitizen.exceptions import InitFailedError, NoAnswersError from tests.utils import skip_below_py_3_10 class FakeQuestion: def __init__(self, expected_return): self.expected_return = expected_return def ask(self): return self.expected_return def unsafe_ask(self): return self.expected_return pre_commit_config_filename = ".pre-commit-config.yaml" cz_hook_config = { "repo": "https://github.com/commitizen-tools/commitizen", "rev": f"v{__version__}", "hooks": [ {"id": "commitizen"}, {"id": "commitizen-branch", "stages": ["push"]}, ], } expected_config = ( "[tool.commitizen]\n" 'name = "cz_conventional_commits"\n' 'tag_format = "$version"\n' 'version_scheme = "semver"\n' 'version = "0.0.1"\n' "update_changelog_on_bump = true\n" "major_version_zero = true\n" ) EXPECTED_DICT_CONFIG = { "commitizen": { "name": "cz_conventional_commits", "tag_format": "$version", "version_scheme": "semver", "version": "0.0.1", "update_changelog_on_bump": True, "major_version_zero": True, } } def test_init_without_setup_pre_commit_hook(tmpdir, mocker: MockFixture, config): mocker.patch( "questionary.select", side_effect=[ FakeQuestion("pyproject.toml"), FakeQuestion("cz_conventional_commits"), FakeQuestion("commitizen"), FakeQuestion("semver"), ], ) mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) mocker.patch("questionary.text", return_value=FakeQuestion("$version")) # Return None to skip hook installation mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) with tmpdir.as_cwd(): commands.Init(config)() with open("pyproject.toml", encoding="utf-8") as toml_file: config_data = toml_file.read() assert config_data == expected_config assert not os.path.isfile(pre_commit_config_filename) def test_init_when_config_already_exists(config, capsys): # Set config path path = os.sep.join(["tests", "pyproject.toml"]) config.add_path(path) commands.Init(config)() captured = capsys.readouterr() assert captured.out == f"Config file {path} already exists\n" def test_init_without_choosing_tag(config, mocker: MockFixture, tmpdir): mocker.patch( "commitizen.commands.init.get_tag_names", return_value=["0.0.2", "0.0.1"] ) mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="0.0.2") mocker.patch( "questionary.select", side_effect=[ FakeQuestion("pyproject.toml"), FakeQuestion("cz_conventional_commits"), FakeQuestion("commitizen"), FakeQuestion(""), ], ) mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) mocker.patch("questionary.text", return_value=FakeQuestion("y")) with tmpdir.as_cwd(): with pytest.raises(NoAnswersError): commands.Init(config)() def test_executed_pre_commit_command(config): init = commands.Init(config) expected_cmd = "pre-commit install --hook-type commit-msg --hook-type pre-push" assert init._gen_pre_commit_cmd(["commit-msg", "pre-push"]) == expected_cmd @pytest.fixture(scope="function") def pre_commit_installed(mocker: MockFixture): # Assume the `pre-commit` is installed mocker.patch( "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", return_value=True, ) # And installation success (i.e. no exception raised) mocker.patch( "commitizen.commands.init.Init._exec_install_pre_commit_hook", return_value=None, ) @pytest.fixture(scope="function", params=["pyproject.toml", ".cz.json", ".cz.yaml"]) def default_choice(request, mocker: MockFixture): mocker.patch( "questionary.select", side_effect=[ FakeQuestion(request.param), FakeQuestion("cz_conventional_commits"), FakeQuestion("commitizen"), FakeQuestion("semver"), ], ) mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) mocker.patch("questionary.text", return_value=FakeQuestion("$version")) mocker.patch( "questionary.checkbox", return_value=FakeQuestion(["commit-msg", "pre-push"]), ) yield request.param def check_cz_config(config: str): """ Check the content of commitizen config is as expected Args: config: The config path """ with open(config) as file: if "json" in config: assert json.load(file) == EXPECTED_DICT_CONFIG elif "yaml" in config: assert yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG else: config_data = file.read() assert config_data == expected_config def check_pre_commit_config(expected: list[dict[str, Any]]): """ Check the content of pre-commit config is as expected """ with open(pre_commit_config_filename) as pre_commit_file: pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) assert pre_commit_config_data == {"repos": expected} @pytest.mark.usefixtures("pre_commit_installed") class TestPreCommitCases: def test_no_existing_pre_commit_conifg(_, default_choice, tmpdir, config): with tmpdir.as_cwd(): commands.Init(config)() check_cz_config(default_choice) check_pre_commit_config([cz_hook_config]) def test_empty_pre_commit_config(_, default_choice, tmpdir, config): with tmpdir.as_cwd(): p = tmpdir.join(pre_commit_config_filename) p.write("") commands.Init(config)() check_cz_config(default_choice) check_pre_commit_config([cz_hook_config]) def test_pre_commit_config_without_cz_hook(_, default_choice, tmpdir, config): existing_hook_config = { "repo": "https://github.com/pre-commit/pre-commit-hooks", "rev": "v1.2.3", "hooks": [{"id", "trailing-whitespace"}], } with tmpdir.as_cwd(): p = tmpdir.join(pre_commit_config_filename) p.write(yaml.safe_dump({"repos": [existing_hook_config]})) commands.Init(config)() check_cz_config(default_choice) check_pre_commit_config([existing_hook_config, cz_hook_config]) def test_cz_hook_exists_in_pre_commit_config(_, default_choice, tmpdir, config): with tmpdir.as_cwd(): p = tmpdir.join(pre_commit_config_filename) p.write(yaml.safe_dump({"repos": [cz_hook_config]})) commands.Init(config)() check_cz_config(default_choice) # check that config is not duplicated check_pre_commit_config([cz_hook_config]) class TestNoPreCommitInstalled: def test_pre_commit_not_installed( _, mocker: MockFixture, config, default_choice, tmpdir ): # Assume `pre-commit` is not installed mocker.patch( "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", return_value=False, ) with tmpdir.as_cwd(): with pytest.raises(InitFailedError): commands.Init(config)() def test_pre_commit_exec_failed( _, mocker: MockFixture, config, default_choice, tmpdir ): # Assume `pre-commit` is installed mocker.patch( "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", return_value=True, ) # But pre-commit installation will fail mocker.patch( "commitizen.commands.init.Init._exec_install_pre_commit_hook", side_effect=InitFailedError("Mock init failed error."), ) with tmpdir.as_cwd(): with pytest.raises(InitFailedError): commands.Init(config)() @skip_below_py_3_10 def test_init_command_shows_description_when_use_help_option( mocker: MockFixture, capsys, file_regression ): testargs = ["cz", "init", "--help"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(SystemExit): cli.main() out, _ = capsys.readouterr() file_regression.check(out, extension=".txt")