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

commitb425ab04de6487066bbcdfe21a5d1e6584f83e73
parent854f78c6cc
authorLucas Galante <[email protected]>
date2026-08-26 12:46
Fix config test flake: serialize tests that mutate global color state

test_plate_root_canonical_spelling failed ~3/10 runs: it and
test_backplate_menubar_statusbar_styling both reload_colors() into the
process-global RwLock statics with conflicting corner_radius values, then
assert getters, on parallel harness threads — one test's reload landing
between the other's reload and asserts read 12.0 where 17.0 was expected.

Add color::test_color_state_lock(), a test-only mutex every
reload-then-assert test holds across its reload + asserts. Inside the
lock, before reloading, each test also fires both once-per-process
live-config loads (load_colors_once via a getter, and
layout::lazy_init_style_registry — a third writer any layout/graph test
can trigger) so neither can rewrite the state from the live config file
mid-assert.

Verified: the narrowed filter (cargo test -p cce-ui --lib config::tests)
reproduced 6/30 failures before, 0/30 after; full --lib suite 0/10.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/color.rs  | 13 +++++++++++++
 src/config.rs | 11 ++++++++++-
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/color.rs b/src/color.rs
index 2457a6f..e15d8eb 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -661,6 +661,19 @@ pub fn reload_colors(content: &str) {
     parse_and_set_colors(content);
 }
 
+/// Test-only: the color state is process-global, so any test that calls
+/// [`reload_colors`] and then asserts getter values races every other such
+/// test on the parallel harness. Each of those tests must hold this lock
+/// across its reload + asserts, and should fire the once-per-process config
+/// loads ([`load_colors_once`] via any getter, and
+/// `layout::lazy_init_style_registry`) inside the lock BEFORE its reload, so
+/// neither can rewrite the state from the live config file mid-assert.
+#[cfg(test)]
+pub(crate) fn test_color_state_lock() -> std::sync::MutexGuard<'static, ()> {
+    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
+    LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
+}
+
 pub fn dropdown_background_color() -> [f32; 4] {
     load_colors_once();
     *DROPDOWN_BACKGROUND_COLOR.read().unwrap()
diff --git a/src/config.rs b/src/config.rs
index c18a935..7f6b412 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -944,8 +944,12 @@ mod tests {
 
     #[test]
     fn test_backplate_menubar_statusbar_styling() {
-        // Trigger load_colors_once first to initialize the Once block from the real config file
+        // Global color state: serialize against the other reload_colors tests,
+        // and fire both once-per-process live-config loads before our reload
+        // so neither can rewrite the state mid-assert.
+        let _guard = crate::color::test_color_state_lock();
         let _ = crate::color::backplate_statusbar_blur();
+        crate::layout::lazy_init_style_registry();
 
         let content = r##"
             style {
@@ -990,7 +994,12 @@ mod tests {
     /// both spellings are present (canonical-first pointer chains).
     #[test]
     fn test_plate_root_canonical_spelling() {
+        // Global color state: serialize against the other reload_colors tests,
+        // and fire both once-per-process live-config loads before our reload
+        // so neither can rewrite the state mid-assert.
+        let _guard = crate::color::test_color_state_lock();
         let _ = crate::color::root_plate_corner_radius();
+        crate::layout::lazy_init_style_registry();
 
         let content = r##"
             style {