rtest 0.2.2

integration test building framework
Documentation
use super::{ProcessId, ResourceId};
use std::{
    collections::{hash_map::Entry, HashMap},
    fmt::Debug,
    hash::Hash,
    ops::{Add, Sub},
};

/// The Environment capsules the whole whole dependency problem information.
///
/// It contains a list of all resources and processes.
/// different algorithms, like backtrack will run on top of the environment to
/// provide their solutions.
pub struct Environment<R, P> {
    resources: HashMap<ResourceId, R>,
    processes: HashMap<ProcessId, P>,
    initial:   Inventory<ResourceId>,
}

#[derive(Default, Debug, Clone)]
pub(crate) struct Inventory<RP> {
    pub items: HashMap<RP, u64>,
}

#[derive(Debug)]
pub struct Step<A> {
    pub(super) process:         ProcessId,
    pub(super) inventory_after: Inventory<ResourceId>,
    pub(super) algorithm_data:  A,
}

#[derive(Debug)]
pub struct Path<A> {
    pub(super) steps: Vec<Step<A>>,
}

impl<A> Default for Path<A> {
    fn default() -> Self { Self { steps: Vec::new() } }
}

impl<A> Path<A> {
    pub fn processes(&self) -> Inventory<ProcessId> {
        let mut inventory = Inventory::default();
        for step in &self.steps {
            inventory.push(step.process);
        }
        inventory
    }

    pub fn push(&mut self, step: Step<A>) { self.steps.push(step) }
}

impl<RP> Inventory<RP>
where
    RP: PartialEq + Eq + Hash + Default + Clone,
{
    #[inline(always)]
    pub fn push(&mut self, element: RP) { *self.items.entry(element).or_default() += 1; }

    pub fn with_list(list: &[RP]) -> Self {
        let mut items = HashMap::new();
        for e in list {
            *items.entry(e.clone()).or_default() += 1;
        }
        Self { items }
    }

    /// returns what is missing to match needed
    #[allow(dead_code)]
    pub fn missing(&self, needed: Self) -> Self {
        let mut missing = Self::default();
        for (rid, count) in needed.items.into_iter() {
            let contains = self.items.get(&rid).cloned().unwrap_or_default();
            if contains < count {
                missing.items.insert(rid, count - contains);
            }
        }
        missing
    }

    pub fn contains(&self, other: &Self) -> bool {
        for (rid, count) in other.items.iter() {
            let contains = self.items.get(rid).cloned().unwrap_or_default();
            if contains < *count {
                return false;
            }
        }
        true
    }
}

impl<RP: Debug + Hash + Eq> Add for Inventory<RP> {
    type Output = Inventory<RP>;

    fn add(mut self, other: Self) -> Self {
        for (rid, count) in other.items.into_iter() {
            *self.items.entry(rid).or_default() += count;
        }
        self
    }
}

impl<RP: Debug + Hash + Eq + Clone> Sub for Inventory<RP> {
    type Output = Inventory<RP>;

    fn sub(mut self, other: Self) -> Self {
        for (rid, count) in other.items.into_iter() {
            match self.items.entry(rid.clone()) {
                Entry::Occupied(mut occ) => {
                    *occ.get_mut() -= count;
                    if *occ.get() == 0u64 {
                        occ.remove();
                    }
                },
                Entry::Vacant(_) if count > 0 => {
                    panic!("Sub Underflow for Inventory {:?}, it has no ressource {:?}", self, rid)
                },
                Entry::Vacant(_) => {},
            }
        }
        self
    }
}

impl<R, P> Environment<R, P> {
    pub(crate) fn new(
        resources: HashMap<ResourceId, R>,
        processes: HashMap<ProcessId, P>,
        initial: Inventory<ResourceId>,
    ) -> Self {
        Self {
            processes,
            resources,
            initial,
        }
    }

    #[allow(dead_code)]
    pub fn resources(&self) -> &HashMap<ResourceId, R> { &self.resources }

    pub fn processes(&self) -> &HashMap<ProcessId, P> { &self.processes }

    pub fn initial(&self) -> &Inventory<ResourceId> { &self.initial }

    #[allow(dead_code)]
    pub fn initial_mut(&mut self) -> &mut Inventory<ResourceId> { &mut self.initial }
}

#[cfg(test)]
mod tests {
    use super::Inventory;

    #[test]
    fn inventory_missing() {
        let have = Inventory {
            items: [(1, 2), (2, 3), (3, 4)].into(),
        };
        let want = Inventory {
            items: [(0, 1), (1, 1), (2, 3), (3, 5), (4, 5)].into(),
        };
        let missing = have.missing(want);
        assert_eq!(missing.items, [(0, 1), (3, 1), (4, 5)].into())
    }

    #[test]
    fn inventory_with_list() {
        let have = Inventory::with_list(&[0, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 3]);
        let want = Inventory {
            items: [(0, 1), (1, 1), (2, 3), (3, 5), (4, 5)].into(),
        };
        assert_eq!(have.items, want.items);
    }

    #[test]
    fn inventory_contains() {
        let a = Inventory::with_list(&[0, 1, 1, 1, 3, 3]);
        let b = Inventory::with_list(&[0, 1]);
        let c = Inventory::with_list(&[0, 1, 1, 1, 1, 3, 3]);
        assert!(a.contains(&a));
        assert!(a.contains(&b));
        assert!(!a.contains(&c));
        assert!(!b.contains(&a));
        assert!(b.contains(&b));
        assert!(!b.contains(&c));
        assert!(c.contains(&a));
        assert!(c.contains(&b));
        assert!(c.contains(&c));
    }
}