Skip to content
Merged
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
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,17 @@ tensorboard --logdir runs/{env-name}
### 测试

```bash
python -m pytest
# 与 PR CI 一致的快速测试(不执行集成、慢速或 Numba 编译测试)
python -m pytest -m "not slow and not integration and not numba"
# CPU 编译回归、跨模块/进程集成分别执行
python -m pytest -m "not integration and (slow or numba)"
python -m pytest -m integration
```

`integration` 包含真实 HTTP、子进程/分布式、ONNX round-trip 及完整 simulator 集成等跨系统契约;
依赖要求由对应 package/测试声明。真实 GPU 测试同样标记为 `integration`,仅在配置好运行时的专用环境执行;
无 GPU 时的 skip 不替代执行分层。完整执行不加 `-m`,会包含这些测试。

RL 测试按所有权组织:`motrix_rl/tests` 覆盖公共契约与控制面;`motrix_rl_builtin/tests`、
`motrix_rl_skrl/tests`、`motrix_rl_rslrl/tests` 覆盖各插件实现;根目录 `tests/` 覆盖 workspace CLI 和 task 配置集成。
插件与 workspace 集成测试需要安装相应依赖;完整验证使用 `sh install.sh --all` 后运行:
Expand Down
4 changes: 3 additions & 1 deletion motrix_deploy/src/motrix_deploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@

"""Framework-independent robot policy deployment runtime."""

__version__ = "0.3.0"
from importlib.metadata import version

