git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit5ce83a50af66e976f8d9be08bb9fdb4a8ad41b5f
parent071cbca846
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 19:37
fix: the test suite's animations switch is its own, not the machine's

motion::enabled() read /run/cce/animations in the test binary too, so the
spreadsheet's two wheel-glide tests and the scroll region's fade test
passed on mains and failed on battery, reading as a broken glide rather
than a borrowed switch. Under cfg(test) it now answers on, or whatever
motion::force_for_test set on the calling thread — thread-local, because
libtest runs tests in parallel and a process-wide override would race
every other test's read. The shipped path is unchanged
(enabled_on_this_machine: the CCE_ANIMATIONS override, else the file,
re-read at most every RECHECK).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md     | 15 +++++++++++++++
 src/motion.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+)

diff --git a/CLAUDE.md b/CLAUDE.md
index 72e3ab5..729e080 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -910,3 +910,18 @@ re-layout on every rect assignment (`apply_label_layout` /
 `relabel_rows`), not a flag; `Adapted::clear_label` is the way a label
 comes OFF a widget, and `slider::detached_strip` reads an empty label as
 none for the widgets that store whatever they are handed.
+
+### The suite's animations switch is its own
+
+`motion::enabled()` reads `/run/cce/animations` in a shipped binary (see
+"Animations switch" in `../cce-compositor/WORKSPACE.md`). Under `cfg(test)`
+it does NOT: it answers on, or whatever `motion::force_for_test` set on the
+calling thread. Until 2026-09-28 the test binary read the machine's file,
+so the spreadsheet's wheel-glide tests and the scroll region's fade test
+passed on mains and failed on battery — the same lesson as cce-designer's
+pinned settings path and lattice, one layer down. A test wanting the
+snap-instead-of-ease path forces it for its thread; nothing sets
+`CCE_ANIMATIONS`, which is process-wide and would race the parallel suite.
+This pins only cce-ui's own suite: a dependent's test binary links cce-ui
+without `cfg(test)`, so a dependent test that eases still follows the
+machine — none does today.
diff --git a/src/motion.rs b/src/motion.rs
index 443bf19..4153f63 100644
--- a/src/motion.rs
+++ b/src/motion.rs
@@ -43,9 +43,44 @@ pub fn read_state() -> Option<bool> {
 
 static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
 
+/// Under `cfg(test)` the switch is the SUITE's, not the machine's: on,
+/// unless a test forces it with [`force_for_test`]. Until 2026-09-28
+/// `enabled` read `/run/cce/animations` in the test binary too, so three
+/// glide and fade tests passed or failed with the laptop's power mode —
+/// off on battery, on when plugged in — and read as a broken glide rather
+/// than a borrowed switch. Thread-local rather than the shared cache,
+/// because libtest runs tests in parallel and a process-wide override set
+/// by one test would race every other test's read; a test that forces it
+/// does so for its own thread only, and the value resets with the thread.
+#[cfg(test)]
+thread_local! {
+    static FORCED: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
+}
+
+/// Set what [`enabled`] answers on this thread, for a test that exercises
+/// the snap-instead-of-ease path. Tests never set `CCE_ANIMATIONS`, since
+/// an environment variable is process-wide.
+#[cfg(test)]
+pub fn force_for_test(value: bool) {
+    FORCED.with(|f| f.set(value));
+}
+
 /// Whether to animate. Every easing in the toolkit asks this before it
 /// steps, and snaps to its target when the answer is no.
 pub fn enabled() -> bool {
+    #[cfg(test)]
+    {
+        return FORCED.with(|f| f.get());
+    }
+    #[cfg(not(test))]
+    enabled_on_this_machine()
+}
+
+/// [`enabled`] as the shipped binary answers it: the `CCE_ANIMATIONS`
+/// override for this process, else the state file, re-read at most every
+/// [`RECHECK`].
+#[cfg(not(test))]
+fn enabled_on_this_machine() -> bool {
     static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
     if let Some(forced) = *ENV.get_or_init(|| std::env::var("CCE_ANIMATIONS").ok().and_then(|v| parse(&v))) {
         return forced;
@@ -74,4 +109,18 @@ mod tests {
         assert_eq!(parse(""), None);
         assert_eq!(parse("disabled"), None);
     }
+
+    /// The suite's switch is on whatever the machine's file says, a test
+    /// can force it off for its own thread only, and another thread still
+    /// sees it on.
+    #[test]
+    fn the_suite_animates_whatever_the_machine_says() {
+        assert!(enabled(), "on by default, not read off {STATE_PATH}");
+        force_for_test(false);
+        assert!(!enabled(), "a test can force the snap path");
+        let elsewhere = std::thread::spawn(enabled).join().unwrap();
+        assert!(elsewhere, "the force is this thread's alone");
+        force_for_test(true);
+        assert!(enabled());
+    }
 }