104 lines
3.0 KiB
Python
104 lines
3.0 KiB
Python
"""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()
|