From e6ad6fe38e8b9e2a3909d34a4a5a28ef6559218a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 3 Apr 2022 15:01:19 +0200 Subject: [PATCH] Mutable: Conditionally modify values in place Signed-off-by: Uwe Klotz --- src/signal/mutable.rs | 35 +++++++++++++++++++++++++++++ tests/signal.rs | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/signal/mutable.rs b/src/signal/mutable.rs index 37b3c08..fca3953 100644 --- a/src/signal/mutable.rs +++ b/src/signal/mutable.rs @@ -1,6 +1,7 @@ use super::Signal; use std; use std::fmt; +use std::panic; use std::pin::Pin; use std::marker::Unpin; use std::ops::{Deref, DerefMut}; @@ -310,6 +311,40 @@ impl Mutable { } } + /// Conditionally modify the value in place + /// + /// Borrows the value mutably and modifies it in place. + /// + /// The value is only considered as modified if the modifying closure + /// returned `true`. Otherwise the value is assumed to be unmodified + /// despite the mutable borrow before. + /// + /// Returns the result of the closure, i.e. if the value has been modified. + pub fn modify(&self, modify: F) -> bool + where + F: FnOnce(&mut A) -> bool, + { + let mut locked_state = self.state().lock.write().unwrap(); + + let result = + panic::catch_unwind(panic::AssertUnwindSafe(|| modify(&mut locked_state.value))); + // If modify() panicked forward the panic to the caller without poisoning the lock. + match result { + Ok(modified) => { + if modified { + locked_state.notify(modified); + } + modified + } + Err(panicked) => { + // Drop the lock to avoid poisoning it. + drop(locked_state); + // Forward the panic to the caller. + panic::resume_unwind(panicked); + } + } + } + // TODO lots of unit tests to verify that it only notifies when the object is mutated // TODO return Result ? // TODO should this inline ? diff --git a/tests/signal.rs b/tests/signal.rs index 0677882..086b179 100644 --- a/tests/signal.rs +++ b/tests/signal.rs @@ -530,3 +530,54 @@ fn test_throttle_timing() { assert_eq!(output.poll_change(cx), Poll::Ready(None)); }); } + +#[test] +fn test_modify() { + #[derive(Debug, Clone, Copy, PartialEq)] + struct State { + counter: usize, + } + + let inc_counter_if_odd = |state: &mut State| { + if state.counter % 2 == 1 { + state.counter += 1; + return true; + } + false + }; + + util::with_noop_context(|cx| { + let state = Mutable::new(State { counter: 1 }); + let initial_state = state.get(); + let modified_state = { + let mut modified_state = initial_state; + assert!(inc_counter_if_odd(&mut modified_state)); + modified_state + }; + assert_ne!(initial_state, modified_state); + + let signal = state.signal(); + pin_mut!(signal); + + // Initial value + assert_eq!( + signal.as_mut().poll_change(cx), + Poll::Ready(Some(initial_state)) + ); + + // Modified + assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending); + assert!(state.modify(inc_counter_if_odd)); + assert_eq!(state.get(), modified_state); + assert_eq!( + signal.as_mut().poll_change(cx), + Poll::Ready(Some(modified_state)) + ); + + // Unmodified + assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending); + assert!(!state.modify(inc_counter_if_odd)); + assert_eq!(state.get(), modified_state); + assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending); + }); +}