trellis-core 0.1.0

Deterministic reactive resource graph core types.
Documentation
use std::cell::Cell;
use std::rc::Rc;

use trellis_core::{DependencyList, DeriveError, Graph, GraphError};

#[test]
fn derived_node_recomputes_when_input_changes() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let input = tx.input::<String>("name").unwrap();
    tx.set_input(input, "trellis".to_owned()).unwrap();
    let len = tx
        .derived::<usize>(
            "name_len",
            DependencyList::new([input.id()]).unwrap(),
            move |ctx| Ok(ctx.input(input)?.len()),
        )
        .unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(result.changed_derived_nodes, vec![len.id()]);
    assert_eq!(graph.derived_value(len).unwrap(), Some(&7));

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(input, "graph".to_owned()).unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(result.changed_inputs, vec![input.id()]);
    assert_eq!(result.changed_derived_nodes, vec![len.id()]);
    assert_eq!(graph.derived_value(len).unwrap(), Some(&5));
}

#[test]
fn unaffected_derived_node_does_not_recompute() {
    let left_runs = Rc::new(Cell::new(0));
    let right_runs = Rc::new(Cell::new(0));

    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let left_input = tx.input::<u64>("left_input").unwrap();
    let right_input = tx.input::<u64>("right_input").unwrap();
    tx.set_input(left_input, 1).unwrap();
    tx.set_input(right_input, 10).unwrap();

    let left_runs_for_derive = Rc::clone(&left_runs);
    let left = tx
        .derived::<u64>(
            "left",
            DependencyList::new([left_input.id()]).unwrap(),
            move |ctx| {
                left_runs_for_derive.set(left_runs_for_derive.get() + 1);
                Ok(*ctx.input(left_input)? + 1)
            },
        )
        .unwrap();

    let right_runs_for_derive = Rc::clone(&right_runs);
    let right = tx
        .derived::<u64>(
            "right",
            DependencyList::new([right_input.id()]).unwrap(),
            move |ctx| {
                right_runs_for_derive.set(right_runs_for_derive.get() + 1);
                Ok(*ctx.input(right_input)? + 1)
            },
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    assert_eq!(left_runs.get(), 1);
    assert_eq!(right_runs.get(), 1);

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(left_input, 2).unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(result.changed_derived_nodes, vec![left.id()]);
    assert_eq!(graph.derived_value(left).unwrap(), Some(&3));
    assert_eq!(graph.derived_value(right).unwrap(), Some(&11));
    assert_eq!(left_runs.get(), 2);
    assert_eq!(right_runs.get(), 1);
}

#[test]
fn derived_node_can_depend_on_another_derived_node() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let input = tx.input::<u64>("input").unwrap();
    tx.set_input(input, 2).unwrap();
    let doubled = tx
        .derived::<u64>(
            "doubled",
            DependencyList::new([input.id()]).unwrap(),
            move |ctx| Ok(*ctx.input(input)? * 2),
        )
        .unwrap();
    let plus_one = tx
        .derived::<u64>(
            "plus_one",
            DependencyList::new([doubled.id()]).unwrap(),
            move |ctx| Ok(*ctx.derived(doubled)? + 1),
        )
        .unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert_eq!(
        result.changed_derived_nodes,
        vec![doubled.id(), plus_one.id()]
    );
    assert_eq!(graph.derived_value(plus_one).unwrap(), Some(&5));
}

#[test]
fn derived_self_cycle_is_rejected() {
    let mut other_graph = Graph::new();
    let mut other_tx = other_graph.begin_transaction().unwrap();
    let foreign = other_tx.input::<u64>("foreign").unwrap();
    other_tx.commit().unwrap();
    drop(other_tx);

    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let error = tx
        .derived::<u64>(
            "self_cycle",
            DependencyList::new([foreign.id()]).unwrap(),
            |_| Ok(0),
        )
        .unwrap_err();

    assert_eq!(error, GraphError::SelfDependency(foreign.id()));
}

#[test]
fn equal_recompute_does_not_propagate_by_default() {
    let downstream_runs = Rc::new(Cell::new(0));

    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let input = tx.input::<u64>("input").unwrap();
    tx.set_input(input, 1).unwrap();
    let parity = tx
        .derived::<u64>(
            "parity",
            DependencyList::new([input.id()]).unwrap(),
            move |ctx| Ok(*ctx.input(input)? % 2),
        )
        .unwrap();
    let downstream_runs_for_derive = Rc::clone(&downstream_runs);
    let downstream = tx
        .derived::<String>(
            "downstream",
            DependencyList::new([parity.id()]).unwrap(),
            move |ctx| {
                downstream_runs_for_derive.set(downstream_runs_for_derive.get() + 1);
                Ok(format!("parity={}", ctx.derived(parity)?))
            },
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(input, 3).unwrap();
    let result = tx.commit().unwrap();
    drop(tx);

    assert!(result.changed_inputs.contains(&input.id()));
    assert!(result.changed_derived_nodes.is_empty());
    assert_eq!(
        graph.derived_value(downstream).unwrap(),
        Some(&"parity=1".to_owned())
    );
    assert_eq!(downstream_runs.get(), 1);
}

#[test]
fn derive_error_does_not_corrupt_committed_value() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let input = tx.input::<u64>("input").unwrap();
    tx.set_input(input, 1).unwrap();
    let derived = tx
        .derived::<u64>(
            "nonzero",
            DependencyList::new([input.id()]).unwrap(),
            move |ctx| {
                let value = *ctx.input(input)?;
                if value == 0 {
                    Err(DeriveError::message("zero"))
                } else {
                    Ok(value)
                }
            },
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

    let mut tx = graph.begin_transaction().unwrap();
    tx.set_input(input, 0).unwrap();
    let tx_id = tx.id();
    let error = tx.commit().unwrap_err();
    assert_eq!(
        tx.commit().unwrap_err(),
        GraphError::TransactionClosed(tx_id)
    );
    drop(tx);

    assert_eq!(
        error,
        GraphError::DeriveFailed(derived.id(), DeriveError::message("zero"))
    );
    assert_eq!(graph.input_value(input).unwrap(), Some(&1));
    assert_eq!(graph.derived_value(derived).unwrap(), Some(&1));
}

#[test]
fn full_recompute_matches_incremental_state() {
    let mut graph = Graph::new();
    let mut tx = graph.begin_transaction().unwrap();
    let input = tx.input::<u64>("input").unwrap();
    tx.set_input(input, 4).unwrap();
    let doubled = tx
        .derived::<u64>(
            "doubled",
            DependencyList::new([input.id()]).unwrap(),
            move |ctx| Ok(*ctx.input(input)? * 2),
        )
        .unwrap();
    tx.commit().unwrap();
    drop(tx);

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

    assert_eq!(check.checked_derived, vec![doubled.id()]);
}