trellis-core 0.1.2

Deterministic reactive resource graph core types.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use trellis_core::{Added, DependencyList, Graph, Removed, Unchanged, Updated};

fn map(entries: &[(&str, u64)]) -> BTreeMap<String, u64> {
    entries
        .iter()
        .map(|(key, value)| ((*key).to_owned(), *value))
        .collect()
}

fn set(entries: &[&str]) -> BTreeSet<String> {
    entries.iter().map(|value| (*value).to_owned()).collect()
}

#[test]
fn map_collection_detects_added_removed_updated_and_unchanged() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<BTreeMap<String, u64>>("source").unwrap();
    tx.set_input(source, map(&[("b", 2), ("a", 1)])).unwrap();
    let collection = tx
        .collection(
            "items",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| Ok(ctx.input(source)?.clone()),
        )
        .unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(result.changed_collection_nodes, vec![collection.id()]);
    assert_eq!(
        graph.map_diff(collection).unwrap().unwrap().added,
        vec![
            Added {
                value: ("a".to_owned(), 1)
            },
            Added {
                value: ("b".to_owned(), 2)
            },
        ]
    );

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(source, map(&[("a", 1), ("b", 3), ("c", 4)]))
        .unwrap();
    tx.commit().unwrap();
    drop(tx);
    let diff = graph.map_diff(collection).unwrap().unwrap();

    assert_eq!(
        diff.updated,
        vec![Updated {
            key: "b".to_owned(),
            previous: 2,
            current: 3
        }]
    );
    assert_eq!(
        diff.unchanged,
        vec![Unchanged {
            value: ("a".to_owned(), 1)
        }]
    );
    assert_eq!(
        diff.added,
        vec![Added {
            value: ("c".to_owned(), 4)
        }]
    );

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(source, map(&[("a", 1)])).unwrap();
    tx.commit().unwrap();
    drop(tx);
    let diff = graph.map_diff(collection).unwrap().unwrap();

    assert_eq!(
        diff.removed,
        vec![
            Removed {
                value: ("b".to_owned(), 3)
            },
            Removed {
                value: ("c".to_owned(), 4)
            },
        ]
    );
}

#[test]
fn set_collection_detects_structural_diff_and_empty_source() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<BTreeSet<String>>("source").unwrap();
    tx.set_input(source, set(&["c", "a", "b"])).unwrap();
    let collection = tx
        .set_collection(
            "members",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| Ok(ctx.input(source)?.clone()),
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    assert_eq!(
        graph.set_diff(collection).unwrap().unwrap().added,
        vec![
            Added {
                value: "a".to_owned()
            },
            Added {
                value: "b".to_owned()
            },
            Added {
                value: "c".to_owned()
            },
        ]
    );

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(source, BTreeSet::new()).unwrap();
    let result = tx.commit().unwrap();
    drop(tx);
    let diff = graph.set_diff(collection).unwrap().unwrap();

    assert_eq!(result.changed_collection_nodes, vec![collection.id()]);
    assert!(diff.added.is_empty());
    assert_eq!(
        diff.removed,
        vec![
            Removed {
                value: "a".to_owned()
            },
            Removed {
                value: "b".to_owned()
            },
            Removed {
                value: "c".to_owned()
            },
        ]
    );
    assert!(
        graph
            .set_collection(collection)
            .unwrap()
            .unwrap()
            .is_empty()
    );
}

#[test]
fn equal_collection_result_produces_empty_diff_and_does_not_propagate() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<u64>("source").unwrap();
    tx.set_input(source, 1).unwrap();
    let collection = tx
        .set_collection(
            "parity",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| {
                let label = if *ctx.input(source)? % 2 == 0 {
                    "even"
                } else {
                    "odd"
                };
                Ok(set(&[label]))
            },
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(source, 3).unwrap();
    let result = tx.commit().unwrap();
    drop(tx);
    let diff = graph.set_diff(collection).unwrap().unwrap();

    assert!(result.changed_collection_nodes.is_empty());
    assert!(diff.is_empty());
    assert_eq!(
        diff.unchanged,
        vec![Unchanged {
            value: "odd".to_owned()
        }]
    );
}

#[test]
fn collection_can_depend_on_collection_in_stable_order() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<BTreeSet<String>>("source").unwrap();
    tx.set_input(source, set(&["bb", "a"])).unwrap();
    let base = tx
        .set_collection(
            "base",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| Ok(ctx.input(source)?.clone()),
        )
        .unwrap();
    let lengths = tx
        .collection(
            "lengths",
            DependencyList::new([base.id()]).unwrap(),
            move |ctx| {
                Ok(ctx
                    .set_collection(base)?
                    .iter()
                    .map(|value| (value.clone(), value.len()))
                    .collect())
            },
        )
        .unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(
        result.changed_collection_nodes,
        vec![base.id(), lengths.id()]
    );
    assert_eq!(graph.map_collection(lengths).unwrap().unwrap()["bb"], 2);
}

#[test]
fn large_collection_diff_is_deterministic() {
    let initial: BTreeMap<u64, u64> = (0..10_000).map(|value| (value, value)).collect();
    let next: BTreeMap<u64, u64> = (5_000..15_000).map(|value| (value, value + 1)).collect();

    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<BTreeMap<u64, u64>>("source").unwrap();
    tx.set_input(source, initial).unwrap();
    let collection = tx
        .collection(
            "large",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| Ok(ctx.input(source)?.clone()),
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(source, next).unwrap();
    tx.commit().unwrap();
    drop(tx);
    let diff = graph.map_diff(collection).unwrap().unwrap();

    assert_eq!(diff.removed.len(), 5_000);
    assert_eq!(diff.updated.len(), 5_000);
    assert_eq!(diff.added.len(), 5_000);
    assert_eq!(diff.removed.first().unwrap().value, (0, 0));
    assert_eq!(diff.updated.first().unwrap().key, 5_000);
    assert_eq!(diff.added.first().unwrap().value, (10_000, 10_001));
}

#[test]
fn full_recompute_includes_collections() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let source = tx.input::<BTreeSet<String>>("source").unwrap();
    tx.set_input(source, set(&["a"])).unwrap();
    let collection = tx
        .set_collection(
            "members",
            DependencyList::new([source.id()]).unwrap(),
            move |ctx| Ok(ctx.input(source)?.clone()),
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    let check = graph.full_recompute_check().unwrap();

    assert!(check.checked_derived.is_empty());
    assert_eq!(check.checked_collections, vec![collection.id()]);
}