File size: 9,817 Bytes
fe0625d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | """
测试模块 02 —— reset() 行为
需求覆盖
--------
* R1:seed 参数控制可复现性
* R2:三通道观测编码语义
* R4:reset 返回 (obs, info)
* RF1:BFS 连通性保证(起终点强制可达)
* RF3:info 新增字段初值
对应用例
--------
TC-03, TC-04, TC-05, TC-06, TC-07
"""
from __future__ import annotations
import numpy as np
import pytest
from maze_env import MazeEnv
class TestReset:
"""验证 reset() 的返回值格式、通道语义与 info 初始值。"""
# ------------------------------------------------------------------ #
# TC-03 返回值格式 #
# ------------------------------------------------------------------ #
@pytest.mark.unit
def test_reset_obs_shape(self, env_zero: MazeEnv) -> None:
"""TC-03a:reset() 返回 obs,形状为 (4, N, N)。
输入: env_zero.reset(),seed=0
期望: obs.shape == (4, 6, 6)
实测: 解包 reset() 返回值
"""
obs, _ = env_zero.reset()
assert obs.shape == (4, 6, 6)
@pytest.mark.unit
def test_reset_obs_dtype(self, env_zero: MazeEnv) -> None:
"""TC-03b:reset() 返回 obs,dtype 为 float32。
输入: env_zero.reset()
期望: obs.dtype == float32
"""
obs, _ = env_zero.reset()
assert obs.dtype == np.float32
@pytest.mark.unit
def test_reset_info_keys(self, env_zero: MazeEnv) -> None:
"""TC-03c:info 包含所有规定字段。
输入: env_zero.reset()
期望: info.keys() 包含五个字段
"""
_, info = env_zero.reset()
required = {"agent_pos", "goal_pos", "step_count",
"hit_wall_count", "success"}
assert required.issubset(info.keys())
# ------------------------------------------------------------------ #
# TC-04 边界与起终点 #
# ------------------------------------------------------------------ #
@pytest.mark.unit
def test_border_walls(self) -> None:
"""TC-04a:四条边界全部为墙(wall_map 四边均为 1)。
输入: MazeEnv(grid_size=8, obstacle_density=0.0, seed=0).reset()
期望: wall[0,:], wall[-1,:], wall[:,0], wall[:,-1] 全为 1.0
实测: obs[0](墙壁通道)各边切片
"""
env = MazeEnv(grid_size=8, obstacle_density=0.0, seed=0)
obs, _ = env.reset()
wall = obs[0]
assert np.all(wall[0, :] == 1.0), "上边界应全为墙"
assert np.all(wall[-1, :] == 1.0), "下边界应全为墙"
assert np.all(wall[:, 0] == 1.0), "左边界应全为墙"
assert np.all(wall[:, -1] == 1.0), "右边界应全为墙"
@pytest.mark.unit
def test_start_goal_not_wall(self) -> None:
"""TC-04b:起点 (1,1) 与终点 (N-2,N-2) 永远不为墙。
输入: MazeEnv(grid_size=8, obstacle_density=0.0, seed=0).reset()
期望: wall[1,1] == 0.0,wall[6,6] == 0.0
实测: obs[0] 对应坐标处的值
"""
env = MazeEnv(grid_size=8, obstacle_density=0.0, seed=0)
obs, _ = env.reset()
N, wall = 8, obs[0]
assert wall[1, 1] == 0.0, "起点 (1,1) 不应为墙"
assert wall[N-2, N-2] == 0.0, "终点 (N-2,N-2) 不应为墙"
# ------------------------------------------------------------------ #
# TC-05 Agent 通道 #
# ------------------------------------------------------------------ #
@pytest.mark.unit
def test_agent_channel_position(self, env_zero: MazeEnv) -> None:
"""TC-05a:obs[1] 在 agent_pos 处为 1.0。
输入: env_zero.reset(),agent_pos=(1,1)
期望: obs[1][1,1] == 1.0
实测: obs[1][ar, ac]
"""
obs, info = env_zero.reset()
ar, ac = info["agent_pos"]
assert obs[1, ar, ac] == 1.0
@pytest.mark.unit
def test_agent_channel_unique(self, env_zero: MazeEnv) -> None:
"""TC-05b:obs[1] 全图仅一个激活格(sum == 1.0)。
输入: env_zero.reset()
期望: obs[1].sum() == 1.0
实测: np.sum(obs[1])
"""
obs, _ = env_zero.reset()
assert float(obs[1].sum()) == 1.0
# ------------------------------------------------------------------ #
# TC-06 终点通道 #
# ------------------------------------------------------------------ #
@pytest.mark.unit
def test_goal_channel_position(self, env_zero: MazeEnv) -> None:
"""TC-06a:obs[2] 在 goal_pos 处为 1.0,grid=6 时 goal=(4,4)。
输入: env_zero.reset()
期望: obs[2][4,4] == 1.0,obs[2].sum() == 1.0
实测: obs[2][gr, gc] 及 sum
"""
obs, info = env_zero.reset()
gr, gc = info["goal_pos"]
assert obs[2, gr, gc] == 1.0
assert float(obs[2].sum()) == 1.0
# ------------------------------------------------------------------ #
# TC-07 info 初始值 #
# ------------------------------------------------------------------ #
@pytest.mark.unit
def test_info_initial_values(self, env_zero: MazeEnv) -> None:
"""TC-07:reset() 后 info 所有幕级统计量为初始值。
输入: env_zero.reset()
期望:
agent_pos == (1, 1)
goal_pos == (4, 4)(N=6 时 N-2=4)
step_count == 0
hit_wall_count == 0
success == False
实测: info 字典各字段
"""
_, info = env_zero.reset()
assert info["agent_pos"] == (1, 1)
assert info["goal_pos"] == (4, 4)
assert info["step_count"] == 0
assert info["hit_wall_count"] == 0
assert info["success"] is False
# ======================================================================
# TC-14 外部注入地图(options["wall_map"])
# ======================================================================
class TestResetWallMapInjection:
"""验证 reset(options={'wall_map': ...}) 外部注入地图路径。"""
@pytest.mark.unit
def test_inject_wall_map_used(self) -> None:
"""TC-14a:注入自定义 wall_map 后,env._wall_map 与注入值一致。
输入: 全零 6×6 wall_map(无障碍)
期望: env.wall_map 与注入值匹配
实测: np.array_equal
"""
import numpy as np
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
custom_map = np.zeros((6, 6), dtype=np.float32)
# 添加边界墙(真实使用场景)
custom_map[0, :] = 1.0
custom_map[-1, :] = 1.0
custom_map[:, 0] = 1.0
custom_map[:, -1] = 1.0
env.reset(options={"wall_map": custom_map})
assert np.array_equal(env.wall_map, custom_map), \
"注入的 wall_map 应被环境直接使用"
@pytest.mark.unit
def test_inject_wall_map_wrong_shape_raises(self) -> None:
"""TC-14b:注入形状不匹配的 wall_map 应抛出 ValueError。
输入: grid_size=6 的环境,注入 5×5 wall_map
期望: ValueError
实测: pytest.raises
"""
import numpy as np
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
bad_map = np.zeros((5, 5), dtype=np.float32)
with pytest.raises(ValueError, match="wall_map"):
env.reset(options={"wall_map": bad_map})
@pytest.mark.unit
def test_inject_custom_start_goal(self) -> None:
"""TC-14c:通过 options 覆盖 start / goal 坐标生效。
输入: options={'start': (1,1), 'goal': (2,2)}
期望: info['agent_pos'] == (1,1),info['goal_pos'] == (2,2)
实测: info 字段
"""
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
_, info = env.reset(options={"start": (1, 1), "goal": (2, 2)})
assert info["agent_pos"] == (1, 1)
assert info["goal_pos"] == (2, 2)
# ======================================================================
# TC-15 只读属性(wall_map / goal_pos / agent_pos)
# ======================================================================
class TestReadOnlyProperties:
"""验证环境暴露的只读属性行为。"""
@pytest.mark.unit
def test_wall_map_property_readonly(self) -> None:
"""TC-15a:wall_map 属性返回不可写视图,写入应抛出 ValueError。
期望: 对返回的 ndarray 赋值触发 ValueError
实测: ValueError
"""
import numpy as np
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
env.reset()
wmap = env.wall_map
with pytest.raises(ValueError):
wmap[0, 0] = 1.0 # 写入只读视图应抛出异常
@pytest.mark.unit
def test_goal_pos_property(self) -> None:
"""TC-15b:goal_pos 属性返回终点坐标 tuple。
期望: isinstance(env.goal_pos, tuple),值为 (N-2, N-2)
实测: 属性值类型与内容
"""
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
env.reset()
gp = env.goal_pos
assert isinstance(gp, tuple)
assert gp == (4, 4)
@pytest.mark.unit
def test_agent_pos_property(self) -> None:
"""TC-15c:agent_pos 属性返回 Agent 当前坐标 tuple。
期望: reset 后 agent_pos == (1, 1)
实测: 属性值
"""
env = MazeEnv(grid_size=6, obstacle_density=0.0, seed=0)
env.reset()
assert env.agent_pos == (1, 1)
|