Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,13 @@ jobs:
- name: Install dependencies
run: uv sync --group tests --frozen
- name: Run tests
run: uv run pytest --numprocesses=3 --cov=dimos/ --junitxml=junit.xml -m 'not (tool or self_hosted or mujoco or self_hosted_large)'
run: uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --numprocesses=3 --cov=dimos/ --junitxml=junit.xml -m 'not (tool or self_hosted or mujoco or self_hosted_large)'
- name: Re-run the failing tests with maximum verbosity
if: failure()
env:
COLOR: yes
run: >- # `exit 1` makes sure that the job remains red with flaky runs
uv run pytest --no-cov -vvvvv --lf -m 'not (tool or self_hosted or mujoco or self_hosted_large)' && exit 1
uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --no-cov -vvvvv --lf -m 'not (tool or self_hosted or mujoco or self_hosted_large)' && exit 1
shell: bash
- name: Turn coverage into xml
run: uv run python -m coverage xml
Expand Down Expand Up @@ -397,13 +397,13 @@ jobs:
echo "ROS_PYTHON_VERSION=$ROS_PYTHON_VERSION"
} >> "$GITHUB_ENV"
- name: Run tests
run: uv run pytest --cov=dimos/ --junitxml=junit.xml -m '(${{ matrix.markers }}) and not (tool or mujoco)'
run: uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --cov=dimos/ --junitxml=junit.xml -m '(${{ matrix.markers }}) and not (tool or mujoco)'
- name: Re-run the failing tests with maximum verbosity
if: failure()
env:
COLOR: yes
run: >- # `exit 1` makes sure that the job remains red with flaky runs
uv run pytest --no-cov -vvvvv --lf -m '(${{ matrix.markers }}) and not (tool or mujoco)' && exit 1
uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --no-cov -vvvvv --lf -m '(${{ matrix.markers }}) and not (tool or mujoco)' && exit 1
shell: bash
- name: Turn coverage into xml
run: uv run python -m coverage xml
Expand Down Expand Up @@ -499,14 +499,14 @@ jobs:
# heavier scenes (`apt`). Software X via xvfb is not enough.
env:
DISPLAY: ":0"
run: uv run pytest --cov=dimos/ --junitxml=junit.xml -m self_hosted_large
run: uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --cov=dimos/ --junitxml=junit.xml -m self_hosted_large

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 --ignore applied to self_hosted_large may permanently suppress coverage

self_hosted_large is the high-memory runner intended for heavier workloads. If dimos_mls_planner or dimos_voxel_ray_tracing are — or ever become — available on that runner (e.g. as a compiled native artifact cached in the runner image), adding the same --ignore flags here means those test directories will never execute there either. A broken change to the ray-tracing or MLS-planner code would not be caught by any CI job. Consider whether the self_hosted_large runner should build and run those suites instead of ignoring them.

- name: Re-run the failing tests with maximum verbosity
if: failure()
env:
COLOR: yes
DISPLAY: ":0"
run: >- # `exit 1` makes sure that the job remains red with flaky runs
uv run pytest --no-cov -vvvvv --lf -m self_hosted_large && exit 1
uv run pytest --ignore=dimos/mapping/ray_tracing --ignore=dimos/navigation/nav_3d/mls_planner --no-cov -vvvvv --lf -m self_hosted_large && exit 1
shell: bash
- name: Turn coverage into xml
run: uv run python -m coverage xml
Expand Down
97 changes: 97 additions & 0 deletions dimos/codebase_checks/test_no_importorskip.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend against this. This will make it more difficult for downstream to test dimos. Instead, I suggest paying more attention to codecov (the CI checks should all be in place now).

For release, we added the error-on-skips to ensure we do test everything before a release (should be an automated step soon), so any damage caused by an importorskip is short term.

As prior art, we use this in several places in aiohttp for similar reasons:
https://github.com/search?q=repo%3Aaio-libs%2Faiohttp%20importorskip&type=code

I also suspect that people will see the importskip being banned in future, and then just use the less preferred try/except pattern to achieve the same result..

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from dimos.constants import DIMOS_PROJECT_ROOT

PATTERN = "importorskip"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Pattern is a bare substring — matches comments and docstrings too

PATTERN = "importorskip" matches any occurrence of the string, including lines like # Do not use importorskip here or a docstring explaining the policy. That's probably the desired strictness (zero-tolerance), but a comment explaining the intent would help reviewers who might otherwise try to "work around" the check with a comment. Worth a one-line note in the constant definition.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


IGNORED_DIRS = {
".venv",
"venv",
"__pycache__",
"node_modules",
".git",
"dist",
"build",
".egg-info",
".tox",
}


