new control robot
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import sys
|
||||
import types
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PACKAGE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PACKAGE_ROOT))
|
||||
|
||||
|
||||
def _install_ros_stubs() -> None:
|
||||
ament_index_python = types.ModuleType("ament_index_python")
|
||||
ament_index_packages = types.ModuleType("ament_index_python.packages")
|
||||
|
||||
def get_package_share_directory(package_name: str) -> str:
|
||||
return str(PACKAGE_ROOT.parent / package_name)
|
||||
|
||||
ament_index_packages.get_package_share_directory = get_package_share_directory
|
||||
ament_index_python.packages = ament_index_packages
|
||||
sys.modules.setdefault("ament_index_python", ament_index_python)
|
||||
sys.modules.setdefault("ament_index_python.packages", ament_index_packages)
|
||||
|
||||
builtin_interfaces = types.ModuleType("builtin_interfaces")
|
||||
builtin_interfaces_msg = types.ModuleType("builtin_interfaces.msg")
|
||||
|
||||
class Duration:
|
||||
def __init__(self, sec: int = 0, nanosec: int = 0):
|
||||
self.sec = sec
|
||||
self.nanosec = nanosec
|
||||
|
||||
builtin_interfaces_msg.Duration = Duration
|
||||
builtin_interfaces.msg = builtin_interfaces_msg
|
||||
sys.modules.setdefault("builtin_interfaces", builtin_interfaces)
|
||||
sys.modules.setdefault("builtin_interfaces.msg", builtin_interfaces_msg)
|
||||
|
||||
trajectory_msgs = types.ModuleType("trajectory_msgs")
|
||||
trajectory_msgs_msg = types.ModuleType("trajectory_msgs.msg")
|
||||
|
||||
class JointTrajectoryPoint:
|
||||
def __init__(self):
|
||||
self.positions = []
|
||||
self.time_from_start = None
|
||||
|
||||
class JointTrajectory:
|
||||
def __init__(self):
|
||||
self.joint_names = []
|
||||
self.points = []
|
||||
|
||||
trajectory_msgs_msg.JointTrajectory = JointTrajectory
|
||||
trajectory_msgs_msg.JointTrajectoryPoint = JointTrajectoryPoint
|
||||
trajectory_msgs.msg = trajectory_msgs_msg
|
||||
sys.modules.setdefault("trajectory_msgs", trajectory_msgs)
|
||||
sys.modules.setdefault("trajectory_msgs.msg", trajectory_msgs_msg)
|
||||
|
||||
sensor_msgs = types.ModuleType("sensor_msgs")
|
||||
sensor_msgs_msg = types.ModuleType("sensor_msgs.msg")
|
||||
|
||||
class JointState:
|
||||
def __init__(self, position=()):
|
||||
self.position = position
|
||||
|
||||
sensor_msgs_msg.JointState = JointState
|
||||
sensor_msgs.msg = sensor_msgs_msg
|
||||
sys.modules.setdefault("sensor_msgs", sensor_msgs)
|
||||
sys.modules.setdefault("sensor_msgs.msg", sensor_msgs_msg)
|
||||
|
||||
std_srvs = types.ModuleType("std_srvs")
|
||||
std_srvs_srv = types.ModuleType("std_srvs.srv")
|
||||
|
||||
class Trigger:
|
||||
class Request:
|
||||
pass
|
||||
|
||||
std_srvs_srv.Trigger = Trigger
|
||||
std_srvs.srv = std_srvs_srv
|
||||
sys.modules.setdefault("std_srvs", std_srvs)
|
||||
sys.modules.setdefault("std_srvs.srv", std_srvs_srv)
|
||||
|
||||
fastmcp = types.ModuleType("fastmcp")
|
||||
|
||||
class _Router:
|
||||
@asynccontextmanager
|
||||
async def lifespan_context(self, _):
|
||||
yield
|
||||
|
||||
class _HttpApp:
|
||||
def __init__(self):
|
||||
self.router = _Router()
|
||||
|
||||
class FastMCP:
|
||||
@classmethod
|
||||
def from_fastapi(cls, app):
|
||||
return cls()
|
||||
|
||||
def http_app(self, path="/mcp"):
|
||||
return _HttpApp()
|
||||
|
||||
fastmcp.FastMCP = FastMCP
|
||||
sys.modules.setdefault("fastmcp", fastmcp)
|
||||
|
||||
rclpy = types.ModuleType("rclpy")
|
||||
rclpy_time = types.ModuleType("rclpy.time")
|
||||
rclpy_duration = types.ModuleType("rclpy.duration")
|
||||
rclpy_node = types.ModuleType("rclpy.node")
|
||||
rclpy_action = types.ModuleType("rclpy.action")
|
||||
|
||||
class Time:
|
||||
pass
|
||||
|
||||
class DurationValue:
|
||||
def __init__(self, seconds: float = 0.0):
|
||||
self.seconds = seconds
|
||||
|
||||
class _Logger:
|
||||
def info(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
class _Parameter:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
class _Publisher:
|
||||
def __init__(self):
|
||||
self.published = []
|
||||
|
||||
def publish(self, msg):
|
||||
self.published.append(msg)
|
||||
|
||||
class _Client:
|
||||
def wait_for_service(self, timeout_sec):
|
||||
return True
|
||||
|
||||
def call_async(self, request):
|
||||
future = types.SimpleNamespace()
|
||||
future.done = lambda: True
|
||||
future.result = lambda: types.SimpleNamespace(success=True, message="")
|
||||
return future
|
||||
|
||||
class Node:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self._parameters = {}
|
||||
self._logger = _Logger()
|
||||
|
||||
def declare_parameter(self, name, default):
|
||||
self._parameters[name] = default
|
||||
|
||||
def get_parameter(self, name):
|
||||
return _Parameter(self._parameters[name])
|
||||
|
||||
def create_subscription(self, *_args, **_kwargs):
|
||||
return object()
|
||||
|
||||
def create_publisher(self, *_args, **_kwargs):
|
||||
return _Publisher()
|
||||
|
||||
def create_client(self, *_args, **_kwargs):
|
||||
return _Client()
|
||||
|
||||
def get_logger(self):
|
||||
return self._logger
|
||||
|
||||
class ActionClient:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
rclpy.init = lambda: None
|
||||
rclpy.spin = lambda node: None
|
||||
rclpy.time = rclpy_time
|
||||
rclpy.duration = rclpy_duration
|
||||
rclpy.Time = Time
|
||||
rclpy_time.Time = Time
|
||||
rclpy_duration.Duration = DurationValue
|
||||
rclpy_node.Node = Node
|
||||
rclpy_action.ActionClient = ActionClient
|
||||
sys.modules.setdefault("rclpy", rclpy)
|
||||
sys.modules.setdefault("rclpy.time", rclpy_time)
|
||||
sys.modules.setdefault("rclpy.duration", rclpy_duration)
|
||||
sys.modules.setdefault("rclpy.node", rclpy_node)
|
||||
sys.modules.setdefault("rclpy.action", rclpy_action)
|
||||
|
||||
tf2_ros = types.ModuleType("tf2_ros")
|
||||
|
||||
class Buffer:
|
||||
def lookup_transform(self, *_args, **_kwargs):
|
||||
raise RuntimeError("tf unavailable in tests")
|
||||
|
||||
class TransformListener:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
tf2_ros.Buffer = Buffer
|
||||
tf2_ros.TransformListener = TransformListener
|
||||
sys.modules.setdefault("tf2_ros", tf2_ros)
|
||||
|
||||
|
||||
_install_ros_stubs()
|
||||
@@ -0,0 +1,65 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from iiwa_web.ros_node import CobotWebNode
|
||||
from iiwa_web import ros_node
|
||||
|
||||
|
||||
class DoneFuture:
|
||||
def __init__(self, result):
|
||||
self._result = result
|
||||
|
||||
def done(self):
|
||||
return True
|
||||
|
||||
def result(self):
|
||||
return self._result
|
||||
|
||||
|
||||
class PendingFuture:
|
||||
def done(self):
|
||||
return False
|
||||
|
||||
def result(self):
|
||||
return None
|
||||
|
||||
|
||||
class GoalHandle:
|
||||
accepted = True
|
||||
|
||||
def __init__(self):
|
||||
self.cancel_calls = 0
|
||||
|
||||
def get_result_async(self):
|
||||
return PendingFuture()
|
||||
|
||||
def cancel_goal_async(self):
|
||||
self.cancel_calls += 1
|
||||
return DoneFuture(None)
|
||||
|
||||
|
||||
class ActionClientStub:
|
||||
def __init__(self, goal_handle):
|
||||
self.goal_handle = goal_handle
|
||||
|
||||
def wait_for_server(self, timeout_sec):
|
||||
return True
|
||||
|
||||
def send_goal_async(self, goal):
|
||||
return DoneFuture(self.goal_handle)
|
||||
|
||||
|
||||
def test_send_action_cancels_goal_when_result_times_out(monkeypatch):
|
||||
node = object.__new__(CobotWebNode)
|
||||
goal_handle = GoalHandle()
|
||||
node._action_clients = {"cobot/move_to_pose": ActionClientStub(goal_handle)}
|
||||
|
||||
timestamps = itertools.chain([0.0, 0.3, 0.6], itertools.repeat(0.9))
|
||||
monkeypatch.setattr(ros_node.time, "monotonic", lambda: next(timestamps))
|
||||
monkeypatch.setattr(ros_node.time, "sleep", lambda _seconds: None)
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
node.send_action(object, "cobot/move_to_pose", object(), timeout=0.5)
|
||||
|
||||
assert goal_handle.cancel_calls == 1
|
||||
@@ -0,0 +1,130 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from iiwa_web import runner
|
||||
|
||||
|
||||
class DummyProcess:
|
||||
def __init__(self, cmd):
|
||||
self.cmd = cmd
|
||||
self.pid = 4321
|
||||
self.stdout = iter(())
|
||||
self.returncode = None
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
|
||||
class DummyThread:
|
||||
def __init__(self, target=None, args=(), daemon=False):
|
||||
self.target = target
|
||||
self.args = args
|
||||
self.daemon = daemon
|
||||
|
||||
def start(self):
|
||||
return None
|
||||
|
||||
|
||||
class UploadStub:
|
||||
def __init__(self, filename: str, content: bytes):
|
||||
self.filename = filename
|
||||
self._content = content
|
||||
|
||||
async def read(self) -> bytes:
|
||||
return self._content
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner_module(monkeypatch, tmp_path):
|
||||
upload_dir = tmp_path / "uploads"
|
||||
upload_dir.mkdir()
|
||||
bag_root = tmp_path / "bags"
|
||||
bag_root.mkdir()
|
||||
|
||||
monkeypatch.setattr(runner, "_UPLOAD_DIR", upload_dir)
|
||||
monkeypatch.setattr(runner, "_BAG_ROOT", bag_root, raising=False)
|
||||
monkeypatch.setattr(runner, "MAX_UPLOAD_BYTES", 8, raising=False)
|
||||
monkeypatch.setattr(runner, "_process", None)
|
||||
|
||||
started = {}
|
||||
|
||||
def fake_popen(cmd, **kwargs):
|
||||
started["cmd"] = cmd
|
||||
proc = DummyProcess(cmd)
|
||||
started["proc"] = proc
|
||||
return proc
|
||||
|
||||
monkeypatch.setattr(runner.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(runner.threading, "Thread", DummyThread)
|
||||
|
||||
return runner, upload_dir, bag_root, started
|
||||
|
||||
|
||||
def _upload_file(name: str, content: bytes) -> UploadStub:
|
||||
return UploadStub(filename=name, content=content)
|
||||
|
||||
|
||||
def test_start_runner_sanitizes_uploaded_filename(runner_module):
|
||||
runner, upload_dir, _bag_root, started = runner_module
|
||||
|
||||
response = asyncio.run(
|
||||
runner.start_runner(
|
||||
config=_upload_file("../escape.json", b"{}"),
|
||||
n_iterations=3,
|
||||
delay_between_iterations=5.0,
|
||||
bag_path="",
|
||||
topics="",
|
||||
joints_action="cobot/move_to_joints",
|
||||
pose_action="cobot/move_to_pose",
|
||||
)
|
||||
)
|
||||
|
||||
assert response["config"] == "escape.json"
|
||||
assert (upload_dir / "escape.json").exists()
|
||||
assert not (upload_dir.parent / "escape.json").exists()
|
||||
config_arg = next(part for part in started["cmd"] if part.startswith("config_path:="))
|
||||
assert Path(config_arg.split(":=", 1)[1]).parent == upload_dir
|
||||
|
||||
|
||||
def test_start_runner_rejects_oversized_upload(runner_module):
|
||||
runner, _upload_dir, _bag_root, started = runner_module
|
||||
|
||||
with pytest.raises(runner.HTTPException) as excinfo:
|
||||
asyncio.run(
|
||||
runner.start_runner(
|
||||
config=_upload_file("config.json", b"123456789"),
|
||||
n_iterations=3,
|
||||
delay_between_iterations=5.0,
|
||||
bag_path="",
|
||||
topics="",
|
||||
joints_action="cobot/move_to_joints",
|
||||
pose_action="cobot/move_to_pose",
|
||||
)
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == 413
|
||||
assert "слишком" in excinfo.value.detail.lower()
|
||||
assert "cmd" not in started
|
||||
|
||||
|
||||
def test_start_runner_rejects_bag_path_outside_allowed_root(runner_module):
|
||||
runner, _upload_dir, _bag_root, started = runner_module
|
||||
|
||||
with pytest.raises(runner.HTTPException) as excinfo:
|
||||
asyncio.run(
|
||||
runner.start_runner(
|
||||
config=_upload_file("config.json", b"{}"),
|
||||
n_iterations=3,
|
||||
delay_between_iterations=5.0,
|
||||
bag_path="/tmp/escape",
|
||||
topics="",
|
||||
joints_action="cobot/move_to_joints",
|
||||
pose_action="cobot/move_to_pose",
|
||||
)
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == 422
|
||||
assert "bag_path" in excinfo.value.detail
|
||||
assert "cmd" not in started
|
||||
@@ -0,0 +1,118 @@
|
||||
import importlib
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from iiwa_web import config_loader
|
||||
|
||||
|
||||
class RecordingBridge:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
|
||||
def publish(self, topic_name, message_type, msg):
|
||||
self.messages.append((topic_name, message_type, msg))
|
||||
|
||||
def get_latest(self, topic_name):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trajectory_module(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
config_loader,
|
||||
"load_joint_names",
|
||||
lambda *args, **kwargs: [f"joint{i}" for i in range(1, 8)],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
config_loader,
|
||||
"load_joint_limits",
|
||||
lambda *args, **kwargs: [(-1.0, 1.0)] * 7,
|
||||
)
|
||||
|
||||
trajectory = importlib.import_module("iiwa_web.trajectory")
|
||||
trajectory = importlib.reload(trajectory)
|
||||
bridge = RecordingBridge()
|
||||
monkeypatch.setattr(trajectory, "get_bridge", lambda: bridge)
|
||||
return trajectory, bridge
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "detail"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"points": [
|
||||
{
|
||||
"positions": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
"time_from_start": 0.0,
|
||||
}
|
||||
]
|
||||
},
|
||||
"positions",
|
||||
),
|
||||
(
|
||||
{
|
||||
"points": [
|
||||
{
|
||||
"positions": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, float("nan")],
|
||||
"time_from_start": 0.0,
|
||||
}
|
||||
]
|
||||
},
|
||||
"конеч",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_send_request_rejects_invalid_payloads(trajectory_module, payload, detail):
|
||||
trajectory, bridge = trajectory_module
|
||||
|
||||
with pytest.raises((ValidationError, HTTPException)) as excinfo:
|
||||
request = trajectory.SendRequest.model_validate(payload)
|
||||
trajectory.send_trajectory(request)
|
||||
|
||||
assert detail.lower() in str(excinfo.value).lower()
|
||||
assert bridge.messages == []
|
||||
|
||||
|
||||
def test_send_trajectory_rejects_non_monotonic_time(trajectory_module):
|
||||
trajectory, bridge = trajectory_module
|
||||
payload = {
|
||||
"points": [
|
||||
{
|
||||
"positions": [0.0] * trajectory.N_JOINTS,
|
||||
"time_from_start": 0.5,
|
||||
},
|
||||
{
|
||||
"positions": [0.1] * trajectory.N_JOINTS,
|
||||
"time_from_start": 0.4,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
request = trajectory.SendRequest.model_validate(payload)
|
||||
trajectory.send_trajectory(request)
|
||||
|
||||
assert "монотон" in str(excinfo.value).lower()
|
||||
assert bridge.messages == []
|
||||
|
||||
|
||||
def test_send_trajectory_enforces_joint_limits_even_when_bypass_requested(trajectory_module):
|
||||
trajectory, bridge = trajectory_module
|
||||
payload = {
|
||||
"points": [
|
||||
{
|
||||
"positions": [100.0] * trajectory.N_JOINTS,
|
||||
"time_from_start": 0.0,
|
||||
}
|
||||
],
|
||||
"validate_limits": False,
|
||||
}
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
request = trajectory.SendRequest.model_validate(payload)
|
||||
trajectory.send_trajectory(request)
|
||||
|
||||
assert "вне диапазона" in excinfo.value.detail.lower()
|
||||
assert bridge.messages == []
|
||||
@@ -0,0 +1,137 @@
|
||||
import pytest
|
||||
|
||||
from iiwa_web import config_loader, ros_node
|
||||
|
||||
|
||||
def test_web_node_defaults_to_loopback_host():
|
||||
node = ros_node.CobotWebNode()
|
||||
|
||||
assert node.get_parameter("host").value == "127.0.0.1"
|
||||
|
||||
|
||||
def test_external_bind_requires_token():
|
||||
import importlib
|
||||
|
||||
monkeypatchable_main = _import_main_with_stubbed_config()
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
monkeypatchable_main.ensure_safe_bind("0.0.0.0", token=None)
|
||||
|
||||
monkeypatchable_main.ensure_safe_bind("0.0.0.0", token="secret-token")
|
||||
|
||||
|
||||
def test_web_settings_load_token():
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
utils_root = Path(__file__).resolve().parents[2] / "iiwa_utils"
|
||||
if str(utils_root) not in sys.path:
|
||||
sys.path.insert(0, str(utils_root))
|
||||
|
||||
from iiwa_utils.setting_loader import _parse_web
|
||||
|
||||
settings = _parse_web(
|
||||
{"enabled": True, "host": "0.0.0.0", "token": "configured-secret"},
|
||||
"/tmp",
|
||||
)
|
||||
|
||||
assert settings.token == "configured-secret"
|
||||
|
||||
|
||||
def test_web_server_passes_configured_token_to_process_environment(monkeypatch):
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
class FakeNode:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.additional_env = kwargs.get("additional_env")
|
||||
|
||||
launch_ros = types.ModuleType("launch_ros")
|
||||
launch_ros_actions = types.ModuleType("launch_ros.actions")
|
||||
launch_ros_actions.Node = FakeNode
|
||||
launch_ros.actions = launch_ros_actions
|
||||
monkeypatch.setitem(sys.modules, "launch_ros", launch_ros)
|
||||
monkeypatch.setitem(sys.modules, "launch_ros.actions", launch_ros_actions)
|
||||
|
||||
launch_root = str(Path(__file__).resolve().parents[2] / "iiwa_bringup" / "launch")
|
||||
if launch_root not in sys.path:
|
||||
sys.path.insert(0, launch_root)
|
||||
|
||||
from supported.optional_nodes import make_web_server_node
|
||||
|
||||
settings = SimpleNamespace(
|
||||
web=SimpleNamespace(
|
||||
host="0.0.0.0",
|
||||
port=8007,
|
||||
endpoints="/tmp/endpoints.yaml",
|
||||
joint_limits="/tmp/joint_limits.yaml",
|
||||
token="configured-secret",
|
||||
)
|
||||
)
|
||||
|
||||
node = make_web_server_node(settings, use_sim_time=False)
|
||||
|
||||
assert node.additional_env == {"IIWA_WEB_TOKEN": "configured-secret"}
|
||||
|
||||
|
||||
def test_bearer_token_is_required_and_compared_in_constant_time(monkeypatch):
|
||||
main = _import_main_with_stubbed_config()
|
||||
compared = []
|
||||
|
||||
def fake_compare(left, right):
|
||||
compared.append((left, right))
|
||||
return left == right
|
||||
|
||||
monkeypatch.setattr(main.hmac, "compare_digest", fake_compare)
|
||||
|
||||
assert main.has_valid_bearer_token(None, "secret-token") is False
|
||||
assert main.has_valid_bearer_token("Bearer wrong", "secret-token") is False
|
||||
assert main.has_valid_bearer_token("Bearer secret-token", "secret-token") is True
|
||||
assert compared == [("wrong", "secret-token"), ("secret-token", "secret-token")]
|
||||
|
||||
|
||||
def test_docs_paths_are_public_but_api_routes_are_not():
|
||||
main = _import_main_with_stubbed_config()
|
||||
|
||||
assert main.is_public_auth_path("/docs")
|
||||
assert main.is_public_auth_path("/docs/oauth2-redirect")
|
||||
assert main.is_public_auth_path("/openapi.json")
|
||||
assert main.is_public_auth_path("/redoc")
|
||||
assert not main.is_public_auth_path("/protected")
|
||||
|
||||
|
||||
def test_openapi_declares_bearer_security_for_swagger_authorize_button():
|
||||
from fastapi import FastAPI
|
||||
|
||||
main = _import_main_with_stubbed_config()
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/protected")
|
||||
def protected():
|
||||
return {"ok": True}
|
||||
|
||||
main.install_bearer_openapi(app, "secret-token")
|
||||
schema = app.openapi()
|
||||
|
||||
assert schema["components"]["securitySchemes"]["BearerAuth"] == {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "opaque-token",
|
||||
}
|
||||
assert schema["paths"]["/protected"]["get"]["security"] == [
|
||||
{"BearerAuth": []}
|
||||
]
|
||||
|
||||
|
||||
def _import_main_with_stubbed_config():
|
||||
monkeypatch_joint_names = [f"joint{i}" for i in range(1, 8)]
|
||||
monkeypatch_joint_limits = [(-1.0, 1.0)] * 7
|
||||
config_loader.load_joint_names = lambda *args, **kwargs: list(monkeypatch_joint_names)
|
||||
config_loader.load_joint_limits = lambda *args, **kwargs: list(monkeypatch_joint_limits)
|
||||
|
||||
import importlib
|
||||
|
||||
main = importlib.import_module("iiwa_web.main")
|
||||
return importlib.reload(main)
|
||||
Reference in New Issue
Block a user