|
| 1 | +# Working on `onvif-tt` with Claude (or any AI agent) |
| 2 | + |
| 3 | +This file is a cold-start brief for AI agents collaborating on this |
| 4 | +repo. It explains what the project is, how its pieces fit together, |
| 5 | +and the conventions that — if you get them wrong — produce subtle |
| 6 | +bugs that look right but aren't. |
| 7 | + |
| 8 | +## What this is |
| 9 | + |
| 10 | +`onvif-tt` is a Linux-native, headless ONVIF conformance test tool — |
| 11 | +an open-source alternative to the closed (members-only, Windows-only) |
| 12 | +ONVIF Device Test Tool. It: |
| 13 | + |
| 14 | +1. Parses the public ONVIF Test Specification HTML corpus into a |
| 15 | + structured catalog (`corpus/parsed.json`, 1,121 test cases). |
| 16 | +2. Lets contributors register Python implementations of individual |
| 17 | + spec IDs via `@register("DEVICE-1-1-2")` decorators. |
| 18 | +3. Runs the registered tests against a Device Under Test via pytest, |
| 19 | + emitting JUnit XML for CI and a structured JSON report whose schema |
| 20 | + is published in `docs/schemas/`. |
| 21 | + |
| 22 | +## Repo layout |
| 23 | + |
| 24 | +``` |
| 25 | +onvif-tt/ |
| 26 | +├── corpus/ |
| 27 | +│ ├── html/ # 22 ONVIF Test Specification files (verbatim, redistributable) |
| 28 | +│ └── parsed.json # generated cache; deterministic-parse-tested |
| 29 | +├── src/onvif_tt/ |
| 30 | +│ ├── specs/ # lxml DocBook parser + dataclasses |
| 31 | +│ ├── registry.py # @register decorator + REGISTRY dict + xfail_on matcher |
| 32 | +│ ├── runtime/ |
| 33 | +│ │ ├── dut.py # ONVIFCamera wrapper, lazy service binding, |
| 34 | +│ │ │ # PullPointHandle, NotifyHandle, SOAP trace |
| 35 | +│ │ ├── features.py # cached GetServices + GetDeviceInformation |
| 36 | +│ │ ├── discovery.py # multicast WS-Discovery Probe helper |
| 37 | +│ │ └── soap_trace.py # zeep plugin that captures envelopes |
| 38 | +│ ├── runner/ |
| 39 | +│ │ ├── dispatch.py # pytest parametrise — one node per registry entry |
| 40 | +│ │ └── plugin.py # CLI options + JSON reporter (xdist-safe) |
| 41 | +│ ├── cases/ # one .py per profile area |
| 42 | +│ │ ├── base.py # Device + capabilities + GetServices flavours |
| 43 | +│ │ ├── auth.py # LOCAL-AUTH-* (no AUTH-* in catalog) |
| 44 | +│ │ ├── discovery.py # WS-Discovery |
| 45 | +│ │ ├── ipconfig.py # LOCAL-NETWORK-* read-only (catalog IPCONFIG-* are writes) |
| 46 | +│ │ ├── media.py # Media v10 + Media2 (adaptive) + consistency tests |
| 47 | +│ │ ├── event.py # GetEventProperties + PullPoint + Basic Notification |
| 48 | +│ │ ├── ptz.py # GetNodes + Move/Stop write ops |
| 49 | +│ │ └── imaging.py # GetImagingSettings + Move/Stop write ops |
| 50 | +│ └── cli.py # argparse: `onvif-tt list|show|corpus|run` |
| 51 | +├── tests/ # Tests for the tool itself (parser + registry) |
| 52 | +├── docs/ |
| 53 | +│ ├── ai-readme.md # How to drive the tool from an LLM tool-use loop |
| 54 | +│ ├── adding-a-test.md # Contributor guide |
| 55 | +│ └── schemas/ # JSON Schema for results.json + corpus/parsed.json |
| 56 | +└── scripts/ # Shell helpers (subscription-cleanup verifier, …) |
| 57 | +``` |
| 58 | + |
| 59 | +## Conventions that matter |
| 60 | + |
| 61 | +These are mistakes that bit us during development. If you reproduce |
| 62 | +them, the tests will pass in spurious ways. |
| 63 | + |
| 64 | +### 1. Look up real test IDs via the catalog before `@register`-ing |
| 65 | + |
| 66 | +The spec ID `DEVICE-1-1-2` is "ALL CAPABILITIES", not "GetDeviceInformation" |
| 67 | +(that's `DEVICE-3-1-9`). The names are not as obvious as they look. |
| 68 | + |
| 69 | +```bash |
| 70 | +onvif-tt show DEVICE-3-1-9 # prints the actual procedure |
| 71 | +onvif-tt list --id-glob "MEDIA2-2-*" --missing # what's left to implement |
| 72 | +``` |
| 73 | + |
| 74 | +If you invent an ID that isn't in the catalog, the CI check |
| 75 | +"every registered spec ID exists in the catalog" will fail. |
| 76 | + |
| 77 | +### 2. `python-onvif-zeep` takes **positional** WSDL parameters |
| 78 | + |
| 79 | +```python |
| 80 | +dut.devicemgmt.GetServices(False) # ✅ |
| 81 | +dut.devicemgmt.GetServices(IncludeCapability=False) # ❌ raises |
| 82 | +dut.devicemgmt.GetCapabilities("All") # ✅ |
| 83 | +dut.devicemgmt.GetCapabilities(Category="All") # ❌ |
| 84 | +``` |
| 85 | + |
| 86 | +For operations with structured arguments, build with `create_type`: |
| 87 | + |
| 88 | +```python |
| 89 | +req = dut.media.create_type("GetStreamUri") |
| 90 | +req.StreamSetup = {"Stream": "RTP-Unicast", "Transport": {"Protocol": "RTSP"}} |
| 91 | +req.ProfileToken = profile.token |
| 92 | +resp = dut.media.GetStreamUri(req) |
| 93 | +``` |
| 94 | + |
| 95 | +### 3. ONVIF "void" responses come back as Python `None` |
| 96 | + |
| 97 | +Operations like `Move`, `Stop`, `AbsoluteMove`, `RelativeMove`, |
| 98 | +`Unsubscribe`, `SetSynchronizationPoint`, … have empty response bodies |
| 99 | +in the WSDL — zeep returns `None`. Never assert `resp is not None` on |
| 100 | +these; the assertion is "no SOAP Fault was raised". |
| 101 | + |
| 102 | +```python |
| 103 | +dut.imaging.Move(req) # ✅ just the call |
| 104 | +resp = dut.imaging.Move(req) |
| 105 | +assert resp is not None # ❌ will FAIL spuriously on a perfectly conformant device |
| 106 | +``` |
| 107 | + |
| 108 | +### 4. PullPoint subscriptions need **two** WSDL bindings |
| 109 | + |
| 110 | +The PullPoint subscription URL is queried via different bindings for |
| 111 | +different operations: |
| 112 | + |
| 113 | +* `PullPointSubscriptionBinding` → `PullMessages`, `SetSynchronizationPoint` |
| 114 | +* `SubscriptionManagerBinding` → `Renew`, `Unsubscribe` |
| 115 | + |
| 116 | +`PullPointHandle` (and `NotifyHandle` for Basic Notification) in |
| 117 | +`runtime/dut.py` already wraps both. Use them; don't roll your own. |
| 118 | + |
| 119 | +```python |
| 120 | +with dut.create_pullpoint("PT60S") as pp: |
| 121 | + pp.pull_messages(timeout="PT3S", limit=5) |
| 122 | + # auto-unsubscribe on __exit__ |
| 123 | +``` |
| 124 | + |
| 125 | +### 5. `LOCAL-*` prefix for tool-author IDs |
| 126 | + |
| 127 | +When the spec catalog has no clean counterpart for the conformance |
| 128 | +intent you want to test (e.g. a basic "GetStreamUri returns a valid |
| 129 | +RTSP URL" smoke), use a `LOCAL-*` ID: |
| 130 | + |
| 131 | +```python |
| 132 | +@register("LOCAL-MEDIA-S-STREAM-URI", profiles={"S"}, mandatory=True, |
| 133 | + requires_services={"devicemgmt", "media"}, |
| 134 | + tags={"local"}) |
| 135 | +``` |
| 136 | + |
| 137 | +The CI check skips `LOCAL-*` IDs when validating "registered ID is in |
| 138 | +catalog". Don't put real-looking IDs there; if it's in `corpus/parsed.json`, |
| 139 | +use the real ID. |
| 140 | + |
| 141 | +### 6. `--allow-writes` for hardware-actuating tests |
| 142 | + |
| 143 | +Anything that moves a focus motor, pans a head, mutates network |
| 144 | +config, sets the system clock, or reboots needs `requires_writes=True`: |
| 145 | + |
| 146 | +```python |
| 147 | +@register("PTZ-3-1-1", ..., requires_writes=True) |
| 148 | +def test_ptz_absolute_move(dut, spec): |
| 149 | + ... |
| 150 | +``` |
| 151 | + |
| 152 | +Without `--allow-writes` on the run command, these skip with a clear |
| 153 | +reason. Default runs are read-only. |
| 154 | + |
| 155 | +### 7. `xfail_on` for known-buggy firmware, not for "test is hard" |
| 156 | + |
| 157 | +If a vendor's firmware reliably violates the spec, document it inline: |
| 158 | + |
| 159 | +```python |
| 160 | +@register("DEVICE-1-1-9", ..., |
| 161 | + xfail_on=[{ |
| 162 | + "Manufacturer": "H264", |
| 163 | + "reason": "Xiongmai stock closes the TCP connection on " |
| 164 | + "invalid GetCapabilities Category instead of " |
| 165 | + "returning a SOAP 1.2 Fault.", |
| 166 | + }]) |
| 167 | +def test_soap_fault_on_invalid_capability(dut, spec): ... |
| 168 | +``` |
| 169 | + |
| 170 | +Matchers compare against `GetDeviceInformation` fields (literal or |
| 171 | +callable). Multiple matchers OR together. If the bug ever gets fixed, |
| 172 | +the test becomes `xpassed` and a Python warning surfaces it. |
| 173 | + |
| 174 | +**Do not** use `xfail_on` to silence flaky tests. If a test is flaky, |
| 175 | +fix the test. |
| 176 | + |
| 177 | +### 8. Don't break corpus determinism |
| 178 | + |
| 179 | +`corpus/parsed.json` is committed AND regenerable. A CI test asserts |
| 180 | +that re-parsing produces byte-identical output. If you touch the |
| 181 | +parser, run: |
| 182 | + |
| 183 | +```bash |
| 184 | +onvif-tt corpus refresh |
| 185 | +pytest tests/test_parser.py |
| 186 | +``` |
| 187 | + |
| 188 | +Both must succeed. |
| 189 | + |
| 190 | +## Adding a new test — quick recipe |
| 191 | + |
| 192 | +```bash |
| 193 | +# 1. Find the right spec ID (don't invent — look it up): |
| 194 | +onvif-tt list --id-glob "EVENT-3-*" --missing --format json | jq |
| 195 | + |
| 196 | +# 2. Read the spec procedure: |
| 197 | +onvif-tt show EVENT-3-1-32 |
| 198 | + |
| 199 | +# 3. Add to the appropriate cases/*.py file. Mirror the spec's |
| 200 | +# Pre-Requisite as requires_services, mark mandatory/optional honestly. |
| 201 | + |
| 202 | +# 4. Verify against the reference camera: |
| 203 | +onvif-tt run --target 10.216.128.71:8899 --user admin --password admin \ |
| 204 | + --id-glob "EVENT-3-1-32" --json-report /tmp/r.json |
| 205 | + |
| 206 | +# 5. Tool's own unit tests still pass: |
| 207 | +pytest tests/ |
| 208 | +``` |
| 209 | + |
| 210 | +Add `xfail_on` only if you observe a real device behaviour you want |
| 211 | +the catalog to remember. Don't speculate. |
| 212 | + |
| 213 | +## When to ask before acting |
| 214 | + |
| 215 | +- **Anything that mutates the DUT** (writes, reboot, config change) — |
| 216 | + only with explicit `--allow-writes` user intent. |
| 217 | +- **Pushing branches / opening PRs** — CI runs on push; ask before |
| 218 | + triggering it on the live repo. |
| 219 | +- **Adding dependencies to `pyproject.toml`** — every dep widens the |
| 220 | + install surface; prefer stdlib or the existing zeep/lxml/pytest |
| 221 | + stack. |
| 222 | + |
| 223 | +## Pointers |
| 224 | + |
| 225 | +- Detailed AI tool-use shapes: `docs/ai-readme.md` |
| 226 | +- JSON Schemas: `docs/schemas/` |
| 227 | +- Contributor docs: `CONTRIBUTING.md`, `docs/adding-a-test.md` |
| 228 | +- Project home: <https://github.com/OpenIPC/onvif-tt> |
0 commit comments