yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
use super::*;
use crate::cli_outbound::Cli;
use crate::projects::balls::parse_list;
use crate::test_support::spawn_guard;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::MutexGuard;
use tempfile::tempdir;

/// A recorded [`BlRunner`]: one canned reply for every method, or an error.
struct FakeRunner {
    reply: Option<String>,
}

impl FakeRunner {
    fn answer(&self) -> io::Result<String> {
        self.reply.clone().ok_or_else(|| io::Error::other("boom"))
    }
}

impl BlRunner for FakeRunner {
    fn list(&self, _project: &Path) -> io::Result<String> {
        self.answer()
    }
    fn list_closed(&self, _project: &Path) -> io::Result<String> {
        self.answer()
    }
    fn show(&self, _project: &Path, _id: &str) -> io::Result<String> {
        self.answer()
    }
}

/// Write an executable fake `bl` under `SPAWN_LOCK` (the crate-wide spawn
/// discipline); the guard is held across the spawn the caller then does.
fn write_bl(dir: &Path, body: &str) -> (PathBuf, MutexGuard<'static, ()>) {
    let guard = spawn_guard();
    let path = dir.join("bl");
    std::fs::write(&path, body).unwrap();
    let mut perms = std::fs::metadata(&path).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&path, perms).unwrap();
    (path, guard)
}

#[test]
fn blcli_runs_bl_json_and_returns_stdout_ignoring_stderr() {
    let dir = tempdir().unwrap();
    // Noise on stderr (exercises the Stderr arm) + a JSON array on stdout.
    let (bin, _g) = write_bl(
        dir.path(),
        "#!/bin/sh\nprintf 'warn\\n' 1>&2\nprintf '[{\"id\":\"bl-1\"}]\\n'\n",
    );
    let runner = BlCli::new(Cli::new(bin));
    let out = runner.list(dir.path()).unwrap();
    assert_eq!(parse_list(&out).len(), 1);
    // The on-demand methods route through the same collector.
    assert!(runner.list_closed(dir.path()).is_ok());
    assert!(runner.show(dir.path(), "bl-1").is_ok());
}

#[test]
fn blcli_errors_on_spawn_failure() {
    let dir = tempdir().unwrap();
    let _g = spawn_guard();
    let missing = BlCli::new(Cli::new(dir.path().join("no-such-bl")));
    assert!(missing.list(dir.path()).is_err());
}

#[test]
fn blcli_errors_on_nonzero_exit() {
    let dir = tempdir().unwrap();
    let (bin, _g) = write_bl(dir.path(), "#!/bin/sh\nprintf 'partial'\nexit 3\n");
    assert!(BlCli::new(Cli::new(bin)).list(dir.path()).is_err());
}

#[test]
fn live_closed_and_detail_parse_or_default_forgivingly() {
    let p = Path::new("/p");
    let ok = FakeRunner {
        reply: Some(r#"[{"id":"bl-1"}]"#.to_owned()),
    };
    assert_eq!(ok.live(p).len(), 1);
    assert_eq!(ok.closed(p).len(), 1);

    let err = FakeRunner { reply: None };
    assert!(err.live(p).is_empty(), "runner error ⇒ no balls");
    assert!(err.closed(p).is_empty());

    let one = FakeRunner {
        reply: Some(r#"{"id":"bl-1","title":"T"}"#.to_owned()),
    };
    assert_eq!(one.detail(p, "bl-1").unwrap().title, "T");
    assert!(err.detail(p, "bl-1").is_none(), "runner error ⇒ None");
    let garbage = FakeRunner {
        reply: Some("nope".to_owned()),
    };
    assert!(garbage.detail(p, "bl-1").is_none(), "unparseable ⇒ None");
}

#[test]
fn identity_prefers_recorded_then_user_then_empty() {
    assert_eq!(
        identity(Some("recorded".to_owned()), Some("user".to_owned())),
        "recorded"
    );
    assert_eq!(identity(None, Some("user".to_owned())), "user");
    assert_eq!(identity(None, None), "");
}