shep 0.19.0

The shep binary: a process manager that keeps a flock of long-running processes alive on macOS, Linux and Windows, with logs, watch and cron restarts, and webhook alerts
Documentation
//! Adding and removing a `hosts` entry: a top-level array, not a table.

use super::*;

/// fails if the writer round-trips through a plain `toml::Table`, or puts
/// `hosts` under `[daemon]`, or lets it land after a table (where TOML would
/// read it as a key of that table).
#[test]
fn add_host_writes_a_top_level_array_and_keeps_comments_and_order() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("shep.toml");
    std::fs::write(&path, PRESERVATION_SEED).unwrap();

    assert!(ShepToml::try_edit(&path, |doc| doc.add_host("amp2")).unwrap());
    assert!(ShepToml::try_edit(&path, |doc| doc.add_host("deploy@build")).unwrap());

    let written = std::fs::read_to_string(&path).unwrap();
    assert_preserves_comments_and_key_order(&written);
    let cfg = DaemonConfig::load(Some(&written), &|_| None).unwrap();
    assert_eq!(cfg.hosts, ["amp2", "deploy@build"], "top level: {written}");
    assert!(!cfg.daemon.log_json);
    assert_eq!(
        ShepToml::read_only(&path).unwrap().host_names().unwrap(),
        ["amp2", "deploy@build"]
    );
}

#[test]
fn add_host_twice_keeps_one_entry() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("shep.toml");

    assert!(ShepToml::try_edit(&path, |doc| doc.add_host("amp2")).unwrap());
    assert!(!ShepToml::try_edit(&path, |doc| doc.add_host("amp2")).unwrap());

    assert_eq!(
        ShepToml::read_only(&path).unwrap().host_names().unwrap(),
        ["amp2"]
    );
}

#[test]
fn remove_host_leaves_every_other_key_alone() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("shep.toml");
    let original = "hosts = [\"amp2\", \"amp3\"]  # the fleet\n[daemon]\nlog_json = true\n";
    std::fs::write(&path, original).unwrap();

    assert!(ShepToml::try_edit(&path, |doc| doc.remove_host("amp2")).unwrap());
    assert!(!ShepToml::try_edit(&path, |doc| doc.remove_host("stranger")).unwrap());

    let written = std::fs::read_to_string(&path).unwrap();
    assert!(written.contains("[daemon]\nlog_json = true"), "{written}");
    assert!(written.contains("# the fleet"), "{written}");
    assert_eq!(
        ShepToml::read_only(&path).unwrap().host_names().unwrap(),
        ["amp3"]
    );
}

#[test]
fn a_hosts_key_that_is_not_an_array_is_wrong_shape_and_the_file_is_untouched() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("shep.toml");
    std::fs::write(&path, "hosts = \"amp2\"\n").unwrap();

    let err = ShepToml::try_edit(&path, |doc| doc.add_host("amp3")).unwrap_err();

    assert!(
        matches!(err, ShepTomlError::WrongShape { key: "hosts", .. }),
        "{err}"
    );
    assert_eq!(
        std::fs::read_to_string(&path).unwrap(),
        "hosts = \"amp2\"\n"
    );
}

#[test]
fn reading_or_removing_from_a_malformed_hosts_key_is_wrong_shape() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("shep.toml");

    for original in ["hosts = \"amp2\"\n", "hosts = [\"amp2\", 5]\n"] {
        std::fs::write(&path, original).unwrap();

        let doc = ShepToml::read_only(&path).unwrap();
        assert!(
            matches!(
                doc.host_names(),
                Err(ShepTomlError::WrongShape { key: "hosts", .. })
            ),
            "{original}"
        );
        let removed = ShepToml::try_edit(&path, |doc| doc.remove_host("amp2"));
        assert!(
            matches!(removed, Err(ShepTomlError::WrongShape { key: "hosts", .. })),
            "{original}"
        );
        assert_eq!(std::fs::read_to_string(&path).unwrap(), original);
    }
}