Skip to main content

bacnet_objects/trend/
mod.rs

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