> ## 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.

# Decorators

> Full reference for @confirms, @tags, @deadline, and @requires_sim. Signatures, behaviour, examples, interaction with pytest collection.

# 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.

```python theme={null}
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

```python theme={null}
def confirms(*req_ids: str) -> Callable: ...
```

| Parameter | Type   | Constraint                                                                                                                                                               |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `req_ids` | `*str` | At 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

```python theme={null}
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

```python theme={null}
def tags(*tags: str) -> Callable: ...
```

### Conventions

By community convention (not enforced):

| Tag       | Meaning                                  |
| --------- | ---------------------------------------- |
| `smoke`   | Run on every PR                          |
| `nightly` | Run only on the nightly suite            |
| `slow`    | Wall-clock > 10 s                        |
| `safety`  | Touches a safety requirement             |
| `flaky`   | Known intermittent; quarantine candidate |

### Example

```python theme={null}
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

```python theme={null}
def deadline(*, milliseconds: int) -> Callable: ...
```

| Parameter      | Type                 | Constraint |
| -------------- | -------------------- | ---------- |
| `milliseconds` | `int` (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

```python theme={null}
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:

```python theme={null}
@deadline(milliseconds=100)
@pytest.mark.timeout(1)   # 1 s hard kill
def test_estop(...): ...
```

## `@requires_sim(engine, *, gpu=False)`

Tells the [job router](/testing/sim-runners) the test needs a simulation runtime.

### Signature

```python theme={null}
def requires_sim(engine: Literal["gazebo", "webots"], *, gpu: bool = False) -> Callable: ...
```

| Parameter | Type                          | Constraint           |
| --------- | ----------------------------- | -------------------- |
| `engine`  | `Literal["gazebo", "webots"]` | Required.            |
| `gpu`     | `bool` (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

```python theme={null}
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:

```python theme={null}
@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

<CardGroup cols={2}>
  <Card title="Pytest plugin" icon="puzzle-piece" href="/sdk/pytest-plugin">
    How the plugin reads these decorators and writes JUnit properties.
  </Card>

  <Card title="Assertions" icon="check-double" href="/sdk/assertions">
    The rclpy-aware companion APIs.
  </Card>

  <Card title="Sim runners" icon="microchip" href="/testing/sim-runners">
    What `@requires_sim` triggers on the runner side.
  </Card>

  <Card title="Wire contract" icon="file-code" href="/sdk/wire-contract">
    The JUnit-with-confirms schema these decorators feed into.
  </Card>
</CardGroup>
