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;