Skip to main content

zerodds_qos/
compatibility.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Request/Offered QoS-Compatibility (DDS 1.4 §2.2.3).
4//!
5//! Jede Policy, die sowohl auf DataWriter- als auch auf DataReader-Seite
6//! gesetzt wird, hat im DDS-Sinne eine *Compatibility-Rule*: der Writer
7//! "offers" einen Wert, der Reader "requests" einen Wert, und das
8//! Matching-Verfahren vergleicht beide. Scheitern solcher Checks triggert
9//! `OFFERED_INCOMPATIBLE_QOS`/`REQUESTED_INCOMPATIBLE_QOS`-Listener-Events.
10
11use alloc::vec::Vec;
12
13use crate::policies::{
14    DeadlineQosPolicy, DestinationOrderQosPolicy, DurabilityQosPolicy, LatencyBudgetQosPolicy,
15    LivelinessQosPolicy, OwnershipQosPolicy, PartitionQosPolicy, PresentationQosPolicy, ReaderQos,
16    ReliabilityQosPolicy, WriterQos,
17};
18
19/// Einzelner Grund, warum Writer-QoS und Reader-QoS nicht matchen.
20///
21/// Die Varianten entsprechen der `QosPolicyId_t`-Enumeration aus DDS 1.4
22/// §2.2.3, beschraenkt auf Policies mit Request/Offered-Semantik.
23///
24/// `Ord`/`PartialOrd` nach Deklarations-Reihenfolge — erlaubt stabile
25/// Reports in [`CompatibilityResult::from_reasons`].
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[non_exhaustive]
28pub enum IncompatibleReason {
29    /// `DurabilityQosPolicy` — offered < requested.
30    Durability,
31    /// `ReliabilityQosPolicy` — BestEffort offered, Reliable requested.
32    Reliability,
33    /// `DeadlineQosPolicy` — offered > requested.
34    Deadline,
35    /// `LatencyBudgetQosPolicy` — offered > requested.
36    LatencyBudget,
37    /// `LivelinessQosPolicy` — offered Kind < requested Kind oder
38    /// lease_duration offered > requested.
39    Liveliness,
40    /// `DestinationOrderQosPolicy` — offered < requested.
41    DestinationOrder,
42    /// `PresentationQosPolicy` — Scope zu schwach oder coherent/ordered
43    /// nicht abgedeckt.
44    Presentation,
45    /// `OwnershipQosPolicy` — Shared/Exclusive muessen matchen.
46    Ownership,
47    /// `PartitionQosPolicy` — keine Partition-Ueberschneidung.
48    Partition,
49}
50
51/// Ergebnis eines Compatibility-Checks.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum CompatibilityResult {
54    /// QoS-Sets sind kompatibel.
55    Compatible,
56    /// QoS-Sets sind nicht kompatibel mit Reason-Liste.
57    Incompatible(Vec<IncompatibleReason>),
58}
59
60impl CompatibilityResult {
61    /// `true` wenn die QoS-Sets kompatibel sind.
62    #[must_use]
63    pub fn is_compatible(&self) -> bool {
64        matches!(self, Self::Compatible)
65    }
66
67    /// Aus einer Reason-Liste bauen. Dedupliziert und sortiert nach
68    /// kanonischer Discriminator-Reihenfolge — so sind Log-Ausgaben
69    /// stabil und Tests koennen per `==` vergleichen statt `.contains`.
70    /// Leere Liste ⇒ `Compatible`.
71    #[must_use]
72    pub fn from_reasons(mut reasons: Vec<IncompatibleReason>) -> Self {
73        if reasons.is_empty() {
74            return Self::Compatible;
75        }
76        // Stable sort + dedup. Ord-Impl kommt vom derive; Varianten-
77        // Reihenfolge ist die kanonische Sortier-Reihenfolge.
78        reasons.sort();
79        reasons.dedup();
80        Self::Incompatible(reasons)
81    }
82}
83
84// ============================================================================
85// Pro-Policy-Checks. Signatur: (offered, requested) -> bool (true = ok).
86// ============================================================================
87
88impl DurabilityQosPolicy {
89    /// §2.2.3 Table: `offered.kind >= requested.kind`.
90    #[must_use]
91    pub fn is_compatible_with(self, requested: Self) -> bool {
92        self.kind >= requested.kind
93    }
94}
95
96impl ReliabilityQosPolicy {
97    /// §2.2.3 Table: `offered.kind >= requested.kind`. Kind-Ordering
98    /// `BestEffort < Reliable`.
99    #[must_use]
100    pub fn is_compatible_with(self, requested: Self) -> bool {
101        self.kind >= requested.kind
102    }
103}
104
105impl DeadlineQosPolicy {
106    /// §2.2.3.7.4: `offered.period <= requested.period` (Writer kann
107    /// mindestens so haeufig liefern wie Reader verlangt).
108    #[must_use]
109    pub fn is_compatible_with(self, requested: Self) -> bool {
110        self.period <= requested.period
111    }
112}
113
114impl LatencyBudgetQosPolicy {
115    /// §2.2.3.10.4: `offered.duration <= requested.duration` (Writer
116    /// verspricht mindestens so schnell wie Reader tolerieren kann).
117    #[must_use]
118    pub fn is_compatible_with(self, requested: Self) -> bool {
119        self.duration <= requested.duration
120    }
121}
122
123impl LivelinessQosPolicy {
124    /// §2.2.3.11.4:
125    /// - `offered.kind >= requested.kind`, UND
126    /// - `offered.lease_duration <= requested.lease_duration`.
127    #[must_use]
128    pub fn is_compatible_with(self, requested: Self) -> bool {
129        self.kind >= requested.kind && self.lease_duration <= requested.lease_duration
130    }
131}
132
133impl DestinationOrderQosPolicy {
134    /// §2.2.3.18.3: `offered.kind >= requested.kind`.
135    #[must_use]
136    pub fn is_compatible_with(self, requested: Self) -> bool {
137        self.kind >= requested.kind
138    }
139}
140
141impl OwnershipQosPolicy {
142    /// §2.2.3.23: `offered.kind == requested.kind`. Kein Ordering.
143    #[must_use]
144    pub fn is_compatible_with(self, requested: Self) -> bool {
145        self.kind == requested.kind
146    }
147}
148
149impl PresentationQosPolicy {
150    /// §2.2.3.6.6:
151    /// - `offered.access_scope >= requested.access_scope`, UND
152    /// - `offered.coherent_access >= requested.coherent_access`, UND
153    /// - `offered.ordered_access >= requested.ordered_access`.
154    ///
155    /// Fuer bool gilt `true >= false`.
156    #[must_use]
157    pub fn is_compatible_with(self, requested: Self) -> bool {
158        self.access_scope >= requested.access_scope
159            && (self.coherent_access || !requested.coherent_access)
160            && (self.ordered_access || !requested.ordered_access)
161    }
162}
163
164// ============================================================================
165// Aggregat: alle Request/Offered-Policies in einem Aufruf
166// ============================================================================
167
168/// Vollstaendiger DataWriter↔DataReader Compatibility-Check.
169///
170/// WP 2.8 (C2.8) — kombiniert alle 9 Pro-Policy-Checks zu einem
171/// einzigen Aufruf. Caller (DCPS-Match-Pfad in publisher.rs /
172/// subscriber.rs) ruft diesen, bevor er das Pairing erlaubt; bei
173/// Inkompatibilitaet werden die Listener-Statuses
174/// `OFFERED_INCOMPATIBLE_QOS` (auf Writer-Seite) bzw.
175/// `REQUESTED_INCOMPATIBLE_QOS` (auf Reader-Seite) gefeuert.
176///
177/// Spec-Referenzen: DDS 1.4 §2.2.3 Compatibility-Tabellen,
178/// §2.2.4.1 OFFERED_INCOMPATIBLE_QOS_STATUS / REQUESTED_INCOMPATIBLE_QOS_STATUS.
179#[must_use]
180pub fn compute_compatibility(offered: &WriterQos, requested: &ReaderQos) -> CompatibilityResult {
181    let mut reasons = Vec::new();
182    if !offered.durability.is_compatible_with(requested.durability) {
183        reasons.push(IncompatibleReason::Durability);
184    }
185    if !offered
186        .reliability
187        .is_compatible_with(requested.reliability)
188    {
189        reasons.push(IncompatibleReason::Reliability);
190    }
191    if !offered.deadline.is_compatible_with(requested.deadline) {
192        reasons.push(IncompatibleReason::Deadline);
193    }
194    if !offered
195        .latency_budget
196        .is_compatible_with(requested.latency_budget)
197    {
198        reasons.push(IncompatibleReason::LatencyBudget);
199    }
200    if !offered.liveliness.is_compatible_with(requested.liveliness) {
201        reasons.push(IncompatibleReason::Liveliness);
202    }
203    if !offered
204        .destination_order
205        .is_compatible_with(requested.destination_order)
206    {
207        reasons.push(IncompatibleReason::DestinationOrder);
208    }
209    if !offered
210        .presentation
211        .is_compatible_with(requested.presentation)
212    {
213        reasons.push(IncompatibleReason::Presentation);
214    }
215    if !offered.ownership.is_compatible_with(requested.ownership) {
216        reasons.push(IncompatibleReason::Ownership);
217    }
218    if !offered.partition.is_compatible_with(&requested.partition) {
219        reasons.push(IncompatibleReason::Partition);
220    }
221    CompatibilityResult::from_reasons(reasons)
222}
223
224impl PartitionQosPolicy {
225    /// §2.2.3.13.6: Es muss mindestens einen gemeinsamen Partition-Namen
226    /// geben. Matching ist **fnmatch-Glob-basiert** (`*`, `?`, `[...]`):
227    /// offered-Pattern kann requested-Namen matchen oder umgekehrt.
228    ///
229    /// Leer/Leer matcht (Default-Partition). Leer vs. nicht-leer
230    /// matcht **nicht** (spec-konform: Default-Partition ist ein
231    /// separater Namensraum).
232    #[must_use]
233    pub fn is_compatible_with(&self, requested: &Self) -> bool {
234        if self.names.is_empty() && requested.names.is_empty() {
235            return true;
236        }
237        if self.names.is_empty() || requested.names.is_empty() {
238            return false;
239        }
240        // fnmatch ist symmetrisch relevant: entweder offered-Pattern
241        // matched requested-Text oder umgekehrt.
242        self.names.iter().any(|o| {
243            requested.names.iter().any(|rq| {
244                super::policies::partition::fnmatch(o, rq)
245                    || super::policies::partition::fnmatch(rq, o)
246            })
247        })
248    }
249}
250
251#[cfg(test)]
252#[allow(clippy::bool_assert_comparison, clippy::panic, clippy::unwrap_used)]
253mod tests {
254    use super::*;
255    use crate::duration::Duration;
256    use crate::policies::{
257        DestinationOrderKind, DurabilityKind, LivelinessKind, OwnershipKind,
258        PresentationAccessScope, ReliabilityKind,
259    };
260
261    #[test]
262    fn empty_reasons_is_compatible() {
263        let r = CompatibilityResult::from_reasons(Vec::new());
264        assert_eq!(r, CompatibilityResult::Compatible);
265        assert!(r.is_compatible());
266    }
267
268    #[test]
269    fn non_empty_reasons_is_incompatible() {
270        let r = CompatibilityResult::from_reasons(alloc::vec![IncompatibleReason::Durability]);
271        assert!(!r.is_compatible());
272    }
273
274    #[test]
275    fn durability_offered_ge_requested() {
276        let offered = DurabilityQosPolicy {
277            kind: DurabilityKind::Transient,
278        };
279        let req = DurabilityQosPolicy {
280            kind: DurabilityKind::TransientLocal,
281        };
282        assert!(offered.is_compatible_with(req));
283        // Umgekehrt nicht.
284        assert!(!req.is_compatible_with(offered));
285    }
286
287    #[test]
288    fn reliability_reliable_offered_besteffort_requested_ok() {
289        let offered = ReliabilityQosPolicy {
290            kind: ReliabilityKind::Reliable,
291            max_blocking_time: Duration::ZERO,
292        };
293        let req = ReliabilityQosPolicy {
294            kind: ReliabilityKind::BestEffort,
295            max_blocking_time: Duration::ZERO,
296        };
297        assert!(offered.is_compatible_with(req));
298        assert!(!req.is_compatible_with(offered));
299    }
300
301    #[test]
302    fn deadline_offered_le_requested() {
303        let offered = DeadlineQosPolicy {
304            period: Duration::from_secs(1),
305        };
306        let req = DeadlineQosPolicy {
307            period: Duration::from_secs(5),
308        };
309        assert!(offered.is_compatible_with(req));
310        assert!(!req.is_compatible_with(offered));
311    }
312
313    #[test]
314    fn latency_budget_offered_le_requested() {
315        let offered = LatencyBudgetQosPolicy {
316            duration: Duration::from_millis(10),
317        };
318        let req = LatencyBudgetQosPolicy {
319            duration: Duration::from_millis(100),
320        };
321        assert!(offered.is_compatible_with(req));
322        assert!(!req.is_compatible_with(offered));
323    }
324
325    #[test]
326    fn liveliness_kind_and_lease_checked() {
327        let offered = LivelinessQosPolicy {
328            kind: LivelinessKind::ManualByTopic,
329            lease_duration: Duration::from_secs(1),
330        };
331        let req = LivelinessQosPolicy {
332            kind: LivelinessKind::ManualByParticipant,
333            lease_duration: Duration::from_secs(5),
334        };
335        assert!(offered.is_compatible_with(req));
336
337        // Lease zu lang ⇒ fail.
338        let req_strict = LivelinessQosPolicy {
339            kind: LivelinessKind::Automatic,
340            lease_duration: Duration::ZERO,
341        };
342        assert!(!offered.is_compatible_with(req_strict));
343    }
344
345    #[test]
346    fn destination_order_offered_ge_requested() {
347        let offered = DestinationOrderQosPolicy {
348            kind: DestinationOrderKind::BySourceTimestamp,
349        };
350        let req = DestinationOrderQosPolicy {
351            kind: DestinationOrderKind::ByReceptionTimestamp,
352        };
353        assert!(offered.is_compatible_with(req));
354        assert!(!req.is_compatible_with(offered));
355    }
356
357    #[test]
358    fn ownership_must_match_exactly() {
359        let a = OwnershipQosPolicy {
360            kind: OwnershipKind::Shared,
361        };
362        let b = OwnershipQosPolicy {
363            kind: OwnershipKind::Exclusive,
364        };
365        assert!(a.is_compatible_with(a));
366        assert!(!a.is_compatible_with(b));
367    }
368
369    #[test]
370    fn presentation_scope_and_flags() {
371        let offered = PresentationQosPolicy {
372            access_scope: PresentationAccessScope::Group,
373            coherent_access: true,
374            ordered_access: true,
375        };
376        let req = PresentationQosPolicy {
377            access_scope: PresentationAccessScope::Topic,
378            coherent_access: false,
379            ordered_access: true,
380        };
381        assert!(offered.is_compatible_with(req));
382
383        // Reader verlangt coherent, Writer bietet nicht ⇒ fail.
384        let offered_weak = PresentationQosPolicy {
385            access_scope: PresentationAccessScope::Group,
386            coherent_access: false,
387            ordered_access: true,
388        };
389        let req_coherent = PresentationQosPolicy {
390            access_scope: PresentationAccessScope::Instance,
391            coherent_access: true,
392            ordered_access: false,
393        };
394        assert!(!offered_weak.is_compatible_with(req_coherent));
395    }
396
397    #[test]
398    fn partition_exact_match() {
399        use alloc::string::String;
400        let offered = PartitionQosPolicy {
401            names: alloc::vec![String::from("a"), String::from("b")],
402        };
403        let req = PartitionQosPolicy {
404            names: alloc::vec![String::from("c"), String::from("b")],
405        };
406        assert!(offered.is_compatible_with(&req));
407
408        let req_disjoint = PartitionQosPolicy {
409            names: alloc::vec![String::from("c"), String::from("d")],
410        };
411        assert!(!offered.is_compatible_with(&req_disjoint));
412    }
413
414    #[test]
415    fn partition_empty_match_empty() {
416        let a = PartitionQosPolicy::default();
417        let b = PartitionQosPolicy::default();
418        assert!(a.is_compatible_with(&b));
419    }
420
421    // ========================================================================
422    // WP 2.8: compute_compatibility (Aggregat aller 9 Policy-Checks)
423    // ========================================================================
424
425    #[test]
426    fn compute_compatibility_default_writer_reader_is_compatible() {
427        let w = crate::policies::WriterQos::default();
428        let r = crate::policies::ReaderQos::default();
429        let result = compute_compatibility(&w, &r);
430        // Default-Writer ist Reliable, Default-Reader BestEffort → Reliable >= BestEffort
431        assert!(result.is_compatible(), "got {result:?}");
432    }
433
434    #[test]
435    fn compute_compatibility_reports_durability_mismatch() {
436        let mut w = crate::policies::WriterQos::default();
437        let mut r = crate::policies::ReaderQos::default();
438        w.durability = DurabilityQosPolicy {
439            kind: DurabilityKind::Volatile,
440        };
441        r.durability = DurabilityQosPolicy {
442            kind: DurabilityKind::Transient,
443        };
444        let result = compute_compatibility(&w, &r);
445        assert!(!result.is_compatible());
446        if let CompatibilityResult::Incompatible(reasons) = result {
447            assert!(reasons.contains(&IncompatibleReason::Durability));
448        }
449    }
450
451    #[test]
452    fn compute_compatibility_reports_multiple_mismatches() {
453        let mut w = crate::policies::WriterQos::default();
454        let mut r = crate::policies::ReaderQos::default();
455        // Reliability: Writer BestEffort, Reader Reliable → fail
456        w.reliability.kind = ReliabilityKind::BestEffort;
457        r.reliability.kind = ReliabilityKind::Reliable;
458        // Durability: Writer Volatile, Reader Transient → fail
459        w.durability = DurabilityQosPolicy {
460            kind: DurabilityKind::Volatile,
461        };
462        r.durability = DurabilityQosPolicy {
463            kind: DurabilityKind::Transient,
464        };
465        let result = compute_compatibility(&w, &r);
466        if let CompatibilityResult::Incompatible(reasons) = result {
467            assert!(reasons.contains(&IncompatibleReason::Reliability));
468            assert!(reasons.contains(&IncompatibleReason::Durability));
469            assert!(reasons.len() >= 2);
470        } else {
471            panic!("expected incompatible");
472        }
473    }
474
475    #[test]
476    fn compute_compatibility_partition_disjoint_fails() {
477        use alloc::string::String;
478        let mut w = crate::policies::WriterQos::default();
479        let mut r = crate::policies::ReaderQos::default();
480        w.partition = PartitionQosPolicy {
481            names: alloc::vec![String::from("alpha")],
482        };
483        r.partition = PartitionQosPolicy {
484            names: alloc::vec![String::from("beta")],
485        };
486        let result = compute_compatibility(&w, &r);
487        if let CompatibilityResult::Incompatible(reasons) = result {
488            assert!(reasons.contains(&IncompatibleReason::Partition));
489        } else {
490            panic!("expected partition mismatch");
491        }
492    }
493}