netsky 0.1.4

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
//! `netsky test [suite|path]` — test runner for tests/{unit,agent}/.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;

const TESTS_DIR: &str = "tests";
const UNIT_SUBDIR: &str = "unit";
const AGENT_SUBDIR: &str = "agent";
const TEST_PREFIX: &str = "test-";
const TEST_EXT: &str = "sh";

pub fn run(scope: &str) -> netsky_core::Result<()> {
    let tests = resolve_tests(scope)?;
    if tests.is_empty() {
        println!("no tests found");
        return Ok(());
    }

    let tests_dir = Path::new(TESTS_DIR);
    let mut pass = 0;
    let mut fail = 0;
    let mut failed = Vec::new();

    for t in &tests {
        let name = t.strip_prefix(tests_dir).unwrap_or(t).display().to_string();
        print!("  {name:<50} ");
        let start = Instant::now();
        let out = Command::new("bash").arg(t).output()?;
        let elapsed = start.elapsed().as_secs();
        if out.status.success() {
            println!("pass {elapsed}s");
            pass += 1;
        } else {
            println!("FAIL {elapsed}s");
            for l in String::from_utf8_lossy(&out.stdout).lines() {
                println!("    {l}");
            }
            for l in String::from_utf8_lossy(&out.stderr).lines() {
                println!("    {l}");
            }
            fail += 1;
            failed.push(name);
        }
    }

    println!();
    if fail == 0 {
        println!("summary: {pass} passed");
        Ok(())
    } else {
        println!("summary: {fail} failed, {pass} passed");
        for n in &failed {
            println!("  - {n}");
        }
        netsky_core::bail!("tests failed")
    }
}

fn resolve_tests(scope: &str) -> netsky_core::Result<Vec<PathBuf>> {
    let tests_dir = Path::new(TESTS_DIR);
    match scope {
        "all" => {
            let mut v = collect_in(&tests_dir.join(UNIT_SUBDIR));
            v.extend(collect_in(&tests_dir.join(AGENT_SUBDIR)));
            v.sort();
            Ok(v)
        }
        UNIT_SUBDIR | AGENT_SUBDIR => {
            let mut v = collect_in(&tests_dir.join(scope));
            v.sort();
            Ok(v)
        }
        other => {
            let candidate = tests_dir.join(other);
            if candidate.is_file() {
                Ok(vec![candidate])
            } else if Path::new(other).is_file() {
                Ok(vec![PathBuf::from(other)])
            } else {
                netsky_core::bail!("no such test: {other}")
            }
        }
    }
}

fn collect_in(dir: &Path) -> Vec<PathBuf> {
    let Ok(entries) = fs::read_dir(dir) else {
        return Vec::new();
    };
    entries
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| {
            p.extension().and_then(|s| s.to_str()) == Some(TEST_EXT)
                && p.file_name()
                    .and_then(|s| s.to_str())
                    .is_some_and(|n| n.starts_with(TEST_PREFIX))
        })
        .collect()
}