Unify documentation rules across languages and fix the Python template
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
19e9b8f2fa
commit
5c0f2f758f
+111
-98
@@ -1,93 +1,125 @@
|
||||
"""Tests for constants module."""
|
||||
"""Tests for the constants module template."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src import constants
|
||||
from src.constants import (
|
||||
APP_FULL_NAME,
|
||||
APP_NAME,
|
||||
APP_TITLE,
|
||||
APP_VERSION,
|
||||
ENV_DEBUG,
|
||||
get_debug_mode,
|
||||
get_version,
|
||||
DEFAULT_DEBUG,
|
||||
VERSION,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_version()
|
||||
# ---------------------------------------------------------------------------
|
||||
SEMVER = re.compile(r"^\d+\.\d+\.\d+")
|
||||
|
||||
|
||||
def test_get_version_returns_string() -> None:
|
||||
"""get_version() should return a string."""
|
||||
assert isinstance(get_version(), str)
|
||||
|
||||
|
||||
def test_get_version_semver_format() -> None:
|
||||
"""get_version() should return a semver-like string X.Y.Z."""
|
||||
version = get_version()
|
||||
assert re.match(r"^\d+\.\d+\.\d+", version), f"Not semver: {version!r}"
|
||||
|
||||
|
||||
def test_get_version_fallback_when_toml_missing(tmp_path: Path) -> None:
|
||||
"""get_version() returns '0.0.0-unknown' when pyproject.toml and _version.py are both missing."""
|
||||
missing = tmp_path / "nonexistent.toml"
|
||||
with patch("src.constants._PYPROJECT_PATH", missing):
|
||||
result = get_version()
|
||||
# Either fallback _version.py exists (from previous run) or returns unknown
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_version_unknown_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_version() returns '0.0.0-unknown' when all sources are unavailable."""
|
||||
missing = tmp_path / "nonexistent.toml"
|
||||
monkeypatch.setattr("src.constants._PYPROJECT_PATH", missing)
|
||||
|
||||
# Patch _version import to also fail
|
||||
with patch("src.constants.Path.write_text", side_effect=OSError):
|
||||
with patch.dict("sys.modules", {"src._version": None}):
|
||||
result = get_version()
|
||||
|
||||
assert isinstance(result, str)
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_debug_mode()
|
||||
# _load_version()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_debug_mode_returns_bool() -> None:
|
||||
"""get_debug_mode() should always return a bool."""
|
||||
assert isinstance(get_debug_mode(), bool)
|
||||
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_get_debug_mode_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns True when ENV_DEBUG=true."""
|
||||
monkeypatch.setenv("ENV_DEBUG", "true")
|
||||
assert get_debug_mode() is True
|
||||
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_get_debug_mode_true_variants(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() accepts '1' and 'yes' as truthy values."""
|
||||
for value in ("1", "yes", "YES", "True", "TRUE"):
|
||||
monkeypatch.setenv("ENV_DEBUG", value)
|
||||
assert get_debug_mode() is True, f"Expected True for ENV_DEBUG={value!r}"
|
||||
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_get_debug_mode_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns False when ENV_DEBUG=false."""
|
||||
monkeypatch.setenv("ENV_DEBUG", "false")
|
||||
assert get_debug_mode() is False
|
||||
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_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns False when ENV_DEBUG is not set."""
|
||||
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 get_debug_mode() is False
|
||||
|
||||
assert constants._load_debug() is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -95,47 +127,28 @@ def test_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_env_debug_is_bool() -> None:
|
||||
"""ENV_DEBUG should be a bool."""
|
||||
assert isinstance(ENV_DEBUG, bool)
|
||||
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_app_version_is_string() -> None:
|
||||
"""APP_VERSION should be a string."""
|
||||
assert isinstance(APP_VERSION, str)
|
||||
def test_default_debug_is_bool() -> None:
|
||||
"""DEFAULT_DEBUG should be a bool."""
|
||||
assert isinstance(DEFAULT_DEBUG, bool)
|
||||
|
||||
|
||||
def test_app_version_semver_format() -> None:
|
||||
"""APP_VERSION should follow semver format X.Y.Z."""
|
||||
assert re.match(r"^\d+\.\d+\.\d+", APP_VERSION), f"Not semver: {APP_VERSION!r}"
|
||||
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_app_name_value() -> None:
|
||||
"""APP_NAME should be 'X4 SavEd'."""
|
||||
assert APP_NAME == "X4 SavEd"
|
||||
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_contains_name_and_version() -> None:
|
||||
"""APP_TITLE should contain APP_NAME and APP_VERSION."""
|
||||
assert APP_NAME in APP_TITLE
|
||||
assert APP_VERSION in APP_TITLE
|
||||
|
||||
|
||||
def test_app_title_dev_suffix_when_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""APP_TITLE ends with '-DEV' when ENV_DEBUG is True."""
|
||||
import importlib
|
||||
import src.constants as consts
|
||||
|
||||
monkeypatch.setenv("ENV_DEBUG", "true")
|
||||
monkeypatch.setattr(consts, "ENV_DEBUG", True)
|
||||
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if True else "")
|
||||
assert title.endswith("-DEV")
|
||||
|
||||
|
||||
def test_app_title_no_dev_suffix_when_not_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""APP_TITLE does not end with '-DEV' when ENV_DEBUG is False."""
|
||||
import src.constants as consts
|
||||
|
||||
monkeypatch.setattr(consts, "ENV_DEBUG", False)
|
||||
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if False else "")
|
||||
assert not title.endswith("-DEV")
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user