blob: 0e5c89e56a11b3c5495d060df520a8f4886846fa [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import asyncio
import tempfile
import unittest
from pathlib import Path
from typing import Any
from driver_lab.api import DriverLab, DriverLabError
from driver_lab.consent import ConsentDecision
from driver_lab.models import AccessRequest, ResourceKind
from driver_lab.session import (
AccessRequirements,
Gpio,
HardwareSession,
I2c,
Interrupt,
MmioRegion,
SessionCapabilities,
Spi,
StateHandle,
TranslationMetadata,
UnsupportedCapabilityError,
)
from driver_lab.transport import (
Denial,
DirectDescription,
FakeDirectTarget,
FakeProxyTarget,
OpenRejection,
OpenSessionRejected,
OperationDenied,
ProxyDescription,
ResourceInfo,
SequenceItem,
SessionMode,
)
CTRL_DIGEST = "sha256:" + "11" * 32
POLICY_DIGEST = "sha256:" + "22" * 32
class ScriptedPrompt:
def __init__(self, decision: ConsentDecision) -> None:
self.decision = decision
async def request_consent(
self, request: AccessRequest, warning: str
) -> ConsentDecision:
return self.decision
def make_proxy_description() -> ProxyDescription:
return ProxyDescription(
protocol_major=1,
protocol_minor=0,
proxy_generation=1,
boot_id="boot-test-1",
resource_digest="sha256:" + "33" * 32,
policy_digest=POLICY_DIGEST,
resources=(
ResourceInfo(
id=1,
name="control",
logical_size=0x100,
digest=CTRL_DIGEST,
),
ResourceInfo(
id=2,
name="status",
logical_size=0x80,
digest="sha256:" + "44" * 32,
),
),
max_snapshot_items=64,
audit_capacity=256,
node_moniker="sample/hardware/node",
)
def make_extended_proxy_description() -> ProxyDescription:
return ProxyDescription(
protocol_major=1,
protocol_minor=0,
proxy_generation=1,
boot_id="boot-test-1",
resource_digest="sha256:" + "33" * 32,
policy_digest=POLICY_DIGEST,
resources=(
ResourceInfo(
id=1,
name="control",
logical_size=0x100,
digest=CTRL_DIGEST,
),
ResourceInfo(
id=2,
name="status",
logical_size=0x80,
digest="sha256:" + "44" * 32,
),
ResourceInfo(
id=3,
name="pin0",
kind=ResourceKind.GPIO,
logical_size=1,
digest="sha256:" + "55" * 32,
),
ResourceInfo(
id=4,
name="i2c-bus",
kind=ResourceKind.I2C,
logical_size=32,
digest="sha256:" + "66" * 32,
),
ResourceInfo(
id=5,
name="spi-bus",
kind=ResourceKind.SPI,
logical_size=32,
digest="sha256:" + "77" * 32,
),
ResourceInfo(
id=6,
name="irq0",
kind=ResourceKind.INTERRUPT,
logical_size=0,
digest="sha256:" + "88" * 32,
),
ResourceInfo(
id=7,
name="state0",
kind=ResourceKind.MMIO,
logical_size=0x20,
digest="sha256:" + "99" * 32,
),
),
max_snapshot_items=64,
audit_capacity=256,
node_moniker="sample/hardware/node",
)
class SessionTest(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self._dir = tempfile.TemporaryDirectory()
self.addCleanup(self._dir.cleanup)
base = Path(self._dir.name)
self.grants_path = base / "grants.toml"
self.evidence_root = base / "evidence"
self.fake_proxy = FakeProxyTarget(make_proxy_description())
self.fake_direct = FakeDirectTarget(
node_id="sample-node",
protocol_name="fuchsia.hardware.gpio/Device",
bound_driver_url="fuchsia-boot:///sample#meta/sample.cm",
)
async def test_proxy_mmio_read_and_write(self) -> None:
self.fake_proxy.set_value(1, 0x10, 0xCAFE_BABE)
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
prompt = ScriptedPrompt(ConsentDecision.ALLOW_ONCE)
async with await lab.attach(
"node-1",
mode="proxy",
session_mode=SessionMode.MUTATING,
consent=prompt,
) as session:
self.assertEqual(session.mode, "proxy")
self.assertTrue(session.capabilities.target_policy)
self.assertTrue(session.capabilities.target_audit)
self.assertTrue(session.capabilities.target_local_timing)
self.assertFalse(session.capabilities.production_driver_active)
mmio = await session.mmio("control")
self.assertEqual(mmio.name, "control")
self.assertEqual(mmio.id, 1)
self.assertEqual(mmio.logical_size, 0x100)
# Read32
val = await mmio.read32(0x10)
self.assertEqual(val, 0xCAFE_BABE)
# Write32 with readback
write_res = await mmio.write32(0x14, 0x1234_5678)
self.assertEqual(write_res.readback_value, 0x1234_5678)
self.assertEqual(self.fake_proxy.get_value(1, 0x14), 0x1234_5678)
# Write32 with precondition
write_res2 = await mmio.write32(
0x14, 0x8765_4321, expected_before=0x1234_5678
)
self.assertEqual(write_res2.readback_value, 0x8765_4321)
self.assertEqual(self.fake_proxy.get_value(1, 0x14), 0x8765_4321)
# Precondition failure
with self.assertRaises(OperationDenied) as ctx:
await mmio.write32(
0x14, 0xAAAA_BBBB, expected_before=0xDEAD_BEEF
)
self.assertEqual(ctx.exception.denial, Denial.PRECONDITION_FAILED)
async def test_proxy_mmio_poll_and_snapshot(self) -> None:
self.fake_proxy.set_value(1, 0x20, 0x0)
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="proxy") as session:
mmio = await session.mmio("control")
# Poll matching value
self.fake_proxy.set_value(1, 0x20, 0x1)
poll_res = await mmio.poll32(0x20, expected=0x1, mask=0x1)
self.assertEqual(poll_res.value, 0x1)
# Poll timeout
self.fake_proxy.set_value(1, 0x24, 0x0)
with self.assertRaises(OperationDenied) as ctx:
await mmio.poll32(0x24, expected=0x1, mask=0x1, timeout_s=0.001)
self.assertEqual(ctx.exception.denial, Denial.TIMEOUT)
# Snapshot32
self.fake_proxy.set_value(1, 0x0, 0x1111)
self.fake_proxy.set_value(1, 0x4, 0x2222)
values = await mmio.snapshot32([0x0, 0x4])
self.assertEqual(values, [0x1111, 0x2222])
async def test_mmio_bounds_and_alignment_validation(self) -> None:
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="proxy") as session:
mmio = await session.mmio("control")
with self.assertRaises(ValueError):
await mmio.read32(-4)
with self.assertRaises(ValueError):
await mmio.read32(
0x100
) # Out of bounds (logical_size is 0x100)
with self.assertRaises(ValueError):
await mmio.read32(0x2) # Misaligned
with self.assertRaises(ValueError):
await mmio.write32(0x100, 0x1)
with self.assertRaises(ValueError):
await mmio.poll32(0x1, expected=0)
with self.assertRaises(ValueError):
await session.mmio("nonexistent_resource")
async def test_translation_metadata_present(self) -> None:
self.assertIsInstance(MmioRegion.read32_metadata, TranslationMetadata)
self.assertTrue(MmioRegion.read32_metadata.directly_translatable)
self.assertIn("Read32", MmioRegion.read32_metadata.cpp_analogue)
self.assertIsInstance(MmioRegion.write32_metadata, TranslationMetadata)
self.assertTrue(MmioRegion.write32_metadata.directly_translatable)
self.assertIsInstance(MmioRegion.poll32_metadata, TranslationMetadata)
self.assertTrue(MmioRegion.poll32_metadata.target_local_timing)
self.assertIsInstance(
MmioRegion.snapshot32_metadata, TranslationMetadata
)
self.assertTrue(MmioRegion.snapshot32_metadata.experiment_only)
# Protocol adapter translation metadata
self.assertIsInstance(Gpio.read_metadata, TranslationMetadata)
self.assertTrue(Gpio.read_metadata.directly_translatable)
self.assertIsInstance(Gpio.write_metadata, TranslationMetadata)
self.assertTrue(Gpio.write_metadata.directly_translatable)
self.assertIsInstance(I2c.transfer_metadata, TranslationMetadata)
self.assertTrue(I2c.transfer_metadata.directly_translatable)
self.assertIsInstance(Spi.transmit_metadata, TranslationMetadata)
self.assertTrue(Spi.transmit_metadata.directly_translatable)
self.assertIsInstance(Interrupt.wait_metadata, TranslationMetadata)
self.assertTrue(Interrupt.wait_metadata.directly_translatable)
async def test_direct_mode_capabilities_and_protocols(self) -> None:
# Register handlers on fake direct target
gpio_state = {"value": True, "direction": "output"}
def handle_read(args: Any) -> dict[str, Any]:
return {"value": gpio_state["value"]}
def handle_write(args: Any) -> dict[str, Any]:
gpio_state["value"] = args.get("value", False)
return {}
self.fake_direct.register_method("read", handle_read)
self.fake_direct.register_method("write", handle_write)
lab = DriverLab(
self.fake_direct,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="direct") as session:
self.assertEqual(session.mode, "direct")
self.assertFalse(session.capabilities.target_policy)
self.assertFalse(session.capabilities.target_audit)
self.assertFalse(session.capabilities.target_local_timing)
self.assertTrue(session.capabilities.production_driver_active)
# Direct published protocol
proto = await session.protocol("fuchsia.hardware.gpio/Device")
res = await proto.read(resource="gpio")
self.assertTrue(res.response["value"])
# Gpio adapter
gpio = await session.gpio("gpio")
self.assertTrue(await gpio.read())
await gpio.write(False)
self.assertFalse(await gpio.read())
# Attempting MMIO or target sequence in direct mode fails closed
with self.assertRaises(UnsupportedCapabilityError):
await session.mmio("control")
with self.assertRaises(UnsupportedCapabilityError):
await session.sequence([])
async def test_proxy_mode_rejects_direct_protocols(self) -> None:
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="proxy") as session:
with self.assertRaises(UnsupportedCapabilityError):
await session.protocol("fuchsia.hardware.gpio/Device")
with self.assertRaises(UnsupportedCapabilityError):
await session.serial("serial")
with self.assertRaises(UnsupportedCapabilityError):
await session.clock("clock")
with self.assertRaises(UnsupportedCapabilityError):
await session.reset("reset")
# Gpio request on proxy that has no GPIO resource raises ValueError
with self.assertRaises(ValueError):
await session.gpio("gpio")
# Interrupt request on proxy that has no interrupt resource raises ValueError
with self.assertRaises(ValueError):
await session.interrupt("interrupt")
async def test_proxy_mode_protocol_adapters(self) -> None:
fake = FakeProxyTarget(make_extended_proxy_description())
fake.set_i2c_response(4, b"\xde\xad")
fake.set_spi_response(5, b"\xbe\xef")
lab = DriverLab(
fake,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
prompt = ScriptedPrompt(ConsentDecision.ALLOW_ONCE)
async with await lab.attach(
"node-1",
mode="proxy",
session_mode=SessionMode.MUTATING,
consent=prompt,
) as session:
# GPIO adapter in proxy mode
gpio = await session.gpio("pin0")
self.assertEqual(gpio.name, "pin0")
self.assertEqual(gpio.id, 3)
await gpio.write(True)
self.assertTrue(await gpio.read())
self.assertTrue(fake.get_gpio(3))
await gpio.write(False)
self.assertFalse(await gpio.read())
self.assertFalse(fake.get_gpio(3))
# I2C adapter in proxy mode
i2c = await session.i2c("i2c-bus")
self.assertEqual(i2c.name, "i2c-bus")
self.assertEqual(i2c.id, 4)
rx_i2c = await i2c.transfer(b"\x01\x02", read_length=2)
self.assertEqual(rx_i2c, b"\xde\xad")
# SPI adapter in proxy mode
spi = await session.spi("spi-bus")
self.assertEqual(spi.name, "spi-bus")
self.assertEqual(spi.id, 5)
rx_spi = await spi.transmit(b"\x03\x04")
self.assertEqual(rx_spi, b"\xbe\xef")
# Interrupt adapter in proxy mode
irq = await session.interrupt("irq0")
self.assertEqual(irq.name, "irq0")
self.assertEqual(irq.id, 6)
# Immediate wait when after_sequence < sequence
fake.trigger_interrupt(6, timestamp_ns=500_000)
irq_res = await irq.wait(after_sequence=0)
self.assertEqual(irq_res.resource, 6)
self.assertEqual(irq_res.sequence, 1)
self.assertEqual(irq_res.count, 1)
self.assertEqual(irq_res.timestamp_ns, 500_000)
self.assertEqual(irq_res.coalesced_count, 1)
# Asynchronous wait satisfied by trigger_interrupt
wait_task = asyncio.create_task(
irq.wait(after_sequence=1, timeout_s=1.0)
)
await asyncio.sleep(0)
fake.trigger_interrupt(6, timestamp_ns=600_000)
irq_res2 = await wait_task
self.assertEqual(irq_res2.resource, 6)
self.assertEqual(irq_res2.sequence, 2)
self.assertEqual(irq_res2.count, 2)
self.assertEqual(irq_res2.timestamp_ns, 600_000)
self.assertEqual(irq_res2.coalesced_count, 0)
async def test_heterogeneous_sequence_proxy_mode(self) -> None:
fake = FakeProxyTarget(make_extended_proxy_description())
fake.set_value(1, 0x10, 0x1111_2222)
fake.set_i2c_response(4, b"\xca\xfe")
fake.set_spi_response(5, b"\xba\xbe")
lab = DriverLab(
fake,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
prompt = ScriptedPrompt(ConsentDecision.ALLOW_ONCE)
async with await lab.attach(
"node-1",
mode="proxy",
session_mode=SessionMode.MUTATING,
consent=prompt,
) as session:
seq_items = [
SequenceItem(kind="read32", resource=1, offset=0x10),
SequenceItem(
kind="write32",
resource=1,
offset=0x14,
value=0x9999,
readback=True,
),
SequenceItem(kind="delay_ns", delay_ns=1000),
SequenceItem(kind="barrier", barrier="memory"),
SequenceItem(kind="gpio_write", resource=3, value=True),
SequenceItem(kind="gpio_read", resource=3),
SequenceItem(
kind="i2c_transfer",
resource=4,
write_data=b"\x01",
read_length=2,
),
SequenceItem(kind="spi_transmit", resource=5, tx_data=b"\x02"),
]
outcome = await session.sequence(seq_items)
self.assertTrue(outcome.complete)
self.assertEqual(len(outcome.results), 8)
self.assertEqual(outcome.results[0].kind, "read32")
self.assertEqual(outcome.results[0].value, 0x1111_2222)
self.assertEqual(outcome.results[1].kind, "write32")
self.assertEqual(outcome.results[1].readback_value, 0x9999)
self.assertEqual(outcome.results[2].kind, "delay_ns")
self.assertEqual(outcome.results[3].kind, "barrier")
self.assertEqual(outcome.results[4].kind, "gpio_write")
self.assertEqual(outcome.results[5].kind, "gpio_read")
self.assertEqual(outcome.results[5].value, 1)
self.assertEqual(outcome.results[6].kind, "i2c_transfer")
self.assertEqual(outcome.results[6].data, b"\xca\xfe")
self.assertEqual(outcome.results[7].kind, "spi_transmit")
self.assertEqual(outcome.results[7].data, b"\xba\xbe")
async def test_selection_rules_in_attach(self) -> None:
lab_proxy = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
# auto with needs_mmio selects proxy
session = await lab_proxy.attach(
"node-1",
mode="auto",
requirements=AccessRequirements(needs_mmio=True),
)
self.assertEqual(session.mode, "proxy")
await session.close()
# direct mode with needs_mmio raises DriverLabError
with self.assertRaises(DriverLabError):
await lab_proxy.attach(
"node-1",
mode="direct",
requirements=AccessRequirements(needs_mmio=True),
)
# Invalid mode string
with self.assertRaises(ValueError):
await lab_proxy.attach("node-1", mode="invalid") # type: ignore[arg-type]
async def test_mutating_session_consent_enforcement(self) -> None:
# Consent denied
prompt_denied = ScriptedPrompt(ConsentDecision.DENY_ONCE)
lab_denied = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
consent=prompt_denied,
)
with self.assertRaises(DriverLabError):
await lab_denied.attach(
"node-1", mode="proxy", session_mode=SessionMode.MUTATING
)
# No consent prompt configured for mutating session
lab_unattended = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
consent=None,
)
with self.assertRaises(DriverLabError):
await lab_unattended.attach(
"node-1", mode="proxy", session_mode=SessionMode.MUTATING
)
async def test_async_cancellation_cleans_up_session(self) -> None:
prompt = ScriptedPrompt(ConsentDecision.ALLOW_ONCE)
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
consent=prompt,
)
session = await lab.attach(
"node-1", mode="proxy", session_mode=SessionMode.MUTATING
)
self.assertEqual(self.fake_proxy.active_mutating_session, 1)
async def worker() -> None:
async with session:
mmio = await session.mmio("control")
await mmio.read32(0x10)
await asyncio.sleep(10.0)
task = asyncio.create_task(worker())
await asyncio.sleep(0.01)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task
self.assertTrue(session.is_closed)
self.assertIsNone(self.fake_proxy.active_mutating_session)
# Calling on closed session fails
with self.assertRaises(UnsupportedCapabilityError):
await session.mmio("control")
async def test_representative_protocols_direct_mode(self) -> None:
target = FakeDirectTarget(node_id="dev", protocol_name="multi")
target.register_method("transfer", lambda a: {"read_data": [1, 2, 3]})
target.register_method("transmit", lambda a: {"rx_data": [4, 5, 6]})
target.register_method("write", lambda a: {"bytes_written": 2})
target.register_method("read", lambda a: {"data": [7, 8]})
target.register_method("enable", lambda a: {})
target.register_method("disable", lambda a: {})
target.register_method("assert_reset", lambda a: {})
target.register_method("deassert_reset", lambda a: {})
target.register_method("wait", lambda a: {"timestamp_ns": 12345})
caps = SessionCapabilities(
mode="direct",
target_policy=False,
target_audit=False,
target_local_timing=False,
fault_isolation="driver_host",
production_driver_active=True,
)
desc = DirectDescription(
node_id="dev", protocol_name="multi", boot_id="b1"
)
fake_session = await target.open_direct_session(None) # type: ignore[arg-type]
session = HardwareSession(
capabilities=caps,
direct_session=fake_session,
direct_description=desc,
)
i2c = await session.i2c("i2c")
rx_i2c = await i2c.transfer(b"abc", read_length=3)
self.assertEqual(rx_i2c, bytes([1, 2, 3]))
spi = await session.spi("spi")
rx_spi = await spi.transmit(b"xyz")
self.assertEqual(rx_spi, bytes([4, 5, 6]))
serial = await session.serial("serial")
written = await serial.write(b"hi")
self.assertEqual(written, 2)
rx_serial = await serial.read(2)
self.assertEqual(rx_serial, bytes([7, 8]))
clock = await session.clock("clock")
await clock.enable()
await clock.disable()
reset = await session.reset("reset")
await reset.assert_reset()
await reset.deassert_reset()
irq = await session.interrupt("irq")
ts = await irq.wait(1.0)
self.assertEqual(ts, 12345)
await session.close()
async def test_disabled_target_rejected(self) -> None:
self.fake_proxy.enabled = False
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
with self.assertRaises(OpenSessionRejected) as cm:
await lab.attach("node-1", mode="proxy")
self.assertEqual(cm.exception.reason, OpenRejection.NOT_ACCEPTING)
async def test_rate_limit_exceeded(self) -> None:
self.fake_proxy.max_ops_per_second = 2
self.fake_proxy.set_value(1, 0x10, 42)
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="proxy") as session:
mmio = await session.mmio("control")
val1 = await mmio.read32(0x10)
self.assertEqual(val1, 42)
val2 = await mmio.read32(0x10)
self.assertEqual(val2, 42)
with self.assertRaises(OperationDenied) as cm:
await mmio.read32(0x10)
self.assertEqual(cm.exception.denial, Denial.LIMIT_EXCEEDED)
async def test_deadline_exceeded(self) -> None:
self.fake_proxy.max_deadline_ns = 50_000_000
self.fake_proxy.set_value(1, 0x10, 42)
lab = DriverLab(
self.fake_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
)
async with await lab.attach("node-1", mode="proxy") as session:
mmio = await session.mmio("control")
with self.assertRaises(OperationDenied) as cm:
await mmio.poll32(0x10, expected=42, timeout_s=0.1)
self.assertEqual(cm.exception.denial, Denial.LIMIT_EXCEEDED)
async def test_proxy_state_handle_read_knob_trigger_poll_snapshot(
self,
) -> None:
ext_proxy = FakeProxyTarget(make_extended_proxy_description())
ext_proxy.set_value(7, 0x00, 0) # STATE_BUSY_FLAG
ext_proxy.set_value(7, 0x04, 0) # STATE_INVARIANT_VIOLATIONS
ext_proxy.set_value(7, 0x08, 0) # KNOB_RACE_DELAY_US
ext_proxy.set_value(7, 0x0C, 0) # KNOB_FIX_ENABLED
ext_proxy.set_value(7, 0x10, 0) # TRIGGER_CONCURRENT_OP
def on_trigger(arg: int) -> None:
delay_us = ext_proxy.get_value(7, 0x08)
fix_enabled = ext_proxy.get_value(7, 0x0C)
if delay_us > 0 and fix_enabled == 0:
violations = ext_proxy.get_value(7, 0x04)
ext_proxy.set_value(7, 0x04, violations + max(arg, 1))
# Trigger slot always reads as 0.
ext_proxy.set_value(7, 0x10, 0)
ext_proxy.set_write_hook(7, 0x10, on_trigger)
prompt = ScriptedPrompt(ConsentDecision.ALLOW_ONCE)
lab = DriverLab(
ext_proxy,
grants_path=self.grants_path,
evidence_root=self.evidence_root,
target_scope="target-1",
node_id="node-1",
consent=prompt,
)
async with await lab.attach(
"node-1",
mode="proxy",
session_mode=SessionMode.MUTATING,
) as session:
state = await session.state("state0")
self.assertIsInstance(state, StateHandle)
self.assertEqual(state.name, "state0")
self.assertEqual(state.id, 7)
# 1. Repro bug: widen race window via knob and fire trigger.
knob_res = await state.write_knob(0x08, 5000)
self.assertEqual(knob_res.readback_value, 5000)
trig_res = await state.trigger(0x10, arg=1)
self.assertIsNone(trig_res.readback_value)
self.assertEqual(await state.read32(0x04), 1)
# 2. Prove fix in-situ: enable fix knob, fire trigger, verify counter stays 1.
await state.write_knob(0x0C, 1)
await state.trigger(0x10, arg=2)
poll_res = await state.poll32(0x04, expected=1)
self.assertEqual(poll_res.value, 1)
snap = await state.snapshot32([0x04, 0x08, 0x0C])
self.assertEqual(snap, [1, 5000, 1])
if __name__ == "__main__":
unittest.main()