def _is_ignored_dir(dirpath: str) -> bool:
parts = dirpath.split(os.sep)
return bool(IGNORED_DIRS.intersection(parts))


def find_importorskip_usages() -> list[tuple[str, int, str]]:
"""Return a list of (rel_path, line_number, line_text) for every violation."""
dimos_dir = DIMOS_PROJECT_ROOT / "dimos"
violations: list[tuple[str, int, str]] = []
# Skip this test file: it necessarily contains the forbidden pattern.
self_path = os.path.realpath(__file__)

for dirpath, dirnames, filenames in os.walk(dimos_dir):
dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRS]

if _is_ignored_dir(dirpath):
continue

for fname in filenames:
if not fname.endswith(".py"):
Comment on lines +49 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant _is_ignored_dir(dirpath) guard

The dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRS] mutation on the line above already prevents os.walk from ever descending into an ignored directory, so dirpath will never contain a component from IGNORED_DIRS during the traversal. The if _is_ignored_dir(dirpath): continue branch is therefore unreachable for any dirpath below dimos_dir and can be safely removed to avoid misleading readers into thinking there are two independent layers of filtering.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

continue

full_path = os.path.join(dirpath, fname)
if os.path.realpath(full_path) == self_path:
continue
rel_path = os.path.relpath(full_path, DIMOS_PROJECT_ROOT)

try:
with open(full_path, encoding="utf-8", errors="replace") as f:
for lineno, line in enumerate(f, start=1):
stripped = line.rstrip("\n")
if PATTERN not in stripped:
continue
violations.append((rel_path, lineno, stripped))
except (OSError, UnicodeDecodeError):
continue

return violations


def test_no_importorskip():
"""
Fail if any file uses `pytest.importorskip`.

`importorskip` silently skips a test (or a whole module) when an optional
dependency is missing. That means a broken test, or a dependency missing
from the dev/CI environment, goes unnoticed because the test never runs.
Install all dependencies locally so the tests actually execute. If a
dependency genuinely cannot be installed in some environment, exclude those
tests explicitly (e.g. a CI `--ignore`, or a platform-conditioned
`collect_ignore` in a conftest) rather than silently skipping per import.
"""
violations = find_importorskip_usages()
if violations:
report_lines = [
f"Found {len(violations)} forbidden use(s) of `pytest.importorskip`. "
"Don't silently skip tests when a dependency is missing -- install all "
"dependencies so the tests run, or exclude the module explicitly "
"(CI `--ignore` / a platform `collect_ignore`):",
"",
]
for path, lineno, text in violations:
report_lines.append(f" {path}:{lineno}: {text.strip()}")
raise AssertionError("\n".join(report_lines))
3 changes: 1 addition & 2 deletions dimos/hardware/manipulators/openarm/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
import struct
import time

import can
import pytest

can = pytest.importorskip("can")