__version__ = version("motrix-deploy")
1 change: 1 addition & 0 deletions motrix_deploy/tests/test_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def test_task_payload_round_trip_and_tamper_rejection(
write_artifact(tmp_path / "short.deploy", manifest, {"policy/model.onnx": POLICY_BYTES})


@pytest.mark.integration
def test_cli_inspect_outputs_json(tmp_path: Path, manifest_factory: Callable[[], DeploymentManifest]) -> None:
output = tmp_path / "fixture.deploy"
write_artifact(output, manifest_factory(), {"policy/model.onnx": POLICY_BYTES})
Expand Down
12 changes: 8 additions & 4 deletions motrix_deploy/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ def test_deploy_run_config_decodes_typed_simulation_runtime() -> None:
assert isinstance(config.runtime, SimRuntimeCfg)
assert config.runtime.backend == "test"
assert config.runtime.viewer is False
assert config.runtime.realtime is None
assert config.runtime.robot_init_key_pose == "lie_down"
assert config.runtime.physics.dt == pytest.approx(0.002)
assert config.runtime.physics.solver_iterations == 100
assert config.runtime.backend == "test"
assert config.duration_s == pytest.approx(0.1)


Expand All @@ -48,6 +44,14 @@ def test_deploy_run_config_defaults_to_unbounded_duration() -> None:
assert config.duration_s is None


@pytest.mark.parametrize("duration_s", [0, -0.1, float("nan"), float("inf")])
def test_cli_duration_rejects_nonpositive_or_nonfinite_budget(duration_s: float) -> None:
from motrix_deploy.cli import _duration_steps

with pytest.raises(ValueError, match="duration_s"):
_duration_steps(duration_s, 0.02)


def test_sim_schema_rejects_unknown_runtime_fields() -> None:
with pytest.raises(OmegaConfBaseException, match="robot_init_key_posee"):
_sim_runtime({"robot_init_key_posee": "lie_down"})
Expand Down
8 changes: 3 additions & 5 deletions motrix_deploy/tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
import subprocess
import sys

import motrix_deploy


def test_package_version() -> None:
assert motrix_deploy.__version__ == "0.3.0"
import pytest


@pytest.mark.integration
def test_cli_help() -> None:
result = subprocess.run(
[sys.executable, "-m", "motrix_deploy.cli", "--help"],
Expand All @@ -26,6 +23,7 @@ def test_cli_help() -> None:
assert "Powered by Hydra" in result.stdout


@pytest.mark.integration
def test_import_does_not_load_optional_or_training_packages() -> None:
forbidden = (
"motrix_deploy_mujoco",
Expand Down
5 changes: 5 additions & 0 deletions motrix_deploy/tests/test_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def _write_linear_model(
onnx.save(model, path)


@pytest.mark.integration
def test_onnx_policy_validates_io_and_runs_float32(tmp_path: Path) -> None:
path = tmp_path / "policy.onnx"
_write_linear_model(path)
Expand All @@ -78,6 +79,7 @@ def test_onnx_policy_validates_io_and_runs_float32(tmp_path: Path) -> None:
assert output.dtype == np.float32


@pytest.mark.integration
def test_onnx_policy_accepts_dynamic_model_batch_for_single_sample_runtime(tmp_path: Path) -> None:
path = tmp_path / "policy.onnx"
_write_linear_model(path, dynamic_batch=True)
Expand All @@ -92,6 +94,7 @@ def test_onnx_policy_accepts_dynamic_model_batch_for_single_sample_runtime(tmp_p
np.testing.assert_array_equal(output, [4.0, 5.0])


@pytest.mark.integration
def test_onnx_policy_rejects_manifest_name_mismatch(tmp_path: Path) -> None:
path = tmp_path / "policy.onnx"
_write_linear_model(path, output_name="actual_action")
Expand All @@ -104,6 +107,7 @@ def test_onnx_policy_rejects_manifest_name_mismatch(tmp_path: Path) -> None:
)


@pytest.mark.integration
def test_onnx_policy_rejects_non_finite_input(tmp_path: Path) -> None:
path = tmp_path / "policy.onnx"
_write_linear_model(path)
Expand All @@ -117,6 +121,7 @@ def test_onnx_policy_rejects_non_finite_input(tmp_path: Path) -> None:
runtime.infer(np.array([np.nan, 0.0, 0.0, 0.0], dtype=np.float32))


@pytest.mark.integration
def test_onnx_policy_rejects_non_finite_output(tmp_path: Path) -> None:
path = tmp_path / "policy.onnx"
_write_linear_model(path, nan_output=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

import motrixsim as mtx
import numpy as np
import pytest
from motrix_deploy_motrixsim.runtime import MotrixSimRuntime
from test_native_motrixsim_runtime import config

from motrix_deploy.contracts import JointControlMode, JointServoCommand, JointTorqueCommand

pytestmark = pytest.mark.integration


def set_simulation_state(
runtime,
Expand Down
44 changes: 15 additions & 29 deletions motrix_deploy_motrixsim/tests/test_native_motrixsim_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def config(**kwargs):
)


@pytest.mark.integration
def test_native_scene_init_pose_preserves_artifact_default_and_clips_servo_limits():
cfg = config()
cfg.scene.objs.robot.init_key_pose = "lie_down"
Expand Down Expand Up @@ -96,25 +97,13 @@ def bind_session(runtime, period=0.006, steps=2):


@pytest.mark.parametrize("gravity,tolerance", [((0.3, -0.2, -4.1), 2e-7), ((-0.1, 0.4, -6.3), 3e-6)])
def test_native_factory_forwards_shared_physics_once_to_compiler(monkeypatch, gravity, tolerance):
from motrix_env_motrixsim.compiler import MotrixSimSceneCompiler

@pytest.mark.integration
def test_native_factory_applies_supplied_physics(gravity, tolerance):
cfg = config()
physics = SimCfg(dt=0.003, solver_iterations=17, solver_tolerance=tolerance, gravity=gravity)
cfg = replace(cfg, physics=physics)
calls = []
original = MotrixSimSceneCompiler.configure_world

def configure_world(compiler, world, sim):
calls.append(sim)
return original(compiler, world, sim)

monkeypatch.setattr(MotrixSimSceneCompiler, "configure_world", configure_world)
runtime = create_simulation_runtime("motrixsim", cfg)
try:
assert runtime.config.physics is physics
assert calls == [physics]
assert calls[0] is physics
options = runtime.world.simulate_option
assert options.timestep == pytest.approx(physics.dt)
assert options.constraint_solver_iterations == physics.solver_iterations
Expand All @@ -124,6 +113,7 @@ def configure_world(compiler, world, sim):
runtime.close()


@pytest.mark.integration
def test_native_factory_session_cadence_and_owner(monkeypatch):
runtime = create_simulation_runtime("motrixsim", config())
spec = runtime.robot.spec
Expand Down Expand Up @@ -154,6 +144,7 @@ def count_apply():
assert runtime.model is None and runtime.data is None


@pytest.mark.integration
def test_native_servo_feedforward_snapshots_and_clipping():
runtime = MotrixSimRuntime(config())
with runtime:
Expand Down Expand Up @@ -191,6 +182,7 @@ def test_native_servo_feedforward_snapshots_and_clipping():


@pytest.mark.parametrize("backend", ["motrixsim", "mujoco"])
@pytest.mark.integration
def test_stop_applies_zero_torque_across_substeps(backend):
if backend == "mujoco":
pytest.importorskip("mujoco")
Expand Down Expand Up @@ -233,21 +225,7 @@ def test_stop_applies_zero_torque_across_substeps(backend):
np.testing.assert_array_equal(controls, 0.0)


def test_native_servo_substeps_only_query_joint_state(monkeypatch):
runtime = MotrixSimRuntime(config())
with runtime:
state = runtime.robot.read_state(0.1)
zeros = np.zeros(runtime.robot.spec.joint_count, dtype=np.float32)
runtime.robot.write_command(JointServoCommand(state.joint_position + 0.01, zeros, zeros, zeros + 10, zeros + 1))

def unexpected_full_state_query(data):
pytest.fail("Servo substeps must not query base state or sensors")

monkeypatch.setattr(runtime.robot, "_read", SimpleNamespace(execute=unexpected_full_state_query))
runtime._step()
assert runtime.simulation_time_s == pytest.approx(runtime.config.physics.dt)


@pytest.mark.integration
def test_native_repeated_reset_preserves_compiled_placement_and_sensor_state():
runtime = MotrixSimRuntime(config())
with runtime:
Expand Down Expand Up @@ -285,6 +263,7 @@ def test_native_repeated_reset_preserves_compiled_placement_and_sensor_state():
np.testing.assert_allclose(position["position"].reshape(3), initial_position, atol=1e-6)


@pytest.mark.integration
def test_native_sensor_bindings_are_actual_live_native_values():
runtime = MotrixSimRuntime(config())
with runtime:
Expand All @@ -304,6 +283,7 @@ def test_native_sensor_bindings_are_actual_live_native_values():
assert np.isfinite(state.base_linear_acceleration).all()


@pytest.mark.integration
def test_native_instance_names_preserve_artifact_local_robot_contract():
cfg = config()
cfg.scene.objs.robot.prefix = "deployed_"
Expand All @@ -318,6 +298,7 @@ def test_native_instance_names_preserve_artifact_local_robot_contract():
assert runtime.run().success


@pytest.mark.integration
def test_native_motor_source_supported():
cfg = config()
robot = cfg.scene.objs.robot
Expand All @@ -339,6 +320,7 @@ def test_native_motor_source_supported():


@pytest.mark.parametrize("period", [0.001, 0.003])
@pytest.mark.integration
def test_native_session_period_requires_whole_physics_steps(period):
runtime = MotrixSimRuntime(config())
with pytest.raises(ValidationError, match="integer multiple"):
Expand All @@ -355,6 +337,7 @@ def test_native_session_period_requires_whole_physics_steps(period):
("base_linear_velocity", "local_linvel"),
],
)
@pytest.mark.integration
def test_native_explicit_sensor_semantics(role, name):
cfg = config()
cfg = replace(cfg, sensor_bindings=replace(cfg.sensor_bindings, **{role: name}))
Expand All @@ -364,6 +347,7 @@ def test_native_explicit_sensor_semantics(role, name):


@pytest.mark.parametrize("role", ["base_angular_velocity", "base_linear_acceleration"])
@pytest.mark.integration
def test_native_missing_sensor_fails_at_model_binding(role):
cfg = config()
cfg = replace(cfg, sensor_bindings=replace(cfg.sensor_bindings, **{role: None}))
Expand All @@ -388,6 +372,7 @@ def test_native_artifact_mapping_validates_sensor_fields(bindings, message):
)


@pytest.mark.integration
def test_native_artifact_mapping_same_contract():
cfg = config()
runtime = create_simulation_runtime(
Expand Down Expand Up @@ -428,6 +413,7 @@ def test_native_artifact_mapping_rejects_unknown_physics_field():
)


@pytest.mark.integration
def test_sdk_viewer_lifecycle_and_keyboard(monkeypatch):
calls = []

Expand Down
3 changes: 3 additions & 0 deletions motrix_deploy_mujoco/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_example_builders_are_import_safe() -> None:


@pytest.mark.parametrize("backend", ["mujoco"])
@pytest.mark.integration
def test_programmatic_example_uses_shared_control_execution(tmp_path, onnx_policy, backend) -> None:
command = [
sys.executable,
Expand Down Expand Up @@ -118,6 +119,7 @@ def test_programmatic_example_uses_shared_control_execution(tmp_path, onnx_polic
assert _result(repeated.stdout)["trace_sha256"] == result["trace_sha256"]


@pytest.mark.integration
def test_simulation_factory_supports_explicit_bound_control_and_world_ownership(
control_scene_config, control_gains
) -> None:
Expand Down Expand Up @@ -168,6 +170,7 @@ def test_simulation_factory_supports_explicit_bound_control_and_world_ownership(
assert runtime.model is None and runtime.data is None


@pytest.mark.integration
def test_robot_commands_do_not_advance_or_own_the_world(control_scene_config, control_gains) -> None:
from motrix_deploy.runtime.context import ControlContext
from motrix_deploy_mujoco.runtime import MujocoRuntime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from types import SimpleNamespace

import numpy as np
import pytest

from motrix_deploy.contracts import JointControlMode, JointServoCommand
from motrix_deploy_mujoco.runtime import MujocoRuntime

pytestmark = pytest.mark.integration


def _motion_fixture(spec):
return SimpleNamespace(
Expand Down
35 changes: 19 additions & 16 deletions motrix_deploy_mujoco/tests/test_joint_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from motrix_env_core.config.scene.actuator import MotorActuatorCfg
from motrix_env_mujoco.compiler import MuJoCoSceneCompiler

pytestmark = pytest.mark.integration


def _servo(spec, **overrides):
zeros = np.zeros(spec.joint_count, dtype=np.float32)
Expand All @@ -22,23 +24,24 @@ def _servo(spec, **overrides):
return JointServoCommand(**fields)


def test_auto_spec_inspection_reuses_original_source_spec_on_first_open(control_scene_config, monkeypatch):
original = MuJoCoSceneCompiler.create_spec
specs = []

def record_spec(compiler, scene, sim):
spec = original(compiler, scene, sim)
specs.append(spec)
return spec

monkeypatch.setattr(MuJoCoSceneCompiler, "create_spec", record_spec)
def test_reopen_preserves_source_limits_and_torque_response(control_scene_config):
runtime = MujocoRuntime(control_scene_config, 0.02)
assert len(specs) == 1 and runtime._source_spec is specs[0]
assert runtime.model is runtime.data is None
with runtime:
assert len(specs) == 1
with runtime:
assert len(specs) == 2 # Reopen starts from source, not already transformed motors.
spec = runtime.robot.spec
torque = np.linspace(-1, 1, spec.joint_count, dtype=np.float32)
observed = []
for _ in range(2):
with runtime:
np.testing.assert_allclose(runtime.robot.spec.torque_limit, spec.torque_limit)
initial = runtime.robot.read_state(0.1)
assert initial.sample_time_ns == 0
runtime.robot.write_command(JointTorqueCommand(torque))
runtime.advance_control_period()
state = runtime.robot.read_state(0.1)
assert state.sample_time_ns == 20_000_000
np.testing.assert_allclose(runtime.data.ctrl, torque)
observed.append(state)
np.testing.assert_allclose(observed[0].joint_position, observed[1].joint_position)
np.testing.assert_allclose(observed[0].joint_velocity, observed[1].joint_velocity)


def test_torque_is_snapshotted_and_applied_directly_every_physics_substep(control_scene_config, monkeypatch):
Expand Down
Loading
Loading