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;