Skip to main content

bacnet_objects/trend/
mod.rs

1//! TrendLog (type 20) and TrendLogMultiple (type 27) objects per ASHRAE 135-2020.
2
3use crate::prelude::*;
4use alloc::borrow::Cow;
5use alloc::collections::VecDeque;
6use alloc::sync::Arc;
7
8use bacnet_types::bitstring::EventTransitionBits;
9use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogRecord};
10use bacnet_types::enums::{
11    ErrorClass, ErrorCode, LoggingType, NotifyType, ObjectType, PropertyIdentifier, Reliability,
12};
13use bacnet_types::error::Error;
14use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
15
16use crate::clock::ClockReader;
17use crate::common::{self, read_property_list_property};
18use crate::log_buffer::{
19    LogBufferRecords, LogRecordBuffer, LogRecordIdentity, log_buffer_read_denied,
20};
21use crate::log_lifecycle::LogLifecycle;
22use crate::log_persistence::store::{DurableLog, LogStore};
23use crate::log_reporting::{BufferReadyReporting, impl_buffer_ready_reporting};
24use crate::log_window::LogWindow;
25use crate::traits::BACnetObject;
26use acquisition::{Acquisition, Rules};
27use cov_settings::CovSettings;
28
29mod acquisition;
30mod cov_settings;
31mod durable;
32mod metadata;
33mod multiple;
34mod multiple_metadata;
35mod references;
36
37pub use acquisition::DEFAULT_LOG_INTERVAL;
38pub use cov_settings::DEFAULT_COV_RESUBSCRIPTION_INTERVAL;
39pub use multiple::TrendLogMultipleObject;
40pub use references::MAX_LOG_DEVICE_OBJECT_PROPERTIES;
41
42/// BACnet TrendLog object (type 20, Clause 12.25).
43///
44/// Ring buffer of timestamped values of the one property
45/// Log_DeviceObjectProperty names. The database's trend poller samples it
46/// every Log_Interval while Logging_Type is POLLED, at clock-aligned times
47/// when Align_Intervals asks for them, and once for each Trigger while it is
48/// TRIGGERED, and while it is COV it logs each change of a property of this
49/// device as a COV notification would report it (#1480); the application may
50/// also call `add_record()`. Records are kept only while Enable is TRUE and
51/// the local time lies between Start_Time and Stop_Time.
52pub struct TrendLogObject {
53    oid: ObjectIdentifier,
54    name: String,
55    description: String,
56    log_enable: bool,
57    stop_when_full: bool,
58    buffer_size: u32,
59    log_buffer: LogRecordBuffer,
60    reliability: Reliability,
61    log_device_object_property: Option<BACnetDeviceObjectPropertyReference>,
62    acquisition: Acquisition,
63    cov: CovSettings,
64    window: LogWindow,
65    reporting: BufferReadyReporting,
66    clock: Option<Arc<dyn ClockReader>>,
67    /// The storage the log commits to, when built with `with_persistence`.
68    store: Option<LogStore<BACnetLogRecord>>,
69}
70
71impl TrendLogObject {
72    /// Create a new Trend Log object with logging enabled; `buffer_size` is the record capacity.
73    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
74        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG, instance)?;
75        Ok(Self {
76            oid,
77            name: name.into(),
78            description: String::new(),
79            log_enable: true,
80            stop_when_full: false,
81            buffer_size,
82            log_buffer: LogRecordBuffer::new(buffer_size),
83            reliability: Reliability::NO_FAULT_DETECTED,
84            log_device_object_property: None,
85            acquisition: Acquisition::new(Rules::TrendLog),
86            cov: CovSettings::default(),
87            window: LogWindow::default(),
88            reporting: BufferReadyReporting::default(),
89            clock: None,
90            store: None,
91        })
92    }
93
94    /// Add a BACnetLogRecord to the trend log buffer.
95    ///
96    /// Success does not guarantee a resident ordinary record: disabled logging
97    /// and a record outside the Start_Time / Stop_Time window are ignored,
98    /// zero-capacity logging may only count, and a stop-before-full
99    /// transition records status instead. Missing/invalid status clocks fail
100    /// atomically with DEVICE / OPERATIONAL_PROBLEM. A successful call serves
101    /// a pending Trigger, which reads FALSE again, even when the record is
102    /// ignored (Enable FALSE, or outside the window): the acquisition was
103    /// made.
104    pub fn add_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
105        self.settle_interruption(Some((record.date, record.time)));
106        self.lifecycle().try_add_ordinary(record)?;
107        self.acquisition.acquired();
108        self.commit_if_counted();
109        Ok(())
110    }
111
112    /// Get the current buffer contents.
113    pub fn records(&self) -> &VecDeque<BACnetLogRecord> {
114        self.log_buffer.records()
115    }
116
117    /// Total_Record_Count: the records collected since the log was created
118    /// or restored, going from 2^32 - 1 on to 1. The newest record in
119    /// [`records`](Self::records) carries this number. Save the two to
120    /// [restore](Self::restore_log_buffer) the log later.
121    pub fn total_record_count(&self) -> u32 {
122        self.log_buffer.total_record_count()
123    }
124
125    /// Restore the log buffer, as a device restarting with saved records
126    /// does, or seed Total_Record_Count with no records, as a test that
127    /// needs the count near its wrap does (#1537).
128    ///
129    /// `records` become the resident records, oldest first. The newest is
130    /// numbered `total_record_count` and each one before it one less, going
131    /// from 1 back to 2^32 - 1, so a count below the number of records is a
132    /// count that has wrapped. The records are kept as given, whatever
133    /// Enable and the Start_Time / Stop_Time window say, and nothing is
134    /// recorded for the restore. BUFFER_READY counts from the restored
135    /// count, as when detection starts.
136    ///
137    /// Fails with [`Error::OutOfRange`], or a record's encoding error,
138    /// leaving the log unchanged, when there are more records than
139    /// Buffer_Size, when records come with a count of zero, when Stop_When_Full
140    /// and Enable are both TRUE and the records fill the buffer (such a log
141    /// stops before its last slot is taken), or when a record would not
142    /// encode.
143    ///
144    /// Restore a log before adding it to an `ObjectDatabase`. A running
145    /// server reaches its objects only through `BACnetObject`, which offers
146    /// no restore on purpose: renumbering a log that peers are reading would
147    /// change what their sequence numbers mean, with no BUFFER_PURGED record
148    /// to tell them, and the BUFFER_READY reports already sent would name
149    /// counts that no longer hold.
150    ///
151    /// A device restoring its saved records after a restart then calls
152    /// [`record_interruption`](Self::record_interruption) with the time it
153    /// came back: Clause 12.25.14 gives a log that status when a power
154    /// failure or reset broke its collection, so readers know samples may
155    /// be missing. That record counts toward Total_Record_Count like any
156    /// other, numbered one past the restored count.
157    pub fn restore_log_buffer(
158        &mut self,
159        total_record_count: u32,
160        records: impl IntoIterator<Item = BACnetLogRecord>,
161    ) -> Result<(), Error> {
162        self.lifecycle()
163            .restore(total_record_count, records.into_iter().collect())
164    }
165
166    /// Append a LOG_INTERRUPTED status record stamped `date` and `time`, as
167    /// a log restored after a restart does (#1537); see
168    /// [`restore_log_buffer`](Self::restore_log_buffer).
169    ///
170    /// The record goes in whatever Enable and the window say, counts toward
171    /// Total_Record_Count, and pushes out the oldest record of a full
172    /// buffer. It also carries LOG_DISABLED while collection is off; when it
173    /// fills a Stop_When_Full buffer it does so and turns Enable FALSE, as
174    /// the log's own status records do. Without a clock bound, as before the
175    /// log is added to a database, the time has to come from the caller: a
176    /// `date` and `time` that aren't an actual moment (every field given,
177    /// the weekday matching the date) fail with [`Error::OutOfRange`] and
178    /// change nothing.
179    ///
180    /// A log reopened from storage ([`with_persistence`](Self::with_persistence))
181    /// appends this record itself; calling this first takes its place.
182    pub fn record_interruption(&mut self, date: Date, time: Time) -> Result<(), Error> {
183        self.lifecycle().record_interruption((date, time))?;
184        if let Some(store) = &mut self.store {
185            store.interruption_recorded();
186        }
187        self.commit_if_counted();
188        Ok(())
189    }
190
191    /// Clear the buffer.
192    pub fn clear(&mut self) {
193        self.log_buffer.clear();
194    }
195
196    /// Set the description string.
197    pub fn set_description(&mut self, desc: impl Into<String>) {
198        self.description = desc.into();
199    }
200
201    /// Set Log_DeviceObjectProperty, the property the log samples, as local
202    /// configuration: the log buffer is left as it is.
203    ///
204    /// A reference may name another device; the poller then logs a failure
205    /// for each sample instead of reading it. `None`, or a reference whose
206    /// object or Device is at the reserved instance 4194303, leaves the log
207    /// without one (#1417). A Device member that isn't a Device identifier
208    /// fails with PROPERTY / VALUE_OUT_OF_RANGE and changes nothing.
209    pub fn set_log_device_object_property(
210        &mut self,
211        reference: Option<BACnetDeviceObjectPropertyReference>,
212    ) -> Result<(), Error> {
213        if let Some(reference) = &reference {
214            crate::device_reference::check_device_member(reference.device_identifier)?;
215        }
216        self.log_device_object_property = reference.and_then(crate::device_reference::set_or_unset);
217        Ok(())
218    }
219
220    /// A client's write of Log_DeviceObjectProperty (#1234): one reference,
221    /// the unset form clearing it (#1417). See [`references::check_written`]
222    /// for the refusals; Null is no reference, so INVALID_DATA_TYPE. A new
223    /// value purges the buffer, leaving a BUFFER_PURGED status record (Clause
224    /// 12.25.8); without a valid clock the purge fails with DEVICE /
225    /// OPERATIONAL_PROBLEM and nothing changes. Writing the value already held
226    /// changes nothing.
227    fn write_log_device_object_property(&mut self, value: PropertyValue) -> Result<(), Error> {
228        let reference = crate::device_reference::decode_reference(&value)?;
229        references::check_written(&reference)?;
230        let reference = crate::device_reference::set_or_unset(reference);
231        if reference != self.log_device_object_property {
232            self.lifecycle().purge()?;
233            self.log_device_object_property = reference;
234        }
235        Ok(())
236    }
237
238    /// Set Logging_Type, as a client's write does: POLLED, TRIGGERED or COV.
239    /// Any other value is PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED
240    /// (Clause 12.25.26) and changes nothing. POLLED with a zero Log_Interval
241    /// sets [`DEFAULT_LOG_INTERVAL`]; TRIGGERED and COV set Log_Interval to
242    /// zero.
243    ///
244    /// While COV, the database's trend poller looks at the referenced
245    /// property on each pass and logs it, with the monitored object's
246    /// Status_Flags, whenever its change passes the Clause 13.1 COV test
247    /// (held to Client_COV_Increment when set) or those flags have changed.
248    /// The first pass after the log starts collecting logs the current value;
249    /// Enable turned off and on within one pass (about 100 ms) isn't seen
250    /// and restarts nothing. Only
251    /// references to this device are followed: one naming another device
252    /// logs PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED once, since no
253    /// COV subscription is made to a peer.
254    pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), Error> {
255        self.acquisition.set_logging_type(logging_type)
256    }
257
258    /// Set Log_Interval in hundredths of a second, as a client's write does.
259    /// While Logging_Type is TRIGGERED it is read-only and this is PROPERTY /
260    /// WRITE_ACCESS_DENIED. Clause 12.25.9's switches apply: a POLLED log's
261    /// nonzero interval set to zero turns it to COV, and a nonzero interval
262    /// set on a COV log turns it to POLLED.
263    pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
264        self.acquisition.set_log_interval(hundredths)
265    }
266
267    /// Set Client_COV_Increment, the increment a COV log holds a numeric
268    /// value's changes to (Clause 12.25.11). `None`, NULL, uses the
269    /// referenced property's own criteria: the object's COV_Increment for a
270    /// numeric Present_Value, any change otherwise. An increment that is
271    /// negative or not finite is PROPERTY / VALUE_OUT_OF_RANGE.
272    pub fn set_client_cov_increment(&mut self, increment: Option<f32>) -> Result<(), Error> {
273        self.cov.set_client_cov_increment(increment)
274    }
275
276    /// Set COV_Resubscription_Interval in seconds (Clause 12.25.10); zero is
277    /// PROPERTY / VALUE_OUT_OF_RANGE. It is stored and served, but unused
278    /// until the log subscribes to another device's property, which it
279    /// doesn't yet: COV logging follows this device's properties only.
280    pub fn set_cov_resubscription_interval(&mut self, seconds: u32) -> Result<(), Error> {
281        self.cov.set_resubscription_interval(seconds)
282    }
283
284    /// Set Start_Time, the local date and time from which records are kept,
285    /// as local configuration: nothing is recorded for the change itself,
286    /// but the log notes at once where the window stands, so a client's
287    /// write that then opens or shuts it is recorded. Every field
288    /// unspecified leaves the start open. Any other value has to name an
289    /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
290    /// weekday may stay unspecified, and unspecified seconds or hundredths
291    /// count as zero.
292    pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
293        self.lifecycle()
294            .configure_window(PropertyIdentifier::START_TIME, (date, time))
295    }
296
297    /// Set Stop_Time, the local date and time from which records are no
298    /// longer kept, under the same rules as
299    /// [`set_start_time`](Self::set_start_time).
300    pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
301        self.lifecycle()
302            .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
303    }
304
305    /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
306    /// times.
307    pub fn set_align_intervals(&mut self, align: bool) {
308        self.acquisition.set_align_intervals(align);
309    }
310
311    /// Set Interval_Offset, the delay after each aligned boundary, in
312    /// hundredths; it applies modulo Log_Interval.
313    pub fn set_interval_offset(&mut self, hundredths: u32) {
314        self.acquisition.set_interval_offset(hundredths);
315    }
316
317    /// Ask for one acquisition, as a local process writing Trigger TRUE does.
318    /// Only a TRIGGERED log takes it; any other is PROPERTY /
319    /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
320    pub fn trigger(&mut self) -> Result<(), Error> {
321        self.acquisition.trigger()
322    }
323
324    /// Set Notification_Threshold, the number of records that makes a
325    /// BUFFER_READY report; zero, the default, makes none.
326    pub fn set_notification_threshold(&mut self, threshold: u32) {
327        self.reporting.set_notification_threshold(threshold);
328    }
329
330    /// Set Notification_Class: the number of the Notification Class whose
331    /// recipients get the log's reports (0 by default).
332    pub fn set_notification_class(&mut self, class: u32) {
333        self.reporting.set_notification_class(class);
334    }
335
336    /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
337    /// set, as it is by default.
338    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
339        self.reporting.set_event_enable(enable);
340    }
341
342    /// Set Notify_Type, sent with each report (EVENT by default).
343    pub fn set_notify_type(&mut self, notify_type: NotifyType) {
344        self.reporting.set_notify_type(notify_type);
345    }
346
347    fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogRecord> {
348        LogLifecycle::new(
349            &mut self.log_buffer,
350            &mut self.log_enable,
351            &mut self.stop_when_full,
352            self.clock.as_ref(),
353            &mut self.window,
354            &mut self.reporting,
355        )
356    }
357}
358
359impl BACnetObject for TrendLogObject {
360    fn object_identifier(&self) -> ObjectIdentifier {
361        self.oid
362    }
363
364    fn object_name(&self) -> &str {
365        &self.name
366    }
367
368    fn read_property(
369        &self,
370        property: PropertyIdentifier,
371        array_index: Option<u32>,
372    ) -> Result<PropertyValue, Error> {
373        // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
374        // Trigger, then Start_Time and Stop_Time.
375        if let Some(value) = self
376            .acquisition
377            .read(property)
378            .or_else(|| self.cov.read(property))
379            .or_else(|| self.window.read(property))
380        {
381            return Ok(value);
382        }
383        let total = self.log_buffer.total_record_count();
384        if let Some(result) = self.reporting.read(property, array_index, total) {
385            return result;
386        }
387        match property {
388            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
389                Ok(PropertyValue::ObjectIdentifier(self.oid))
390            }
391            p if p == PropertyIdentifier::OBJECT_NAME => {
392                Ok(PropertyValue::CharacterString(self.name.clone()))
393            }
394            p if p == PropertyIdentifier::DESCRIPTION => {
395                Ok(PropertyValue::CharacterString(self.description.clone()))
396            }
397            p if p == PropertyIdentifier::OBJECT_TYPE => {
398                Ok(PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw()))
399            }
400            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
401            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
402                Ok(PropertyValue::Boolean(self.stop_when_full))
403            }
404            p if p == PropertyIdentifier::BUFFER_SIZE => {
405                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
406            }
407            p if p == PropertyIdentifier::RECORD_COUNT => {
408                Ok(PropertyValue::Unsigned(self.records().len() as u64))
409            }
410            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
411                Ok(PropertyValue::Unsigned(u64::from(total)))
412            }
413            // Clause 12.25.30 lets only IN_ALARM (from Event_State) and FAULT
414            // (from Reliability) move on a Trend Log; OVERRIDDEN and
415            // OUT_OF_SERVICE are always FALSE. Table 12-29 has no
416            // Out_Of_Service property (#985).
417            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
418                StatusFlags::empty(),
419                self.reliability,
420                false,
421                self.reporting.event_state(),
422            )),
423            p if p == PropertyIdentifier::RELIABILITY => {
424                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
425            }
426            // ReadRange pages it through `log_buffer_internal`.
427            p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
428            // The Clause 21 encoding; while no reference is set, the empty
429            // element a Trend Log Multiple grows by (#1417).
430            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
431                Ok(crate::device_reference::optional_reference_value(
432                    self.log_device_object_property.as_ref(),
433                    ObjectType::ANALOG_INPUT,
434                ))
435            }
436            p if p == PropertyIdentifier::PROPERTY_LIST => {
437                read_property_list_property(&self.property_list(), array_index)
438            }
439            _ => Err(Error::protocol(
440                ErrorClass::PROPERTY,
441                ErrorCode::UNKNOWN_PROPERTY,
442            )),
443        }
444    }
445
446    fn write_property(
447        &mut self,
448        property: PropertyIdentifier,
449        array_index: Option<u32>,
450        value: PropertyValue,
451        _priority: Option<u8>,
452    ) -> Result<(), Error> {
453        if property == PropertyIdentifier::LOG_ENABLE {
454            if let PropertyValue::Boolean(v) = value {
455                return self.lifecycle().write_enable(v);
456            }
457            return Err(Error::protocol(
458                ErrorClass::PROPERTY,
459                ErrorCode::INVALID_DATA_TYPE,
460            ));
461        }
462        if let Some(result) = self
463            .acquisition
464            .write(property, &value)
465            .or_else(|| self.cov.write(property, &value))
466        {
467            return result;
468        }
469        if let Some(result) = self.window.write(property, &value) {
470            // A change that opens or closes the window is recorded at once.
471            result?;
472            self.lifecycle().refresh_window();
473            return Ok(());
474        }
475        if property == PropertyIdentifier::STOP_WHEN_FULL {
476            if let PropertyValue::Boolean(v) = value {
477                return self.lifecycle().write_stop_when_full(v);
478            }
479            return Err(Error::protocol(
480                ErrorClass::PROPERTY,
481                ErrorCode::INVALID_DATA_TYPE,
482            ));
483        }
484        if property == PropertyIdentifier::RECORD_COUNT {
485            if let PropertyValue::Unsigned(0) = value {
486                return self.lifecycle().purge();
487            }
488            return Err(Error::protocol(
489                ErrorClass::PROPERTY,
490                ErrorCode::INVALID_DATA_TYPE,
491            ));
492        }
493        // Clause 12.25 Table 12-29 lists Reliability as plain O with no
494        // writability footnote, and unlike the intrinsic-reporting objects the
495        // Trend Log Reliability_Evaluation_Inhibit paragraph requires
496        // NO_FAULT_DETECTED while evaluation is inhibited, without an exception
497        // for a client-supplied Reliability value while Out_Of_Service is TRUE.
498        // Nothing in Clause 12.25 grants a
499        // network client this property: the log owns it (logging status and
500        // fault indication), so every write is refused.
501        if property == PropertyIdentifier::RELIABILITY {
502            return Err(Error::protocol(
503                ErrorClass::PROPERTY,
504                ErrorCode::WRITE_ACCESS_DENIED,
505            ));
506        }
507        if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
508            return self.write_log_device_object_property(value);
509        }
510        let total = self.log_buffer.total_record_count();
511        if let Some(result) = self.reporting.write(property, array_index, &value, total) {
512            return result;
513        }
514        if let Some(result) = common::write_description(&mut self.description, property, &value) {
515            return result;
516        }
517        Err(crate::common::unhandled_write_error(
518            self.property_metadata().as_ref(),
519            property,
520            array_index,
521        ))
522    }
523
524    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
525        metadata::for_object(self)
526    }
527
528    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
529        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
530    }
531
532    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
533        self.clock = clock;
534        self.settle_interruption(None);
535    }
536
537    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
538        self.store.is_some().then_some(self as _)
539    }
540
541    fn advance_monotonic_time_internal(&mut self, now: core::time::Duration) -> bool {
542        self.commit_if_interval(now);
543        false
544    }
545
546    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
547        Some(self.log_buffer.identities())
548    }
549
550    fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
551        Some(&self.log_buffer)
552    }
553
554    fn add_trend_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
555        self.add_record(record)
556    }
557
558    fn refresh_log_window_internal(&mut self) -> bool {
559        self.settle_interruption(None);
560        let recorded = self.lifecycle().refresh_window();
561        self.commit_if_counted();
562        recorded
563    }
564
565    fn log_collecting_internal(&self) -> bool {
566        self.log_enable && self.window.is_open()
567    }
568
569    impl_buffer_ready_reporting!(reporting, log_buffer);
570}
571
572#[cfg(test)]
573mod log_record_tests;
574
575#[cfg(test)]
576mod multiple_options_tests;
577
578#[cfg(test)]
579mod options_tests;
580
581#[cfg(test)]
582mod reference_tests;
583
584#[cfg(test)]
585mod tests;