"""Tests for the constants module template.""" import re import sys from pathlib import Path import pytest from src import constants from src.constants import ( APP_FULL_NAME, APP_NAME, APP_TITLE, DEFAULT_DEBUG, VERSION, ) SEMVER = re.compile(r"^\d+\.\d+\.\d+") def _write_pyproject(tmp_path: Path, version: str = "1.2.3") -> Path: """Create a minimal pyproject.toml carrying the given version.""" pyproject = tmp_path / "pyproject.toml" pyproject.write_text(f'[project]\nname = "demo"\nversion = "{version}"\n', encoding="utf-8") return pyproject # --------------------------------------------------------------------------- # _load_version() # --------------------------------------------------------------------------- def test_load_version_reads_pyproject(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """_load_version() takes the version from [project] in pyproject.toml.""" monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) monkeypatch.setattr(constants, "_VERSION_FILE", tmp_path / "_version.py") assert constants._load_version() == "1.2.3" def test_load_version_writes_fallback_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """_load_version() regenerates _version.py so frozen builds keep the version.""" version_file = tmp_path / "_version.py" monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) monkeypatch.setattr(constants, "_VERSION_FILE", version_file) constants._load_version() assert '__version__ = "1.2.3"' in version_file.read_text(encoding="utf-8") def test_load_version_survives_unwritable_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """An unwritable _version.py location must not break version loading.""" monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) monkeypatch.setattr(constants, "_VERSION_FILE", tmp_path / "missing_dir" / "_version.py") assert constants._load_version() == "1.2.3" def test_load_version_falls_back_to_version_module(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Without pyproject.toml the version comes from src/_version.py.""" from src import _version monkeypatch.setattr(constants, "_PYPROJECT", tmp_path / "nonexistent.toml") assert constants._load_version() == _version.__version__ def test_load_version_missing_version_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """A pyproject.toml without [project] version falls through to the next source.""" pyproject = tmp_path / "pyproject.toml" pyproject.write_text('[project]\nname = "demo"\n', encoding="utf-8") monkeypatch.setattr(constants, "_PYPROJECT", pyproject) monkeypatch.setitem(sys.modules, "src._version", None) assert constants._load_version() == "0.0.0" def test_load_version_malformed_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """A malformed pyproject.toml falls through instead of raising.""" pyproject = tmp_path / "pyproject.toml" pyproject.write_text("[project\nversion = ", encoding="utf-8") monkeypatch.setattr(constants, "_PYPROJECT", pyproject) monkeypatch.setitem(sys.modules, "src._version", None) assert constants._load_version() == "0.0.0" def test_load_version_last_resort(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """With no source available at all, _load_version() returns '0.0.0'.""" monkeypatch.setattr(constants, "_PYPROJECT", tmp_path / "nonexistent.toml") monkeypatch.setitem(sys.modules, "src._version", None) assert constants._load_version() == "0.0.0" # --------------------------------------------------------------------------- # _load_debug() # --------------------------------------------------------------------------- @pytest.mark.parametrize("value", ["true", "TRUE", "True", "1", "yes", "YES"]) def test_load_debug_truthy_values(value: str, monkeypatch: pytest.MonkeyPatch) -> None: """_load_debug() accepts 'true', '1' and 'yes' in any casing.""" monkeypatch.setenv("ENV_DEBUG", value) assert constants._load_debug() is True @pytest.mark.parametrize("value", ["false", "FALSE", "0", "no", "", "maybe"]) def test_load_debug_falsy_values(value: str, monkeypatch: pytest.MonkeyPatch) -> None: """Anything outside the accepted true-values is False.""" monkeypatch.setenv("ENV_DEBUG", value) assert constants._load_debug() is False def test_load_debug_defaults_to_false(monkeypatch: pytest.MonkeyPatch) -> None: """_load_debug() returns False when ENV_DEBUG is not set.""" monkeypatch.delenv("ENV_DEBUG", raising=False) assert constants._load_debug() is False # --------------------------------------------------------------------------- # Module-level constants # --------------------------------------------------------------------------- def test_app_name_is_set() -> None: """APP_NAME and APP_FULL_NAME must be filled in per project.""" assert isinstance(APP_NAME, str) and APP_NAME assert isinstance(APP_FULL_NAME, str) and APP_FULL_NAME def test_default_debug_is_bool() -> None: """DEFAULT_DEBUG should be a bool.""" assert isinstance(DEFAULT_DEBUG, bool) def test_version_is_prefixed_semver() -> None: """VERSION is the project version prefixed with 'v'.""" assert VERSION.startswith("v") assert SEMVER.match(VERSION.removeprefix("v")), f"Not semver: {VERSION!r}" def test_version_dev_suffix_matches_debug_flag() -> None: """VERSION ends with 'DEV' (no separator) exactly when DEFAULT_DEBUG is True.""" assert VERSION.endswith("DEV") is DEFAULT_DEBUG def test_app_title_is_full_name_and_version() -> None: """APP_TITLE joins APP_FULL_NAME and VERSION with a single space.""" assert APP_TITLE == f"{APP_FULL_NAME} {VERSION}"