Testing on the Instrument
Testing a Device shows that a module is consistent; FieldTestPipeline,
in plesty.lib.test.field_test, shows that it works on the real instrument. A module
can pass every test against a simulated instrument and still fail on the real one:
only the real one checks that the commands are what the instrument expects, and only
a real connection can hang, or leave an old reply behind for the next question.
Each check records whether it passed and why, and every call to the instrument is timed and classified. The report therefore also has a timing profile (fastest, median, 95th percentile and slowest time per command) and failure statistics (attempts and failures per command), so timeouts can be set from measurements instead of guesses.
This test talks to hardware, so it is not run in CI. A device module keeps it in
tests/field_test.py and runs it next to the instrument:
# tests/field_test.py
from plesty.lib.test.field_test import FieldTestPipeline
from plesty.mydevice import Device
PIPELINE = FieldTestPipeline(
Device,
"USB0::0x1313::0x8078::P0000001::INSTR",
repetitions=20,
safe_ops=["measure_power"],
report_path="reports/field-test-mydevice",
)
if __name__ == "__main__":
report = PIPELINE.run()
raise SystemExit(0 if report.ok else 1)
plesty init field-test, a command of plesty-sdk, generates this file after asking
about the instrument.
What it checks
In this order:
Test |
Checks |
|---|---|
|
the address leads to a connected instrument |
|
connect, identity, |
|
every writable parameter can be read, written back and read again; the instrument is left as it was |
|
the instrument’s own limits and options against the declared ones; declared limits wider than the instrument’s fail |
|
every operation allowed to run (see below) can be called, in the way its kind says |
|
a question abandoned halfway does not leave its answer for the next question |
|
the device recovers from a forced timeout, and the first command after reconnecting works |
|
every readable parameter and allowed operation, repeated |
By default a failed check is recorded and the rest still run, so one failure does not
cost the timings of the others. With stop_on_failure=True the run stops at the first
failure, and the checks not run are recorded as skipped: useful when a failure leaves
the instrument in a state where the rest would measure nothing. run(gates=[...])
runs only some checks; reset() clears the results, to run again.
Which operations may run
An operation is called only if it is listed in safe_ops, or declared with
safe=True (a "safe": true entry in the module’s operation file counts). Without
any, the operations check is skipped: nothing that moves or emits ever runs
unattended.
How an allowed operation is called depends on its kind, which the module
declares with @expose_to_api(kind=...) or a "kind" entry in its operation file:
Kind |
How the tests call it |
|---|---|
|
with its declared arguments, as often as needed |
|
the same, but recorded as acting on the instrument |
|
moved a |
|
once; never repeated |
|
once, after the motions |
|
never: the parameter checks cover settings |
@expose_to_api(kind="motion", position_key="MO.Position", step=1.0)
def move_absolute(self, position: float) -> float: ...
op_kinds={"move_absolute": OpKind("motion", position_key="MO.Position", step=1.0)}
on the pipeline replaces what the module declares. A motion without a position key or
a step fails the check rather than guessing how far a part may move.
Arguments
Argument |
Default |
Meaning |
|---|---|---|
arguments after the class |
passed to its constructor, typically the address |
|
|
|
how often each command is repeated in |
|
|
where to write the report: |
|
|
operations allowed to run, and operations never to run |
|
|
the kind of each operation, replacing what the module declares |
|
|
the arguments to call an operation with, instead of generated ones |
|
|
parameters left out of every check |
|
|
the only parameters to check; easier than |
|
chosen automatically |
the two parameters |
|
|
an operation slow enough to be abandoned halfway, for |
|
|
the timeout used to abandon a question on purpose |
|
|
pause between repetitions in |
|
|
relative difference allowed when a number is read back |
|
|
the share of failures per command |
|
|
attempts per call in total; |
|
|
set it when connecting starts an application and takes long: the checks that need a connection of their own are skipped, saying why, and the rest share one connection |
|
|
stop at the first failed check |
|
|
makes generated arguments repeatable |
Reports
The report at report_path includes the computer’s name and the instrument’s
address, so keep it within the lab. For a report that can be shared, give
instrument=Instrument(model="PM100D", sensor="S155C") (from
plesty.lib.test.report_artifact) and reports_dir="reports":
the run then also writes reports/<sensor or model>/field-test.json, without the
address or serial numbers, which a module can publish. archive_dir names a shared
folder, such as a lab network drive, that receives a copy of every full report.
propose_schema_update() compares the module’s parameter file with what the
instrument reported, and returns a proposed change. Nothing is written until the
proposal’s apply() is called, so every change is reviewed.