Skip to main content

zerodds_dcps/
condition.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `Condition` hierarchy + `WaitSet` (DDS DCPS 1.4 §2.2.2.1.6).
4//!
5//! Conditions are the DDS variant of "future readiness": a
6//! `Condition` object carries a `trigger_value()` boolean that
7//! becomes `true` when a particular event occurs. Several conditions
8//! are collected in a [`WaitSet`], which then blocks in
9//! [`WaitSet::wait`] until any condition triggers (or a timeout is
10//! reached).
11//!
12//! # Spec hierarchy
13//!
14//! - `Condition` (base, only trigger_value)
15//!   - `StatusCondition` (see [`crate::entity::StatusCondition`])
16//!   - `GuardCondition` (manually settable, defined here)
17//!   - `ReadCondition` (based on SampleInfo state, defined here)
18//!   - `QueryCondition` (ReadCondition + SQL filter, defined here)
19//!
20//! The trait is object-safe; WaitSet holds `Arc<dyn Condition>`.
21
22extern crate alloc;
23
24use alloc::sync::Arc;
25use alloc::vec::Vec;
26use core::sync::atomic::{AtomicBool, Ordering};
27use core::time::Duration;
28
29#[cfg(feature = "std")]
30use std::sync::{Condvar, Mutex};
31
32use crate::error::{DdsError, Result};
33
34/// Condition trait — Spec §2.2.2.1.6 base class.
35pub trait Condition: Send + Sync {
36    /// True if the event of this condition is currently pending.
37    /// Spec §2.2.2.1.6 `get_trigger_value`.
38    fn get_trigger_value(&self) -> bool;
39}
40
41// Re-export the StatusCondition from entity.rs so callers only need to
42// import condition::*.
43pub use crate::entity::StatusCondition;
44
45impl Condition for StatusCondition {
46    fn get_trigger_value(&self) -> bool {
47        self.trigger_value()
48    }
49}
50
51/// `ReadCondition` — Spec §2.2.2.5.8 / §2.2.4.5 trigger state.
52///
53/// A ReadCondition is bound to a DataReader and triggers `true` when
54/// the reader holds samples that match all three masks
55/// (sample_state_mask, view_state_mask, instance_state_mask).
56///
57/// Design: the trigger logic itself (the actual "does the reader have
58/// samples with these masks?" query) is injected by the caller as a
59/// closure, because the DataReader sample cache cannot be queried in
60/// an object-safe way without further infrastructure changes. The
61/// DCPS API consumer (usually DataReader::create_readcondition)
62/// provides the closure in the form `(sm, vm, im) -> bool`.
63pub struct ReadCondition {
64    sample_state_mask: u32,
65    view_state_mask: u32,
66    instance_state_mask: u32,
67    trigger: Arc<dyn Fn(u32, u32, u32) -> bool + Send + Sync>,
68}
69
70impl core::fmt::Debug for ReadCondition {
71    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72        f.debug_struct("ReadCondition")
73            .field("sample_state_mask", &self.sample_state_mask)
74            .field("view_state_mask", &self.view_state_mask)
75            .field("instance_state_mask", &self.instance_state_mask)
76            .finish_non_exhaustive()
77    }
78}
79
80impl ReadCondition {
81    /// Constructor with a trigger closure.
82    #[must_use]
83    pub fn new<F>(
84        sample_state_mask: u32,
85        view_state_mask: u32,
86        instance_state_mask: u32,
87        trigger: F,
88    ) -> Arc<Self>
89    where
90        F: Fn(u32, u32, u32) -> bool + Send + Sync + 'static,
91    {
92        Arc::new(Self {
93            sample_state_mask,
94            view_state_mask,
95            instance_state_mask,
96            trigger: Arc::new(trigger),
97        })
98    }
99
100    /// Spec §2.2.2.5.8 `get_sample_state_mask`.
101    #[must_use]
102    pub fn get_sample_state_mask(&self) -> u32 {
103        self.sample_state_mask
104    }
105
106    /// Spec §2.2.2.5.8 `get_view_state_mask`.
107    #[must_use]
108    pub fn get_view_state_mask(&self) -> u32 {
109        self.view_state_mask
110    }
111
112    /// Spec §2.2.2.5.8 `get_instance_state_mask`.
113    #[must_use]
114    pub fn get_instance_state_mask(&self) -> u32 {
115        self.instance_state_mask
116    }
117}
118
119impl Condition for ReadCondition {
120    fn get_trigger_value(&self) -> bool {
121        (self.trigger)(
122            self.sample_state_mask,
123            self.view_state_mask,
124            self.instance_state_mask,
125        )
126    }
127}
128
129/// `QueryCondition` — Spec §2.2.2.5.9. Extends ReadCondition with a
130/// SQL filter expression (DDS-DCPS Annex B). The filter is evaluated
131/// per sample (see [`Self::evaluate`]); the parse step happens once in
132/// the constructor.
133pub struct QueryCondition {
134    base: Arc<ReadCondition>,
135    query_expression: alloc::string::String,
136    query_parameters: Mutex<Vec<alloc::string::String>>,
137    parsed: zerodds_sql_filter::Expr,
138}
139
140impl core::fmt::Debug for QueryCondition {
141    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142        f.debug_struct("QueryCondition")
143            .field("query_expression", &self.query_expression)
144            .field("base", &self.base)
145            .finish_non_exhaustive()
146    }
147}
148
149impl QueryCondition {
150    /// Constructor — Spec §2.2.2.5.2.5 `create_querycondition` /
151    /// §2.2.2.5.9. The SQL expression is parsed immediately; a
152    /// syntactically invalid expression returns `BadParameter`.
153    ///
154    /// # Errors
155    /// `BadParameter` if the SQL expression does not parse.
156    pub fn new(
157        base: Arc<ReadCondition>,
158        query_expression: impl Into<alloc::string::String>,
159        query_parameters: Vec<alloc::string::String>,
160    ) -> Result<Arc<Self>> {
161        let expr_str = query_expression.into();
162        let parsed = zerodds_sql_filter::parse(&expr_str).map_err(|_| DdsError::BadParameter {
163            what: "QueryCondition: invalid SQL filter expression",
164        })?;
165        Ok(Arc::new(Self {
166            base,
167            query_expression: expr_str,
168            query_parameters: Mutex::new(query_parameters),
169            parsed,
170        }))
171    }
172
173    /// Evaluates the SQL filter against a sample. Spec §2.2.2.5.9.6 —
174    /// only samples with `evaluate(...)==Ok(true)` count toward the
175    /// `read_w_condition`/`take_w_condition` result.
176    ///
177    /// Parameter strings are converted into `String` `Value`s; the
178    /// caller can pass typed parameters via
179    /// [`Self::evaluate_with_values`].
180    ///
181    /// # Errors
182    /// Lock poisoning or SQL eval error (UnknownField/TypeMismatch/
183    /// MissingParam).
184    pub fn evaluate<R: zerodds_sql_filter::RowAccess>(&self, row: &R) -> Result<bool> {
185        let params = self
186            .query_parameters
187            .lock()
188            .map_err(|_| DdsError::PreconditionNotMet {
189                reason: "query parameters poisoned",
190            })?;
191        let values: Vec<zerodds_sql_filter::Value> = params
192            .iter()
193            .map(|s| zerodds_sql_filter::Value::String(s.clone()))
194            .collect();
195        self.parsed
196            .evaluate(row, &values)
197            .map_err(|_| DdsError::PreconditionNotMet {
198                reason: "QueryCondition SQL evaluation failed",
199            })
200    }
201
202    /// Like [`Self::evaluate`], but with an explicitly typed parameter
203    /// slice (e.g. for int/float parameters that would not match as a
204    /// string cast).
205    ///
206    /// # Errors
207    /// SQL eval error.
208    pub fn evaluate_with_values<R: zerodds_sql_filter::RowAccess>(
209        &self,
210        row: &R,
211        params: &[zerodds_sql_filter::Value],
212    ) -> Result<bool> {
213        self.parsed
214            .evaluate(row, params)
215            .map_err(|_| DdsError::PreconditionNotMet {
216                reason: "QueryCondition SQL evaluation failed",
217            })
218    }
219
220    /// Spec §2.2.2.5.9.4 `get_query_expression`.
221    #[must_use]
222    pub fn get_query_expression(&self) -> &str {
223        &self.query_expression
224    }
225
226    /// Spec §2.2.2.5.9.5 `get_query_parameters`.
227    #[must_use]
228    pub fn get_query_parameters(&self) -> Vec<alloc::string::String> {
229        self.query_parameters
230            .lock()
231            .map(|p| p.clone())
232            .unwrap_or_default()
233    }
234
235    /// Spec §2.2.2.5.9.6 `set_query_parameters`.
236    ///
237    /// # Errors
238    /// `PreconditionNotMet` on lock poisoning.
239    pub fn set_query_parameters(&self, params: Vec<alloc::string::String>) -> Result<()> {
240        let mut current =
241            self.query_parameters
242                .lock()
243                .map_err(|_| DdsError::PreconditionNotMet {
244                    reason: "query parameters poisoned",
245                })?;
246        *current = params;
247        Ok(())
248    }
249
250    /// Access to the base ReadCondition (Spec: QueryCondition extends
251    /// ReadCondition).
252    #[must_use]
253    pub fn base(&self) -> &Arc<ReadCondition> {
254        &self.base
255    }
256}
257
258impl Condition for QueryCondition {
259    fn get_trigger_value(&self) -> bool {
260        // The trigger is inherited from the base ReadCondition (state-
261        // mask match); the per-sample filter evaluation happens in
262        // DataReader::read_w_condition/take_w_condition via
263        // [`Self::evaluate`].
264        self.base.get_trigger_value()
265    }
266}
267
268/// `GuardCondition` — manually triggerable by the user (Spec
269/// §2.2.2.1.7).
270///
271/// Typical use: an application thread sets `set_trigger_value(true)`
272/// to wake a blocked WaitSet (e.g. for shutdown signaling).
273#[derive(Debug, Default)]
274pub struct GuardCondition {
275    triggered: AtomicBool,
276}
277
278impl GuardCondition {
279    /// New GuardCondition, initially `trigger_value=false`.
280    #[must_use]
281    pub fn new() -> Arc<Self> {
282        Arc::new(Self::default())
283    }
284
285    /// Sets the trigger value. Spec §2.2.2.1.7 `set_trigger_value`.
286    pub fn set_trigger_value(&self, value: bool) {
287        self.triggered.store(value, Ordering::Release);
288    }
289}
290
291impl Condition for GuardCondition {
292    fn get_trigger_value(&self) -> bool {
293        self.triggered.load(Ordering::Acquire)
294    }
295}
296
297/// `WaitSet` — Spec §2.2.2.1.6.
298///
299/// A WaitSet collects 0..N `Arc<dyn Condition>` and blocks in
300/// [`Self::wait`] until at least one triggers or a timeout is reached.
301/// Returns the list of **triggered** conditions.
302#[cfg(feature = "std")]
303pub struct WaitSet {
304    inner: Arc<WaitSetInner>,
305}
306
307#[cfg(feature = "std")]
308struct WaitSetInner {
309    conditions: Mutex<Vec<Arc<dyn Condition>>>,
310    /// Notify pair for poll-based wait. Conditions that set their
311    /// trigger_value can wake the WaitSet via `notify()` — in the
312    /// current state we only poll, however, because we do not want to
313    /// force the conditions to hold a backref.
314    cvar: Condvar,
315    /// Idle sleep between poll ticks (1ms — short enough for latency,
316    /// long enough for low CPU load).
317    poll_interval: core::time::Duration,
318    /// Mutex guard for the Condvar.
319    locked: Mutex<()>,
320    /// `true` while a thread is blocked in `wait()`. Spec §2.2.2.1.6:
321    /// "WaitSet MAY be used by only one application thread at a time"
322    /// — concurrent `wait()` calls MUST return `PreconditionNotMet`.
323    waiting: AtomicBool,
324}
325
326/// DoS cap for conditions in a single WaitSet (Spec §2.2.2.1.6: no
327/// explicit cap, but the RESOURCE_LIMITS counterpart). Protects
328/// against unbounded growth in pathological cases.
329pub const MAX_WAITSET_CONDITIONS: usize = 1024;
330
331#[cfg(feature = "std")]
332impl WaitSet {
333    /// New empty WaitSet.
334    #[must_use]
335    pub fn new() -> Self {
336        Self {
337            inner: Arc::new(WaitSetInner {
338                conditions: Mutex::new(Vec::new()),
339                cvar: Condvar::new(),
340                poll_interval: Duration::from_millis(1),
341                locked: Mutex::new(()),
342                waiting: AtomicBool::new(false),
343            }),
344        }
345    }
346
347    /// Attaches a condition. Spec §2.2.2.1.6 `attach_condition`.
348    ///
349    /// # Errors
350    /// `PreconditionNotMet` if the internal lock is poisoned.
351    pub fn attach_condition(&self, cond: Arc<dyn Condition>) -> Result<()> {
352        let mut conds = self
353            .inner
354            .conditions
355            .lock()
356            .map_err(|_| DdsError::PreconditionNotMet {
357                reason: "waitset conditions poisoned",
358            })?;
359        // Idempotent: the same Arc identity is not added twice.
360        if conds.iter().any(|c| Arc::ptr_eq(c, &cond)) {
361            return Ok(());
362        }
363        // Spec §2.2.2.1.6 / §2.2.1.1.6 — DoS cap.
364        if conds.len() >= MAX_WAITSET_CONDITIONS {
365            return Err(DdsError::OutOfResources {
366                what: "waitset condition count",
367            });
368        }
369        conds.push(cond);
370        Ok(())
371    }
372
373    /// Detaches a condition. Spec §2.2.2.1.6 `detach_condition`.
374    ///
375    /// # Errors
376    /// `PreconditionNotMet` if the internal lock is poisoned.
377    pub fn detach_condition(&self, cond: &Arc<dyn Condition>) -> Result<()> {
378        let mut conds = self
379            .inner
380            .conditions
381            .lock()
382            .map_err(|_| DdsError::PreconditionNotMet {
383                reason: "waitset conditions poisoned",
384            })?;
385        conds.retain(|c| !Arc::ptr_eq(c, cond));
386        Ok(())
387    }
388
389    /// Current set of attached conditions.
390    ///
391    /// # Errors
392    /// `PreconditionNotMet` on lock poisoning.
393    pub fn get_conditions(&self) -> Result<Vec<Arc<dyn Condition>>> {
394        let conds = self
395            .inner
396            .conditions
397            .lock()
398            .map_err(|_| DdsError::PreconditionNotMet {
399                reason: "waitset conditions poisoned",
400            })?;
401        Ok(conds.clone())
402    }
403
404    /// Blocks until at least one condition triggers or the timeout
405    /// elapses. Returns the list of triggered conditions. Spec
406    /// §2.2.2.1.6 `wait`.
407    ///
408    /// **Implementation:** polling at a 1ms interval. A production-
409    /// grade WaitSet would offer conditions notify hooks; that is a
410    /// follow-up optimization.
411    ///
412    /// # Errors
413    /// * [`DdsError::Timeout`] if no condition triggers within the
414    ///   timeout.
415    /// * [`DdsError::PreconditionNotMet`] on lock poisoning.
416    pub fn wait(&self, timeout: Duration) -> Result<Vec<Arc<dyn Condition>>> {
417        // Spec §2.2.2.1.6 — single-thread wait. Atomic compare_exchange
418        // prevents two threads from entering `wait()` at the same time.
419        // RAII guard via a local struct that resets the flag on drop —
420        // more robust against panics in poll_active.
421        if self
422            .inner
423            .waiting
424            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
425            .is_err()
426        {
427            return Err(DdsError::PreconditionNotMet {
428                reason: "waitset already in wait() — single-thread-wait per spec",
429            });
430        }
431        struct WaitGuard<'a>(&'a AtomicBool);
432        impl Drop for WaitGuard<'_> {
433            fn drop(&mut self) {
434                self.0.store(false, Ordering::Release);
435            }
436        }
437        let _guard = WaitGuard(&self.inner.waiting);
438
439        let deadline = std::time::Instant::now() + timeout;
440        loop {
441            let active = self.poll_active()?;
442            if !active.is_empty() {
443                return Ok(active);
444            }
445            let now = std::time::Instant::now();
446            if now >= deadline {
447                return Err(DdsError::Timeout);
448            }
449            // Sleep until the next poll tick or the deadline.
450            let sleep_for = (deadline - now).min(self.inner.poll_interval);
451            let cvlock = self
452                .inner
453                .locked
454                .lock()
455                .map_err(|_| DdsError::PreconditionNotMet {
456                    reason: "waitset locked poisoned",
457                })?;
458            let _ = self.inner.cvar.wait_timeout(cvlock, sleep_for);
459            // (Discarding the result is fine — we poll again at the
460            //  top of the loop anyway.)
461        }
462    }
463
464    /// `wait_until_any_or_all_triggered` — internal helper.
465    fn poll_active(&self) -> Result<Vec<Arc<dyn Condition>>> {
466        let conds = self
467            .inner
468            .conditions
469            .lock()
470            .map_err(|_| DdsError::PreconditionNotMet {
471                reason: "waitset conditions poisoned",
472            })?;
473        Ok(conds
474            .iter()
475            .filter(|c| c.get_trigger_value())
476            .cloned()
477            .collect())
478    }
479
480    /// Wakes a blocked `wait()` without a trigger_value change having
481    /// occurred (e.g. for shutdown).
482    ///
483    /// Currently this only has an effect if the WaitSet is blocked in
484    /// `wait_timeout` — the caller should still attach a
485    /// `GuardCondition` so that the wakeup has a definite result.
486    pub fn notify(&self) {
487        self.inner.cvar.notify_all();
488    }
489}
490
491#[cfg(feature = "std")]
492impl Default for WaitSet {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498#[cfg(feature = "std")]
499impl Clone for WaitSet {
500    fn clone(&self) -> Self {
501        Self {
502            inner: Arc::clone(&self.inner),
503        }
504    }
505}
506
507#[cfg(feature = "std")]
508impl core::fmt::Debug for WaitSet {
509    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
510        let n = self.inner.conditions.lock().map(|c| c.len()).unwrap_or(0);
511        f.debug_struct("WaitSet").field("conditions", &n).finish()
512    }
513}
514
515#[cfg(test)]
516#[cfg(feature = "std")]
517#[allow(clippy::expect_used, clippy::unwrap_used)]
518mod tests {
519    use super::*;
520    use std::thread;
521    use std::time::Instant;
522
523    #[test]
524    fn guard_condition_starts_false() {
525        let g = GuardCondition::new();
526        assert!(!g.get_trigger_value());
527    }
528
529    #[test]
530    fn guard_condition_set_trigger() {
531        let g = GuardCondition::new();
532        g.set_trigger_value(true);
533        assert!(g.get_trigger_value());
534        g.set_trigger_value(false);
535        assert!(!g.get_trigger_value());
536    }
537
538    #[test]
539    fn waitset_attach_detach_idempotent() {
540        let ws = WaitSet::new();
541        let g = GuardCondition::new();
542        let cond: Arc<dyn Condition> = g.clone();
543        ws.attach_condition(cond.clone()).unwrap();
544        ws.attach_condition(cond.clone()).unwrap(); // idempotent
545        assert_eq!(ws.get_conditions().unwrap().len(), 1);
546        ws.detach_condition(&cond).unwrap();
547        assert!(ws.get_conditions().unwrap().is_empty());
548    }
549
550    #[test]
551    fn waitset_wait_returns_immediately_if_already_triggered() {
552        let ws = WaitSet::new();
553        let g = GuardCondition::new();
554        g.set_trigger_value(true);
555        let cond: Arc<dyn Condition> = g.clone();
556        ws.attach_condition(cond).unwrap();
557        let triggered = ws.wait(Duration::from_secs(1)).unwrap();
558        assert_eq!(triggered.len(), 1);
559    }
560
561    #[test]
562    fn waitset_wait_timeout_returns_err() {
563        let ws = WaitSet::new();
564        let g = GuardCondition::new();
565        let cond: Arc<dyn Condition> = g.clone();
566        ws.attach_condition(cond).unwrap();
567        let start = Instant::now();
568        let res = ws.wait(Duration::from_millis(50));
569        assert!(matches!(res, Err(DdsError::Timeout)));
570        // We waited at least ~50ms
571        assert!(start.elapsed() >= Duration::from_millis(40));
572    }
573
574    #[test]
575    fn waitset_wakes_when_guard_triggers() {
576        let ws = WaitSet::new();
577        let g = GuardCondition::new();
578        let cond: Arc<dyn Condition> = g.clone();
579        ws.attach_condition(cond).unwrap();
580
581        let g_clone = g.clone();
582        let handle = thread::spawn(move || {
583            thread::sleep(Duration::from_millis(20));
584            g_clone.set_trigger_value(true);
585        });
586
587        let start = Instant::now();
588        let triggered = ws.wait(Duration::from_secs(2)).unwrap();
589        let elapsed = start.elapsed();
590        handle.join().unwrap();
591
592        assert_eq!(triggered.len(), 1);
593        // Should return quickly, not the full timeout
594        assert!(elapsed < Duration::from_millis(500), "elapsed={elapsed:?}");
595    }
596
597    #[test]
598    fn waitset_with_multiple_conditions_returns_all_triggered() {
599        let ws = WaitSet::new();
600        let g1 = GuardCondition::new();
601        let g2 = GuardCondition::new();
602        ws.attach_condition(g1.clone()).unwrap();
603        ws.attach_condition(g2.clone()).unwrap();
604        g1.set_trigger_value(true);
605        g2.set_trigger_value(true);
606        let triggered = ws.wait(Duration::from_millis(100)).unwrap();
607        assert_eq!(triggered.len(), 2);
608    }
609
610    #[test]
611    fn status_condition_implements_condition_trait() {
612        let state = crate::entity::EntityState::new();
613        let sc = crate::entity::StatusCondition::new(state.clone());
614        sc.set_enabled_statuses(0b0010);
615        assert!(!sc.get_trigger_value());
616        state.set_status_bits(0b0010);
617        assert!(sc.get_trigger_value());
618    }
619
620    #[test]
621    fn waitset_clone_shares_state() {
622        let ws1 = WaitSet::new();
623        let ws2 = ws1.clone();
624        let g = GuardCondition::new();
625        ws1.attach_condition(g.clone()).unwrap();
626        // Both have the same condition (same inner via Arc).
627        assert_eq!(ws2.get_conditions().unwrap().len(), 1);
628    }
629
630    #[test]
631    fn waitset_default_is_empty() {
632        let ws = WaitSet::default();
633        assert!(ws.get_conditions().unwrap().is_empty());
634    }
635
636    // ---- §2.2.2.1.6 single-thread wait + resource limit ----
637
638    #[test]
639    fn waitset_concurrent_wait_returns_precondition_not_met() {
640        // Spec §2.2.2.1.6 — a WaitSet may only be used by one thread at
641        // a time. A second wait() call MUST return PreconditionNotMet.
642        let ws = WaitSet::new();
643        // No condition attached → wait runs into the timeout.
644        let ws_clone = ws.clone();
645        let handle = thread::spawn(move || ws_clone.wait(Duration::from_millis(100)));
646        // Brief pause so that thread A sets the waiting flag.
647        thread::sleep(Duration::from_millis(20));
648        let res = ws.wait(Duration::from_millis(10));
649        assert!(matches!(res, Err(DdsError::PreconditionNotMet { .. })));
650        let _ = handle.join().unwrap();
651    }
652
653    #[test]
654    fn waitset_sequential_waits_after_first_returns_succeed() {
655        // After the first wait() completes (by timeout), a sequential
656        // second wait() must work again.
657        let ws = WaitSet::new();
658        let r1 = ws.wait(Duration::from_millis(10));
659        assert!(matches!(r1, Err(DdsError::Timeout)));
660        // Sequential: the second call after the first return succeeds.
661        let r2 = ws.wait(Duration::from_millis(10));
662        assert!(matches!(r2, Err(DdsError::Timeout)));
663    }
664
665    #[test]
666    fn waitset_attach_above_max_returns_out_of_resources() {
667        // §2.2.1.1.6 RC OUT_OF_RESOURCES — DoS cap.
668        let ws = WaitSet::new();
669        for _ in 0..MAX_WAITSET_CONDITIONS {
670            let g = GuardCondition::new();
671            ws.attach_condition(g).unwrap();
672        }
673        // The next attach must return OOR.
674        let g = GuardCondition::new();
675        let res = ws.attach_condition(g);
676        assert!(matches!(res, Err(DdsError::OutOfResources { .. })));
677        assert_eq!(ws.get_conditions().unwrap().len(), MAX_WAITSET_CONDITIONS);
678    }
679
680    #[test]
681    fn waitset_attach_idempotent_does_not_count_against_cap() {
682        // Same Arc identity attached twice → no duplicate, no counter
683        // increment.
684        let ws = WaitSet::new();
685        let g = GuardCondition::new();
686        let cond: Arc<dyn Condition> = g.clone();
687        ws.attach_condition(cond.clone()).unwrap();
688        ws.attach_condition(cond.clone()).unwrap();
689        ws.attach_condition(cond).unwrap();
690        assert_eq!(ws.get_conditions().unwrap().len(), 1);
691    }
692
693    // ---- §2.2.2.5.8 ReadCondition + §2.2.4.5 trigger state ----
694
695    #[test]
696    fn read_condition_returns_trigger_from_closure() {
697        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
698        // The trigger closure returns true if all three masks are ANY.
699        let cond = ReadCondition::new(
700            sample_state_mask::ANY,
701            view_state_mask::ANY,
702            instance_state_mask::ANY,
703            |sm, vm, im| {
704                sm == sample_state_mask::ANY
705                    && vm == view_state_mask::ANY
706                    && im == instance_state_mask::ANY
707            },
708        );
709        assert_eq!(cond.get_sample_state_mask(), sample_state_mask::ANY);
710        assert_eq!(cond.get_view_state_mask(), view_state_mask::ANY);
711        assert_eq!(cond.get_instance_state_mask(), instance_state_mask::ANY);
712        assert!(cond.get_trigger_value());
713    }
714
715    #[test]
716    fn read_condition_trigger_false_when_closure_says_false() {
717        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
718        // The closure always returns false → no data available.
719        let cond = ReadCondition::new(
720            sample_state_mask::NOT_READ,
721            view_state_mask::NEW,
722            instance_state_mask::ALIVE,
723            |_, _, _| false,
724        );
725        assert!(!cond.get_trigger_value());
726    }
727
728    #[test]
729    fn read_condition_implements_condition_trait_object_safe() {
730        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
731        let cond = ReadCondition::new(
732            sample_state_mask::ANY,
733            view_state_mask::ANY,
734            instance_state_mask::ANY,
735            |_, _, _| true,
736        );
737        let dyn_cond: Arc<dyn Condition> = cond;
738        assert!(dyn_cond.get_trigger_value());
739    }
740
741    #[test]
742    fn read_condition_attaches_to_waitset() {
743        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
744        // §2.2.4.5: ReadCondition as a WaitSet trigger.
745        let ws = WaitSet::new();
746        let cond = ReadCondition::new(
747            sample_state_mask::ANY,
748            view_state_mask::ANY,
749            instance_state_mask::ANY,
750            |_, _, _| true, // always triggered
751        );
752        ws.attach_condition(cond.clone()).unwrap();
753        let triggered = ws.wait(Duration::from_millis(50)).unwrap();
754        assert_eq!(triggered.len(), 1);
755    }
756
757    // ---- §2.2.2.5.9 QueryCondition ----
758
759    #[test]
760    fn query_condition_inherits_trigger_from_base() {
761        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
762        let base = ReadCondition::new(
763            sample_state_mask::ANY,
764            view_state_mask::ANY,
765            instance_state_mask::ANY,
766            |_, _, _| true,
767        );
768        let qc = QueryCondition::new(base, "x > 10", alloc::vec::Vec::new()).unwrap();
769        assert!(qc.get_trigger_value());
770        assert_eq!(qc.get_query_expression(), "x > 10");
771        assert!(qc.get_query_parameters().is_empty());
772    }
773
774    #[test]
775    fn query_condition_set_query_parameters_roundtrip() {
776        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
777        let base = ReadCondition::new(
778            sample_state_mask::ANY,
779            view_state_mask::ANY,
780            instance_state_mask::ANY,
781            |_, _, _| false,
782        );
783        let qc = QueryCondition::new(base, "color = %0", alloc::vec!["RED".into()]).unwrap();
784        assert_eq!(qc.get_query_parameters(), alloc::vec!["RED".to_string()]);
785        qc.set_query_parameters(alloc::vec!["BLUE".into(), "GREEN".into()])
786            .unwrap();
787        assert_eq!(
788            qc.get_query_parameters(),
789            alloc::vec!["BLUE".to_string(), "GREEN".to_string()]
790        );
791    }
792
793    #[test]
794    fn query_condition_trigger_inherits_false_from_base() {
795        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
796        let base = ReadCondition::new(
797            sample_state_mask::READ,
798            view_state_mask::NOT_NEW,
799            instance_state_mask::NOT_ALIVE,
800            |_, _, _| false, // no matching samples
801        );
802        let qc = QueryCondition::new(base, "x > 0", alloc::vec::Vec::new()).unwrap();
803        assert!(!qc.get_trigger_value());
804    }
805
806    #[test]
807    fn query_condition_base_returns_correct_read_condition() {
808        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
809        let base = ReadCondition::new(
810            sample_state_mask::NOT_READ,
811            view_state_mask::NEW,
812            instance_state_mask::ALIVE,
813            |_, _, _| true,
814        );
815        let qc = QueryCondition::new(base.clone(), "x > 0", alloc::vec::Vec::new()).unwrap();
816        // base() returns the same Arc identity.
817        assert!(Arc::ptr_eq(&base, qc.base()));
818    }
819
820    #[test]
821    fn query_condition_rejects_invalid_sql() {
822        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
823        let base = ReadCondition::new(
824            sample_state_mask::ANY,
825            view_state_mask::ANY,
826            instance_state_mask::ANY,
827            |_, _, _| true,
828        );
829        let r = QueryCondition::new(base, "x > >", alloc::vec::Vec::new());
830        assert!(matches!(r, Err(DdsError::BadParameter { .. })));
831    }
832
833    #[test]
834    fn query_condition_evaluate_filters_per_sample() {
835        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
836        use zerodds_sql_filter::{RowAccess, Value};
837        struct R(i64);
838        impl RowAccess for R {
839            fn get(&self, p: &str) -> Option<Value> {
840                if p == "x" {
841                    Some(Value::Int(self.0))
842                } else {
843                    None
844                }
845            }
846        }
847        let base = ReadCondition::new(
848            sample_state_mask::ANY,
849            view_state_mask::ANY,
850            instance_state_mask::ANY,
851            |_, _, _| true,
852        );
853        let qc = QueryCondition::new(base, "x > 10", alloc::vec::Vec::new()).unwrap();
854        assert!(qc.evaluate(&R(42)).unwrap());
855        assert!(!qc.evaluate(&R(5)).unwrap());
856    }
857
858    #[test]
859    fn query_condition_evaluate_with_typed_values_int_param() {
860        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
861        use zerodds_sql_filter::{RowAccess, Value};
862        struct R(i64);
863        impl RowAccess for R {
864            fn get(&self, p: &str) -> Option<Value> {
865                if p == "x" {
866                    Some(Value::Int(self.0))
867                } else {
868                    None
869                }
870            }
871        }
872        let base = ReadCondition::new(
873            sample_state_mask::ANY,
874            view_state_mask::ANY,
875            instance_state_mask::ANY,
876            |_, _, _| true,
877        );
878        let qc = QueryCondition::new(base, "x > %0", alloc::vec::Vec::new()).unwrap();
879        assert!(qc.evaluate_with_values(&R(42), &[Value::Int(10)]).unwrap());
880        assert!(!qc.evaluate_with_values(&R(5), &[Value::Int(10)]).unwrap());
881    }
882
883    #[test]
884    fn query_condition_evaluate_uses_string_params_by_default() {
885        use crate::sample_info::{instance_state_mask, sample_state_mask, view_state_mask};
886        use zerodds_sql_filter::{RowAccess, Value};
887        struct R(alloc::string::String);
888        impl RowAccess for R {
889            fn get(&self, p: &str) -> Option<Value> {
890                if p == "color" {
891                    Some(Value::String(self.0.clone()))
892                } else {
893                    None
894                }
895            }
896        }
897        let base = ReadCondition::new(
898            sample_state_mask::ANY,
899            view_state_mask::ANY,
900            instance_state_mask::ANY,
901            |_, _, _| true,
902        );
903        let qc = QueryCondition::new(base, "color = %0", alloc::vec!["RED".into()]).unwrap();
904        assert!(qc.evaluate(&R("RED".into())).unwrap());
905        assert!(!qc.evaluate(&R("BLUE".into())).unwrap());
906    }
907
908    #[test]
909    fn waitset_panic_in_wait_releases_waiting_flag() {
910        // If a caller aborts wait() via a panicking
911        // Condition::get_trigger_value, the waiting flag must still be
912        // released (RAII guard). Modeled by a drop test of the wait
913        // guard: we simulate with a normal timeout return and check
914        // that a new wait is possible afterward (no hard lock).
915        let ws = WaitSet::new();
916        let _ = ws.wait(Duration::from_millis(5));
917        // A sequential wait is allowed → the waiting flag went back to
918        // 0 after the guard was dropped.
919        let r = ws.wait(Duration::from_millis(5));
920        assert!(matches!(r, Err(DdsError::Timeout)));
921    }
922}