Skip to content

Commit a1edca2

Browse files
Oscar Valoisclaude
andcommitted
fix(tests): tolerate scheduler drift in transition_engine start-time reads
CI run #142 (post-v0.3.15) failed Test (macos) on `engine_stores_multiple_transitions`. Same class of flake fixed in #10 for `tui::app` animation tests: tests assert that `current()` returns *exactly* `from` immediately after `start()`, but `current()` reads `Instant::now() - started_at`, so any non-zero gap produces channel drift through the OKLCH/RGB interpolation path. Linux measures sub-ms gaps and rounds back to `from.srgb8()`; macOS-latest under load shows 5+ ms gaps and rounds to e.g. (254, 0, 0). Three sibling tests share the pattern: - transition_starts_at_from_color - engine_stores_multiple_transitions - engine_update_or_start_creates_if_missing All three relaxed via a `assert_color_near` helper with ±5/255 channel tolerance — large enough to absorb any plausible scheduler delay on a 100ms transition (5ms ≈ t=0.05, eased to ≈0.005, → ≈1.3/255 expected delta — tolerance is 4× that). `engine_stores_multiple_transitions` additionally adds an explicit sentinel-default check first, so a missing-key regression now reports "key not stored" instead of being conflated with start-color drift. No production code change. Zero binary impact — v0.3.15 release is unaffected; this only un-reds CI on main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9fa5306 commit a1edca2

1 file changed

Lines changed: 38 additions & 5 deletions

File tree

crates/halcon-cli/src/tui/transition_engine.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,38 @@ impl TransitionEngine {
219219
mod tests {
220220
use super::*;
221221

222+
/// Channel-wise tolerance for "right after start" color reads.
223+
///
224+
/// `current()` uses `Instant::now() - started_at`, so even a 1-2 ms gap
225+
/// between `start()` and the read produces a few units of channel drift
226+
/// for short-duration transitions (5 ms of a 100 ms transition is t≈0.05,
227+
/// eased to ≈0.005 → ≈1.3/255 channel delta on a 255-unit step).
228+
/// Linux runners measure sub-millisecond gaps; macOS-latest under load
229+
/// routinely shows 5+ ms. Strict equality is therefore inherently flaky
230+
/// on macOS — see PR #10's animation tests for the same class of fix.
231+
const START_DRIFT_TOLERANCE: i16 = 5;
232+
233+
fn assert_color_near(actual: ThemeColor, expected: ThemeColor, ctx: &str) {
234+
let [ar, ag, ab] = actual.srgb8();
235+
let [er, eg, eb] = expected.srgb8();
236+
let dr = (ar as i16 - er as i16).abs();
237+
let dg = (ag as i16 - eg as i16).abs();
238+
let db = (ab as i16 - eb as i16).abs();
239+
assert!(
240+
dr <= START_DRIFT_TOLERANCE
241+
&& dg <= START_DRIFT_TOLERANCE
242+
&& db <= START_DRIFT_TOLERANCE,
243+
"{ctx}: expected ~({er},{eg},{eb}) ±{START_DRIFT_TOLERANCE}, got ({ar},{ag},{ab})"
244+
);
245+
}
246+
222247
#[test]
223248
fn transition_starts_at_from_color() {
224249
let from = ThemeColor::from_srgb8([255, 0, 0]);
225250
let to = ThemeColor::from_srgb8([0, 0, 255]);
226251
let transition = ColorTransition::new(from, to, Duration::from_millis(100));
227252

228-
let current = transition.current();
229-
assert_eq!(current.srgb8(), from.srgb8());
253+
assert_color_near(transition.current(), from, "fresh transition");
230254
}
231255

232256
#[test]
@@ -302,21 +326,30 @@ mod tests {
302326
let red = ThemeColor::from_srgb8([255, 0, 0]);
303327
let green = ThemeColor::from_srgb8([0, 255, 0]);
304328
let blue = ThemeColor::from_srgb8([0, 0, 255]);
329+
// Sentinel default — disambiguates "key missing" from "transition is at start".
330+
let sentinel = ThemeColor::from_srgb8([42, 42, 42]);
305331

306332
engine.start("border", red, green, Duration::from_millis(100));
307333
engine.start("bg", red, blue, Duration::from_millis(100));
308334

309-
assert_eq!(engine.current("border", red).srgb8(), red.srgb8());
310-
assert_eq!(engine.current("bg", red).srgb8(), red.srgb8());
335+
let border = engine.current("border", sentinel);
336+
let bg = engine.current("bg", sentinel);
337+
assert_ne!(border.srgb8(), sentinel.srgb8(), "border key not stored");
338+
assert_ne!(bg.srgb8(), sentinel.srgb8(), "bg key not stored");
339+
assert_color_near(border, red, "border at start");
340+
assert_color_near(bg, red, "bg at start");
311341
}
312342

313343
#[test]
314344
fn engine_update_or_start_creates_if_missing() {
315345
let mut engine = TransitionEngine::new();
316346
let red = ThemeColor::from_srgb8([255, 0, 0]);
347+
let sentinel = ThemeColor::from_srgb8([42, 42, 42]);
317348

318349
engine.update_or_start("border", red, Duration::from_millis(100));
319-
assert_eq!(engine.current("border", red).srgb8(), red.srgb8());
350+
let border = engine.current("border", sentinel);
351+
assert_ne!(border.srgb8(), sentinel.srgb8(), "border key not created");
352+
assert_color_near(border, red, "border at start");
320353
}
321354

322355
#[test]

0 commit comments

Comments
 (0)