Skip to main content

bacnet_objects/
log_buffer.rs

1//! Shared resident storage and identity projection for BACnet log objects.
2
3use std::collections::VecDeque;
4
5use bacnet_encoding::constructed::{
6    encode_event_log_record, encode_log_multiple_record, encode_log_record,
7};
8use bacnet_types::bitstring::LogStatus;
9use bacnet_types::constructed::{
10    BACnetEventLogRecord, BACnetLogMultipleRecord, BACnetLogRecord, EventLogDatum, LogData,
11    LogDatum,
12};
13use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
14use bacnet_types::error::Error;
15use bacnet_types::primitives::{Date, Time};
16use bytes::BytesMut;
17
18use crate::property_metadata::{
19    PropertyConformance::RequiredRead, PropertyMetadata, PropertyWriteCapability::ReadOnly,
20};
21
22// Shared conformance rows only. LOG_BUFFER is a present, read-only BACnetLIST
23// that ReadProperty refuses; ReadRange pages it through `LogBufferRecords`.
24pub(crate) const BUFFER_SIZE_METADATA: PropertyMetadata =
25    PropertyMetadata::new(P::BUFFER_SIZE, RequiredRead, None, ReadOnly);
26pub(crate) const LOG_BUFFER_METADATA: PropertyMetadata =
27    PropertyMetadata::new(P::LOG_BUFFER, RequiredRead, None, ReadOnly);
28pub(crate) const TOTAL_RECORD_COUNT_METADATA: PropertyMetadata =
29    PropertyMetadata::new(P::TOTAL_RECORD_COUNT, RequiredRead, None, ReadOnly);
30
31/// The answer a log object gives a ReadProperty of its Log_Buffer.
32///
33/// Clauses 12.25.14, 12.27.13, 12.30.19 and 12.64.10 open a log buffer to
34/// ReadRange (and, for an Audit Log, AuditLogQuery) only, so a property read
35/// names the property as present but not readable this way
36/// (Clause 15.5.1.3.1).
37pub(crate) fn log_buffer_read_denied() -> Error {
38    Error::Protocol {
39        class: ErrorClass::PROPERTY.to_raw() as u32,
40        code: ErrorCode::READ_ACCESS_DENIED.to_raw() as u32,
41    }
42}
43
44/// A log object's Log_Buffer as ReadRange pages it: the resident records,
45/// oldest first, each framed on demand as its Clause 21 record production,
46/// with the identity ReadRange selects it by.
47///
48/// The records align element for element with
49/// [`crate::traits::BACnetObject::log_record_identities_internal`]. A page
50/// encodes only the records it visits, and the built-in logs find them
51/// without walking the whole buffer, so a narrow window over a full log
52/// stays cheap (#1536).
53pub trait LogBufferRecords {
54    /// The number of resident records.
55    fn record_count(&self) -> usize;
56
57    /// Append the record at `index` (0 is the oldest) to `buf`.
58    ///
59    /// Encoding cannot fail: the built-in logs refuse a record that would not
60    /// encode when it is added, and an implementation must keep that promise
61    /// too. Panics when `index` is not below
62    /// [`record_count`](Self::record_count).
63    fn encode_record(&self, index: usize, buf: &mut BytesMut);
64
65    /// The identity of the record at `index` (0 is the oldest). Panics when
66    /// `index` is not below [`record_count`](Self::record_count).
67    fn record_identity(&self, index: usize) -> LogRecordIdentity;
68
69    /// The index of the resident record numbered `sequence_number`, or
70    /// `None` when no resident record has that number.
71    ///
72    /// The default visits every record. The built-in logs number their
73    /// records consecutively and compute the index instead.
74    fn record_position(&self, sequence_number: u64) -> Option<usize> {
75        (0..self.record_count())
76            .find(|&index| self.record_identity(index).sequence_number() == sequence_number)
77    }
78
79    /// Whether every resident timestamp is an actual moment, and if so
80    /// whether they run in order, which lets a search by time bisect them.
81    ///
82    /// The default visits every record. The built-in logs keep the answer
83    /// up to date as records come and go.
84    fn timestamp_order(&self) -> TimestampOrder {
85        let mut order = TimestampOrder::Ascending;
86        let mut previous = None;
87        for index in 0..self.record_count() {
88            let Some(key) = self.record_identity(index).timestamp_key() else {
89                return TimestampOrder::Unkeyed;
90            };
91            if previous.is_some_and(|previous| key < previous) {
92                order = TimestampOrder::Unordered;
93            }
94            previous = Some(key);
95        }
96        order
97    }
98}
99
100/// A record timestamp as ReadRange By-Time compares it: the civil date and
101/// time, most significant field first, with the day of the week left out.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
103pub struct TimestampKey {
104    year: u16,
105    month: u8,
106    day: u8,
107    hour: u8,
108    minute: u8,
109    second: u8,
110    hundredths: u8,
111}
112
113impl TimestampKey {
114    /// The key of `date` and `time`, or `None` unless every field is
115    /// specified and in range. The day of the week has to name a weekday
116    /// too, though the key then leaves it out; the day of the month is
117    /// checked against 31, not against its month.
118    pub fn of(date: Date, time: Time) -> Option<Self> {
119        let year = date.actual_year()?;
120        let valid = (1..=12).contains(&date.month)
121            && (1..=31).contains(&date.day)
122            && (1..=7).contains(&date.day_of_week)
123            && time.hour <= 23
124            && time.minute <= 59
125            && time.second <= 59
126            && time.hundredths <= 99;
127        valid.then_some(Self {
128            year,
129            month: date.month,
130            day: date.day,
131            hour: time.hour,
132            minute: time.minute,
133            second: time.second,
134            hundredths: time.hundredths,
135        })
136    }
137}
138
139/// How a log buffer's timestamps stand for a search by time
140/// ([`LogBufferRecords::timestamp_order`]).
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum TimestampOrder {
143    /// Some resident timestamp is not an actual moment
144    /// ([`TimestampKey::of`] refuses it), so the buffer can't be searched by
145    /// time at all.
146    Unkeyed,
147    /// Every timestamp is an actual moment and none is earlier than the one
148    /// before it, so a search can bisect them.
149    Ascending,
150    /// Every timestamp is an actual moment, but the clock went back at least
151    /// once, so a search has to visit each record.
152    Unordered,
153}
154
155/// The largest encoded value, in octets, that the trend pollers log as an
156/// any-value. A larger value (a long string, a big array) is logged as a
157/// PROPERTY / VALUE_TOO_LONG failure instead, so that one Trend Log record
158/// stays small enough for a ReadRange page on a 480-octet APDU.
159pub const ANY_VALUE_MAX_OCTETS: usize = 256;
160
161/// Stable object-owned identity for one resident log record.
162///
163/// Identity views returned by [`crate::traits::BACnetObject::log_record_identities_internal`]
164/// are ordered oldest-to-newest and align element-for-element with the owning
165/// object's resident records and `LOG_BUFFER` projection. Sequence numbers are
166/// always nonzero. Event and Trend logs count in Unsigned32 and wrap from
167/// `u32::MAX` to 1; Audit Log counts in Unsigned64 and wraps from `u64::MAX`.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct LogRecordIdentity {
170    sequence_number: u64,
171    date: Date,
172    time: Time,
173}
174
175impl LogRecordIdentity {
176    /// Construct an identity, rejecting zero as an invalid record sequence.
177    pub fn new(sequence_number: u64, date: Date, time: Time) -> Option<Self> {
178        (sequence_number != 0).then_some(Self {
179            sequence_number,
180            date,
181            time,
182        })
183    }
184
185    /// Return this record's nonzero sequence number.
186    pub fn sequence_number(&self) -> u64 {
187        self.sequence_number
188    }
189
190    /// Return the date owned by this record.
191    pub fn date(&self) -> Date {
192        self.date
193    }
194
195    /// Return the time owned by this record.
196    pub fn time(&self) -> Time {
197        self.time
198    }
199
200    /// This record's timestamp as ReadRange By-Time compares it, or `None`
201    /// when it is not an actual moment.
202    pub fn timestamp_key(&self) -> Option<TimestampKey> {
203        TimestampKey::of(self.date, self.time)
204    }
205}
206
207/// A record a [`LogRecordBuffer`] can hold: it carries its own timestamp,
208/// the shared lifecycle can build the log-status record of its family, and
209/// it encodes as its family's Clause 21 production.
210pub(crate) trait ResidentLogRecord: Clone {
211    /// The local date and time the record was acquired.
212    fn timestamp(&self) -> (Date, Time);
213    /// A log-status record carrying `status`.
214    fn log_status(date: Date, time: Time, status: LogStatus) -> Self;
215    /// Append the record's wire form to `buf`, leaving it unchanged on error.
216    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error>;
217}
218
219impl ResidentLogRecord for BACnetLogRecord {
220    fn timestamp(&self) -> (Date, Time) {
221        (self.date, self.time)
222    }
223
224    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
225        Self {
226            date,
227            time,
228            log_datum: LogDatum::LogStatus(status),
229            status_flags: None,
230        }
231    }
232
233    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
234        encode_log_record(self, buf)
235    }
236}
237
238impl ResidentLogRecord for BACnetEventLogRecord {
239    fn timestamp(&self) -> (Date, Time) {
240        (self.date, self.time)
241    }
242
243    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
244        Self {
245            date,
246            time,
247            log_datum: EventLogDatum::LogStatus(status),
248        }
249    }
250
251    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
252        encode_event_log_record(self, buf)
253    }
254}
255
256impl ResidentLogRecord for BACnetLogMultipleRecord {
257    fn timestamp(&self) -> (Date, Time) {
258        (self.date, self.time)
259    }
260
261    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
262        Self {
263            date,
264            time,
265            log_data: LogData::LogStatus(status),
266        }
267    }
268
269    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
270        encode_log_multiple_record(self, buf)
271    }
272}
273
274/// A log's resident records and Total_Record_Count.
275///
276/// The records are numbered consecutively up to the count, so the buffer
277/// derives any record's identity, or the index of any number, from the
278/// count alone. It also keeps two tallies as records come and go, which
279/// answer [`LogBufferRecords::timestamp_order`] without a walk: the
280/// records whose timestamp is not an actual moment, and the neighbouring
281/// pairs where the later record is stamped earlier, as after the clock was
282/// set back.
283#[derive(Clone)]
284pub(crate) struct LogRecordBuffer<R = BACnetLogRecord> {
285    capacity: u32,
286    records: VecDeque<R>,
287    total_record_count: u32,
288    unkeyed: usize,
289    descents: usize,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub(crate) enum OrdinaryAdmission {
294    IgnoredDisabled,
295    Inserted,
296    CountOnly,
297    StopBeforeFull,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub(crate) enum ForcedAdmission {
302    Inserted,
303    CountOnly,
304}
305
306impl<R: ResidentLogRecord> LogRecordBuffer<R> {
307    pub(crate) fn new(capacity: u32) -> Self {
308        Self {
309            capacity,
310            records: VecDeque::new(),
311            total_record_count: 0,
312            unkeyed: 0,
313            descents: 0,
314        }
315    }
316
317    pub(crate) fn admit_ordinary(
318        &mut self,
319        record: R,
320        enabled: bool,
321        stop_when_full: bool,
322    ) -> OrdinaryAdmission {
323        if !enabled {
324            return OrdinaryAdmission::IgnoredDisabled;
325        }
326        if stop_when_full && self.next_record_would_fill() {
327            return OrdinaryAdmission::StopBeforeFull;
328        }
329
330        match self.insert_counted(record) {
331            ForcedAdmission::Inserted => OrdinaryAdmission::Inserted,
332            ForcedAdmission::CountOnly => OrdinaryAdmission::CountOnly,
333        }
334    }
335
336    pub(crate) fn insert_forced(&mut self, record: R) -> ForcedAdmission {
337        self.insert_counted(record)
338    }
339
340    pub(crate) fn is_full(&self) -> bool {
341        self.records.len() >= self.capacity as usize
342    }
343
344    pub(crate) fn next_record_would_fill_positive_capacity(&self) -> bool {
345        self.capacity > 0 && self.next_record_would_fill()
346    }
347
348    pub(crate) fn records(&self) -> &VecDeque<R> {
349        &self.records
350    }
351
352    pub(crate) fn total_record_count(&self) -> u32 {
353        self.total_record_count
354    }
355
356    pub(crate) fn capacity(&self) -> u32 {
357        self.capacity
358    }
359
360    pub(crate) fn clear(&mut self) {
361        self.records.clear();
362        self.unkeyed = 0;
363        self.descents = 0;
364    }
365
366    pub(crate) fn identities(&self) -> Vec<LogRecordIdentity> {
367        (0..self.records.len())
368            .map(|index| self.record_identity(index))
369            .collect()
370    }
371
372    /// Replace the resident records with `records`, oldest first, the newest
373    /// numbered `total_record_count` and each one before it one less, below
374    /// 1 wrapping to 2^32 - 1 (#1537). A count below the number of records
375    /// is therefore a count that has wrapped: Clause 12.25.16 lets the count
376    /// start again at 1, after which it no longer says how many records
377    /// were collected.
378    ///
379    /// Refused, leaving the buffer as it was: more records than Buffer_Size;
380    /// records with a count of zero, which only a log that never collected
381    /// one has; records that fill the buffer of a log that `stops_when_full`
382    /// (Stop_When_Full and Enable both TRUE), since such a log stops before
383    /// its last slot is taken; and a record that would not encode.
384    pub(crate) fn restore(
385        &mut self,
386        total_record_count: u32,
387        records: VecDeque<R>,
388        stops_when_full: bool,
389    ) -> Result<(), Error> {
390        let held = records.len();
391        if held > self.capacity as usize {
392            return Err(Error::OutOfRange(format!(
393                "{held} log records exceed Buffer_Size {}",
394                self.capacity
395            )));
396        }
397        if total_record_count == 0 && held > 0 {
398            return Err(Error::OutOfRange(format!(
399                "{held} log records restored with a Total_Record_Count of 0"
400            )));
401        }
402        if stops_when_full && held > 0 && held >= self.capacity as usize {
403            return Err(Error::OutOfRange(
404                "a log with Stop_When_Full and Enable TRUE can't hold a full buffer".into(),
405            ));
406        }
407        let mut scratch = BytesMut::new();
408        for record in &records {
409            record.encode(&mut scratch)?;
410            scratch.clear();
411        }
412
413        self.clear();
414        for record in records {
415            self.push_back(record);
416        }
417        self.total_record_count = total_record_count;
418        Ok(())
419    }
420
421    fn next_record_would_fill(&self) -> bool {
422        self.capacity == 0 || self.records.len().saturating_add(1) >= self.capacity as usize
423    }
424
425    fn insert_counted(&mut self, record: R) -> ForcedAdmission {
426        self.total_record_count = next_sequence(self.total_record_count);
427        if self.capacity == 0 {
428            return ForcedAdmission::CountOnly;
429        }
430        if self.is_full() {
431            self.pop_front();
432        }
433        self.push_back(record);
434        ForcedAdmission::Inserted
435    }
436
437    /// Append `record`, counting it into the timestamp tallies.
438    fn push_back(&mut self, record: R) {
439        if self
440            .records
441            .back()
442            .is_some_and(|last| descends(last, &record))
443        {
444            self.descents += 1;
445        }
446        if timestamp_key(&record).is_none() {
447            self.unkeyed += 1;
448        }
449        self.records.push_back(record);
450    }
451
452    /// Drop the oldest record, taking it out of the timestamp tallies. The
453    /// pair it formed with its successor was counted when that one came.
454    fn pop_front(&mut self) {
455        let Some(oldest) = self.records.pop_front() else {
456            return;
457        };
458        if timestamp_key(&oldest).is_none() {
459            self.unkeyed -= 1;
460        }
461        if self
462            .records
463            .front()
464            .is_some_and(|next| descends(&oldest, next))
465        {
466            self.descents -= 1;
467        }
468    }
469}
470
471impl<R: ResidentLogRecord> LogBufferRecords for LogRecordBuffer<R> {
472    fn record_count(&self) -> usize {
473        self.records.len()
474    }
475
476    fn encode_record(&self, index: usize, buf: &mut BytesMut) {
477        // `LogLifecycle::try_add_ordinary` and `restore` refuse a record that
478        // would not encode, the lifecycle's own status records always
479        // encode, and a resident record is never changed.
480        self.records[index]
481            .encode(buf)
482            .expect("every resident log record encodes");
483    }
484
485    fn record_identity(&self, index: usize) -> LogRecordIdentity {
486        let (date, time) = self.records[index].timestamp();
487        let newer = self.records.len() - 1 - index;
488        LogRecordIdentity {
489            sequence_number: u64::from(sequence_before(self.total_record_count, newer)),
490            date,
491            time,
492        }
493    }
494
495    fn record_position(&self, sequence_number: u64) -> Option<usize> {
496        let sequence_number = u32::try_from(sequence_number)
497            .ok()
498            .filter(|&number| number != 0)?;
499        let newer = sequence_distance(sequence_number, self.total_record_count);
500        let newer = usize::try_from(newer)
501            .ok()
502            .filter(|&newer| newer < self.records.len())?;
503        Some(self.records.len() - 1 - newer)
504    }
505
506    fn timestamp_order(&self) -> TimestampOrder {
507        if self.unkeyed > 0 {
508            TimestampOrder::Unkeyed
509        } else if self.descents > 0 {
510            TimestampOrder::Unordered
511        } else {
512            TimestampOrder::Ascending
513        }
514    }
515}
516
517fn timestamp_key<R: ResidentLogRecord>(record: &R) -> Option<TimestampKey> {
518    let (date, time) = record.timestamp();
519    TimestampKey::of(date, time)
520}
521
522/// Whether `later`, coming right after `earlier`, is stamped before it. A
523/// pair with a timestamp that is not an actual moment never counts: such a
524/// record is tallied on its own, and the buffer can't be searched by time
525/// while it is resident.
526fn descends<R: ResidentLogRecord>(earlier: &R, later: &R) -> bool {
527    matches!(
528        (timestamp_key(earlier), timestamp_key(later)),
529        (Some(earlier), Some(later)) if later < earlier
530    )
531}
532
533/// Sequence numbers run 1 to 2^32 - 1 and then start again at 1, a cycle of
534/// 2^32 - 1 numbers that never includes 0.
535const SEQUENCE_CYCLE: u64 = u32::MAX as u64;
536
537fn next_sequence(sequence_number: u32) -> u32 {
538    if sequence_number == u32::MAX {
539        1
540    } else {
541        sequence_number + 1
542    }
543}
544
545/// The number `steps` places before `sequence_number` in the cycle.
546fn sequence_before(sequence_number: u32, steps: usize) -> u32 {
547    let steps = steps as u64 % SEQUENCE_CYCLE;
548    let offset = u64::from(sequence_number).saturating_sub(1);
549    ((offset + SEQUENCE_CYCLE - steps) % SEQUENCE_CYCLE) as u32 + 1
550}
551
552/// How many places after `from` the number `to` comes in the cycle.
553fn sequence_distance(from: u32, to: u32) -> u64 {
554    (u64::from(to) + SEQUENCE_CYCLE - u64::from(from)) % SEQUENCE_CYCLE
555}
556
557#[cfg(test)]
558#[path = "log_buffer_lookup_tests.rs"]
559mod lookup_tests;
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
565    use bacnet_types::primitives::{Date, Time};
566
567    fn record(hour: u8) -> BACnetLogRecord {
568        BACnetLogRecord {
569            date: Date {
570                year: 126,
571                month: 8,
572                day: 31,
573                day_of_week: 1,
574            },
575            time: Time {
576                hour,
577                minute: 0,
578                second: 0,
579                hundredths: 0,
580            },
581            log_datum: LogDatum::UnsignedValue(hour as u64),
582            status_flags: None,
583        }
584    }
585
586    #[test]
587    fn log_buffer_assigns_one_and_wraps_max_without_zero() {
588        let mut buffer = LogRecordBuffer::new(2);
589        assert_eq!(buffer.total_record_count(), 0);
590        assert!(LogRecordIdentity::new(0, record(0).date, record(0).time).is_none());
591
592        assert_eq!(
593            buffer.admit_ordinary(record(1), true, false),
594            OrdinaryAdmission::Inserted
595        );
596        assert_eq!(buffer.identities()[0].sequence_number(), 1);
597
598        buffer.clear();
599        buffer.restore(u32::MAX, VecDeque::new(), false).unwrap();
600        assert_eq!(
601            buffer.admit_ordinary(record(2), true, false),
602            OrdinaryAdmission::Inserted
603        );
604        assert_eq!(buffer.total_record_count(), 1);
605        assert_eq!(buffer.identities()[0].sequence_number(), 1);
606    }
607
608    #[test]
609    fn log_buffer_rejections_do_not_consume_sequence() {
610        let mut buffer = LogRecordBuffer::new(1);
611        assert_eq!(
612            buffer.admit_ordinary(record(1), false, false),
613            OrdinaryAdmission::IgnoredDisabled
614        );
615        assert_eq!(buffer.total_record_count(), 0);
616
617        assert_eq!(
618            buffer.admit_ordinary(record(2), true, true),
619            OrdinaryAdmission::StopBeforeFull
620        );
621        assert_eq!(buffer.total_record_count(), 0);
622        assert!(buffer.identities().is_empty());
623    }
624
625    #[test]
626    fn log_buffer_zero_capacity_counts_without_retaining() {
627        let mut ring = LogRecordBuffer::new(0);
628        assert_eq!(
629            ring.admit_ordinary(record(1), true, false),
630            OrdinaryAdmission::CountOnly
631        );
632        assert_eq!(
633            ring.admit_ordinary(record(2), true, false),
634            OrdinaryAdmission::CountOnly
635        );
636        assert!(ring.records().is_empty());
637        assert_eq!(ring.total_record_count(), 2);
638
639        let mut stop_when_full = LogRecordBuffer::new(0);
640        assert_eq!(
641            stop_when_full.admit_ordinary(record(1), true, true),
642            OrdinaryAdmission::StopBeforeFull
643        );
644        assert!(stop_when_full.records().is_empty());
645        assert_eq!(stop_when_full.total_record_count(), 0);
646    }
647
648    #[test]
649    fn log_buffer_fifo_eviction_keeps_survivor_identity_and_alignment() {
650        let mut buffer = LogRecordBuffer::new(2);
651        assert_eq!(
652            buffer.admit_ordinary(record(1), true, false),
653            OrdinaryAdmission::Inserted
654        );
655        assert_eq!(
656            buffer.admit_ordinary(record(2), true, false),
657            OrdinaryAdmission::Inserted
658        );
659        let survivor = buffer.identities()[1];
660        assert_eq!(
661            buffer.admit_ordinary(record(3), true, false),
662            OrdinaryAdmission::Inserted
663        );
664
665        let identities = buffer.identities();
666        assert_eq!(
667            identities,
668            vec![
669                survivor,
670                LogRecordIdentity::new(3, record(3).date, record(3).time).unwrap()
671            ]
672        );
673        assert_eq!(identities[0].sequence_number(), 2);
674        assert_ne!(identities[0].sequence_number(), 1);
675        for (record, identity) in buffer.records().iter().zip(&identities) {
676            assert_eq!(identity.date(), record.date);
677            assert_eq!(identity.time(), record.time);
678        }
679    }
680
681    #[test]
682    fn log_buffer_wrap_and_eviction_keep_modular_fifo_alignment() {
683        let mut buffer = LogRecordBuffer::new(3);
684        buffer
685            .restore(u32::MAX - 1, VecDeque::new(), false)
686            .unwrap();
687        assert_eq!(
688            buffer.admit_ordinary(record(1), true, false),
689            OrdinaryAdmission::Inserted
690        );
691        assert_eq!(
692            buffer.admit_ordinary(record(2), true, false),
693            OrdinaryAdmission::Inserted
694        );
695        assert_eq!(
696            buffer.admit_ordinary(record(3), true, false),
697            OrdinaryAdmission::Inserted
698        );
699        assert_eq!(
700            buffer
701                .identities()
702                .iter()
703                .map(LogRecordIdentity::sequence_number)
704                .collect::<Vec<_>>(),
705            vec![u64::from(u32::MAX), 1, 2]
706        );
707
708        assert_eq!(
709            buffer.admit_ordinary(record(4), true, false),
710            OrdinaryAdmission::Inserted
711        );
712        assert_eq!(
713            buffer
714                .identities()
715                .iter()
716                .map(LogRecordIdentity::sequence_number)
717                .collect::<Vec<_>>(),
718            vec![1, 2, 3]
719        );
720        assert_eq!(
721            buffer
722                .records()
723                .iter()
724                .map(|record| record.time.hour)
725                .collect::<Vec<_>>(),
726            vec![2, 3, 4]
727        );
728    }
729
730    #[test]
731    fn log_buffer_clear_preserves_counter_and_constructor_resets_it() {
732        let mut buffer = LogRecordBuffer::new(2);
733        assert_eq!(
734            buffer.admit_ordinary(record(1), true, false),
735            OrdinaryAdmission::Inserted
736        );
737        assert_eq!(
738            buffer.admit_ordinary(record(2), true, false),
739            OrdinaryAdmission::Inserted
740        );
741        buffer.clear();
742
743        assert!(buffer.records().is_empty());
744        assert!(buffer.identities().is_empty());
745        assert_eq!(buffer.total_record_count(), 2);
746        assert_eq!(
747            buffer.admit_ordinary(record(3), true, false),
748            OrdinaryAdmission::Inserted
749        );
750        assert_eq!(buffer.identities()[0].sequence_number(), 3);
751        assert_eq!(
752            LogRecordBuffer::<BACnetLogRecord>::new(2).total_record_count(),
753            0
754        );
755    }
756
757    #[test]
758    fn log_buffer_duplicate_timestamps_keep_distinct_sequences() {
759        let mut buffer = LogRecordBuffer::new(2);
760        let duplicate = record(4);
761        assert_eq!(
762            buffer.admit_ordinary(duplicate.clone(), true, false),
763            OrdinaryAdmission::Inserted
764        );
765        assert_eq!(
766            buffer.admit_ordinary(duplicate, true, false),
767            OrdinaryAdmission::Inserted
768        );
769
770        let identities = buffer.identities();
771        assert_eq!(identities[0].date(), identities[1].date());
772        assert_eq!(identities[0].time(), identities[1].time());
773        assert_eq!(identities[0].sequence_number(), 1);
774        assert_eq!(identities[1].sequence_number(), 2);
775    }
776
777    #[test]
778    fn log_buffer_clone_restore_preserves_payloads_and_derived_identities() {
779        let mut buffer = LogRecordBuffer::new(2);
780        assert_eq!(
781            buffer.admit_ordinary(record(1), true, false),
782            OrdinaryAdmission::Inserted
783        );
784        assert_eq!(
785            buffer.admit_ordinary(record(2), true, false),
786            OrdinaryAdmission::Inserted
787        );
788        let records = buffer.records().clone();
789        let identities = buffer.identities();
790        let total = buffer.total_record_count();
791
792        let snapshot = buffer.clone();
793        buffer.clear();
794        buffer = snapshot;
795
796        assert_eq!(buffer.records(), &records);
797        assert_eq!(buffer.identities(), identities);
798        assert_eq!(buffer.total_record_count(), total);
799    }
800}