thrall 0.0.15

thrall: the foot — a tool-execution client for a yog server
Documentation
//! Every channel a box holds, served at once — each under its own identity,
//! and each answering under its own name.

use super::super::fan;
use super::boom;
use super::{advertised, aside, echo, engine_at, receipt, refusal, restored, row, set, work};
use crate::channel::entries;
use crate::test_support::{Notices, Scratch, unwaited};

/// **Every channel this box holds is served at once**, each under its own
/// identity and its own material, and each answers the sentence that stopped it
/// under its own name.
#[test]
fn every_entry_is_served_and_each_answers_under_its_own_name() {
    let scratch = Scratch::new();
    let _one = engine_at(
        &scratch.join("north"),
        vec![
            advertised(),
            work(vec![row("i-1", "Bash")]),
            refusal("north stopped"),
        ],
    );
    let _two = engine_at(&scratch.join("south"), vec![refusal("south stopped")]);
    // A third entry that was made and never provisioned: its refusal is its
    // own, and it does not cost the other two.
    std::fs::create_dir_all(scratch.join("empty")).expect("mkdir");

    let said = fan(
        entries::read_dir(scratch.path()),
        set(),
        echo,
        &aside(),
        &unwaited(),
    );
    assert_eq!(said.len(), 3);
    assert!(said[0].starts_with("empty: "), "{said:?}");
    assert!(said[0].contains("is an empty entry"), "{said:?}");
    assert_eq!(said[1], "north: north stopped");
    assert_eq!(said[2], "south: south stopped");
}

/// A box that holds no entry stops instantly with nothing to say. What to make
/// of that belongs to the caller, which is the one that knows where entries
/// would have been filed.
#[test]
fn a_box_with_no_entry_has_nothing_to_report() {
    let scratch = Scratch::new();
    assert_eq!(
        fan(
            entries::read_dir(scratch.path()),
            set(),
            echo,
            &aside(),
            &unwaited()
        ),
        Vec::<String>::new()
    );
}

/// **A notice carries the name of the channel that raised it**, the same
/// prefix that channel's ending sentence carries. A box holding two engines
/// that is told it was disarmed, and not by which one, has been told nothing it
/// can act on — the two channels have different identities and different
/// operators.
#[test]
fn a_notice_names_the_channel_that_raised_it() {
    let (notices, sink) = Notices::new();
    let scratch = Scratch::new();
    let _one = engine_at(
        &scratch.join("north"),
        vec![
            advertised(),
            work(vec![row("i-1", "Bash")]),
            receipt("i-1"),
            restored(),
            work(vec![row("i-2", "Bash")]),
            refusal("north stopped"),
        ],
    );
    let _two = engine_at(&scratch.join("south"), vec![refusal("south stopped")]);
    let said = fan(
        entries::read_dir(scratch.path()),
        set(),
        echo,
        &sink,
        &unwaited(),
    );
    assert_eq!(said, ["north: north stopped", "south: south stopped"]);
    let heard = notices.heard();
    let disarm: Vec<&String> = heard
        .iter()
        .filter(|line| line.contains("was not the set in force"))
        .collect();
    assert_eq!(disarm.len(), 1, "{heard:?}");
    assert!(disarm[0].starts_with("north: "), "{heard:?}");
    // The two ending sentences are the other two lines, each under its own
    // name: the sink carries every channel's terminal sentence since bl-e834.
    assert_eq!(heard.len(), 3, "{heard:?}");
    assert!(
        heard.contains(&"north: north stopped".to_owned()),
        "{heard:?}"
    );
    assert!(
        heard.contains(&"south: south stopped".to_owned()),
        "{heard:?}"
    );
}

/// **One engine's conversation does not take the others down.** An executor
/// that dies is named as its own outcome, under the channel it killed, and the
/// channel beside it still answers.
#[test]
fn a_channel_that_dies_is_named_and_does_not_take_the_others_with_it() {
    let scratch = Scratch::new();
    let _one = engine_at(
        &scratch.join("north"),
        vec![advertised(), work(vec![row("i-1", "Bash")])],
    );
    let _two = engine_at(&scratch.join("south"), vec![refusal("south stopped")]);
    let said = fan(
        entries::read_dir(scratch.path()),
        set(),
        boom,
        &aside(),
        &unwaited(),
    );
    assert_eq!(
        said,
        [
            "north: the channel ended by panicking",
            "south: south stopped"
        ]
    );
}