xtask-todo-lib 0.1.32

Todo workspace library and cargo devshell subcommand
Documentation
//! Basic `run_with` tests: pwd, mkdir, ls, echo, help, save, cd, todo list/stats/add.

use std::io::Cursor;
use std::sync::{Mutex, OnceLock, PoisonError};

use super::super::command::{execute_pipeline, run_builtin, ExecContext, RunResult};
use super::super::parser::{Pipeline, SimpleCommand};
use super::super::run_with;
use super::super::vfs::Vfs;
use super::super::vm::SessionHolder;

fn path_env_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
}

#[test]
fn run_with_pwd_mkdir_ls_exit() {
    let input = "pwd\nmkdir foo\nls\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains(" $ "), "expected prompt in output: {out}");
    assert!(out.contains("foo"), "expected ls to list foo: {out}");
}

#[test]
fn run_with_echo_and_exit() {
    let input = "echo hello\nquit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("hello"), "expected echo output: {out}");
}

#[test]
fn run_with_usage_error() {
    let mut stdin = Cursor::new("");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = run_with(
        &["a".to_string(), "b".to_string(), "c".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert!(r.is_err());
}

#[test]
fn run_with_help() {
    let input = "help\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("Supported commands:"));
    assert!(out.contains("pwd"));
    assert!(out.contains("todo"));
}

#[test]
fn run_with_help_lists_builtin_command_set() {
    let input = "help\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    for cmd in [
        "pwd",
        "cd",
        "ls",
        "mkdir",
        "cat",
        "touch",
        "echo",
        "save",
        "export-readonly",
        "export_readonly",
        "todo",
        "rustup",
        "cargo",
        "exit, quit",
        "help",
    ] {
        assert!(out.contains(cmd), "help should mention {cmd}: {out}");
    }
}

#[test]
fn run_with_save() {
    let input = "mkdir x\nsave /tmp/devshell_save_test.bin\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &[
            "dev_shell".to_string(),
            "/tmp/devshell_save_test.bin".to_string(),
        ],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let _ = std::fs::remove_file("/tmp/devshell_save_test.bin");
}

#[test]
fn run_with_todo_list_and_stats() {
    let input = "todo list\ntodo stats\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("total: 0") || out.contains("open:") || out.contains("completed:"));
}

#[test]
fn run_with_todo_add_and_list() {
    let _g = crate::test_support::cwd_mutex();
    let dir = std::env::temp_dir().join(format!("devshell_todo_add_{}", std::process::id()));
    let _ = std::fs::create_dir_all(&dir);
    let cwd = std::env::current_dir().unwrap();
    let _ = std::env::set_current_dir(&dir);
    let input = "todo add buy milk\ntodo list\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    let _ = std::env::set_current_dir(&cwd);
    let _ = std::fs::remove_file(dir.join(".todo.json"));
    let _ = std::fs::remove_dir(&dir);
    r.unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("buy milk") || out.contains("1.") || out.contains(" $ "));
}

#[test]
fn execute_pipeline_empty_returns_continue() {
    let mut vfs = Vfs::new();
    let mut vm_session = SessionHolder::new_host();
    let mut stdin = Cursor::new(vec![]);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let mut ctx = ExecContext {
        vfs: &mut vfs,
        stdin: &mut stdin,
        stdout: &mut stdout,
        stderr: &mut stderr,
        vm_session: &mut vm_session,
    };
    let pipeline = Pipeline { commands: vec![] };
    let r = execute_pipeline(&mut ctx, &pipeline).unwrap();
    assert_eq!(r, RunResult::Continue);
}

#[test]
fn execute_pipeline_exit_returns_exit() {
    let mut vfs = Vfs::new();
    let mut vm_session = SessionHolder::new_host();
    let mut stdin = Cursor::new(vec![]);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let mut ctx = ExecContext {
        vfs: &mut vfs,
        stdin: &mut stdin,
        stdout: &mut stdout,
        stderr: &mut stderr,
        vm_session: &mut vm_session,
    };
    let pipeline = Pipeline {
        commands: vec![SimpleCommand {
            argv: vec!["exit".to_string()],
            redirects: vec![],
        }],
    };
    let r = execute_pipeline(&mut ctx, &pipeline).unwrap();
    assert_eq!(r, RunResult::Exit);
}

#[test]
fn run_builtin_pwd_covers_wrapper() {
    let mut vfs = Vfs::new();
    let mut vm_session = SessionHolder::new_host();
    let mut stdin = Cursor::new(vec![]);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let mut ctx = ExecContext {
        vfs: &mut vfs,
        stdin: &mut stdin,
        stdout: &mut stdout,
        stderr: &mut stderr,
        vm_session: &mut vm_session,
    };
    let cmd = SimpleCommand {
        argv: vec!["pwd".to_string()],
        redirects: vec![],
    };
    run_builtin(&mut ctx, &cmd).unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains('/'));
}

#[test]
fn run_with_cd_and_pwd() {
    let input = "mkdir /a\ncd /a\npwd\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("/a"));
}

#[test]
fn run_with_rust_tools_missing_in_path_are_diagnostic() {
    let _g = path_env_lock();
    let old_path = std::env::var_os("PATH");
    std::env::set_var("PATH", "/nonexistent_devshell_path_404");
    let input = "cargo --version\nrustup --version\nexit\n";
    let mut stdin = Cursor::new(input);
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run_with(
        &["dev_shell".to_string()],
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .unwrap();
    match old_path {
        Some(v) => std::env::set_var("PATH", v),
        None => std::env::remove_var("PATH"),
    }
    let err = String::from_utf8(stderr).unwrap();
    assert!(
        err.contains("cargo not found in PATH"),
        "stderr should contain CargoNotFound diagnostic: {err}"
    );
    assert!(
        err.contains("rustup not found in PATH"),
        "stderr should contain RustupNotFound diagnostic: {err}"
    );
}