Files
Tasmota/lib/libesp32/berry_matter/tests/button_stability_tests.be
T
SoWizardandClaude Opus 5.5 185e41c274 Matter: fix stale host test fixtures (#25161)
run_tests.be and button_stability_tests.be stop with assert_failed right
after "Matter epoch conversion for UTC and local time: OK" on development.
Three test-only mistakes, each hiding the next:

- run_tests.be checked the RSSI item's stored TLV type before encoding.
  "Clamp RSSI" (#25126) stores signed values as I4; the encoded bytes are
  still the 1-byte 00C3, which the next assert checks. Check the value only.
- TestGarageDevice (run_tests.be) lacks disable_bridge_mode, which
  Matter_Plugin_Device.init() reads since #25099.
- StabilityDevice (button_stability_tests.be) lacks it too.

Tests only; no firmware files change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:32:35 +02:00

165 lines
6.3 KiB
Plaintext

#!/usr/bin/env -S ../../berry/berry -g
# Off-device soak test for the four virtual Matter buttons and real event queues.
# Run from lib/libesp32/berry_matter/tests:
# ../../berry/berry -g button_stability_tests.be
# Reuse the existing source loader and regressions, avoiding a second bootstrap.
# No network or filesystem persistence occurs in the soak workload.
var regression_source = open("run_tests.be")
var regression_code = regression_source.read()
regression_source.close()
compile(regression_code)()
regression_code = nil
import gc
import matter
# Keep the soak's working objects independent of the main script's registers.
def test_button_stability()
class MemoryEventHandler : matter.EventHandler
var persistence_blocks
# The sole EventHandler override replaces its filesystem persistence boundary.
# Start just below the uint32 boundary to check event numbers cross it intact.
def load_event_no_persisted()
if self.counter_event_no == nil
self.counter_event_no = int64.fromstring("4294967276")
self.persistence_blocks = 0
end
self.counter_event_no_persisted = self.counter_event_no.add(self.EVENT_NO_INCR)
self.persistence_blocks += 1
end
end
class TestSubscriptionTransport
var delivered, previous_number, encoded_bytes
def init()
self.delivered = 0
self.encoded_bytes = 0
end
# Real EventHandler delivery stops at the subscription/transport boundary.
# Serialize and decode every event through the production TLV implementation.
def event_published(event)
if self.previous_number != nil
assert(event.event_no > self.previous_number, "event number did not increase")
end
self.previous_number = event.event_no
var raw = event.to_raw_bytes()
assert(event.to_raw_bytes() == raw, "cached serialization changed")
var tlv = matter.TLV.parse(raw)
var decoded = matter.EventReportIB().from_TLV(tlv).event_data
assert(decoded.event_number == event.event_no, "event number changed in TLV")
assert(decoded.path.endpoint == event.endpoint)
assert(decoded.path.cluster == 0x003B)
assert(decoded.path.event == event.event_id)
assert(decoded.path.is_urgent && decoded.priority == 1)
assert(decoded.epoch_timestamp == event.epoch_timestamp)
var fields = tlv.findsub(1).findsub(7)
assert(fields.findsubval(0) == 1)
assert(fields.findsubval(1) == (event.data1 != nil ? event.data1.val : nil))
assert(fields.findsubval(2) == nil)
self.delivered += 1
self.encoded_bytes += size(raw)
# Model report completion, including cache regeneration for a retransmit.
event.compact()
assert(event.raw_tlv == nil)
assert(event.to_raw_bytes() == raw, "retransmission serialization changed")
end
end
class TestInteractionModel
var subs_shop
def init() self.subs_shop = TestSubscriptionTransport() end
end
class TestMessageHandler
var im
def init() self.im = TestInteractionModel() end
end
class StabilityDevice
var events, message_handler, updates, disable_bridge_mode
def init()
self.updates = 0
self.disable_bridge_mode = false
self.message_handler = TestMessageHandler()
self.events = MemoryEventHandler(self)
end
def attribute_updated(endpoint, cluster, attribute, fabric_specific)
assert(endpoint >= 34 && endpoint <= 37)
assert(cluster == 0x003B && attribute == 1)
self.updates += 1
end
end
def assert_queues(handler)
for queue : [handler.queue_debug, handler.queue_info, handler.queue_critical]
assert(size(queue) <= handler.EVENT_QUEUE_SIZE_MAX, "unbounded event queue")
var previous = nil
for event : queue
if previous != nil assert(event.event_no > previous) end
previous = event.event_no
end
end
end
var device = StabilityDevice()
var buttons = []
for endpoint : 34..37
buttons.push(matter.Plugin_Virt_Sensor_GenericSwitch_Btn(device, endpoint, {}))
end
var transport = device.message_handler.im.subs_shop
var completed = 0
def run_batch()
var delivered_before = transport.delivered
var updates_before = device.updates
# The 20-gesture pattern covers all 4 endpoints with each of 1..5 presses.
for gesture : 0..999
var button = buttons[gesture % 4]
var presses = (gesture % 5) + 1
var previous = transport.delivered
assert(button.update_virtual({"Presses": presses}) == nil)
assert(transport.delivered - previous == presses * 3)
for other : buttons assert(other.shadow_position == 0, "button remained pressed") end
assert_queues(device.events)
if gesture % 20 == 0 device.events.every_second() end
end
assert(transport.delivered - delivered_before == 9000)
assert(device.updates - updates_before == 6000)
assert(size(device.events.queue_debug) == 10)
assert(size(device.events.queue_info) == 10)
assert(size(device.events.queue_critical) == 0)
device.events.compact()
completed += 1000
end
# Warm all paths and fill the bounded queues before measuring retained memory.
run_batch()
var samples = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
gc.collect()
gc.collect()
samples[0] = gc.allocated()
for batch : 1..12
run_batch()
gc.collect()
gc.collect()
samples[batch] = gc.allocated()
end
var smallest = samples[0]
var largest = samples[0]
for sample : samples
if sample < smallest smallest = sample end
if sample > largest largest = sample end
end
print("Matter button soak gestures:", completed, "events:", transport.delivered,
"serialized bytes:", transport.encoded_bytes)
print("Retained bytes after equal 1000-gesture batches:", samples)
print("Retained memory range:", largest - smallest,
"persistence block reservations:", device.events.persistence_blocks)
assert(completed == 13000 && transport.delivered == 117000)
assert(device.events.counter_event_no.high32() == 1, "event number rollover was not exercised")
assert(device.events.persistence_blocks > 100, "counter reservation boundary was not exercised")
# Permit a small fixed interpreter bookkeeping fluctuation, never per-event growth.
assert(largest - smallest <= 1024, "event processing leaks retained memory after GC")
print("Matter virtual-button stability tests: OK")
end
test_button_stability()