indikit.testing¶
Test a driver by driving it the way a client would - no indiserver, no
sockets, no hardware. See
Testing without hardware for
the guide, and tests/test_weather_example.py for a worked set.
DeviceHarness¶
indikit.testing ¶
DeviceHarness: drive a driver in a test without indiserver.
A driver's value is in what it emits - the def it announces, the set it
publishes from a poll, the message it logs when hardware refuses a command.
This module gives that a first-class seam::
async def test_shutter_opens():
harness = DeviceHarness(MyDome())
await harness.setup()
await harness.write("DOME_SHUTTER", SHUTTER_OPEN=True)
assert harness.latest("DOME_SHUTTER").state is IPState.BUSY
assert "Shutter" in harness.messages[-1]
No sockets, no subprocess, no XML. Writes go through the device's real dispatch
path - the @on_new map, the device-name guard, the serialisation lock - so a
handler that works here works under indiserver.
Failures are not isolated here, and that is the one place the harness departs
from the runtime on purpose. Under indiserver a raising handler or tick is
caught, reported to the client as an ERROR message and stepped over; in a
test that would turn a bug into a missing emission and an assertion failure ten
lines away from its cause. So :meth:DeviceHarness.write and
:meth:DeviceHarness.tick let the exception out with its traceback, and the
runtime's isolation is covered where it lives, in tests/test_driver.py.
For coverage of the wire itself (framing, chunk boundaries, the codec), drive a
:class:~indikit.driver.runtime.DriverRuntime over byte streams instead; see
tests/test_driver.py.
DeviceHarness ¶
DeviceHarness(device: Device, *, config_dir: Path | None = None)
A device plus every message it has emitted, driven like a client would.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device
|
Device
|
The device under test. It is bound to this harness on construction, so
|
required |
config_dir
|
Path or None
|
Where the device saves and loads its configuration, standing in for
what the runtime would inject. Pass pytest's |
None
|
Attach to device and start recording what it emits.
Source code in src/indikit/testing.py
117 118 119 120 121 | |
config_path
property
¶
config_path: Path
The file this device's configuration is saved to.
It need not exist: before a save there is nothing there, and asserting that is most of what a persistence test does.
Returns:
| Name | Type | Description |
|---|---|---|
path |
Path
|
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
Raised if the harness was built without a |
setup
async
¶
setup() -> None
Send a getProperties, running the device's :meth:Device.setup.
The same trigger indiserver provides at startup. Calling it again
re-announces the already-defined properties, as a late-joining client
would see.
Source code in src/indikit/testing.py
149 150 151 152 153 154 155 156 | |
write
async
¶
write(name: str, values: dict[str, Any] | None = None, **kwargs: Any) -> None
Send a client write to property name, as a real client would.
Only the named elements are sent - a partial write, which is what INDI
clients actually do - and switch values accept bool or the wire
strings as well as :class:~indikit.protocol.ISState.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The property to write to. It must already be defined. |
required |
values
|
dict
|
Element values keyed by name, for names that are not valid Python identifiers. |
None
|
**kwargs
|
object
|
Element values by name (the common case). |
{}
|
Raises:
| Type | Description |
|---|---|
KeyError
|
Raised if no property with that name has been defined. |
Source code in src/indikit/testing.py
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
tick
async
¶
tick(job: str) -> None
Run one iteration of an @every job, by method name.
Runs the job body directly under the device guard - the schedule is the runtime's business, and a test should not have to wait out an interval to see one tick. A job that raises raises here too, rather than being swallowed the way the runtime swallows it; see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
str
|
The name of the |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
Raised if the device has no |
Source code in src/indikit/testing.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
defs ¶
defs(name: str | None = None) -> list[Vector]
Return the vectors the device has defined.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Restrict to one property name; all of them when omitted. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
vectors |
list of Vector
|
The defined vectors, in emission order. |
Source code in src/indikit/testing.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
sets ¶
sets(name: str | None = None) -> list[Vector]
Return the value updates the device has published.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Restrict to one property name; all of them when omitted. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
vectors |
list of Vector
|
The published vectors, in emission order. |
Source code in src/indikit/testing.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
deletes ¶
deletes() -> list[DelProperty]
Return every delProperty the device has sent.
Returns:
| Name | Type | Description |
|---|---|---|
deletions |
list of DelProperty
|
The deletions, in emission order. |
Source code in src/indikit/testing.py
251 252 253 254 255 256 257 258 259 | |
latest ¶
latest(name: str) -> Vector
Return what a client would currently hold for one property.
The last def or set recorded for it, falling back to the
device's live vector when nothing has been emitted - after
:meth:clear, say, or for a property whose "on_change" policy has
had nothing to report. The two agree, because every change a client
would care about is exactly what gets published.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The property name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
vector |
Vector
|
The latest state of that property. |
Raises:
| Type | Description |
|---|---|
KeyError
|
Raised if no property with that name has been defined. |
Source code in src/indikit/testing.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
clear ¶
clear() -> None
Drop the recorded messages, keeping the device as it is.
Useful between the arrange and act halves of a test, so assertions run against what one action produced rather than the whole history.
Source code in src/indikit/testing.py
290 291 292 293 294 295 296 | |