""" 测试模块 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)