Testing Helpers¶
chumicro_knobs.testing provides FakeEncoderSource and FakeAnalogSource, hand-driven stand-ins for the hardware a knob reads. Turn the shaft or park the wiper, tick the knob, and assert on what it made of them. The module declares __chumicro_test_support__ = True, so the deploy filter keeps it on the host and it never lands on a device.
This is the piece that makes knob behavior testable at a desk. A menu that wraps at the end, a volume figure that stops at 20, a brightness slider that has to hold still while a converter wanders underneath: all of that is ordinary logic, and with a fake source it runs on a laptop, in milliseconds, without a shaft to turn ten thousand times.
Usage¶
Build the knob with source= instead of pins, then script the movement:
from chumicro_knobs import Encoder
from chumicro_knobs.testing import FakeEncoderSource
def test_the_menu_ring_carries_past_the_last_entry():
source = FakeEncoderSource()
menu = Encoder(source=source, bounds=(0, 3), wrap=True)
source.turn(5)
menu.check(0)
assert menu.position == 1
turn() moves the shaft in whole detents, positive one way and negative the other, which is what the runtime sources publish once they have divided the pulses down. Nothing here reads a clock: a knob hands the timestamp it was given straight down to its source, so the test decides what time it is and stays instant and deterministic.
A source can also start with a count already on it, standing in for a knob built while the shaft was somewhere other than home:
The first check() treats that as the starting point and reports no movement, so a restart mid-session behaves the way a real board does.
Turns add up between ticks¶
Several turns queued before one check() arrive together, which is how a test covers the spin that happened while the loop was busy elsewhere:
That is the case worth writing a test for, because it is the one a fast desk loop never reproduces by itself and a device with a socket read in it hits every day.
An analog knob with no wiper¶
FakeAnalogSource carries a reading on the same 0 to 65535 scale every runtime reports on. set_raw() parks the wiper, and small moves are how a test proves the deadband is doing its job:
from chumicro_knobs import AnalogKnob
from chumicro_knobs.testing import FakeAnalogSource
def test_a_parked_wiper_holds_the_brightness_step():
source = FakeAnalogSource()
brightness = AnalogKnob(source=source)
source.set_raw(32768)
brightness.check(0)
assert brightness.value == 50
source.set_raw(33168) # 400 counts of wander
assert brightness.check(10) is False # inside the deadband, so nothing moved
assert brightness.value == 50
Give the knob a deadband= of its own to test a tighter or looser setting, and FakeAnalogSource(raw=...) to start it somewhere along the sweep.
What the fakes record¶
Both fakes keep a tally of what the knob asked of them, so a test can assert on the conversation as well as on the reading:
| Hook | What it holds |
|---|---|
source.poll_calls |
How many times the knob asked this source for a reading |
source.last_poll_ms |
The tick that last reading was asked for, which proves the knob passes the loop's shared timestamp down rather than fetching a clock of its own |
source.deinit_calls |
How many times the knob released the source |
The guide covers what the readings mean once they arrive, and the API reference lists every method on both fakes.