Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.roboticks.io/llms.txt

Use this file to discover all available pages before exploring further.

Fault injection

Functional tests prove the robot works. Fault tests prove it fails safely. Both are required by every safety standard you’ll certify against (ISO 13849, IEC 61508, EU MR 2023/1230 Annex III). Roboticks ships four fault-injection primitives that operate over the rclpy DDS layer without modifying the system under test.
Fault injection runs between rclpy and the node — it manipulates the DDS layer for the test process. It does not require root, does not need a custom DDS vendor, and does not patch your node.

The four primitives

PrimitiveWhat it doesUse for
drop_messages(topic, rate)Probabilistically drops messages on a topicSensor stalls, lossy WiFi, partial packet loss
delay_messages(topic, ms)Adds latency to every message on a topicNetwork jitter, slow sensors, perception lag
kill_node(name)SIGTERMs a node, then re-asserts it stays downNode crash recovery, watchdog tests
corrupt_topic(topic, mutator)Mutates message payloads with a callableBit-flip simulation, out-of-range values, malformed input
All four are context managers. The fault is scoped to the with block. On exit, the original behaviour is restored, even if the test asserts inside the block.
from roboticks.fault_injection import (
    drop_messages, delay_messages, kill_node, corrupt_topic,
)
Full signatures in SDK fault injection reference.

When to use injection vs. mocks

MockFault injection
SpeedFast (μs)Real-time (ms-s)
RealismUnit-level fictionThe actual DDS round-trip with the fault overlaid
Catches integration bugsNoYes
Catches QoS misconfigNoYes
Required by safety auditorsSometimes acceptedAlways preferred
Rule of thumb: mock until the node is built; inject once it’s running. Roboticks’s evidence pack tags fault-injection tests separately so the auditor sees them without you having to label by hand.

Example: E-stop survives comms loss

The safety requirement is “E-stop functions even if the supervisor’s heartbeat is lost.” A passing test asserts the robot enters a safe stop state when /supervisor/heartbeat stops arriving.
import pytest
from roboticks import confirms, deadline
from roboticks.fault_injection import drop_messages
from roboticks.assertions import assert_topic_published
from std_msgs.msg import Bool

@confirms("REQ-014", "REQ-015")
@deadline(milliseconds=500)
def test_estop_engages_on_heartbeat_loss(ros_context, robot_node):
    with drop_messages("/supervisor/heartbeat", rate=1.0):  # 100% drop
        engaged = assert_topic_published(
            topic="/safety/estop_engaged",
            msg_type=Bool,
            within=0.3,
            predicate=lambda m: m.data is True,
        )
    assert engaged.data

Example: navigation handles a slow LIDAR

from roboticks import confirms, requires_sim
from roboticks.fault_injection import delay_messages
from roboticks.assertions import assert_topic_published
from geometry_msgs.msg import Twist

@confirms("REQ-061")
@requires_sim("gazebo")
def test_nav_degrades_gracefully_with_lidar_lag(ros_context, nav_stack):
    with delay_messages("/scan", ms=400):
        msg = assert_topic_published("/cmd_vel", Twist, within=10.0)
        # Robot should slow down, not crash, not stop entirely
        assert 0.0 < msg.linear.x < 0.3

Example: watchdog restarts a dead planner

from roboticks import confirms
from roboticks.fault_injection import kill_node
from roboticks.assertions import assert_topic_published
from lifecycle_msgs.msg import TransitionEvent

@confirms("REQ-098")
def test_planner_respawned_by_watchdog(ros_context):
    with kill_node("nav2_planner"):
        # Watchdog should observe the death and republish a transition
        evt = assert_topic_published(
            "/nav2_planner/transition_event",
            TransitionEvent,
            within=5.0,
            predicate=lambda m: m.transition.label == "configure",
        )
        assert evt is not None

Example: sensor corruption

from roboticks import confirms
from roboticks.fault_injection import corrupt_topic
from sensor_msgs.msg import LaserScan

def clamp_high(msg: LaserScan) -> LaserScan:
    msg.ranges = [float("inf")] * len(msg.ranges)
    return msg

@confirms("REQ-072")
def test_planner_ignores_inf_only_scans(ros_context, nav_stack):
    with corrupt_topic("/scan", mutator=clamp_high):
        # Planner should not emit cmd_vel when LIDAR is all-infinity
        with pytest.raises(TimeoutError):
            assert_topic_published("/cmd_vel", Twist, within=2.0)

Stacking faults

Context managers nest:
with delay_messages("/scan", ms=200), drop_messages("/odom", rate=0.1):
    ...
The DDS interceptor multiplexes both faults. Order is irrelevant; the per-topic state is isolated.

What surfaces in the evidence pack

Fault-injection tests get a dedicated section in the evidence pack PDF: requirement → fault primitive → topic → result. Auditors quote that section verbatim when defending the safety case.

Next

Fault injection reference

Full signatures, rate semantics, lifecycle interaction.

MCAP capture

Pair fault injection with bag-recording for failure forensics.

Launch testing

Inject faults across a multi-node graph.

Standards mapping

Which standards require which kind of fault test.