command-stream 1.7.0

Modern shell command execution library with streaming, async iteration, and event support
Documentation
use command_stream::{dev, DevEvent, DevOptions};
use std::time::Duration;

#[test]
fn watched_child() {
    if std::env::var_os("COMMAND_STREAM_DEV_CHILD").is_some() {
        std::thread::sleep(Duration::from_secs(60));
    }
}

#[tokio::test]
async fn watcher_restarts_for_create_edit_and_delete_and_closes() {
    tokio::time::timeout(Duration::from_secs(10), async {
        let root = tempfile::tempdir().unwrap();
        let cwd = root.path().canonicalize().unwrap();
        let file = cwd.join("scripts");
        std::fs::create_dir(&file).unwrap();
        let mut controller = dev(DevOptions {
            cwd: cwd.clone(),
            watch: vec!["./scripts/*.rs".into()],
            command: vec![
                std::env::current_exe()
                    .unwrap()
                    .to_string_lossy()
                    .into_owned(),
                "--exact".into(),
                "watched_child".into(),
                "--nocapture".into(),
            ],
            interval: Duration::from_millis(10),
            debounce: Duration::ZERO,
            ..Default::default()
        })
        .await
        .unwrap();
        let mut pid = match controller.next_event().await.unwrap() {
            DevEvent::Restarted { files, pid } => {
                assert!(files.is_empty());
                pid.unwrap()
            }
            DevEvent::Error(message) => panic!("{message}"),
        };
        let watched = file.join("app.rs");
        for revision in [Some("first"), Some("second longer"), None] {
            if let Some(content) = revision {
                std::fs::write(&watched, content).unwrap();
            } else {
                std::fs::remove_file(&watched).unwrap();
            }
            match controller.next_event().await.unwrap() {
                DevEvent::Restarted { files, pid: next } => {
                    assert_eq!(files, vec![watched.clone()]);
                    assert_stopped(pid);
                    pid = next.unwrap();
                }
                DevEvent::Error(message) => panic!("{message}"),
            }
        }
        controller.close().await.unwrap();
        assert_stopped(pid);
    })
    .await
    .expect("watcher test exceeded 10 seconds");
}

fn assert_stopped(pid: u32) {
    #[cfg(unix)]
    assert!(nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), None).is_err());
    #[cfg(not(unix))]
    let _ = pid;
}

#[tokio::test]
async fn invalid_watcher_options_fail_immediately() {
    assert!(dev(DevOptions {
        interval: Duration::ZERO,
        ..Default::default()
    })
    .await
    .is_err());
    assert!(dev(DevOptions {
        command: vec![],
        ..Default::default()
    })
    .await
    .is_err());
    assert!(dev(DevOptions {
        watch: vec!["[".into()],
        initial: false,
        ..Default::default()
    })
    .await
    .is_err());
}

#[tokio::test]
async fn default_patterns_match_root_sources_and_ignore_build_output() {
    tokio::time::timeout(Duration::from_secs(5), async {
        let root = tempfile::tempdir().unwrap();
        let cwd = root.path().canonicalize().unwrap();
        std::fs::create_dir(cwd.join("target")).unwrap();
        let mut controller = dev(DevOptions {
            cwd: cwd.clone(),
            command: vec![
                std::env::current_exe()
                    .unwrap()
                    .to_string_lossy()
                    .into_owned(),
                "--exact".into(),
                "watched_child".into(),
            ],
            initial: false,
            interval: Duration::from_millis(10),
            debounce: Duration::ZERO,
            ..Default::default()
        })
        .await
        .unwrap();
        std::fs::write(cwd.join("target/generated.rs"), "ignored").unwrap();
        assert!(
            tokio::time::timeout(Duration::from_millis(50), controller.next_event())
                .await
                .is_err()
        );
        let source = cwd.join("main.rs");
        std::fs::write(&source, "watched").unwrap();
        let pid = match controller.next_event().await.unwrap() {
            DevEvent::Restarted { files, pid } => {
                assert_eq!(files, vec![source]);
                pid.unwrap()
            }
            DevEvent::Error(message) => panic!("{message}"),
        };
        controller.close().await.unwrap();
        assert_stopped(pid);
    })
    .await
    .expect("default-pattern test exceeded five seconds");
}