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

# Pytest Plugin

> How the roboticks pytest11 plugin reads decorators, stamps the schema version, injects user_properties, and emits JUnit-with-confirms. Internals and kill switches.

# Pytest plugin

The Roboticks pytest plugin is the bridge between in-code `@confirms` decorators and the JUnit XML the platform ingests. It loads automatically via the `pytest11` entry point and writes properties at three lifecycle points: session-start (schema version), per-test setup/teardown (active-test thread-local), and per-test report (decorator metadata + nodeid + attached artifacts).

<Info>
  You almost never touch this plugin directly. It loads on `pip install roboticks` and does the right thing. This page is for the times you need to debug it or override its behaviour.
</Info>

## How it loads

The plugin is declared as a pytest11 entry point in the SDK's `pyproject.toml`:

```toml theme={null}
[project.entry-points.pytest11]
roboticks = "roboticks.pytest_plugin"
```

When `pip install roboticks` runs, pytest discovers the entry point on next session-start. No `conftest.py` change required.

Verify it's loaded:

```bash theme={null}
pytest --trace-config 2>&1 | grep roboticks
# > active plugins: roboticks-0.2.0a0 at .../roboticks/pytest_plugin.py
```

## What it does, in order

```mermaid theme={null}
%%{init: {"theme": "neutral", "themeVariables": {"primaryColor": "#4040ff"}} }%%
flowchart TD
    SS["pytest_sessionstart"] --> S1["stamp roboticks_schema_version=2<br/>+ sdk.version, python.version<br/>as suite-level properties"]
    SS --> CO["pytest_collection_modifyitems"]
    CO --> C1["scan each item for<br/>@confirms, @tags, @deadline,<br/>@requires_sim attributes"]
    C1 --> RTS["pytest_runtest_setup"]
    RTS --> RTS1["stash the active test function<br/>in a thread-local so<br/>attach_artifact() / capture_mcap()<br/>can resolve it without plumbing"]
    RTS --> RT["pytest_runtest_call / teardown"]
    RT --> RT1["enforce @deadline (monotonic timer)<br/>capture mcap_capture handles<br/>buffer fault_injection state"]
    RT --> SF["pytest_runtest_makereport"]
    SF --> SF1["emit roboticks.nodeid + decorator props<br/>+ one roboticks.attach.{kind} per file<br/>registered via attach_artifact()"]
```

## Properties emitted

Per session (suite-level):

```xml theme={null}
<property name="roboticks_schema_version" value="2"/>
<property name="roboticks.sdk.version" value="0.2.0a0"/>
<property name="roboticks.python.version" value="3.13.1"/>
```

Per test (testcase-level), always:

```xml theme={null}
<property name="roboticks.nodeid" value="tests/test_estop.py::test_estop_halts_motion"/>
```

Per test (testcase-level), only when set by decorators:

```xml theme={null}
<property name="roboticks.confirms" value="REQ-001,REQ-014"/>
<property name="roboticks.tags" value="safety,smoke"/>
<property name="roboticks.deadline_ms" value="100"/>
<property name="roboticks.requires_sim" value="gazebo:gpu"/>
<property name="roboticks.fault_injection" value='[{"primitive":"drop","topic":"/scan","rate":0.3}]'/>
<property name="roboticks.mcap.path" value="captures/test_estop.mcap"/>
```

Per test (testcase-level), one per file the test registered via `attach_artifact()`:

```xml theme={null}
<property name="roboticks.attach.mcap" value="/tmp/run/test_estop.mcap"/>
<property name="roboticks.attach.attachments" value="/tmp/run/before.png"/>
<property name="roboticks.attach.attachments" value="/tmp/run/after.png"/>
<property name="roboticks.attach.logs" value="/tmp/run/decision_log.jsonl"/>
```

The runner uploader walks every `roboticks.attach.*` on the testcase and posts each file to the platform's per-test-case S3 prefix. See the full [wire contract](/sdk/wire-contract) for property semantics and the [artifact attachment API](/sdk/overview#attach_artifact).

