|
1 | | -"""Regression tests (backend-free) for the libero task fixes. |
2 | | -
|
3 | | -Covers two independent hardening fixes: |
4 | | -
|
5 | | -* ``Libero90BaseTask._get_initial_states`` must degrade to None (handler |
6 | | - defaults) when the trajectory is missing/empty or the scenario is robotless, |
7 | | - matching the already-hardened ``LiberoBaseTask``. Previously it indexed |
8 | | - ``scenario.robots[0]`` and called get_traj with no guard, so those cases |
9 | | - crashed. |
10 | | -* Every ``libero_pick_*`` task must declare ``robots=["franka"]``. 9 of 10 |
11 | | - omitted it, so they instantiated with an empty robot list — a robotless pick |
12 | | - task can never succeed and eval scores a silent 0%. |
| 1 | +"""Regression tests for two LIBERO content bugs (backend-free). |
| 2 | +
|
| 3 | +- scene3 "turn on the stove" termination did ``(stove_joint > thr).all(dim=-1)`` |
| 4 | + on a ``(N,)`` tensor, collapsing the per-env result to a scalar (reducing over |
| 5 | + the batch). The sibling scene9 does it correctly. |
| 6 | +- two pick tasks had registration-id typos (a misspelled alias and a primary id |
| 7 | + missing the ``pick_`` prefix that every sibling uses). |
13 | 8 | """ |
14 | 9 |
|
15 | 10 | from __future__ import annotations |
16 | 11 |
|
17 | 12 | import pathlib |
18 | 13 |
|
19 | 14 | import pytest |
| 15 | +import torch |
| 16 | + |
| 17 | +from metasim.types import ObjectState, TensorState |
20 | 18 |
|
21 | 19 | _LIBERO = pathlib.Path(__file__).resolve().parents[1] / "roboverse_pack" / "tasks" / "libero" |
22 | 20 |
|
23 | 21 |
|
| 22 | +@pytest.mark.general |
| 23 | +def test_stove_terminated_is_per_env(): |
| 24 | + from roboverse_pack.tasks.libero_90.libero_kitchen_scene3_turn_on_the_stove import ( |
| 25 | + LiberoKitchenScene3TurnOnStoveTask, |
| 26 | + ) |
| 27 | + |
| 28 | + states = TensorState( |
| 29 | + objects={ |
| 30 | + "flat_stove": ObjectState( |
| 31 | + root_state=torch.zeros(2, 13), |
| 32 | + joint_pos=torch.tensor([[0.6], [0.1]]), # env0 on, env1 off |
| 33 | + joint_vel=torch.zeros(2, 1), |
| 34 | + ) |
| 35 | + }, |
| 36 | + robots={}, |
| 37 | + cameras={}, |
| 38 | + ) |
| 39 | + task = LiberoKitchenScene3TurnOnStoveTask.__new__(LiberoKitchenScene3TurnOnStoveTask) |
| 40 | + out = task._terminated(states) |
| 41 | + assert out.shape == (2,), f"_terminated must be per-env (2,), got {tuple(out.shape)}" |
| 42 | + assert out.tolist() == [True, False] |
| 43 | + |
| 44 | + |
| 45 | +@pytest.mark.general |
| 46 | +def test_libero_pick_registration_ids_are_correct(): |
| 47 | + soup = (_LIBERO / "libero_pick_alphabet_soup.py").read_text() |
| 48 | + assert "pick_alphnabet_soup" not in soup, "misspelled alias 'pick_alphnabet_soup'" |
| 49 | + assert '"libero.pick_alphabet_soup", "pick_alphabet_soup"' in soup |
| 50 | + |
| 51 | + juice = (_LIBERO / "libero_pick_orange_juice.py").read_text() |
| 52 | + assert '"libero.orange_juice"' not in juice, "primary id is missing the 'pick_' prefix" |
| 53 | + assert '"libero.pick_orange_juice", "pick_orange_juice"' in juice |
| 54 | + |
| 55 | + |
| 56 | +@pytest.mark.general |
| 57 | +def test_all_libero_pick_tasks_have_a_robot(): |
| 58 | + """Every ``libero_pick_*`` task is a "pick up X and place in basket" task and |
| 59 | + therefore MUST declare a robot arm. Previously 9 of 10 omitted |
| 60 | + ``robots=["franka"]`` (only butter had it), so the scenario instantiated with |
| 61 | + an empty robot list — a robotless pick task can never succeed and eval scores |
| 62 | + a silent 0%. This pins every sibling to declare a robot. |
| 63 | + """ |
| 64 | + import importlib |
| 65 | + |
| 66 | + pick_files = sorted(_LIBERO.glob("libero_pick_*.py")) |
| 67 | + assert pick_files, "no libero_pick_* task files found" |
| 68 | + missing = [] |
| 69 | + for f in pick_files: |
| 70 | + mod = importlib.import_module(f"roboverse_pack.tasks.libero.{f.stem}") |
| 71 | + # the task class defines a class-level ScenarioCfg |
| 72 | + for obj in vars(mod).values(): |
| 73 | + scen = getattr(obj, "scenario", None) |
| 74 | + if scen is not None and hasattr(scen, "robots"): |
| 75 | + if not scen.robots: |
| 76 | + missing.append(f.stem) |
| 77 | + break |
| 78 | + assert not missing, f"libero_pick tasks with no robot (robotless pick can never succeed): {missing}" |
| 79 | + |
| 80 | + |
24 | 81 | @pytest.mark.general |
25 | 82 | def test_libero_90_get_initial_states_degrades_gracefully(monkeypatch): |
26 | 83 | """Libero90BaseTask._get_initial_states must degrade to None (handler |
@@ -54,28 +111,3 @@ def _raise(*a, **k): |
54 | 111 | # 3. get_traj returns empty → None (empty guard before len()) |
55 | 112 | monkeypatch.setattr(mod, "get_traj", lambda *a, **k: ([], None, None)) |
56 | 113 | assert t._get_initial_states() is None |
57 | | - |
58 | | - |
59 | | -@pytest.mark.general |
60 | | -def test_all_libero_pick_tasks_have_a_robot(): |
61 | | - """Every ``libero_pick_*`` task is a "pick up X and place in basket" task and |
62 | | - therefore MUST declare a robot arm. Previously 9 of 10 omitted |
63 | | - ``robots=["franka"]`` (only butter had it), so the scenario instantiated with |
64 | | - an empty robot list — a robotless pick task can never succeed and eval scores |
65 | | - a silent 0%. This pins every sibling to declare a robot. |
66 | | - """ |
67 | | - import importlib |
68 | | - |
69 | | - pick_files = sorted(_LIBERO.glob("libero_pick_*.py")) |
70 | | - assert pick_files, "no libero_pick_* task files found" |
71 | | - missing = [] |
72 | | - for f in pick_files: |
73 | | - mod = importlib.import_module(f"roboverse_pack.tasks.libero.{f.stem}") |
74 | | - # the task class defines a class-level ScenarioCfg |
75 | | - for obj in vars(mod).values(): |
76 | | - scen = getattr(obj, "scenario", None) |
77 | | - if scen is not None and hasattr(scen, "robots"): |
78 | | - if not scen.robots: |
79 | | - missing.append(f.stem) |
80 | | - break |
81 | | - assert not missing, f"libero_pick tasks with no robot (robotless pick can never succeed): {missing}" |
|
0 commit comments