"""Unit tests for the `undercooked` CLI (task 03): precedence, exit codes, wiring. `_resolve` and `_uvicorn_log_config` are pure, so precedence is testable without a socket. `main()` is tested with uvicorn's `run` replaced by a recorder — the real server is started by the integration/phase criteria, not by a unit test. """ from __future__ import annotations import copy import os import subprocess import sys from pathlib import Path from typing import TYPE_CHECKING, Any import pytest import uvicorn import uvicorn.config from undercooked.__main__ import ( APP_IMPORT_STRING, EXIT_CONFIG_ERROR, EXIT_OK, RunOptions, _cli_overrides, _resolve, _uvicorn_log_config, build_parser, main, ) from undercooked.config import ( ENV_KEYS, ConfigError, Settings, apply_cli_overrides, get_settings, reset_settings_cache, ) if TYPE_CHECKING: from collections.abc import Iterator pytestmark = pytest.mark.unit REPO_ROOT = Path(__file__).resolve().parents[2] # A string in an assertion, never a socket bind: the CLI flag is what is under test. ALL_INTERFACES = "0.0.0.0" # noqa: S104 - a test assertion, not a socket bind @pytest.fixture(autouse=True) def pristine_env() -> Iterator[None]: """`main()` publishes CLI flags into `os.environ`; nothing may leak out.""" saved = dict(os.environ) reset_settings_cache() yield os.environ.clear() os.environ.update(saved) reset_settings_cache() @pytest.fixture def uvicorn_calls(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]: """Replace uvicorn's `run` with a recorder; returns the recorded invocations.""" calls: list[dict[str, Any]] = [] def fake_run(target: str, **kwargs: Any) -> None: calls.append({"target": target, **kwargs}) monkeypatch.setattr(uvicorn, "run", fake_run) return calls @pytest.fixture def bare_env(monkeypatch: pytest.MonkeyPatch) -> None: """No `UNDERCOOKED_*`/`DEBUGPY*` in the environment: defaults are observable.""" for key in list(os.environ): if key.startswith(("UNDERCOOKED_", "DEBUGPY")): monkeypatch.delenv(key, raising=False) monkeypatch.setenv("PYTHON_DOTENV_DISABLED", "true") def parse(argv: list[str]) -> Any: """Parse ``argv`` the way `main` would, without serving anything.""" return build_parser().parse_args(argv) # ----------------------------------------------------------------------------- parser def test_every_flag_defaults_to_unset() -> None: args = parse([]) assert (args.host, args.port, args.reload, args.levels_dir) == (None, None, False, None) def test_flags_are_parsed() -> None: args = parse( ["--host", ALL_INTERFACES, "--port", "9000", "--reload", "--levels-dir", "kitchens"] ) assert (args.host, args.port, args.reload) == (ALL_INTERFACES, 9000, True) assert args.levels_dir == Path("kitchens") def test_help_names_the_env_fallback() -> None: text = build_parser().format_help() assert "--levels-dir" in text assert "UNDERCOOKED_PORT" in text # ----------------------------------------------------------------------------- precedence def test_cli_beats_environment_and_defaults() -> None: settings = Settings(host="10.0.0.1", port=8123, levels_dir=Path("env-levels")) options = _resolve( parse(["--host", "127.0.0.1", "--port", "9999", "--levels-dir", "cli-levels"]), settings ) assert options == RunOptions( host="127.0.0.1", port=9999, reload=False, levels_dir=Path("cli-levels") ) def test_settings_supply_everything_the_cli_left_unset() -> None: settings = Settings(host=ALL_INTERFACES, port=8123, levels_dir=Path("env-levels")) options = _resolve(parse([]), settings) assert options == RunOptions( host=ALL_INTERFACES, port=8123, reload=False, levels_dir=Path("env-levels") ) def test_settings_default_to_the_documented_defaults_in_a_bare_environment( bare_env: None, ) -> None: options = _resolve(parse([]), get_settings()) assert options == RunOptions( host="127.0.0.1", port=8000, reload=False, levels_dir=Path("levels") ) def test_environment_beats_the_builtin_default(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("UNDERCOOKED_PORT", "8123") monkeypatch.setenv("UNDERCOOKED_HOST", ALL_INTERFACES) reset_settings_cache() options = _resolve(parse([]), get_settings()) assert (options.host, options.port) == (ALL_INTERFACES, 8123) def test_reload_is_a_flag_not_an_environment_variable() -> None: assert _resolve(parse(["--reload"]), Settings()).reload is True assert _resolve(parse([]), Settings()).reload is False def test_cli_overrides_only_report_flags_that_were_given() -> None: assert _cli_overrides(parse([])) == {} assert _cli_overrides(parse(["--port", "9000", "--levels-dir", "x"])) == { "port": 9000, "levels_dir": Path("x"), } # ----------------------------------------------------------------------------- main() def test_main_serves_the_app_string_with_resolved_options( uvicorn_calls: list[dict[str, Any]], bare_env: None ) -> None: assert main([]) == EXIT_OK assert len(uvicorn_calls) == 1 call = uvicorn_calls[0] assert call["target"] == APP_IMPORT_STRING # reload-capable "module:attribute" form assert (call["host"], call["port"], call["reload"]) == ("127.0.0.1", 8000, False) assert "undercooked" in call["log_config"]["loggers"] def test_main_passes_cli_flags_to_uvicorn( uvicorn_calls: list[dict[str, Any]], bare_env: None ) -> None: assert main(["--host", ALL_INTERFACES, "--port", "9123", "--reload"]) == EXIT_OK call = uvicorn_calls[0] assert (call["host"], call["port"], call["reload"]) == (ALL_INTERFACES, 9123, True) def test_main_publishes_cli_flags_to_the_environment( uvicorn_calls: list[dict[str, Any]], bare_env: None ) -> None: """A uvicorn --reload child re-reads the environment, so flags must live there.""" assert main(["--levels-dir", "kitchens", "--port", "9124"]) == EXIT_OK assert os.environ[ENV_KEYS["levels_dir"]] == "kitchens" assert os.environ[ENV_KEYS["port"]] == "9124" assert get_settings().levels_dir == Path("kitchens") assert get_settings().port == 9124 def test_main_validates_cli_values_like_environment_values( uvicorn_calls: list[dict[str, Any]], capsys: pytest.CaptureFixture[str] ) -> None: assert main(["--port", "0"]) == EXIT_CONFIG_ERROR assert uvicorn_calls == [] err = capsys.readouterr().err assert err.strip().startswith("undercooked:") assert "UNDERCOOKED_PORT" in err assert err.count("\n") == 1 # a one-line message, not a traceback def test_main_reports_an_invalid_environment( uvicorn_calls: list[dict[str, Any]], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.setenv("UNDERCOOKED_TICK_HZ", "99") reset_settings_cache() assert main([]) == EXIT_CONFIG_ERROR assert uvicorn_calls == [] assert "UNDERCOOKED_TICK_HZ=99" in capsys.readouterr().err def test_unknown_flag_is_an_argument_error() -> None: with pytest.raises(SystemExit) as excinfo: main(["--nope"]) assert excinfo.value.code == EXIT_CONFIG_ERROR def test_an_unknown_settings_field_is_a_config_error() -> None: with pytest.raises(ConfigError, match="not a configuration field"): apply_cli_overrides(**{"nonsense": 1}) # ----------------------------------------------------------------------------- logging def test_uvicorn_log_config_adds_our_loggers_without_touching_uvicorns() -> None: base = copy.deepcopy(uvicorn.config.LOGGING_CONFIG) config = _uvicorn_log_config(base) assert config is not base assert set(base["loggers"]) == {"uvicorn", "uvicorn.error", "uvicorn.access"} ours = config["loggers"]["undercooked"] assert ours["handlers"] == ["default"] assert ours["level"] == "INFO" assert config["loggers"]["uvicorn"] is base["loggers"]["uvicorn"] assert set(config["formatters"]) == set(base["formatters"]) # the handler exists # ----------------------------------------------------------------------------- the console script def test_python_m_undercooked_prints_usage_and_exits_zero() -> None: result = subprocess.run( # fixed argv, the repo's own interpreter [sys.executable, "-m", "undercooked", "--help"], cwd=REPO_ROOT, capture_output=True, text=True, check=False, timeout=120, ) assert result.returncode == EXIT_OK, result.stderr assert "usage: undercooked" in result.stdout assert "--reload" in result.stdout