## The kill switch

If the plugin breaks something — a pytest plugin conflict, a CI environment that can't tolerate the entry-point loading order, an investigation where you want the bare JUnit — disable just the property-writing without uninstalling the SDK:

```bash theme={null}
pytest --no-roboticks-junit-extras tests/
```

This drops every `roboticks.*` property write. Decorators still work in-process (they still mutate function attributes); they just don't surface in the XML.

The schema-version stamp is also dropped — so a downstream platform will treat the result as stock JUnit (the [wire contract](/sdk/wire-contract) defines the "no schema version" fallback).

You can also disable the plugin entirely the normal pytest way:

```bash theme={null}
pytest -p no:roboticks tests/
```

## user\_properties

The plugin uses pytest's standard `user_properties` mechanism. Each `roboticks.*` property is a tuple `(key, value)` appended to `request.node.user_properties` during `pytest_runtest_makereport`. pytest's built-in JUnit reporter writes them out as `<property>` elements automatically.

This means you can read them in your own conftest hook:

```python theme={null}
# conftest.py
def pytest_runtest_makereport(item, call):
    if call.when == "call":
        props = dict(item.user_properties)
        confirms = props.get("roboticks.confirms", "")
        print(f"{item.nodeid} confirms: {confirms}")
```

## Schema version stamping

The plugin reads its own version from `roboticks.__version__` and the schema version from `roboticks._version.SCHEMA_VERSION`. These are independent integers — the SDK can ship 0.2.0, 0.3.0, 0.4.0 still emitting schema version 2, until a wire change bumps it.

To override the schema version at runtime (e.g. for testing a future-version platform):

```bash theme={null}
ROBOTICKS_SCHEMA_VERSION_OVERRIDE=3 pytest tests/
```

The override only changes the *stamp*; the actual property shapes are still whatever the running SDK knows how to emit.

## Debugging the plugin

### Show plugin events

```bash theme={null}
pytest -p no:cacheprovider --tb=short -v tests/ 2>&1 | grep roboticks
```

### Dry-run property emission

The plugin exposes a CLI to render what would be written without running pytest:

```bash theme={null}
python -m roboticks.pytest_plugin --dry-run tests/
```

Output:

```text theme={null}
tests/test_estop.py::test_estop_halts_motion
  roboticks.confirms=REQ-001,REQ-014
  roboticks.deadline_ms=100
  roboticks.tags=safety
```

### Force-regenerate JUnit from a re-parse

If you already have a JUnit XML without the plugin's stamps (because the plugin was disabled, or an old SDK), the stitcher utility back-fills:

```bash theme={null}
roboticks-stitch-junit \
  --in test_results/junit.xml \
  --confirms-from tests/  \
  --out test_results/junit.stitched.xml
```

The stitcher walks the test files, re-imports each, reads decorator attributes, and writes properties into a copy of the XML. Useful for retro-fitting old CI pipelines.

## What the plugin does **not** do

* It does **not** modify pytest's test selection. `@requires_sim` is metadata; the platform router uses it, not pytest itself.
* It does **not** swallow assertion errors. `@deadline` raises an `AssertionError` subclass; pytest reports it normally.
* It does **not** auto-load the assertion helpers. Those import only when you import them.
* It does **not** require rclpy. The plugin runs on any pytest installation.

## Next

<CardGroup cols={2}>
  <Card title="Decorators" icon="at" href="/sdk/decorators">
    What the plugin is reading.
  </Card>

  <Card title="Wire contract" icon="file-code" href="/sdk/wire-contract">
    What the plugin is writing.
  </Card>

  <Card title="Modules" icon="folder-tree" href="/sdk/modules">
    Where to find the plugin source.
  </Card>

  <Card title="CI recipes" icon="gear" href="/testing/ci-recipes">
    How the plugin's output gets to the platform.
  </Card>
</CardGroup>
