relux 0.3.2

Expect-style integration test framework for interactive shell programs
Documentation
use std::hash::Hash;
use std::path::PathBuf;
use std::sync::Arc;

use elsa::sync::FrozenMap;

// ─── SharedTable ────────────────────────────────────────────

/// Mutable shared table — populated incrementally, potentially from multiple threads.
/// Write-once semantics: first insert wins, subsequent inserts for the same key are ignored.
pub struct SharedTable<K, V> {
    map: Arc<FrozenMap<K, Box<V>>>,
}

impl<K, V> SharedTable<K, V>
where
    K: Eq + Hash,
{
    pub fn new() -> Self {
        Self {
            map: Arc::new(FrozenMap::new()),
        }
    }

    pub fn insert(&self, key: K, value: V) {
        self.map.insert(key, Box::new(value));
    }

    pub fn get(&self, key: &K) -> Option<&V> {
        self.map.get(key)
    }

    pub fn contains(&self, key: &K) -> bool {
        self.map.get(key).is_some()
    }

    pub fn len(&self) -> usize {
        self.map.len()
    }

    pub fn is_empty(&self) -> bool {
        self.map.len() == 0
    }

    pub fn as_vec(&self) -> Vec<(K, &V)>
    where
        K: Clone,
    {
        self.map
            .keys_cloned()
            .into_iter()
            .map(|k| {
                let v = self.map.get(&k).unwrap();
                (k, v)
            })
            .collect()
    }
}

impl<K, V> std::fmt::Debug for SharedTable<K, V> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SharedTable").finish_non_exhaustive()
    }
}

impl<K, V> Default for SharedTable<K, V>
where
    K: Eq + Hash,
{
    fn default() -> Self {
        Self::new()
    }
}

impl<K, V> Clone for SharedTable<K, V> {
    fn clone(&self) -> Self {
        Self {
            map: Arc::clone(&self.map),
        }
    }
}

// ─── FileId ─────────────────────────────────────────────────

/// Absolute file path, used as the stable file identity.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FileId {
    path: PathBuf,
}

impl FileId {
    pub fn new(path: PathBuf) -> Self {
        Self { path }
    }

    pub fn path(&self) -> &PathBuf {
        &self.path
    }
}

// ─── SourceFile ─────────────────────────────────────────────

#[derive(Debug, Clone)]
pub struct SourceFile {
    pub path: PathBuf,
    pub source: String,
}

// ─── Tests ──────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::hash_map::DefaultHasher;
    use std::hash::Hasher;

    // ── SharedTable ─────────────────────────────────────────

    #[test]
    fn shared_table_insert_and_get() {
        let t = SharedTable::new();
        t.insert("a", 1);
        assert_eq!(t.get(&"a"), Some(&1));
    }

    #[test]
    fn shared_table_get_missing_returns_none() {
        let t: SharedTable<&str, i32> = SharedTable::new();
        assert_eq!(t.get(&"x"), None);
    }

    #[test]
    fn shared_table_first_insert_wins() {
        let t = SharedTable::new();
        t.insert("a", 1);
        t.insert("a", 2);
        // FrozenMap: first insert wins
        assert_eq!(t.get(&"a"), Some(&1));
    }

    #[test]
    fn shared_table_contains_true() {
        let t = SharedTable::new();
        t.insert("a", 1);
        assert!(t.contains(&"a"));
    }

    #[test]
    fn shared_table_contains_false() {
        let t: SharedTable<&str, i32> = SharedTable::new();
        assert!(!t.contains(&"a"));
    }

    #[test]
    fn shared_table_clone_shares_state() {
        let t = SharedTable::new();
        let t2 = t.clone();
        t.insert("a", 1);
        assert_eq!(t2.get(&"a"), Some(&1));
    }

    #[test]
    fn shared_table_original_sees_clone_inserts() {
        let t = SharedTable::new();
        let t2 = t.clone();
        t2.insert("b", 42);
        assert_eq!(t.get(&"b"), Some(&42));
    }

    #[test]
    fn shared_table_empty_new() {
        let t: SharedTable<String, String> = SharedTable::new();
        assert_eq!(t.get(&"anything".to_string()), None);
    }

    #[test]
    fn shared_table_multiple_keys() {
        let t = SharedTable::new();
        for i in 0..100 {
            t.insert(i, i * 10);
        }
        for i in 0..100 {
            assert_eq!(t.get(&i), Some(&(i * 10)));
        }
    }

    #[test]
    fn shared_table_get_returns_ref() {
        let t = SharedTable::new();
        t.insert("a", vec![1, 2, 3]);
        let v = t.get(&"a").unwrap();
        assert_eq!(v, &vec![1, 2, 3]);
        // References are stable — get again returns same value
        let v2 = t.get(&"a").unwrap();
        assert_eq!(v, v2);
    }

    #[test]
    fn shared_table_len() {
        let t = SharedTable::new();
        t.insert("a", 1);
        t.insert("b", 2);
        assert_eq!(t.len(), 2);
    }

    #[test]
    fn shared_table_len_empty() {
        let t: SharedTable<&str, i32> = SharedTable::new();
        assert_eq!(t.len(), 0);
        assert!(t.is_empty());
    }

    // ── FileId ──────────────────────────────────────────────

    #[test]
    fn file_id_equality() {
        let a = FileId::new(PathBuf::from("/a/b.relux"));
        let b = FileId::new(PathBuf::from("/a/b.relux"));
        assert_eq!(a, b);
    }

    #[test]
    fn file_id_inequality() {
        let a = FileId::new(PathBuf::from("/a/b.relux"));
        let b = FileId::new(PathBuf::from("/a/c.relux"));
        assert_ne!(a, b);
    }

    #[test]
    fn file_id_hash_consistency() {
        let a = FileId::new(PathBuf::from("/a/b.relux"));
        let b = FileId::new(PathBuf::from("/a/b.relux"));
        let mut ha = DefaultHasher::new();
        a.hash(&mut ha);
        let mut hb = DefaultHasher::new();
        b.hash(&mut hb);
        assert_eq!(ha.finish(), hb.finish());
    }

    #[test]
    fn file_id_absolute_vs_relative() {
        let abs = FileId::new(PathBuf::from("/a/b"));
        let rel = FileId::new(PathBuf::from("a/b"));
        assert_ne!(abs, rel);
    }
}