Skip to main content

bacnet_objects/
log_buffer.rs

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