harmos-runtime 0.1.4

Harmos runtime: managed state, undo, recovery, background work and typed streams.
Documentation
/* ***
 *
 * Harmos
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * ***
 */

/* === Modules === */

/* === Re-Exports === */

/* === Imports === */

// Standard Library Imports

// Third Party Imports
use support::{Change, Refusal, VAULT, alice, bob, deposit, deposit_to, ledger, runtime, withdraw};
// Local Imports
use crate::state::support;
use harmos_runtime::{Error, state::At, state::Focus, state::Position, state::Reversals};

/* === Constants === */

/* === Macros === */

/* === Type Aliases === */

/* === Structs === */

/* === Enums === */

/* === Traits === */

/* === Implementations === */

/* === Public Functions === */

/* === Private Functions === */

#[tokio::test]
async fn conflicting_undo_refused() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());
    let theirs = runtime.state.as_principal(bob());

    mine.commit(deposit(40)).await.expect("accepted");
    theirs.commit(withdraw(40)).await.expect("the world moves on");

    let before = ledger(&runtime).await;
    let frontier = runtime.state.applied();

    let refused = mine.undo(alice(), Focus::all()).await;

    assert!(matches!(refused, Err(Error::UndoConflict(Refusal::Overdrawn))));
    assert_eq!(runtime.state.applied(), frontier, "a conflicting undo appends nothing");
    assert_eq!(ledger(&runtime).await, before, "the inverse ran its own check before anything mutated");
    assert_eq!(history(&runtime).await, vec![Position(1), Position(2)]);
}

#[tokio::test]
async fn undo_redo_append() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());

    let taken = mine.commit(deposit(40)).await.expect("accepted");
    let answer = mine.undo(alice(), Focus::all()).await.expect("alice has an entry to undo");

    assert_eq!(answer.position, Position(2), "history got longer");
    assert_eq!(support::balance(&runtime, support::HOUSE).await, 0);
    assert_eq!(history(&runtime).await, vec![taken.position, answer.position]);

    let restored = mine.redo(alice(), Focus::all()).await.expect("the undone entry is redoable");

    assert_eq!(restored.position, Position(3));
    assert_eq!(support::balance(&runtime, support::HOUSE).await, 40);
}

#[tokio::test]
async fn undo_selects_within() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());
    let theirs = runtime.state.as_principal(bob());

    mine.commit(deposit_to(VAULT, 7)).await.expect("accepted");
    theirs.commit(deposit(40)).await.expect("accepted");

    // Bob's newer entry neither blocks alice's undo nor is chosen by it.
    mine.undo(alice(), Focus::all()).await.expect("alice's own entry is still the candidate");

    assert_eq!(support::balance(&runtime, VAULT).await, 0);
    assert_eq!(support::balance(&runtime, support::HOUSE).await, 40);
    assert!(
        matches!(mine.undo(alice(), Focus::all()).await, Err(Error::NothingToUndo)),
        "alice cannot reach into bob's history for a second undo"
    );
}

#[tokio::test]
async fn reversals_report_undo() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());

    assert_eq!(
        available(&runtime, Focus::all()).await.value,
        Reversals { undoable: false, redoable: false },
        "an empty history offers neither direction"
    );

    let taken = mine.commit(deposit(40)).await.expect("accepted");
    let answered = available(&runtime, Focus::all()).await;

    assert_eq!(answered.value, Reversals { undoable: true, redoable: false });
    assert_eq!(answered.position, taken.position, "the answer says which point in history it reflects");

    mine.undo(alice(), Focus::all()).await.expect("undone");
    assert_eq!(
        available(&runtime, Focus::all()).await.value,
        Reversals { undoable: false, redoable: true },
        "the entry moved from one side to the other"
    );

    mine.redo(alice(), Focus::all()).await.expect("restored");
    assert_eq!(available(&runtime, Focus::all()).await.value, Reversals { undoable: true, redoable: false });
}

#[tokio::test]
async fn barrier_blocks_undo() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());

    mine.commit(deposit(40)).await.expect("accepted");
    mine.commit(Change::Freeze).await.expect("accepted");

    assert_eq!(
        available(&runtime, Focus::all()).await.value,
        Reversals { undoable: false, redoable: false },
        "a change that declared no inverse is a barrier, not a candidate behind it"
    );
    assert!(
        matches!(mine.undo(alice(), Focus::all()).await, Err(Error::NothingToUndo)),
        "the read and the verb cannot disagree about the barrier"
    );
}

#[tokio::test]
async fn availability_per_principal() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());
    let theirs = runtime.state.as_principal(bob());

    mine.commit(deposit_to(VAULT, 7)).await.expect("accepted");
    theirs.commit(deposit(40)).await.expect("accepted");

    assert!(
        available(&runtime, Focus::all()).await.value.undoable,
        "bob's newer entry neither lights alice's button nor stands in its way"
    );
    assert!(!available(&runtime, only(support::HOUSE)).await.value.undoable, "alice's own entry is outside the scope being asked about");
}

#[tokio::test]
async fn availability_appends_nothing() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());

    mine.commit(deposit(40)).await.expect("accepted");
    let frontier = runtime.state.applied();

    available(&runtime, Focus::all()).await;
    available(&runtime, Focus::all()).await;

    assert_eq!(runtime.state.applied(), frontier, "a question about undo is a read, not a commit");
}

#[tokio::test]
async fn forward_commit_clears() {
    let runtime = runtime().await;
    let mine = runtime.state.as_principal(alice());

    mine.commit(deposit(40)).await.expect("accepted");
    mine.undo(alice(), Focus::all()).await.expect("undone");

    mine.commit(deposit(7)).await.expect("the present diverges");

    assert!(matches!(mine.redo(alice(), Focus::all()).await, Err(Error::NothingToRedo)), "a new forward commit truncates the redo branch");
}

async fn history(runtime: &harmos_runtime::Runtime<support::Bank>) -> Vec<Position> {
    runtime.state.query::<Change>(Position::ORIGIN).await.map(|entry| entry.expect("the tail holds the whole history").position).collect()
}

async fn available(runtime: &harmos_runtime::Runtime<support::Bank>, scope: Focus<support::Bank>) -> At<Reversals> {
    runtime.state.reversals(alice(), scope).await.expect("the writer answers")
}

fn only(account: support::Account) -> Focus<support::Bank> {
    Focus::matching(move |change: &Change| support::account(change) == Some(account))
}

/* === Tests === */