Skip to content

Commit d9aa52d

Browse files
committed
test: add PettingZoo parallel env test suite
1 parent 0cbce62 commit d9aa52d

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

‎tests/envs/test_pettingzoo.py‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Tests for PettingZoo ParallelEnv integration.
2+
3+
Skipped automatically if pettingzoo is not installed
4+
(e.g. in base CI without the [multi-agent] extra).
5+
"""
6+
from __future__ import annotations
7+
8+
import pytest
9+
10+
# Skip entire module if pettingzoo is not available
11+
pytest.importorskip(
12+
"pettingzoo.test",
13+
reason="pettingzoo not installed; run with: pip install highway-env[multi-agent]",
14+
)
15+
from pettingzoo.test import parallel_api_test
16+
17+
import highway_env # noqa: F401 — triggers env registration
18+
from highway_env.envs.intersection_pz_env import (
19+
ConnectedLaneIntersectionParallelEnv,
20+
IntersectionParallelEnv,
21+
)
22+
from highway_env.envs.common.pettingzoo_env import HighwayParallelEnv
23+
24+
25+
# -----------------------------------------------------------------------
26+
# Fixtures
27+
# -----------------------------------------------------------------------
28+
29+
@pytest.fixture
30+
def intersection_env():
31+
env = IntersectionParallelEnv(config={"controlled_vehicles": 2, "duration": 10})
32+
yield env
33+
env.close()
34+
35+
36+
@pytest.fixture
37+
def connected_lane_env():
38+
env = ConnectedLaneIntersectionParallelEnv(
39+
config={"controlled_vehicles": 2, "duration": 10}
40+
)
41+
yield env
42+
env.close()
43+
44+
45+
# -----------------------------------------------------------------------
46+
# PettingZoo API conformance
47+
# -----------------------------------------------------------------------
48+
49+
def test_parallel_api_test_intersection():
50+
"""PettingZoo's built-in API conformance checker must pass."""
51+
env = IntersectionParallelEnv(config={"controlled_vehicles": 2, "duration": 5})
52+
parallel_api_test(env, num_cycles=10)
53+
env.close()
54+
55+
56+
def test_parallel_api_test_connected_lane():
57+
"""Same for the connected-lane variant."""
58+
env = ConnectedLaneIntersectionParallelEnv(
59+
config={"controlled_vehicles": 2, "duration": 5}
60+
)
61+
parallel_api_test(env, num_cycles=10)
62+
env.close()
63+
64+
65+
# -----------------------------------------------------------------------
66+
# Observation / action space correctness
67+
# -----------------------------------------------------------------------
68+
69+
def test_reset_returns_agent_dict(intersection_env):
70+
"""reset() must return dicts keyed by agent name."""
71+
obs, infos = intersection_env.reset(seed=42)
72+
assert isinstance(obs, dict)
73+
assert isinstance(infos, dict)
74+
assert set(obs.keys()) == set(intersection_env.possible_agents)
75+
assert set(infos.keys()) == set(intersection_env.possible_agents)
76+
77+
78+
def test_obs_in_observation_space(intersection_env):
79+
"""Every observation must lie within the declared observation space."""
80+
obs, _ = intersection_env.reset(seed=0)
81+
while intersection_env.agents:
82+
actions = {a: intersection_env.action_space(a).sample() for a in intersection_env.agents}
83+
obs, rewards, terminations, truncations, infos = intersection_env.step(actions)
84+
for agent, agent_obs in obs.items():
85+
assert intersection_env.observation_space(agent).contains(agent_obs), (
86+
f"{agent}: obs not in observation_space"
87+
)
88+
89+
90+
def test_possible_agents_is_stable(intersection_env):
91+
"""possible_agents must not change across the episode."""
92+
intersection_env.reset(seed=1)
93+
initial_possible = list(intersection_env.possible_agents)
94+
while intersection_env.agents:
95+
actions = {a: intersection_env.action_space(a).sample() for a in intersection_env.agents}
96+
intersection_env.step(actions)
97+
assert intersection_env.possible_agents == initial_possible
98+
99+
100+
# -----------------------------------------------------------------------
101+
# Per-agent termination semantics
102+
# -----------------------------------------------------------------------
103+
104+
def test_terminated_agents_removed_from_agents(intersection_env):
105+
"""Agents flagged as terminated must NOT appear in self.agents next step."""
106+
intersection_env.reset(seed=2)
107+
while intersection_env.agents:
108+
pre_step_agents = set(intersection_env.agents)
109+
actions = {a: intersection_env.action_space(a).sample() for a in intersection_env.agents}
110+
_, _, terminations, truncations, _ = intersection_env.step(actions)
111+
112+
for agent in pre_step_agents:
113+
if terminations.get(agent, False) or truncations.get(agent, False):
114+
assert agent not in intersection_env.agents, (
115+
f"{agent} terminated/truncated but still in env.agents"
116+
)
117+
118+
119+
def test_episode_ends_when_all_agents_done(intersection_env):
120+
"""Episode loop must naturally exit when all agents terminate."""
121+
intersection_env.reset(seed=3)
122+
steps = 0
123+
max_steps = 500
124+
while intersection_env.agents and steps < max_steps:
125+
actions = {a: intersection_env.action_space(a).sample() for a in intersection_env.agents}
126+
intersection_env.step(actions)
127+
steps += 1
128+
# If we reach here without error, the loop terminated correctly
129+
assert steps < max_steps, "Episode never ended — env may be stuck"
130+
131+
132+
# -----------------------------------------------------------------------
133+
# Seeding / reproducibility
134+
# -----------------------------------------------------------------------
135+
136+
def test_reset_with_same_seed_is_reproducible():
137+
"""Two resets with the same seed must produce identical initial observations."""
138+
import numpy as np
139+
env1 = IntersectionParallelEnv(config={"controlled_vehicles": 2, "duration": 5})
140+
env2 = IntersectionParallelEnv(config={"controlled_vehicles": 2, "duration": 5})
141+
142+
obs1, _ = env1.reset(seed=99)
143+
obs2, _ = env2.reset(seed=99)
144+
145+
for agent in obs1:
146+
np.testing.assert_array_equal(obs1[agent], obs2[agent])
147+
148+
env1.close()
149+
env2.close()
150+
151+
152+
# -----------------------------------------------------------------------
153+
# Deprecation warning for MultiAgentWrapper
154+
# -----------------------------------------------------------------------
155+
156+
def test_multi_agent_wrapper_deprecation_warning():
157+
"""Using MultiAgentWrapper must raise DeprecationWarning."""
158+
import warnings
159+
import gymnasium as gym
160+
161+
with warnings.catch_warnings(record=True) as caught:
162+
warnings.simplefilter("always")
163+
env = gym.make("intersection-multi-agent-v1")
164+
env.close()
165+
166+
deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
167+
assert len(deprecation_warnings) > 0, (
168+
"Expected DeprecationWarning from MultiAgentWrapper but none was raised"
169+
)
170+
assert any(
171+
"MultiAgentWrapper" in str(w.message) for w in deprecation_warnings
172+
), "Expected a DeprecationWarning mentioning MultiAgentWrapper"

0 commit comments

Comments
 (0)