Skip to main content

Decorators

The four decorators in roboticks are the SDK’s smallest surface and the one users touch most. All four work on any callable that pytest collects — functions, methods, parametrized variants, async tests. They stack in any order.
from roboticks import confirms, tags, deadline, requires_sim

@confirms(*req_ids)

Records which requirement IDs the decorated test confirms. The pytest plugin reads this and emits a <property name="roboticks.confirms" value="REQ-001,REQ-014"/> into the JUnit XML at session-end.

Signature

def confirms(*req_ids: str) -> Callable: ...
ParameterTypeConstraint
req_ids*strAt least one. Must match a known requirement ID after platform ingestion, otherwise the platform reports the test as “confirms an unknown requirement” in the Check Run.

Behaviour

  • Stacking@confirms("A") on a class plus @confirms("B") on a method unions to {"A", "B"}. Useful when an entire test class addresses one umbrella requirement and individual methods address sub-requirements.
  • Parametrize — A @pytest.mark.parametrize variant inherits the function’s @confirms set; all branches confirm the same requirement IDs. If you need different requirements per branch, split into multiple test functions.
  • Skipped tests — A skipped test still records its @confirms (so the matrix shows which requirements are intended to be covered, even when the test didn’t run on this commit). The matrix surfaces them with a “skipped” badge.

Example

from roboticks import confirms

@confirms("REQ-014")
class TestEStop:

    @confirms("REQ-015")  # union: {REQ-014, REQ-015}
    def test_engages_on_button(self):
        ...

    @confirms("REQ-016")  # union: {REQ-014, REQ-016}
    def test_engages_on_heartbeat_loss(self):
        ...

@tags(*tags)

Free-form labels for filtering. The platform doesn’t interpret them — they are pass-through metadata for your filters, dashboards, and policies.

Signature

def tags(*tags: str) -> Callable: ...

Conventions

By community convention (not enforced):
TagMeaning
smokeRun on every PR
nightlyRun only on the nightly suite
slowWall-clock > 10 s
safetyTouches a safety requirement
flakyKnown intermittent; quarantine candidate

Example

from roboticks import tags, confirms

@confirms("REQ-101")
@tags("nightly", "perception", "slow")
def test_full_warehouse_traversal(...):
    ...
The platform exposes tag-based test filters in Project → Tests → Filter by tag.

@deadline(milliseconds=int)

Fails the test if its wall-clock duration exceeds the budget. Useful for soft real-time guarantees that don’t otherwise show up as an assertion.

Signature

def deadline(*, milliseconds: int) -> Callable: ...
ParameterTypeConstraint
millisecondsint (keyword-only)> 0

Behaviour

  • The decorator wraps the test body in a time.monotonic() measurement and raises DeadlineExceeded (an AssertionError subclass) if the test runs over.
  • Reported in JUnit XML as <property name="roboticks.deadline_ms" value="100"/>.
  • The platform surfaces the actual duration vs the deadline in the run detail. A test that passes its assertions but blows the deadline is failed.

Example

from roboticks import confirms, deadline

@confirms("REQ-001")
@deadline(milliseconds=100)
def test_estop_halts_within_100ms(robot):
    robot.trigger_estop()
    robot.wait_until_stopped()
    # No explicit time assertion — @deadline handles it.

Interaction with pytest timeouts

@deadline and pytest-timeout are independent. pytest-timeout kills the process; @deadline only fails the test. Use both for hard-stop budgets:
@deadline(milliseconds=100)
@pytest.mark.timeout(1)   # 1 s hard kill
def test_estop(...): ...

@requires_sim(engine, *, gpu=False)

Tells the job router the test needs a simulation runtime.

Signature

def requires_sim(engine: Literal["gazebo", "webots"], *, gpu: bool = False) -> Callable: ...
ParameterTypeConstraint
engineLiteral["gazebo", "webots"]Required.
gpubool (keyword-only)Defaults to False.

Behaviour

  • Reported in JUnit XML as <property name="roboticks.requires_sim" value="gazebo:gpu"/> or "gazebo" if gpu=False.
  • The platform’s scheduler refuses to dispatch the test to a runner that doesn’t satisfy the engine + GPU label.
  • Local pytest runs do not skip the test — they run it. If sim isn’t available locally the test will fail at the rclpy step, which is the right behaviour: a developer needs to know they’re trying to run a sim test without a sim.

Example

from roboticks import confirms, requires_sim

@confirms("REQ-031")
@requires_sim("gazebo", gpu=True)
def test_navigation_through_warehouse(...):
    ...

Stacking order

Decorators are evaluated bottom-up (closest-to-function first), but for these four the order doesn’t change behaviour — they are independent metadata. Pick a convention and stick with it. The convention in our examples:
@confirms(...)      # outermost: what
@tags(...)          #            why
@requires_sim(...)  #            where
@deadline(...)      # innermost: how fast
def test_x(...): ...

What about @pytest.mark.X?

Roboticks decorators are not pytest markers. They mutate the function’s attributes; the pytest plugin reads those attributes at collection time. This avoids pytest’s marker-registration warnings and keeps @confirms working on classes (where @pytest.mark.X semantics are subtle). You can still use pytest markers freely. They land in the JUnit XML as standard pytest properties; they don’t conflict with the roboticks.* namespace.

Next

Pytest plugin

How the plugin reads these decorators and writes JUnit properties.

Assertions

The rclpy-aware companion APIs.

Sim runners

What @requires_sim triggers on the runner side.

Wire contract

The JUnit-with-confirms schema these decorators feed into.