"""Unit tests for LanderCliEnv.""" from pathlib import Path import numpy as np import pytest from gymnasium.utils.env_checker import check_env # Add parent dir to sys.path so we can import lander_cli_env import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from lander_cli_env import LanderCliEnv REPO_ROOT = Path(__file__).resolve().parents[2] CLI_BINARY = REPO_ROOT / "publish" / "GameCli" / "GameCli" @pytest.fixture(scope="module") def cli_binary_exists(): if not CLI_BINARY.exists(): pytest.skip(f"CLI binary not found at {CLI_BINARY} — run `dotnet publish GameCli -c Release -o publish/GameCli`") return CLI_BINARY def test_env_opens_and_closes(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) env.close() # should not raise def test_observation_space_is_seven_dim(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) try: assert env.observation_space.shape == (7,) finally: env.close() def test_action_space_is_discrete_four(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) try: assert env.action_space.n == 4 finally: env.close() def test_reset_returns_seven_dim_observation(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) try: obs, info = env.reset(seed=42) assert isinstance(obs, np.ndarray) assert obs.shape == (7,) assert obs.dtype == np.float32 finally: env.close() def test_step_advances_and_returns_five_tuple(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) try: env.reset(seed=0) obs, reward, terminated, truncated, info = env.step(0) # noop assert obs.shape == (7,) assert isinstance(reward, float) assert terminated is False # never terminates assert isinstance(truncated, bool) finally: env.close() def test_reset_randomizes_target_across_episodes(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists)) try: # Two resets with different seeds should generally give different targets. env.reset(seed=1) target1 = env._target env.reset(seed=2) target2 = env._target assert target1 != target2 finally: env.close() def test_truncation_at_max_steps(cli_binary_exists): env = LanderCliEnv(cli_binary=str(cli_binary_exists), max_episode_steps=5) try: env.reset(seed=0) truncated = False for _ in range(5): _, _, _, truncated, _ = env.step(0) assert truncated is True finally: env.close() def test_check_env_passes(cli_binary_exists): """Gymnasium's own env sanity checker — spaces, dtypes, reset/step contract.""" env = LanderCliEnv(cli_binary=str(cli_binary_exists), max_episode_steps=50) try: # skip_render_check because we don't implement rendering check_env(env.unwrapped, skip_render_check=True) finally: env.close()