From fdcc54128064c97614f251f7ef986e28cb4b168b Mon Sep 17 00:00:00 2001 From: guillemdb Date: Fri, 6 Feb 2026 10:25:33 +0100 Subject: [PATCH 1/4] Fix test run on headless environments Signed-off-by: guillemdb --- Makefile | 18 +++++++++--------- src/plangym/scripts/import_retro_roms.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 8576d7e..29cbb1a 100644 --- a/Makefile +++ b/Makefile @@ -73,24 +73,24 @@ test: test-doctest test-parallel test-singlecore test-ray .PHONY: test-parallel test-parallel: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 RAY_RUNTIME_ENV_CREATE_WORKING_DIR=0 \ uv run pytest -n $n -s -o log_cli=true -o log_cli_level=info \ --ignore=tests/vectorization/test_ray.py tests .PHONY: test-ray test-ray: - RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 RAY_RUNTIME_ENV_CREATE_WORKING_DIR=0 MUJOCO_GL=egl \ + DISPLAY= RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 RAY_RUNTIME_ENV_CREATE_WORKING_DIR=0 MUJOCO_GL=egl \ uv run pytest -s -o log_cli=true -o log_cli_level=info tests/vectorization/test_ray.py .PHONY: test-singlecore test-singlecore: - PYTEST_XDIST_AUTO_NUM_WORKERS=1 PYVIRTUALDISPLAY_DISPLAYFD=0 \ + DISPLAY= MUJOCO_GL=egl PYTEST_XDIST_AUTO_NUM_WORKERS=1 PYVIRTUALDISPLAY_DISPLAYFD=0 \ uv run pytest -s -o log_cli=true -o log_cli_level=info tests/control/test_classic_control.py .PHONY: test-doctest test-doctest: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 \ uv run pytest --doctest-modules -n $n -s -o log_cli=true -o log_cli_level=info src # ============ Code Coverage ============ @@ -103,35 +103,35 @@ codecov-parallel: codecov-parallel-1 codecov-parallel-2 codecov-parallel-3 codec .PHONY: codecov-parallel-1 codecov-parallel-1: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ uv run pytest -n $n -s -o log_cli=true -o log_cli_level=info \ --cov=./ --cov-report=xml:coverage_parallel_1.xml --cov-config=pyproject.toml \ tests/test_core.py tests/test_registry.py tests/test_utils.py .PHONY: codecov-parallel-2 codecov-parallel-2: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ uv run pytest -n $n -s -o log_cli=true -o log_cli_level=info \ --cov=./ --cov-report=xml:coverage_parallel_2.xml --cov-config=pyproject.toml \ tests/videogames .PHONY: codecov-parallel-3 codecov-parallel-3: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ uv run pytest -n $n -s -o log_cli=true -o log_cli_level=info \ --cov=./ --cov-report=xml:coverage_parallel_3.xml --cov-config=pyproject.toml \ tests/control .PHONY: codecov-vectorization codecov-vectorization: - PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_CLASSIC_CONTROL=1 SKIP_RENDER=True \ uv run pytest -n 0 -s -o log_cli=true -o log_cli_level=info \ --cov=./ --cov-report=xml:coverage_vectorization.xml --cov-config=pyproject.toml \ tests/vectorization .PHONY: codecov-singlecore codecov-singlecore: - PYTEST_XDIST_AUTO_NUM_WORKERS=1 PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_RENDER=True \ + DISPLAY= MUJOCO_GL=egl PYTEST_XDIST_AUTO_NUM_WORKERS=1 PYVIRTUALDISPLAY_DISPLAYFD=0 SKIP_RENDER=True \ uv run pytest --doctest-modules -s -o log_cli=true -o log_cli_level=info \ --cov=./ --cov-report=xml --cov-config=pyproject.toml \ tests/control/test_classic_control.py diff --git a/src/plangym/scripts/import_retro_roms.py b/src/plangym/scripts/import_retro_roms.py index 676b888..7cd551d 100644 --- a/src/plangym/scripts/import_retro_roms.py +++ b/src/plangym/scripts/import_retro_roms.py @@ -16,7 +16,7 @@ try: import retro.data except Exception: - sys.exit(0) + retro = None flogging.setup() logger = logging.getLogger("import-roms") From e5a49dc5c21eaca1f853271ac286c481686c70e5 Mon Sep 17 00:00:00 2001 From: guillemdb Date: Fri, 6 Feb 2026 10:36:29 +0100 Subject: [PATCH 2/4] Improve tests to make sure we can extract RGB in headless environments Signed-off-by: guillemdb --- tests/control/test_mujoco.py | 33 +++++++++++++++++++++++++++++++++ tests/videogames/test_atari.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/tests/control/test_mujoco.py b/tests/control/test_mujoco.py index 145497f..3fe7351 100644 --- a/tests/control/test_mujoco.py +++ b/tests/control/test_mujoco.py @@ -66,6 +66,39 @@ def test_mujoco_attributes(self, env): assert unwrapped.model.nv > 0 # Has velocity coordinates +class TestMujocoImageExtraction: + """Test that RGB images can be extracted with non-image obs_type.""" + + def test_coords_obs_with_image_extraction(self): + """Verify get_image() returns RGB when using obs_type='coords' (default).""" + env = MujocoEnv("Ant-v4", render_mode="rgb_array", autoreset=False) + _state, obs, _info = env.reset() + # Coords observation should be a 1D float array + assert obs.ndim == 1 + assert obs.dtype in (numpy.float32, numpy.float64) + # get_image() should return a valid RGB image + img = env.get_image() + assert img.ndim == 3 + assert img.shape[2] == 3 + assert img.dtype == numpy.uint8 + env.close() + + def test_coords_obs_return_image(self): + """Verify return_image=True populates info['rgb'] with obs_type='coords'.""" + env = MujocoEnv("Ant-v4", render_mode="rgb_array", return_image=True, autoreset=False) + _state, obs, info = env.reset() + assert "rgb" in info + assert info["rgb"].ndim == 3 + assert info["rgb"].shape[2] == 3 + # Also check step + obs, _reward, _term, _trunc, info = env.step(env.sample_action()) + assert obs.ndim == 1 + assert "rgb" in info + assert info["rgb"].ndim == 3 + assert info["rgb"].shape[2] == 3 + env.close() + + class TestMujocoParallel: """Test MujocoEnv parallel execution for planning reliability.""" diff --git a/tests/videogames/test_atari.py b/tests/videogames/test_atari.py index 56aa484..47a7feb 100644 --- a/tests/videogames/test_atari.py +++ b/tests/videogames/test_atari.py @@ -47,6 +47,39 @@ def test_get_image(self): obs = env.get_image() assert isinstance(obs, numpy.ndarray) + def test_ram_obs_with_image_extraction(self): + """Verify that RGB images can be extracted when using obs_type='ram'.""" + env = qbert_ram() + _state, obs, _info = env.reset() + # RAM observation should be 1D uint8 array of shape (128,) + assert obs.ndim == 1 + assert obs.shape == (128,) + assert obs.dtype == numpy.uint8 + # get_image() should still return a valid RGB image + img = env.get_image() + assert img.ndim == 3 + assert img.shape == (210, 160, 3) + assert img.dtype == numpy.uint8 + env.close() + + def test_ram_obs_return_image(self): + """Verify return_image=True populates info['rgb'] with obs_type='ram'.""" + env = AtariEnv( + name="ALE/Qbert-v5", obs_type="ram", return_image=True, + clone_seeds=False, autoreset=False, + ) + _state, obs, info = env.reset() + assert "rgb" in info + assert info["rgb"].ndim == 3 + assert info["rgb"].shape == (210, 160, 3) + # Also check step + obs, _reward, _term, _trunc, info = env.step(env.sample_action()) + assert obs.shape == (128,) + assert "rgb" in info + assert info["rgb"].ndim == 3 + assert info["rgb"].shape == (210, 160, 3) + env.close() + def test_n_actions(self, env): n_actions = env.n_actions assert isinstance(n_actions, int | np.int64) From b1a083b6cdfde5848a7076d7349569f058181cd4 Mon Sep 17 00:00:00 2001 From: guillemdb Date: Fri, 6 Feb 2026 11:01:36 +0100 Subject: [PATCH 3/4] decouple dependency loading Signed-off-by: guillemdb --- .github/workflows/push.yml | 54 +++++++++++++++--------------- Dockerfile | 1 + Makefile | 5 +++ Makefile.docker | 2 +- README.md | 12 +++++-- src/plangym/control/__init__.py | 33 +++++++++++++----- src/plangym/environment_names.py | 41 +++++++++++++++++------ src/plangym/registry.py | 4 ++- src/plangym/videogames/__init__.py | 36 +++++++++++++++----- tests/control/test_mujoco.py | 2 +- tests/videogames/test_atari.py | 7 ++-- 11 files changed, 136 insertions(+), 61 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index cd2255c..fdadb02 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -71,7 +71,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: | set -x - sudo apt-get install -y xvfb libglu1-mesa x11-utils + sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev x11-utils uv sync --all-extras unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip uv run python -m stable_retro.import "uncompressed ROMs" @@ -81,31 +81,31 @@ jobs: run: | uv run python -c "import stable_retro; games = stable_retro.data.list_games(); print(f'Total games: {len(games)}'); assert len(games) > 60, 'ROM import failed'" - - name: Install MacOS test and package dependencies - if: ${{ matrix.os == 'macos-latest' }} - run: | - set -x - # XQuartz not needed - rendering tests are skipped on macOS (SKIP_RENDER=True) - # macOS doesn't support headless rendering (osmesa/egl) - only Linux does - brew install swig libzip - uv sync --all-extras - # Fix a bug in retro.data where it tries to load an inexistent version file - # Only applies to Python 3.10 where stable-retro is installed - RETRO_DIR="/Users/runner/work/plangym/plangym/.venv/lib/python${{ matrix.python-version }}/site-packages/retro" - if [ -d "$RETRO_DIR" ] && [ ! -f "$RETRO_DIR/VERSION.txt" ]; then - echo "0.9.1" > "$RETRO_DIR/VERSION.txt" - fi - - name: Import Retro ROMs (macOS Intel) - if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }} - run: | - set -x - unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip - uv run python -m stable_retro.import "uncompressed ROMs" - - - name: Verify ROM import success (macOS Intel) - if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }} - run: | - uv run python -c "import stable_retro; games = stable_retro.data.list_games(); print(f'Total games: {len(games)}'); assert len(games) > 60, 'ROM import failed'" + - name: Install MacOS test and package dependencies + if: ${{ matrix.os == 'macos-latest' }} + run: | + set -x + # XQuartz not needed - rendering tests are skipped on macOS (SKIP_RENDER=True) + # macOS doesn't support headless rendering (osmesa/egl) - only Linux does + brew install swig libzip + uv sync --all-extras + # Fix a bug in retro.data where it tries to load an inexistent version file + # Only applies to Python 3.10 where stable-retro is installed + RETRO_DIR="/Users/runner/work/plangym/plangym/.venv/lib/python${{ matrix.python-version }}/site-packages/retro" + if [ -d "$RETRO_DIR" ] && [ ! -f "$RETRO_DIR/VERSION.txt" ]; then + echo "0.9.1" > "$RETRO_DIR/VERSION.txt" + fi + - name: Import Retro ROMs (macOS Intel) + if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }} + run: | + set -x + unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip + uv run python -m stable_retro.import "uncompressed ROMs" + + - name: Verify ROM import success (macOS Intel) + if: ${{ matrix.os == 'macos-latest' && runner.arch == 'X64' }} + run: | + uv run python -c "import stable_retro; games = stable_retro.data.list_games(); print(f'Total games: {len(games)}'); assert len(games) > 60, 'ROM import failed'" - name: Run Pytest on MacOS if: ${{ matrix.os == 'macos-latest' }} @@ -202,7 +202,7 @@ jobs: UV_SYSTEM_PYTHON: 1 run: | set -x - sudo apt-get install -y xvfb libglu1-mesa x11-utils + sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev x11-utils uv lock uv pip install -r pyproject.toml --all-extras uv pip install dist/*.whl diff --git a/Dockerfile b/Dockerfile index a90d779..53d3f4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ FROM ubuntu:22.04 RUN apt-get update && \ apt-get install -y --no-install-suggests --no-install-recommends \ make cmake git xvfb build-essential curl ssh wget ca-certificates swig \ + libegl1-mesa-dev libglu1-mesa libgl1-mesa-glx \ python3 python3-dev && \ wget -O - https://bootstrap.pypa.io/get-pip.py | python3 diff --git a/Makefile b/Makefile index 29cbb1a..478a15a 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,11 @@ ROM_PASSWORD ?= "NO_PASSWORD" VERSION ?= latest MUJOCO_PATH?=~/.mujoco +.PHONY: install-system-deps +install-system-deps: + sudo apt-get update + sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev libgl1-mesa-glx x11-utils + .PHONY: install-mujoco install-mujoco: mkdir ${MUJOCO_PATH} diff --git a/Makefile.docker b/Makefile.docker index 66f0fb2..572f8e6 100644 --- a/Makefile.docker +++ b/Makefile.docker @@ -82,5 +82,5 @@ install-python-libs: install-env-deps: apt-get update apt-get install -y --no-install-suggests --no-install-recommends \ - libglfw3 libglew-dev libgl1-mesa-glx libosmesa6 xvfb swig + libglfw3 libglew-dev libgl1-mesa-glx libegl1-mesa-dev libosmesa6 xvfb swig diff --git a/README.md b/README.md index 1188d1f..a8e89af 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,12 @@ Ubuntu / Debian ```bash +# Install all system dependencies for headless rendering (EGL, GLU, X11) +make install-system-deps + +# Or manually: sudo apt-get update -sudo apt-get install -y xvfb libglu1-mesa x11-utils +sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev libgl1-mesa-glx x11-utils ``` For NES environments (nes-py): @@ -91,8 +95,12 @@ fi WSL2 (Windows) ```bash +# Install all system dependencies for headless rendering (EGL, GLU, X11) +make install-system-deps + +# Or manually: sudo apt-get update -sudo apt-get install -y xvfb libglu1-mesa x11-utils +sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev libgl1-mesa-glx x11-utils ``` For GUI rendering, install an X server on Windows (e.g., VcXsrv) or use headless mode. diff --git a/src/plangym/control/__init__.py b/src/plangym/control/__init__.py index 6f10a4c..476e1f2 100644 --- a/src/plangym/control/__init__.py +++ b/src/plangym/control/__init__.py @@ -1,8 +1,25 @@ -"""Module that contains environments representing control tasks.""" - -from plangym.control.balloon import BalloonEnv -from plangym.control.box_2d import Box2DEnv -from plangym.control.classic_control import ClassicControl -from plangym.control.dm_control import DMControlEnv -from plangym.control.lunar_lander import LunarLander -from plangym.control.mujoco import MujocoEnv +"""Module that contains environments representing control tasks. + +Imports are lazy so that missing optional dependencies (e.g. mujoco, dm-control) +do not prevent importing unrelated environment classes. +""" + +import importlib + +_SUBMODULES = { + "BalloonEnv": "plangym.control.balloon", + "Box2DEnv": "plangym.control.box_2d", + "ClassicControl": "plangym.control.classic_control", + "DMControlEnv": "plangym.control.dm_control", + "LunarLander": "plangym.control.lunar_lander", + "MujocoEnv": "plangym.control.mujoco", +} + +__all__ = list(_SUBMODULES) + + +def __getattr__(name: str): + if name in _SUBMODULES: + module = importlib.import_module(_SUBMODULES[name]) + return getattr(module, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/plangym/environment_names.py b/src/plangym/environment_names.py index db82855..8e6c8a3 100644 --- a/src/plangym/environment_names.py +++ b/src/plangym/environment_names.py @@ -43,19 +43,40 @@ "Walker2d-v5", ] -try: - import retro.data +_RETRO = None +_DM_CONTROL = None - RETRO = retro.data.list_games() -except Exception: # pragma: no cover - RETRO = [] -try: - from dm_control import suite +def _load_retro(): + global _RETRO + if _RETRO is None: + try: + import retro.data - DM_CONTROL = list(suite.ALL_TASKS) -except (ImportError, OSError, AttributeError): # pragma: no cover - DM_CONTROL = [] + _RETRO = retro.data.list_games() + except Exception: # pragma: no cover + _RETRO = [] + return _RETRO + + +def _load_dm_control(): + global _DM_CONTROL + if _DM_CONTROL is None: + try: + from dm_control import suite + + _DM_CONTROL = list(suite.ALL_TASKS) + except (ImportError, OSError, AttributeError): # pragma: no cover + _DM_CONTROL = [] + return _DM_CONTROL + + +def __getattr__(name: str): + if name == "RETRO": + return _load_retro() + if name == "DM_CONTROL": + return _load_dm_control() + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") ATARI = [ diff --git a/src/plangym/registry.py b/src/plangym/registry.py index 3303e4e..f085a06 100644 --- a/src/plangym/registry.py +++ b/src/plangym/registry.py @@ -1,10 +1,12 @@ """Functionality for instantiating the environment by passing the environment id.""" -from plangym.environment_names import ATARI, BOX_2D, CLASSIC_CONTROL, DM_CONTROL, MUJOCO, RETRO +from plangym.environment_names import ATARI, BOX_2D, CLASSIC_CONTROL, MUJOCO def get_planenv_class(name, domain_name, state): """Return the class corresponding to the environment name.""" + from plangym.environment_names import DM_CONTROL, RETRO + # if name == "MinimalPacman-v0": # return MinimalPacman # elif name == "MinimalPong-v0": diff --git a/src/plangym/videogames/__init__.py b/src/plangym/videogames/__init__.py index 8616eab..ba1ec5d 100644 --- a/src/plangym/videogames/__init__.py +++ b/src/plangym/videogames/__init__.py @@ -1,18 +1,36 @@ """Module that contains environments representing video games. +Imports are lazy so that missing optional dependencies (e.g. ale-py, stable-retro) +do not prevent importing unrelated environment classes. + .. note:: **Python Version Compatibility**: - ``RetroEnv``: Requires ``stable-retro``, only available on Python 3.10. """ +import importlib + from plangym import warn_import_error -from plangym.videogames.atari import AtariEnv -from plangym.videogames.montezuma import MontezumaEnv -from plangym.videogames.nes import MarioEnv - -try: - from plangym.videogames.retro import RetroEnv -except ImportError: - RetroEnv = None - warn_import_error("RetroEnv", "stable-retro is only available on Python 3.10.") + +_SUBMODULES = { + "AtariEnv": "plangym.videogames.atari", + "MontezumaEnv": "plangym.videogames.montezuma", + "MarioEnv": "plangym.videogames.nes", + "RetroEnv": "plangym.videogames.retro", +} + +__all__ = list(_SUBMODULES) + + +def __getattr__(name: str): + if name in _SUBMODULES: + try: + module = importlib.import_module(_SUBMODULES[name]) + return getattr(module, name) + except ImportError: + if name == "RetroEnv": + warn_import_error("RetroEnv", "stable-retro is only available on Python 3.10.") + return None + raise + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/control/test_mujoco.py b/tests/control/test_mujoco.py index 3fe7351..ed4f33f 100644 --- a/tests/control/test_mujoco.py +++ b/tests/control/test_mujoco.py @@ -75,7 +75,7 @@ def test_coords_obs_with_image_extraction(self): _state, obs, _info = env.reset() # Coords observation should be a 1D float array assert obs.ndim == 1 - assert obs.dtype in (numpy.float32, numpy.float64) + assert obs.dtype in {numpy.float32, numpy.float64} # get_image() should return a valid RGB image img = env.get_image() assert img.ndim == 3 diff --git a/tests/videogames/test_atari.py b/tests/videogames/test_atari.py index 47a7feb..1a93058 100644 --- a/tests/videogames/test_atari.py +++ b/tests/videogames/test_atari.py @@ -65,8 +65,11 @@ def test_ram_obs_with_image_extraction(self): def test_ram_obs_return_image(self): """Verify return_image=True populates info['rgb'] with obs_type='ram'.""" env = AtariEnv( - name="ALE/Qbert-v5", obs_type="ram", return_image=True, - clone_seeds=False, autoreset=False, + name="ALE/Qbert-v5", + obs_type="ram", + return_image=True, + clone_seeds=False, + autoreset=False, ) _state, obs, info = env.reset() assert "rgb" in info From 7799caf90557088bfd09924b1b2bb679a501eaff Mon Sep 17 00:00:00 2001 From: guillemdb Date: Fri, 6 Feb 2026 11:05:09 +0100 Subject: [PATCH 4/4] Fix tests Signed-off-by: guillemdb --- .github/workflows/push.yml | 2 ++ tests/control/test_mujoco.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index fdadb02..c1e037f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -71,6 +71,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: | set -x + sudo apt-get update sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev x11-utils uv sync --all-extras unzip -o -P ${{ secrets.ROM_PASSWORD }} uncompressed_ROMs.zip @@ -202,6 +203,7 @@ jobs: UV_SYSTEM_PYTHON: 1 run: | set -x + sudo apt-get update sudo apt-get install -y xvfb libglu1-mesa libegl1-mesa-dev x11-utils uv lock uv pip install -r pyproject.toml --all-extras diff --git a/tests/control/test_mujoco.py b/tests/control/test_mujoco.py index ed4f33f..05b0d35 100644 --- a/tests/control/test_mujoco.py +++ b/tests/control/test_mujoco.py @@ -1,4 +1,5 @@ import operator +import os import sys import numpy @@ -69,13 +70,14 @@ def test_mujoco_attributes(self, env): class TestMujocoImageExtraction: """Test that RGB images can be extracted with non-image obs_type.""" + @pytest.mark.skipif(os.getenv("SKIP_RENDER", False), reason="No display in CI.") def test_coords_obs_with_image_extraction(self): """Verify get_image() returns RGB when using obs_type='coords' (default).""" env = MujocoEnv("Ant-v4", render_mode="rgb_array", autoreset=False) _state, obs, _info = env.reset() # Coords observation should be a 1D float array assert obs.ndim == 1 - assert obs.dtype in {numpy.float32, numpy.float64} + assert numpy.issubdtype(obs.dtype, numpy.floating) # get_image() should return a valid RGB image img = env.get_image() assert img.ndim == 3 @@ -83,6 +85,7 @@ def test_coords_obs_with_image_extraction(self): assert img.dtype == numpy.uint8 env.close() + @pytest.mark.skipif(os.getenv("SKIP_RENDER", False), reason="No display in CI.") def test_coords_obs_return_image(self): """Verify return_image=True populates info['rgb'] with obs_type='coords'.""" env = MujocoEnv("Ant-v4", render_mode="rgb_array", return_image=True, autoreset=False)