ek-ui 0.9.3

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! L1:pick 清单读写 + spawn/boot-wait 协议(用 powershell.exe 当零依赖子进程替身)。

use std::path::Path;
use std::time::Duration;

use ek_ui::launch::{LaunchFail, launch, spawn_detached, unique_marker, wait_ready};
use ek_ui::pick::{PickErr, read_pick, write_pick_in};

fn tmp_sub(tag: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!("ek-ui-test-{}-{tag}", std::process::id()))
}

// ---- 清单 ----

#[test]
fn k0_pick_roundtrip() {
    let dir = tmp_sub("k0");
    let written = write_pick_in(&dir, &["dm", "oms", "ui"]).unwrap();
    let got = read_pick(&written).unwrap();
    assert_eq!(
        got,
        vec!["dm".to_string(), "oms".to_string(), "ui".to_string()]
    );
}

#[test]
fn k1_pick_strict_rules() {
    let dir = tmp_sub("k1");
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("bad.list");
    std::fs::write(&path, "# 注释行\n dm oms\ndm\n").unwrap();
    let err = read_pick(&path).unwrap_err();
    // "dm oms" 是带空白的坏行(行号 2,内容去尾后原文)。
    assert!(
        matches!(err, PickErr::BadLine(2, ref s) if s.contains("dm oms")),
        "got {err:?}"
    );

    let empty = dir.join("empty.list");
    std::fs::write(&empty, "  \n# 仅注释\n\n").unwrap();
    assert_eq!(read_pick(&empty).unwrap_err(), PickErr::Empty);

    // 写侧防御同样拒空白/前缀。
    let werr = write_pick_in(&dir, &["dm oms"]).unwrap_err();
    assert!(werr.to_string().contains("whitespace"));
    assert_eq!(write_pick_in(&dir, &[]).unwrap_err(), PickErr::Empty);
}

// ---- spawn 协议(powershell.exe 子进程替身) ----

fn ps_cmd(script: &str) -> Vec<String> {
    vec![
        "-NoProfile".to_string(),
        "-Command".to_string(),
        script.to_string(),
    ]
}

#[test]
fn k2_launch_success_ready_marker() {
    let dir = tmp_sub("k2");
    let marker = unique_marker(&dir);
    let script = format!(
        "Set-Content -LiteralPath '{}' -Value 'ready'; exit 0",
        marker.display()
    );
    let r = launch(
        Path::new("powershell"),
        &ps_cmd(&script),
        &marker,
        Duration::from_secs(20),
    );
    assert!(r.is_ok(), "expected ok, got {r:?}");
    assert!(marker.is_file(), "ready marker must exist");
}

#[test]
fn k3_launch_exit_without_marker() {
    let dir = tmp_sub("k3");
    let marker = unique_marker(&dir);
    let script = "$host.UI.WriteErrorLine('boom'); exit 1";
    let r = launch(
        Path::new("powershell"),
        &ps_cmd(script),
        &marker,
        Duration::from_secs(20),
    );
    match r {
        Err(LaunchFail::Exited { code, stderr_tail }) => {
            assert_eq!(code, 1);
            assert!(stderr_tail.contains("boom"), "tail: {stderr_tail:?}");
        }
        other => panic!("expected Exited(1), got {other:?}"),
    }
}

#[test]
fn k4_launch_timeout_notready_and_kill() {
    let dir = tmp_sub("k4");
    let marker = unique_marker(&dir); // 无人写它:人为只点时路
    let mut child =
        spawn_detached(Path::new("powershell"), &ps_cmd("Start-Sleep -Seconds 8")).unwrap();
    let short = Duration::from_millis(700);
    let r = wait_ready(&mut child, &marker, short);
    match r {
        Err(LaunchFail::NoReadySignal { timeout }) => assert_eq!(timeout, short),
        other => panic!("expected NoReadySignal, got {other:?}"),
    }
    // 等满后子进程已被 kill;保险起见再 kill 幂等。
    let _ = child.kill();
}