nvkartik commited on
Commit
b633d71
·
1 Parent(s): 6ac7a1e

added tests

Browse files
Files changed (2) hide show
  1. tests/test_arena_processor.py +406 -0
  2. tests/test_envs.py +151 -0
tests/test_arena_processor.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Copyright 2025 The HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import pytest
17
+ import torch
18
+
19
+ from lerobot.configs.types import (
20
+ FeatureType,
21
+ PipelineFeatureType,
22
+ PolicyFeature,
23
+ )
24
+ from lerobot.processor.env_processor import IsaaclabArenaProcessorStep
25
+ from lerobot.utils.constants import OBS_IMAGES, OBS_STATE, OBS_STR
26
+
27
+ # Test constants
28
+ BATCH_SIZE = 2
29
+ STATE_DIM = 16
30
+ IMG_HEIGHT = 64
31
+ IMG_WIDTH = 64
32
+
33
+ # Generic test keys (not real robot keys)
34
+ TEST_STATE_KEY = "test_state_obs"
35
+ TEST_CAMERA_KEY = "test_rgb_cam"
36
+
37
+
38
+ @pytest.fixture
39
+ def processor():
40
+ """Default processor with test keys."""
41
+ return IsaaclabArenaProcessorStep(
42
+ state_keys=(TEST_STATE_KEY,),
43
+ camera_keys=(TEST_CAMERA_KEY,),
44
+ )
45
+
46
+
47
+ @pytest.fixture
48
+ def sample_observation():
49
+ """Sample IsaacLab Arena observation with state and camera data."""
50
+ return {
51
+ f"{OBS_STR}.policy": {
52
+ TEST_STATE_KEY: torch.randn(BATCH_SIZE, STATE_DIM),
53
+ },
54
+ f"{OBS_STR}.camera_obs": {
55
+ TEST_CAMERA_KEY: torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
56
+ },
57
+ }
58
+
59
+
60
+ # =============================================================================
61
+ # State Processing Tests
62
+ # =============================================================================
63
+
64
+
65
+ def test_state_extraction(processor, sample_observation):
66
+ """Test that state is extracted and converted to float32."""
67
+ processed = processor.observation(sample_observation)
68
+
69
+ assert OBS_STATE in processed
70
+ assert processed[OBS_STATE].shape == (BATCH_SIZE, STATE_DIM)
71
+ assert processed[OBS_STATE].dtype == torch.float32
72
+
73
+
74
+ def test_state_concatenation_multiple_keys():
75
+ """Test that multiple state keys are concatenated in order."""
76
+ dim1, dim2 = 10, 6
77
+ processor = IsaaclabArenaProcessorStep(
78
+ state_keys=("state_alpha", "state_beta"),
79
+ camera_keys=(),
80
+ )
81
+
82
+ obs = {
83
+ f"{OBS_STR}.policy": {
84
+ "state_alpha": torch.ones(BATCH_SIZE, dim1),
85
+ "state_beta": torch.ones(BATCH_SIZE, dim2) * 2,
86
+ },
87
+ }
88
+
89
+ processed = processor.observation(obs)
90
+
91
+ state = processed[OBS_STATE]
92
+ assert state.shape == (BATCH_SIZE, dim1 + dim2)
93
+ # Verify ordering: first dim1 elements are 1s, last dim2 are 2s
94
+ assert torch.all(state[:, :dim1] == 1.0)
95
+ assert torch.all(state[:, dim1:] == 2.0)
96
+
97
+
98
+ def test_state_flattening_higher_dims():
99
+ """Test that state with dim > 2 is flattened to (B, -1)."""
100
+ processor = IsaaclabArenaProcessorStep(
101
+ state_keys=("multidim_state",),
102
+ camera_keys=(),
103
+ )
104
+
105
+ # Shape (B, 4, 4) -> should flatten to (B, 16)
106
+ obs = {
107
+ f"{OBS_STR}.policy": {
108
+ "multidim_state": torch.randn(BATCH_SIZE, 4, 4),
109
+ },
110
+ }
111
+
112
+ processed = processor.observation(obs)
113
+
114
+ assert processed[OBS_STATE].shape == (BATCH_SIZE, 16)
115
+
116
+
117
+ def test_state_filters_to_configured_keys():
118
+ """Test that only configured state_keys are extracted."""
119
+ processor = IsaaclabArenaProcessorStep(
120
+ state_keys=("included_key",),
121
+ camera_keys=(),
122
+ )
123
+
124
+ obs = {
125
+ f"{OBS_STR}.policy": {
126
+ "included_key": torch.randn(BATCH_SIZE, 10),
127
+ "excluded_key": torch.randn(BATCH_SIZE, 6), # Should be ignored
128
+ },
129
+ }
130
+
131
+ processed = processor.observation(obs)
132
+
133
+ # Only included_key (dim 10) should be included
134
+ assert processed[OBS_STATE].shape == (BATCH_SIZE, 10)
135
+
136
+
137
+ def test_missing_state_key_skipped():
138
+ """Test that missing state keys in observation are skipped."""
139
+ processor = IsaaclabArenaProcessorStep(
140
+ state_keys=("present_key", "missing_key"),
141
+ camera_keys=(),
142
+ )
143
+
144
+ obs = {
145
+ f"{OBS_STR}.policy": {
146
+ "present_key": torch.randn(BATCH_SIZE, 10),
147
+ # missing_key not present
148
+ },
149
+ }
150
+
151
+ processed = processor.observation(obs)
152
+
153
+ # Should only have present_key
154
+ assert processed[OBS_STATE].shape == (BATCH_SIZE, 10)
155
+
156
+
157
+ # =============================================================================
158
+ # Camera/Image Processing Tests
159
+ # =============================================================================
160
+
161
+
162
+ def test_camera_permutation_bhwc_to_bchw(processor, sample_observation):
163
+ """Test images are permuted from (B, H, W, C) to (B, C, H, W)."""
164
+ processed = processor.observation(sample_observation)
165
+
166
+ img_key = f"{OBS_IMAGES}.{TEST_CAMERA_KEY}"
167
+ assert img_key in processed
168
+ img = processed[img_key]
169
+ assert img.shape == (BATCH_SIZE, 3, IMG_HEIGHT, IMG_WIDTH)
170
+
171
+
172
+ def test_camera_uint8_to_normalized_float32(processor):
173
+ """Test that uint8 images are normalized to float32 [0, 1]."""
174
+ obs = {
175
+ f"{OBS_STR}.camera_obs": {
176
+ TEST_CAMERA_KEY: torch.full((BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), 255, dtype=torch.uint8),
177
+ },
178
+ }
179
+
180
+ processed = processor.observation(obs)
181
+
182
+ img = processed[f"{OBS_IMAGES}.{TEST_CAMERA_KEY}"]
183
+ assert img.dtype == torch.float32
184
+ assert torch.allclose(img, torch.ones_like(img))
185
+
186
+
187
+ def test_camera_float32_passthrough(processor):
188
+ """Test that float32 images are kept as float32."""
189
+ original_img = torch.rand(BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3, dtype=torch.float32)
190
+ obs = {
191
+ f"{OBS_STR}.camera_obs": {
192
+ TEST_CAMERA_KEY: original_img.clone(),
193
+ },
194
+ }
195
+
196
+ processed = processor.observation(obs)
197
+
198
+ img = processed[f"{OBS_IMAGES}.{TEST_CAMERA_KEY}"]
199
+ assert img.dtype == torch.float32
200
+ # Values should be same (just permuted)
201
+ expected = original_img.permute(0, 3, 1, 2)
202
+ assert torch.allclose(img, expected)
203
+
204
+
205
+ def test_camera_other_dtype_converted_to_float(processor):
206
+ """Test that non-uint8, non-float32 dtypes are converted to float."""
207
+ obs = {
208
+ f"{OBS_STR}.camera_obs": {
209
+ TEST_CAMERA_KEY: torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.int32),
210
+ },
211
+ }
212
+
213
+ processed = processor.observation(obs)
214
+
215
+ img = processed[f"{OBS_IMAGES}.{TEST_CAMERA_KEY}"]
216
+ assert img.dtype == torch.float32
217
+
218
+
219
+ def test_camera_filters_to_configured_keys():
220
+ """Test that only configured camera_keys are extracted."""
221
+ processor = IsaaclabArenaProcessorStep(
222
+ state_keys=(),
223
+ camera_keys=("included_cam",),
224
+ )
225
+
226
+ obs = {
227
+ f"{OBS_STR}.camera_obs": {
228
+ "included_cam": torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
229
+ "excluded_cam": torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
230
+ },
231
+ }
232
+
233
+ processed = processor.observation(obs)
234
+
235
+ assert f"{OBS_IMAGES}.included_cam" in processed
236
+ assert f"{OBS_IMAGES}.excluded_cam" not in processed
237
+
238
+
239
+ def test_camera_key_preserved_exactly():
240
+ """Test that camera key name is used exactly (no suffix stripping)."""
241
+ processor = IsaaclabArenaProcessorStep(
242
+ state_keys=(),
243
+ camera_keys=("my_cam_rgb",),
244
+ )
245
+
246
+ obs = {
247
+ f"{OBS_STR}.camera_obs": {
248
+ "my_cam_rgb": torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
249
+ },
250
+ }
251
+
252
+ processed = processor.observation(obs)
253
+
254
+ # Key should be exactly as configured, with _rgb suffix intact
255
+ assert f"{OBS_IMAGES}.my_cam_rgb" in processed
256
+ assert f"{OBS_IMAGES}.my_cam" not in processed
257
+
258
+
259
+ # =============================================================================
260
+ # Edge Cases & Missing Data Tests
261
+ # =============================================================================
262
+
263
+
264
+ def test_missing_camera_obs_section(processor):
265
+ """Test processor handles observation without camera_obs section."""
266
+ obs = {
267
+ f"{OBS_STR}.policy": {
268
+ TEST_STATE_KEY: torch.randn(BATCH_SIZE, STATE_DIM),
269
+ },
270
+ }
271
+
272
+ processed = processor.observation(obs)
273
+
274
+ assert OBS_STATE in processed
275
+ assert not any(k.startswith(OBS_IMAGES) for k in processed)
276
+
277
+
278
+ def test_missing_policy_obs_section(processor):
279
+ """Test processor handles observation without policy section."""
280
+ obs = {
281
+ f"{OBS_STR}.camera_obs": {
282
+ TEST_CAMERA_KEY: torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
283
+ },
284
+ }
285
+
286
+ processed = processor.observation(obs)
287
+
288
+ assert f"{OBS_IMAGES}.{TEST_CAMERA_KEY}" in processed
289
+ assert OBS_STATE not in processed
290
+
291
+
292
+ def test_empty_observation(processor):
293
+ """Test processor handles empty observation dict."""
294
+ processed = processor.observation({})
295
+
296
+ assert OBS_STATE not in processed
297
+ assert not any(k.startswith(OBS_IMAGES) for k in processed)
298
+
299
+
300
+ def test_no_matching_state_keys():
301
+ """Test processor when no state keys match observation."""
302
+ processor = IsaaclabArenaProcessorStep(
303
+ state_keys=("nonexistent_key",),
304
+ camera_keys=(),
305
+ )
306
+
307
+ obs = {
308
+ f"{OBS_STR}.policy": {
309
+ "some_other_key": torch.randn(BATCH_SIZE, STATE_DIM),
310
+ },
311
+ }
312
+
313
+ processed = processor.observation(obs)
314
+
315
+ # No state because no keys matched
316
+ assert OBS_STATE not in processed
317
+
318
+
319
+ def test_no_matching_camera_keys():
320
+ """Test processor when no camera keys match observation."""
321
+ processor = IsaaclabArenaProcessorStep(
322
+ state_keys=(),
323
+ camera_keys=("nonexistent_cam",),
324
+ )
325
+
326
+ obs = {
327
+ f"{OBS_STR}.camera_obs": {
328
+ "some_other_cam": torch.randint(
329
+ 0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8
330
+ ),
331
+ },
332
+ }
333
+
334
+ processed = processor.observation(obs)
335
+
336
+ assert not any(k.startswith(OBS_IMAGES) for k in processed)
337
+
338
+
339
+ # =============================================================================
340
+ # Configuration Tests
341
+ # =============================================================================
342
+
343
+
344
+ def test_default_keys():
345
+ """Test default state_keys and camera_keys values."""
346
+ processor = IsaaclabArenaProcessorStep()
347
+
348
+ assert processor.state_keys == ("robot_joint_pos",)
349
+ assert processor.camera_keys == ("robot_pov_cam_rgb",)
350
+
351
+
352
+ def test_custom_keys_configuration():
353
+ """Test processor with custom state and camera keys."""
354
+ processor = IsaaclabArenaProcessorStep(
355
+ state_keys=("pos_xyz", "quat_wxyz", "grip_val"),
356
+ camera_keys=("front_view", "wrist_view"),
357
+ )
358
+
359
+ obs = {
360
+ f"{OBS_STR}.policy": {
361
+ "pos_xyz": torch.randn(BATCH_SIZE, 3),
362
+ "quat_wxyz": torch.randn(BATCH_SIZE, 4),
363
+ "grip_val": torch.randn(BATCH_SIZE, 1),
364
+ },
365
+ f"{OBS_STR}.camera_obs": {
366
+ "front_view": torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
367
+ "wrist_view": torch.randint(0, 255, (BATCH_SIZE, IMG_HEIGHT, IMG_WIDTH, 3), dtype=torch.uint8),
368
+ },
369
+ }
370
+
371
+ processed = processor.observation(obs)
372
+
373
+ # State should be concatenated: 3 + 4 + 1 = 8
374
+ assert processed[OBS_STATE].shape == (BATCH_SIZE, 8)
375
+ # Both cameras should be present
376
+ assert f"{OBS_IMAGES}.front_view" in processed
377
+ assert f"{OBS_IMAGES}.wrist_view" in processed
378
+
379
+
380
+ # =============================================================================
381
+ # transform_features Tests
382
+ # =============================================================================
383
+
384
+
385
+ def test_transform_features_passthrough(processor):
386
+ """Test that transform_features returns features unchanged."""
387
+ input_features = {
388
+ PipelineFeatureType.OBSERVATION: {
389
+ "observation.state": PolicyFeature(
390
+ type=FeatureType.STATE,
391
+ shape=(16,),
392
+ ),
393
+ "observation.images.cam": PolicyFeature(
394
+ type=FeatureType.VISUAL,
395
+ shape=(3, 64, 64),
396
+ ),
397
+ },
398
+ PipelineFeatureType.ACTION: {
399
+ "action": PolicyFeature(type=FeatureType.ACTION, shape=(7,)),
400
+ },
401
+ }
402
+
403
+ output_features = processor.transform_features(input_features)
404
+
405
+ # Should be unchanged
406
+ assert output_features == input_features
tests/test_envs.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from isaaclab_env_wrapper import IsaacLabEnvWrapper
2
+ from unittest.mock import MagicMock
3
+ import gym
4
+ import numpy as np
5
+ import torch
6
+
7
+
8
+ def _create_mock_isaaclab_env(num_envs: int = 2, device: str = "cpu"):
9
+ """Create a mock IsaacLab environment for testing."""
10
+ mock_env = MagicMock()
11
+ mock_env.num_envs = num_envs
12
+ mock_env.device = device
13
+ mock_env.observation_space = gym.spaces.Dict(
14
+ {"policy": gym.spaces.Box(low=-1, high=1, shape=(num_envs, 54), dtype=np.float32)}
15
+ )
16
+ mock_env.action_space = gym.spaces.Box(low=-1, high=1, shape=(36,), dtype=np.float32)
17
+ mock_env.metadata = {}
18
+ return mock_env
19
+
20
+
21
+ def test_isaaclab_wrapper_init():
22
+ """Test IsaacLabEnvWrapper initialization."""
23
+ mock_env = _create_mock_isaaclab_env(num_envs=4)
24
+
25
+ wrapper = IsaacLabEnvWrapper(
26
+ mock_env,
27
+ episode_length=300,
28
+ task="Test task",
29
+ render_mode="rgb_array",
30
+ )
31
+
32
+ assert wrapper.num_envs == 4
33
+ assert wrapper._max_episode_steps == 300
34
+ assert wrapper.task == "Test task"
35
+ assert wrapper.render_mode == "rgb_array"
36
+ assert wrapper.device == "cpu"
37
+ assert len(wrapper.envs) == 4
38
+
39
+
40
+ def test_isaaclab_wrapper_reset():
41
+ """Test IsaacLabEnvWrapper reset."""
42
+ mock_env = _create_mock_isaaclab_env(num_envs=2)
43
+ mock_obs = {"policy": torch.randn(2, 54)}
44
+ mock_env.reset.return_value = (mock_obs, {})
45
+
46
+ wrapper = IsaacLabEnvWrapper(mock_env, episode_length=100)
47
+ obs, info = wrapper.reset(seed=42)
48
+
49
+ mock_env.reset.assert_called_once_with(seed=42, options=None)
50
+ assert "final_info" in info
51
+ assert "is_success" in info["final_info"]
52
+ assert len(info["final_info"]["is_success"]) == 2
53
+
54
+
55
+ def test_isaaclab_wrapper_reset_with_seed_list():
56
+ """Test that seed list is handled correctly (IsaacLab expects single seed)."""
57
+ mock_env = _create_mock_isaaclab_env(num_envs=2)
58
+ mock_env.reset.return_value = ({"policy": torch.randn(2, 54)}, {})
59
+
60
+ wrapper = IsaacLabEnvWrapper(mock_env)
61
+ wrapper.reset(seed=[42, 43, 44])
62
+
63
+ # Should extract first seed
64
+ mock_env.reset.assert_called_once_with(seed=42, options=None)
65
+
66
+
67
+ def test_isaaclab_wrapper_step():
68
+ """Test IsaacLabEnvWrapper step."""
69
+ mock_env = _create_mock_isaaclab_env(num_envs=2)
70
+ mock_env.step.return_value = (
71
+ {"policy": torch.randn(2, 54)},
72
+ torch.tensor([0.5, 0.3]),
73
+ torch.tensor([False, False]),
74
+ torch.tensor([False, True]),
75
+ {},
76
+ )
77
+ # Mock termination manager
78
+ mock_env.termination_manager.get_term.return_value = torch.tensor([False, True])
79
+
80
+ wrapper = IsaacLabEnvWrapper(mock_env)
81
+ actions = np.random.randn(2, 36).astype(np.float32)
82
+ obs, reward, terminated, truncated, info = wrapper.step(actions)
83
+
84
+ assert reward.dtype == np.float32
85
+ assert terminated.dtype == bool
86
+ assert truncated.dtype == bool
87
+ assert len(reward) == 2
88
+ assert "final_info" in info
89
+ assert "is_success" in info["final_info"]
90
+
91
+
92
+ def test_isaaclab_wrapper_call_method():
93
+ """Test IsaacLabEnvWrapper call method."""
94
+ mock_env = _create_mock_isaaclab_env(num_envs=3)
95
+
96
+ wrapper = IsaacLabEnvWrapper(mock_env, episode_length=200, task="My task")
97
+
98
+ # Test _max_episode_steps
99
+ result = wrapper.call("_max_episode_steps")
100
+ assert result == [200, 200, 200]
101
+
102
+ # Test task
103
+ result = wrapper.call("task")
104
+ assert result == ["My task", "My task", "My task"]
105
+
106
+
107
+ def test_isaaclab_wrapper_render():
108
+ """Test IsaacLabEnvWrapper render."""
109
+ mock_env = _create_mock_isaaclab_env(num_envs=2)
110
+ mock_frames = torch.randint(0, 255, (2, 480, 640, 3), dtype=torch.uint8)
111
+ mock_env.render.return_value = mock_frames
112
+
113
+ wrapper = IsaacLabEnvWrapper(mock_env, render_mode="rgb_array")
114
+ frame = wrapper.render()
115
+
116
+ assert frame is not None
117
+ assert frame.shape == (480, 640, 3) # Returns first env frame
118
+
119
+
120
+ def test_isaaclab_wrapper_render_all():
121
+ """Test IsaacLabEnvWrapper render_all."""
122
+ mock_env = _create_mock_isaaclab_env(num_envs=2)
123
+ mock_frames = torch.randint(0, 255, (2, 480, 640, 3), dtype=torch.uint8)
124
+ mock_env.render.return_value = mock_frames
125
+
126
+ wrapper = IsaacLabEnvWrapper(mock_env, render_mode="rgb_array")
127
+ frames = wrapper.render_all()
128
+
129
+ assert len(frames) == 2
130
+ assert all(f.shape == (480, 640, 3) for f in frames)
131
+
132
+
133
+ def test_isaaclab_wrapper_render_none():
134
+ """Test render returns None when render_mode is not rgb_array."""
135
+ mock_env = _create_mock_isaaclab_env()
136
+
137
+ wrapper = IsaacLabEnvWrapper(mock_env, render_mode=None)
138
+ assert wrapper.render() is None
139
+
140
+
141
+ def test_isaaclab_wrapper_close():
142
+ """Test IsaacLabEnvWrapper close."""
143
+ mock_env = _create_mock_isaaclab_env()
144
+ mock_app = MagicMock()
145
+
146
+ wrapper = IsaacLabEnvWrapper(mock_env, simulation_app=mock_app)
147
+ wrapper.close()
148
+
149
+ mock_env.close.assert_called_once()
150
+ mock_app.app.close.assert_called_once()
151
+ assert wrapper._closed