trellis-core 0.2.1

Deterministic reconciler: state changes in; resource commands, output frames, and auditable receipts out.
Documentation
use trellis_core::{
    Graph, HostResourceCommandState, HostResourceOutcome, HostResourceStatus, HostStatusClass,
    ResourceKey, Revision, ScopeId, classify_host_resource_status,
};

fn scopes() -> (ScopeId, ScopeId) {
    let mut graph = Graph::<()>::new_with_command_type();
    let mut tx = graph.begin_transaction().unwrap();
    let primary = tx.create_scope("primary").unwrap();
    let other = tx.create_scope("other").unwrap();
    tx.commit().unwrap();
    (primary, other)
}

fn status(
    scope: ScopeId,
    command_revision: u64,
    status: HostResourceOutcome,
) -> HostResourceStatus {
    HostResourceStatus::new(
        ResourceKey::new("device:1"),
        scope,
        Revision::new(command_revision),
        Revision::new(100),
        status,
    )
}

fn live_state(
    scope: ScopeId,
    command_revision: u64,
    scope_owns_resource: bool,
) -> HostResourceCommandState {
    HostResourceCommandState {
        scope,
        command_revision: Revision::new(command_revision),
        resource_is_live: true,
        scope_owns_resource,
    }
}

fn closed_state(scope: ScopeId, command_revision: u64) -> HostResourceCommandState {
    HostResourceCommandState {
        scope,
        command_revision: Revision::new(command_revision),
        resource_is_live: false,
        scope_owns_resource: false,
    }
}

#[test]
fn live_resource_status_classifies_current_and_duplicate_revisions() {
    let (scope, _) = scopes();
    let status = status(scope, 3, HostResourceOutcome::Open);
    let state = live_state(scope, 3, true);

    assert_eq!(
        classify_host_resource_status(&status, Some(state), false),
        HostStatusClass::Current
    );
    assert_eq!(
        classify_host_resource_status(&status, Some(state), true),
        HostStatusClass::Duplicate
    );
}

#[test]
fn live_resource_status_classifies_stale_and_future_revisions() {
    let (scope, _) = scopes();
    let state = live_state(scope, 3, true);

    assert_eq!(
        classify_host_resource_status(
            &status(scope, 2, HostResourceOutcome::Failed("lost".to_owned())),
            Some(state),
            false
        ),
        HostStatusClass::Stale
    );
    assert_eq!(
        classify_host_resource_status(
            &status(scope, 4, HostResourceOutcome::Open),
            Some(state),
            false
        ),
        HostStatusClass::Future
    );
}

#[test]
fn live_resource_status_is_late_for_unknown_resource_or_non_owner_scope() {
    let (scope, other) = scopes();

    assert_eq!(
        classify_host_resource_status(&status(scope, 3, HostResourceOutcome::Open), None, false),
        HostStatusClass::Late
    );
    assert_eq!(
        classify_host_resource_status(
            &status(other, 3, HostResourceOutcome::Open),
            Some(live_state(scope, 3, false)),
            false
        ),
        HostStatusClass::Late
    );
}

#[test]
fn closed_resource_accepts_only_matching_scope_closed_ack() {
    let (scope, other) = scopes();
    let state = closed_state(scope, 3);

    assert_eq!(
        classify_host_resource_status(
            &status(scope, 3, HostResourceOutcome::Closed),
            Some(state),
            false
        ),
        HostStatusClass::Current
    );
    assert_eq!(
        classify_host_resource_status(
            &status(scope, 3, HostResourceOutcome::Open),
            Some(state),
            false
        ),
        HostStatusClass::Late
    );
    assert_eq!(
        classify_host_resource_status(
            &status(other, 3, HostResourceOutcome::Closed),
            Some(state),
            false
        ),
        HostStatusClass::Late
    );
}