from dimos.hardware.manipulators.openarm.driver import (
CTRL_MODE_MIT,
KD_MAX,
Expand Down
32 changes: 32 additions & 0 deletions dimos/manipulation/visualization/viser/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import platform

# viser[urdf] has no installable wheel set on linux-aarch64 (see the `tests`
# dependency group marker in pyproject.toml), so these tests cannot be collected
# there: their module-level imports pull in viser. Mirror the skipif_aarch64
# convention (dimos/conftest.py) at collection time. test_state.py is
# intentionally NOT listed -- it imports only viser.state (no viser dependency)
# and runs on every platform.
collect_ignore = (
[
"test_gui_status.py",
"test_operation_worker.py",
"test_viser_visualization.py",
"test_visualizer_lifecycle.py",
]
if platform.machine() == "aarch64"
else []
)
2 changes: 0 additions & 2 deletions dimos/manipulation/visualization/viser/test_gui_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

import pytest

pytest.importorskip("viser", reason="Viser optional dependency is not installed")

from dimos.manipulation.visualization.types import TargetEvaluation
from dimos.manipulation.visualization.viser.adapter import InProcessViserAdapter
from dimos.manipulation.visualization.viser.config import ViserVisualizationConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@

import pytest

pytest.importorskip("viser", reason="Viser optional dependency is not installed")

from dimos.manipulation.visualization.types import RobotInfo
from dimos.manipulation.visualization.viser.adapter import InProcessViserAdapter
from dimos.manipulation.visualization.viser.config import ViserVisualizationConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import numpy as np
import pytest

pytest.importorskip("viser", reason="Viser optional dependency is not installed")

from dimos.manipulation.visualization.types import RobotInfo, TargetEvaluation
from dimos.manipulation.visualization.viser.adapter import InProcessViserAdapter
from dimos.manipulation.visualization.viser.animation import (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

import pytest

pytest.importorskip("viser", reason="Viser optional dependency is not installed")

from dimos.manipulation.planning.spec.config import RobotModelConfig
from dimos.manipulation.planning.spec.models import PlanningSceneInfo
from dimos.manipulation.visualization.viser import (
Expand Down
2 changes: 0 additions & 2 deletions dimos/mapping/ray_tracing/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
from numpy.typing import NDArray
import pytest

pytest.importorskip("dimos_voxel_ray_tracing")

from dimos.mapping.ray_tracing.transformer import RayTraceMap
from dimos.memory2.type.observation import Observation
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
Expand Down
2 changes: 0 additions & 2 deletions dimos/mapping/ray_tracing/test_voxel_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import numpy as np
import pytest

pytest.importorskip("dimos_voxel_ray_tracing")

from dimos.mapping.ray_tracing.voxel_map import VoxelRayMapper

ORIGIN = (0.0, 0.0, 0.0)
Expand Down
3 changes: 0 additions & 3 deletions dimos/memory2/codecs/test_codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,4 @@ def test_lcm_type_returns_lcm(self) -> None:
assert isinstance(codec_for(PoseStamped), LcmCodec)

def test_image_type_returns_jpeg(self) -> None:
pytest.importorskip("turbojpeg")
from dimos.memory2.codecs.jpeg import JpegCodec

assert isinstance(codec_for(Image), JpegCodec)
2 changes: 0 additions & 2 deletions dimos/navigation/nav_3d/mls_planner/test_mls_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import numpy as np
import pytest

pytest.importorskip("dimos_mls_planner")

from dimos.navigation.nav_3d.mls_planner.mls_planner import MLSPlanner


Expand Down
3 changes: 0 additions & 3 deletions dimos/navigation/nav_3d/mls_planner/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

import numpy as np
from numpy.typing import NDArray
import pytest

pytest.importorskip("dimos_mls_planner")

from dimos.memory2.type.observation import Observation
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
Expand Down
2 changes: 0 additions & 2 deletions dimos/perception/fiducial/test_fixture_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
from dimos.perception.fiducial.marker_detect import detect_markers_in_image
from dimos.perception.fiducial.marker_tf_module import MarkerTfModule

pytest.importorskip("cv2.aruco")


@pytest.fixture(scope="module")
def layout() -> BoardLayout:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
import numpy as np
import pytest

pytest.importorskip("cv2.aruco")

from dimos.memory2.store.memory import MemoryStore
from dimos.memory2.type.observation import Observation
from dimos.msgs.geometry_msgs.Quaternion import Quaternion
Expand Down
3 changes: 0 additions & 3 deletions dimos/perception/fiducial/test_marker_pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import cv2
import numpy as np
import pytest

from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.msgs.sensor_msgs.Image import Image, ImageFormat
Expand All @@ -25,8 +24,6 @@
marker_reprojection_error,
)

pytest.importorskip("cv2.aruco")


def test_camera_optical_frame_id_resolution() -> None:
ts = 1.0
Expand Down
2 changes: 0 additions & 2 deletions dimos/perception/fiducial/test_marker_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import numpy as np
import pytest

pytest.importorskip("cv2.aruco")

from dimos.memory2.type.observation import Observation
from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.msgs.sensor_msgs.Image import Image, ImageFormat
Expand Down
8 changes: 4 additions & 4 deletions dimos/protocol/pubsub/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import pytest
import turbojpeg

from dimos.core.transport import (
JpegLcmTransport,
Expand Down Expand Up @@ -108,10 +109,9 @@ def test_make_pubsub_transport_shm_uses_SHMTransport() -> None:


def test_make_pubsub_transport_jpeg_shm_uses_JpegShmTransport() -> None:
# The Python `turbojpeg` package is importable even when the native
# libturbojpeg.so is missing; the RuntimeError only fires when TurboJPEG()
# is actually constructed. Probe by trying to instantiate it.
turbojpeg = pytest.importorskip("turbojpeg")
# The native libturbojpeg.so can be missing even when the `turbojpeg`
# package imports fine; the RuntimeError only fires when TurboJPEG() is
# actually constructed. Probe by trying to instantiate it.
try:
turbojpeg.TurboJPEG()
except RuntimeError as exc:
Expand Down
3 changes: 1 addition & 2 deletions dimos/simulation/engines/test_robot_sim_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
from pathlib import Path
from typing import Any

import mujoco
import pytest

mujoco = pytest.importorskip("mujoco")

from dimos.simulation.engines.mujoco_engine import MujocoEngine
from dimos.simulation.engines.robot_sim_binding import (
RobotSimSpec,
Expand Down
Loading