Skip to main content

bacnet_server/
fault_detection.rs

1//! Fault detection / reliability evaluation.
2//!
3//! The [`FaultDetector`] periodically invokes each object's opt-in,
4//! object-owned reliability evaluation hook.
5
6use std::collections::HashMap;
7use std::sync::Mutex;
8
9use bacnet_objects::database::ObjectDatabase;
10use bacnet_objects::traits::ReliabilityEvaluation;
11use bacnet_types::primitives::ObjectIdentifier;
12
13/// A reliability change detected by the fault detector.
14#[derive(Debug, Clone, PartialEq)]
15pub struct ReliabilityChange {
16    /// The object whose reliability changed.
17    pub object_id: ObjectIdentifier,
18    /// Previous reliability value (raw u32).
19    pub old_reliability: u32,
20    /// New reliability value (raw u32).
21    pub new_reliability: u32,
22}
23
24/// Fault detection engine.
25///
26/// Call [`FaultDetector::evaluate`] periodically (the bundled server uses a
27/// 10-second interval) against the object database. Every object is visited;
28/// objects opt in by overriding
29/// [`BACnetObject::evaluate_reliability_internal`](bacnet_objects::traits::BACnetObject::evaluate_reliability_internal).
30/// The default object hook is a no-op.
31pub struct FaultDetector {
32    /// Timeout after which an object is considered to have a communication
33    /// failure. Set to `None` to disable communication-failure detection.
34    pub comm_timeout: Option<std::time::Duration>,
35    warned_internal_failures: Mutex<HashMap<ObjectIdentifier, String>>,
36}
37
38impl Default for FaultDetector {
39    fn default() -> Self {
40        Self {
41            comm_timeout: Some(std::time::Duration::from_secs(60)),
42            warned_internal_failures: Mutex::new(HashMap::new()),
43        }
44    }
45}
46
47impl FaultDetector {
48    /// Create a new fault detector with the given communication timeout.
49    pub fn new(comm_timeout: Option<std::time::Duration>) -> Self {
50        Self {
51            comm_timeout,
52            warned_internal_failures: Mutex::new(HashMap::new()),
53        }
54    }
55
56    fn should_warn_internal_failure(&self, oid: ObjectIdentifier, error: &str) -> bool {
57        let mut warned = self
58            .warned_internal_failures
59            .lock()
60            .unwrap_or_else(|poisoned| poisoned.into_inner());
61        if warned.get(&oid).is_some_and(|previous| previous == error) {
62            false
63        } else {
64            warned.insert(oid, error.to_owned());
65            true
66        }
67    }
68
69    fn clear_internal_failure(&self, oid: &ObjectIdentifier) {
70        self.warned_internal_failures
71            .lock()
72            .unwrap_or_else(|poisoned| poisoned.into_inner())
73            .remove(oid);
74    }
75
76    fn prune_internal_failures(&self, db: &ObjectDatabase) {
77        self.warned_internal_failures
78            .lock()
79            .unwrap_or_else(|poisoned| poisoned.into_inner())
80            .retain(|oid, _| db.get(oid).is_some());
81    }
82
83    /// Evaluate reliability for all objects in the database.
84    ///
85    /// Each object owns both the decision and any mutation. Engineering
86    /// `Min_Pres_Value` / `Max_Pres_Value` metadata is not a reliability fault
87    /// algorithm. Only a successful hook result of
88    /// [`ReliabilityEvaluation::Changed`] produces a [`ReliabilityChange`].
89    /// Hook errors are rate-limited per object and produce no change record.
90    ///
91    /// Returns a list of changes that object hooks successfully applied.
92    pub fn evaluate(&self, db: &mut ObjectDatabase) -> Vec<ReliabilityChange> {
93        let mut changes = Vec::new();
94        db.for_each_object_mut(|oid, object| {
95            if object.reliability_evaluation_inhibited_internal() {
96                self.clear_internal_failure(&oid);
97                return;
98            }
99            match object.evaluate_reliability_internal() {
100                Ok(ReliabilityEvaluation::Unchanged) => {
101                    self.clear_internal_failure(&oid);
102                }
103                Ok(ReliabilityEvaluation::Changed {
104                    old_reliability,
105                    new_reliability,
106                }) => {
107                    self.clear_internal_failure(&oid);
108                    changes.push(ReliabilityChange {
109                        object_id: oid,
110                        old_reliability,
111                        new_reliability,
112                    });
113                }
114                Err(error) => {
115                    let error_text = error.to_string();
116                    if self.should_warn_internal_failure(oid, &error_text) {
117                        tracing::warn!(
118                            object = %oid,
119                            error = %error,
120                            "Fault detection object-owned reliability evaluation failed"
121                        );
122                    }
123                }
124            }
125        });
126        self.prune_internal_failures(db);
127        changes
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use std::borrow::Cow;
134    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
135    use std::sync::Arc;
136
137    use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
138    use bacnet_objects::traits::BACnetObject;
139    use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier, Reliability};
140    use bacnet_types::error::Error;
141    use bacnet_types::primitives::PropertyValue;
142    use tracing::span::{Attributes, Id, Record};
143    use tracing::{Event, Level, Metadata, Subscriber};
144
145    use super::*;
146
147    const HOOK_ERROR: &str = "test reliability hook failed";
148
149    struct WarningCounter {
150        warnings: Arc<AtomicUsize>,
151    }
152
153    impl Subscriber for WarningCounter {
154        fn enabled(&self, metadata: &Metadata<'_>) -> bool {
155            *metadata.level() == Level::WARN
156        }
157
158        fn new_span(&self, _span: &Attributes<'_>) -> Id {
159            Id::from_u64(1)
160        }
161
162        fn record(&self, _span: &Id, _values: &Record<'_>) {}
163
164        fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
165
166        fn event(&self, event: &Event<'_>) {
167            if *event.metadata().level() == Level::WARN {
168                self.warnings.fetch_add(1, Ordering::SeqCst);
169            }
170        }
171
172        fn enter(&self, _span: &Id) {}
173
174        fn exit(&self, _span: &Id) {}
175    }
176
177    fn with_warning_counter(test: impl FnOnce(&AtomicUsize)) {
178        let warnings = Arc::new(AtomicUsize::new(0));
179        let subscriber = WarningCounter {
180            warnings: Arc::clone(&warnings),
181        };
182        tracing::subscriber::with_default(subscriber, || test(warnings.as_ref()));
183    }
184
185    fn read_reliability(db: &ObjectDatabase, oid: ObjectIdentifier) -> u32 {
186        match db
187            .get(&oid)
188            .unwrap()
189            .read_property(PropertyIdentifier::RELIABILITY, None)
190            .unwrap()
191        {
192            PropertyValue::Enumerated(value) => value,
193            other => panic!("expected Enumerated Reliability, got {other:?}"),
194        }
195    }
196
197    struct OptInReliabilityObject {
198        oid: ObjectIdentifier,
199        name: String,
200        reliability: u32,
201        target_reliability: u32,
202        fail: Arc<AtomicBool>,
203    }
204
205    impl OptInReliabilityObject {
206        fn new(instance: u32, name: &str, target_reliability: u32, fail: Arc<AtomicBool>) -> Self {
207            Self {
208                oid: ObjectIdentifier::new(ObjectType::BINARY_INPUT, instance).unwrap(),
209                name: name.to_owned(),
210                reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
211                target_reliability,
212                fail,
213            }
214        }
215    }
216
217    impl BACnetObject for OptInReliabilityObject {
218        fn object_identifier(&self) -> ObjectIdentifier {
219            self.oid
220        }
221
222        fn object_name(&self) -> &str {
223            &self.name
224        }
225
226        fn read_property(
227            &self,
228            property: PropertyIdentifier,
229            _array_index: Option<u32>,
230        ) -> Result<PropertyValue, Error> {
231            if property == PropertyIdentifier::RELIABILITY {
232                Ok(PropertyValue::Enumerated(self.reliability))
233            } else {
234                Err(Error::Encoding("test property is unsupported".into()))
235            }
236        }
237
238        fn write_property(
239            &mut self,
240            _property: PropertyIdentifier,
241            _array_index: Option<u32>,
242            _value: PropertyValue,
243            _priority: Option<u8>,
244        ) -> Result<(), Error> {
245            Err(Error::Encoding("test object is read-only".into()))
246        }
247
248        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
249            Cow::Borrowed(&[PropertyIdentifier::RELIABILITY])
250        }
251
252        fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
253            if self.fail.load(Ordering::SeqCst) {
254                return Err(Error::Encoding(HOOK_ERROR.into()));
255            }
256            if self.reliability == self.target_reliability {
257                return Ok(ReliabilityEvaluation::Unchanged);
258            }
259
260            let old_reliability = self.reliability;
261            self.reliability = self.target_reliability;
262            Ok(ReliabilityEvaluation::Changed {
263                old_reliability,
264                new_reliability: self.reliability,
265            })
266        }
267    }
268
269    struct InhibitedProbeObject {
270        oid: ObjectIdentifier,
271        calls: Arc<AtomicUsize>,
272    }
273
274    impl BACnetObject for InhibitedProbeObject {
275        fn object_identifier(&self) -> ObjectIdentifier {
276            self.oid
277        }
278
279        fn object_name(&self) -> &str {
280            "inhibited-probe"
281        }
282
283        fn read_property(
284            &self,
285            _property: PropertyIdentifier,
286            _array_index: Option<u32>,
287        ) -> Result<PropertyValue, Error> {
288            Err(Error::Encoding("test property is unsupported".into()))
289        }
290
291        fn write_property(
292            &mut self,
293            _property: PropertyIdentifier,
294            _array_index: Option<u32>,
295            _value: PropertyValue,
296            _priority: Option<u8>,
297        ) -> Result<(), Error> {
298            Err(Error::Encoding("test object is read-only".into()))
299        }
300
301        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
302            Cow::Borrowed(&[])
303        }
304
305        fn reliability_evaluation_inhibited_internal(&self) -> bool {
306            true
307        }
308
309        fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
310            self.calls.fetch_add(1, Ordering::SeqCst);
311            Err(Error::Encoding(HOOK_ERROR.into()))
312        }
313    }
314
315    #[test]
316    fn inhibited_object_skips_hook_and_clears_stale_failure_warning() {
317        let detector = FaultDetector::default();
318        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 77).unwrap();
319        let calls = Arc::new(AtomicUsize::new(0));
320        let mut db = ObjectDatabase::new();
321        db.add(Box::new(InhibitedProbeObject {
322            oid,
323            calls: Arc::clone(&calls),
324        }))
325        .unwrap();
326        assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
327
328        assert!(detector.evaluate(&mut db).is_empty());
329        assert_eq!(calls.load(Ordering::SeqCst), 0, "inhibited hook was called");
330        assert!(
331            detector.should_warn_internal_failure(oid, HOOK_ERROR),
332            "inhibition must clear an old warning so recovery cannot leave stale state"
333        );
334    }
335
336    #[test]
337    fn identical_internal_failure_warning_is_suppressed_until_state_changes() {
338        let detector = FaultDetector::default();
339        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
340
341        assert!(detector.should_warn_internal_failure(oid, "first error"));
342        assert!(!detector.should_warn_internal_failure(oid, "first error"));
343        assert!(detector.should_warn_internal_failure(oid, "different error"));
344        assert!(!detector.should_warn_internal_failure(oid, "different error"));
345
346        detector.clear_internal_failure(&oid);
347        assert!(detector.should_warn_internal_failure(oid, "first error"));
348    }
349
350    #[test]
351    fn stock_analog_engineering_bounds_never_change_reliability() {
352        let mut db = ObjectDatabase::new();
353
354        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
355        ai.set_min_pres_value(0.0);
356        ai.set_max_pres_value(100.0);
357        ai.set_present_value(200.0);
358        let ai_oid = ai.object_identifier();
359        db.add(Box::new(ai)).unwrap();
360
361        let mut ao = AnalogOutputObject::new(1, "AO-1", 62).unwrap();
362        ao.set_min_pres_value(0.0);
363        ao.set_max_pres_value(100.0);
364        ao.write_property(
365            PropertyIdentifier::PRESENT_VALUE,
366            None,
367            PropertyValue::Real(150.0),
368            Some(8),
369        )
370        .unwrap();
371        let ao_oid = ao.object_identifier();
372        db.add(Box::new(ao)).unwrap();
373
374        let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
375        av.set_min_pres_value(0.0);
376        av.set_max_pres_value(100.0);
377        av.set_present_value(-10.0);
378        let av_oid = av.object_identifier();
379        db.add(Box::new(av)).unwrap();
380
381        let changes = FaultDetector::default().evaluate(&mut db);
382
383        assert!(changes.is_empty());
384        for oid in [ai_oid, ao_oid, av_oid] {
385            assert_eq!(
386                read_reliability(&db, oid),
387                Reliability::NO_FAULT_DETECTED.to_raw()
388            );
389        }
390        let ao = db.get(&ao_oid).unwrap();
391        assert_eq!(
392            ao.read_property(PropertyIdentifier::PRESENT_VALUE, None)
393                .unwrap(),
394            PropertyValue::Real(150.0)
395        );
396        assert_eq!(
397            ao.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(8))
398                .unwrap(),
399            PropertyValue::Real(150.0)
400        );
401    }
402
403    #[test]
404    fn configured_ai_and_av_changes_are_recorded_and_seen_by_intrinsic_fault_observation() {
405        let mut db = ObjectDatabase::new();
406
407        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
408        ai.configure_fault_out_of_range(10.0, 20.0).unwrap();
409        ai.set_present_value(21.0);
410        let ai_oid = ai.object_identifier();
411        db.add(Box::new(ai)).unwrap();
412
413        let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
414        av.configure_fault_out_of_range(10.0, 20.0).unwrap();
415        av.write_property(
416            PropertyIdentifier::PRESENT_VALUE,
417            None,
418            PropertyValue::Real(9.0),
419            Some(8),
420        )
421        .unwrap();
422        let av_oid = av.object_identifier();
423        db.add(Box::new(av)).unwrap();
424
425        let detector = FaultDetector::default();
426        let changes = detector.evaluate(&mut db);
427        assert_eq!(changes.len(), 2);
428        assert!(changes.contains(&ReliabilityChange {
429            object_id: ai_oid,
430            old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
431            new_reliability: Reliability::OVER_RANGE.to_raw(),
432        }));
433        assert!(changes.contains(&ReliabilityChange {
434            object_id: av_oid,
435            old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
436            new_reliability: Reliability::UNDER_RANGE.to_raw(),
437        }));
438        assert!(detector.evaluate(&mut db).is_empty());
439
440        for oid in [ai_oid, av_oid] {
441            let proposal = db
442                .get_mut(&oid)
443                .unwrap()
444                .evaluate_intrinsic_reporting()
445                .expect("range Reliability must be observed as an intrinsic fault");
446            assert_eq!(proposal.change.from, EventState::NORMAL);
447            assert_eq!(proposal.change.to, EventState::FAULT);
448        }
449    }
450
451    // The public sensor setters intentionally retain their debug assertions.
452    // This release-only test exercises the production path where those setters
453    // can receive non-finite application data without changing their API.
454    #[cfg(not(debug_assertions))]
455    #[test]
456    fn non_finite_configured_analogs_create_no_change_and_keep_owned_faults() {
457        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
458        ai.configure_fault_out_of_range(10.0, 20.0).unwrap();
459        ai.set_present_value(21.0);
460        ai.evaluate_reliability_internal().unwrap();
461        ai.set_present_value(f32::NAN);
462        let ai_oid = ai.object_identifier();
463
464        let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
465        av.configure_fault_out_of_range(10.0, 20.0).unwrap();
466        av.set_present_value(9.0);
467        av.evaluate_reliability_internal().unwrap();
468        av.set_present_value(f32::INFINITY);
469        let av_oid = av.object_identifier();
470
471        let mut db = ObjectDatabase::new();
472        db.add(Box::new(ai)).unwrap();
473        db.add(Box::new(av)).unwrap();
474        let detector = FaultDetector::default();
475
476        assert!(detector.evaluate(&mut db).is_empty());
477        assert!(detector.evaluate(&mut db).is_empty());
478        assert_eq!(
479            read_reliability(&db, ai_oid),
480            Reliability::OVER_RANGE.to_raw()
481        );
482        assert_eq!(
483            read_reliability(&db, av_oid),
484            Reliability::UNDER_RANGE.to_raw()
485        );
486
487        let ai = db.get_mut(&ai_oid).unwrap();
488        ai.write_property(
489            PropertyIdentifier::OUT_OF_SERVICE,
490            None,
491            PropertyValue::Boolean(true),
492            None,
493        )
494        .unwrap();
495        ai.write_property(
496            PropertyIdentifier::PRESENT_VALUE,
497            None,
498            PropertyValue::Real(9.0),
499            None,
500        )
501        .unwrap();
502        ai.write_property(
503            PropertyIdentifier::OUT_OF_SERVICE,
504            None,
505            PropertyValue::Boolean(false),
506            None,
507        )
508        .unwrap();
509        db.get_mut(&av_oid)
510            .unwrap()
511            .write_property(
512                PropertyIdentifier::PRESENT_VALUE,
513                None,
514                PropertyValue::Real(21.0),
515                Some(8),
516            )
517            .unwrap();
518
519        let changes = detector.evaluate(&mut db);
520        assert_eq!(changes.len(), 2);
521        assert!(changes.contains(&ReliabilityChange {
522            object_id: ai_oid,
523            old_reliability: Reliability::OVER_RANGE.to_raw(),
524            new_reliability: Reliability::UNDER_RANGE.to_raw(),
525        }));
526        assert!(changes.contains(&ReliabilityChange {
527            object_id: av_oid,
528            old_reliability: Reliability::UNDER_RANGE.to_raw(),
529            new_reliability: Reliability::OVER_RANGE.to_raw(),
530        }));
531    }
532
533    #[test]
534    fn existing_nonzero_in_service_reliability_is_preserved() {
535        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
536        ai.set_min_pres_value(0.0);
537        ai.set_max_pres_value(100.0);
538        ai.set_present_value(50.0);
539        ai.set_reliability_internal(Reliability::NO_SENSOR.to_raw())
540            .unwrap();
541        let oid = ai.object_identifier();
542        let mut db = ObjectDatabase::new();
543        db.add(Box::new(ai)).unwrap();
544
545        assert!(FaultDetector::default().evaluate(&mut db).is_empty());
546        assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
547    }
548
549    #[test]
550    fn out_of_service_client_write_and_restore_remain_object_owned() {
551        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
552        ai.set_reliability_internal(Reliability::OVER_RANGE.to_raw())
553            .unwrap();
554        let oid = ai.object_identifier();
555        let mut db = ObjectDatabase::new();
556        db.add(Box::new(ai)).unwrap();
557
558        let object = db.get_mut(&oid).unwrap();
559        object
560            .write_property(
561                PropertyIdentifier::OUT_OF_SERVICE,
562                None,
563                PropertyValue::Boolean(true),
564                None,
565            )
566            .unwrap();
567        object
568            .write_property(
569                PropertyIdentifier::RELIABILITY,
570                None,
571                PropertyValue::Enumerated(Reliability::NO_SENSOR.to_raw()),
572                None,
573            )
574            .unwrap();
575
576        assert!(FaultDetector::default().evaluate(&mut db).is_empty());
577        assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
578
579        db.get_mut(&oid)
580            .unwrap()
581            .write_property(
582                PropertyIdentifier::OUT_OF_SERVICE,
583                None,
584                PropertyValue::Boolean(false),
585                None,
586            )
587            .unwrap();
588        assert_eq!(read_reliability(&db, oid), Reliability::OVER_RANGE.to_raw());
589    }
590
591    #[test]
592    fn only_hook_returned_changed_creates_a_change_record() {
593        let fail = Arc::new(AtomicBool::new(false));
594        let object =
595            OptInReliabilityObject::new(1, "custom-hook", Reliability::NO_SENSOR.to_raw(), fail);
596        let oid = object.object_identifier();
597        let mut db = ObjectDatabase::new();
598        db.add(Box::new(object)).unwrap();
599        let detector = FaultDetector::default();
600        assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
601
602        assert_eq!(
603            detector.evaluate(&mut db),
604            vec![ReliabilityChange {
605                object_id: oid,
606                old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
607                new_reliability: Reliability::NO_SENSOR.to_raw(),
608            }]
609        );
610        assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
611        assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
612        assert!(detector.evaluate(&mut db).is_empty());
613        assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
614    }
615
616    #[test]
617    fn failing_hook_warning_is_rate_limited_and_restored_after_recovery() {
618        let fail = Arc::new(AtomicBool::new(true));
619        let object = OptInReliabilityObject::new(
620            1,
621            "failing-hook",
622            Reliability::NO_SENSOR.to_raw(),
623            Arc::clone(&fail),
624        );
625        let oid = object.object_identifier();
626        let mut db = ObjectDatabase::new();
627        db.add(Box::new(object)).unwrap();
628        let detector = FaultDetector::default();
629
630        with_warning_counter(|warnings| {
631            assert!(detector.evaluate(&mut db).is_empty());
632            assert_eq!(warnings.load(Ordering::SeqCst), 1);
633            assert_eq!(
634                read_reliability(&db, oid),
635                Reliability::NO_FAULT_DETECTED.to_raw()
636            );
637
638            assert!(detector.evaluate(&mut db).is_empty());
639            assert_eq!(warnings.load(Ordering::SeqCst), 1);
640
641            fail.store(false, Ordering::SeqCst);
642            assert_eq!(
643                detector.evaluate(&mut db),
644                vec![ReliabilityChange {
645                    object_id: oid,
646                    old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
647                    new_reliability: Reliability::NO_SENSOR.to_raw(),
648                }]
649            );
650            assert_eq!(warnings.load(Ordering::SeqCst), 1);
651
652            fail.store(true, Ordering::SeqCst);
653            assert!(detector.evaluate(&mut db).is_empty());
654            assert_eq!(warnings.load(Ordering::SeqCst), 2);
655
656            fail.store(false, Ordering::SeqCst);
657            assert!(detector.evaluate(&mut db).is_empty());
658            assert_eq!(warnings.load(Ordering::SeqCst), 2);
659
660            fail.store(true, Ordering::SeqCst);
661            assert!(detector.evaluate(&mut db).is_empty());
662            assert_eq!(warnings.load(Ordering::SeqCst), 3);
663        });
664    }
665
666    #[test]
667    fn deleted_failure_warning_is_pruned_and_oid_reuse_warns() {
668        let fail = Arc::new(AtomicBool::new(true));
669        let object = OptInReliabilityObject::new(
670            1,
671            "removed-failing-hook",
672            Reliability::NO_FAULT_DETECTED.to_raw(),
673            Arc::clone(&fail),
674        );
675        let oid = object.object_identifier();
676        let mut db = ObjectDatabase::new();
677        db.add(Box::new(object)).unwrap();
678        let detector = FaultDetector::default();
679
680        with_warning_counter(|warnings| {
681            assert!(detector.evaluate(&mut db).is_empty());
682            assert_eq!(warnings.load(Ordering::SeqCst), 1);
683
684            assert!(detector.evaluate(&mut db).is_empty());
685            assert_eq!(warnings.load(Ordering::SeqCst), 1);
686
687            assert!(db.remove(&oid).is_some());
688            assert!(detector.evaluate(&mut db).is_empty());
689            assert_eq!(warnings.load(Ordering::SeqCst), 1);
690
691            db.add(Box::new(OptInReliabilityObject::new(
692                1,
693                "replacement-failing-hook",
694                Reliability::NO_FAULT_DETECTED.to_raw(),
695                fail,
696            )))
697            .unwrap();
698            assert!(detector.evaluate(&mut db).is_empty());
699            assert_eq!(warnings.load(Ordering::SeqCst), 2);
700        });
701    }
702}