use std::num::NonZeroUsize;
use lru::LruCache;
use super::core::Asset;
use crate::{MacAddr, Timestamp};
#[derive(Debug)]
pub struct Inventory {
cache: LruCache<MacAddr, Asset>,
}
impl Inventory {
pub fn new(capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity.max(1)).unwrap();
Self {
cache: LruCache::new(cap),
}
}
pub fn absorb(&mut self, mut update: Asset) -> &Asset {
let mac = update.mac;
if let Some(existing) = self.cache.get_mut(&mac) {
existing.merge(&update);
} else {
if update.last_seen == Timestamp::default() {
update.last_seen = Timestamp::default();
}
self.cache.put(mac, update);
}
self.cache.peek(&mac).expect("just inserted")
}
pub fn absorb_at(&mut self, mut update: Asset, ts: Timestamp) -> &Asset {
update.last_seen = ts;
self.absorb(update)
}
pub fn get(&self, mac: &MacAddr) -> Option<&Asset> {
self.cache.peek(mac)
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn capacity(&self) -> usize {
self.cache.cap().get()
}
pub fn iter(&self) -> impl Iterator<Item = (&MacAddr, &Asset)> {
self.cache.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::AssetSourceSet;
fn mac(b: u8) -> MacAddr {
MacAddr([b; 6])
}
#[test]
fn empty_inventory_has_zero_len() {
let inv = Inventory::new(16);
assert_eq!(inv.len(), 0);
assert!(inv.is_empty());
assert_eq!(inv.capacity(), 16);
}
#[test]
fn capacity_zero_is_treated_as_one() {
let inv = Inventory::new(0);
assert_eq!(inv.capacity(), 1);
}
#[test]
fn absorb_inserts_new_record() {
let mut inv = Inventory::new(4);
let mut a = Asset::new(mac(1));
a.seen_via |= AssetSourceSet::ARP;
let stored = inv.absorb(a);
assert!(stored.seen_via.contains(AssetSourceSet::ARP));
assert_eq!(inv.len(), 1);
}
#[test]
fn absorb_merges_two_messages_same_mac() {
let mut inv = Inventory::new(4);
let mut a = Asset::new(mac(1));
a.seen_via |= AssetSourceSet::ARP;
a.hostname = Some("first".into());
inv.absorb(a);
let mut b = Asset::new(mac(1));
b.seen_via |= AssetSourceSet::DHCP;
b.hostname = Some("second".into());
inv.absorb(b);
assert_eq!(inv.len(), 1, "same MAC merged into one entry");
let got = inv.get(&mac(1)).unwrap();
assert!(got.seen_via.contains(AssetSourceSet::ARP));
assert!(got.seen_via.contains(AssetSourceSet::DHCP));
assert_eq!(got.hostname.as_deref(), Some("second"));
}
#[test]
fn lru_eviction_drops_oldest() {
let mut inv = Inventory::new(2);
inv.absorb(Asset::new(mac(1)));
inv.absorb(Asset::new(mac(2)));
inv.absorb(Asset::new(mac(3))); assert!(
inv.get(&mac(1)).is_none(),
"mac(1) should have been evicted"
);
assert!(inv.get(&mac(2)).is_some());
assert!(inv.get(&mac(3)).is_some());
}
#[test]
fn get_does_not_promote_lru_order() {
let mut inv = Inventory::new(2);
inv.absorb(Asset::new(mac(1)));
inv.absorb(Asset::new(mac(2)));
let _ = inv.get(&mac(1));
let _ = inv.get(&mac(1));
inv.absorb(Asset::new(mac(3)));
assert!(
inv.get(&mac(1)).is_none(),
"mac(1) must still be evicted; get does not promote"
);
}
#[test]
fn absorb_at_stamps_observation_time() {
let mut inv = Inventory::new(4);
let ts = Timestamp::new(123, 0);
inv.absorb_at(Asset::new(mac(1)), ts);
assert_eq!(inv.get(&mac(1)).unwrap().last_seen, ts);
}
}