feat(training): implement LanderCliEnv wrapping the GameCli subprocess
This commit is contained in:
103
Training/tests/test_lander_cli_env.py
Normal file
103
Training/tests/test_lander_cli_env.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user