mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
[light] Address Copilot review feedback on PR #15726
- mock_addressable_light.h: add direct <memory>/<cstdint>/<cstddef> includes - test: use asyncio.get_running_loop() instead of deprecated get_event_loop() - test: rebase timing to command-issue time (not first-nonzero) and use absolute progress for assertion 2, so late-transition check can't skew when the first nonzero sample happens to land near the assertion-1 limit
This commit is contained in:
+4
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
|
||||
@@ -40,18 +40,20 @@ async def test_addressable_light_transition(
|
||||
|
||||
# Track the raw-byte sensor. It polls every 10ms in the fixture, and
|
||||
# ESPHome sensors publish on every change, so we collect a time series.
|
||||
loop = asyncio.get_event_loop()
|
||||
# Samples are stored as (seconds_since_command_issue, value). Times
|
||||
# before the command was issued are negative.
|
||||
loop = asyncio.get_running_loop()
|
||||
samples: list[tuple[float, float]] = []
|
||||
start_time: float | None = None
|
||||
command_time: float | None = None
|
||||
|
||||
def on_state(state: object) -> None:
|
||||
nonlocal start_time
|
||||
if not isinstance(state, SensorState) or state.key != sensor.key:
|
||||
return
|
||||
now = loop.time()
|
||||
if start_time is None:
|
||||
start_time = now
|
||||
samples.append((now - start_time, state.state))
|
||||
# If the command hasn't been issued yet, use 0 as the origin;
|
||||
# those samples get negative times and are excluded below.
|
||||
origin = command_time if command_time is not None else now
|
||||
samples.append((now - origin, state.state))
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
@@ -61,6 +63,7 @@ async def test_addressable_light_transition(
|
||||
# Start transition: off -> full white over 1 second. This is the
|
||||
# scenario from the bug report, compressed in time.
|
||||
transition_s = 1.0
|
||||
command_time = loop.time()
|
||||
client.light_command(
|
||||
key=light.key,
|
||||
state=True,
|
||||
@@ -72,48 +75,38 @@ async def test_addressable_light_transition(
|
||||
# Let the full transition run, plus margin for the final sample.
|
||||
await asyncio.sleep(transition_s + 0.2)
|
||||
|
||||
# Partition samples by transition progress. We reset the time origin
|
||||
# at the moment the first post-command sample arrives, since there is
|
||||
# some latency between issuing the command and the sensor observing
|
||||
# the transition begin.
|
||||
assert samples, "no sensor samples received"
|
||||
|
||||
# Find first sample where the transition started producing nonzero
|
||||
# output (or fall back to the first sample).
|
||||
first_nonzero_idx = next((i for i, (_, v) in enumerate(samples) if v > 0), None)
|
||||
assert first_nonzero_idx is not None, (
|
||||
"raw byte never rose above 0 during the transition — the fade stalled"
|
||||
)
|
||||
|
||||
t0 = samples[first_nonzero_idx][0]
|
||||
# Collect samples from the first nonzero point onward, re-based to t=0.
|
||||
rel = [(t - t0, v) for (t, v) in samples[first_nonzero_idx:]]
|
||||
# Only look at samples that arrived after the command was issued.
|
||||
post_command = [(t, v) for (t, v) in samples if t >= 0]
|
||||
assert post_command, "no sensor samples received after command was issued"
|
||||
|
||||
# Assertion 1: the transition is not stalled. With the bug, the raw
|
||||
# byte stays at 0 until ~90% of the transition duration. With the fix,
|
||||
# it becomes nonzero in the first ~30% (for gamma 2.8, pre-gamma 76
|
||||
# clears the gamma threshold at progress ~0.30). We assert that the
|
||||
# first nonzero sample arrives well before 70% of the transition,
|
||||
# giving generous slack for scheduling jitter.
|
||||
first_nonzero_time = samples[first_nonzero_idx][0] - samples[0][0]
|
||||
assert first_nonzero_time < transition_s * 0.7, (
|
||||
f"raw byte only rose above 0 at t={first_nonzero_time:.3f}s "
|
||||
f"(>{transition_s * 0.7:.3f}s) — transition is stalling"
|
||||
# clears the gamma threshold at progress ~0.30). Require the first
|
||||
# nonzero sample to land well before 70% of the transition duration,
|
||||
# measured from the command-issue time.
|
||||
first_nonzero = next(((t, v) for (t, v) in post_command if v > 0), None)
|
||||
assert first_nonzero is not None, (
|
||||
"raw byte never rose above 0 during the transition — the fade stalled"
|
||||
)
|
||||
assert first_nonzero[0] < transition_s * 0.7, (
|
||||
f"raw byte only rose above 0 at t={first_nonzero[0]:.3f}s "
|
||||
f"(>{transition_s * 0.7:.3f}s after command) — transition is stalling"
|
||||
)
|
||||
|
||||
# Assertion 2: by the time the transition has had 70% of its duration
|
||||
# to run from its first visible step, the raw byte should be at least
|
||||
# ~half of its final value. This catches "barely moves then jumps at
|
||||
# the end" regressions.
|
||||
late_samples = [v for (t, v) in rel if t >= transition_s * 0.7]
|
||||
# Assertion 2: by 70% of the transition duration after the command,
|
||||
# the raw byte should have reached a substantial fraction of its final
|
||||
# value. This catches "barely moves then jumps at the end" regressions
|
||||
# that pass assertion 1 but still stall most of the range.
|
||||
late_samples = [v for (t, v) in post_command if t >= transition_s * 0.7]
|
||||
assert late_samples, "no samples captured late in transition"
|
||||
assert max(late_samples) >= 100, (
|
||||
f"raw byte peaked at only {max(late_samples)} late in transition "
|
||||
"(expected >= 100 for white target at gamma 2.8)"
|
||||
f"raw byte peaked at only {max(late_samples)} at/after 70% of "
|
||||
"transition (expected >= 100 for white target at gamma 2.8)"
|
||||
)
|
||||
|
||||
# Assertion 3: final value reaches target. Gamma 2.8 of 255 is 255.
|
||||
final_samples = [v for (_, v) in samples[-5:]]
|
||||
final_samples = [v for (_, v) in post_command[-5:]]
|
||||
assert max(final_samples) >= 250, (
|
||||
f"final raw byte was {max(final_samples)}, expected >= 250"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user