use anyhow::Context; use mlua::{Table, Value}; use std::ffi::OsString; use std::path::PathBuf; use std::str::FromStr; #[cfg(test)] mod tests { use super::*; use mirror_core::MirrorTarget; use mlua::{FromLua, Lua}; fn parse(src: &str) -> mlua::Result { let lua = Lua::new(); // 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表) let get_env = lua .create_function(|lua, key: String| -> mlua::Result { let value = std::env::var(key).unwrap_or_default(); let segments: Vec = if value.is_empty() { Vec::new() } else { std::env::split_paths(std::ffi::OsStr::new(&value)) .map(|p| p.to_string_lossy().into_owned()) .collect() }; lua.create_sequence_from(segments) }) .unwrap(); lua.globals().set("get_env", get_env).unwrap(); let value = lua.load(src).eval::()?; let t = MirrorTarget::from_lua(value, &lua); println!("读取出的数据:{:?}", t.clone()?); t } #[test] fn parses_basic_config() { let cfg = parse( r#" return { target = "C:/tools/git.exe", args = { "--no-pager",2}, env = { PATH = { "C:/tools/git/bin", "C:/Windows", get_env("PATH")}, HOME = "C:/tools/home", CONST = 3, BOOL = true, }, aliases = { -- 1. 整体字符串(会自动 trim 首尾空格,保留为单个参数) st = "status -s", -- 2. 连续数组表 (Sequence Table) lg = { "log", "--oneline", "-n", 10 }, -- 3. 标量数字与布尔值支持 v = 1, quiet = true, -- 4. 嵌套别名组合(加载期会自动展开并进行死环检测) base_log = "log --graph", all_log = { "mr:base_log", "--all","D:\\CNWei\\CNW\\Rust","D:/CNWei/CNW/Rust/" ,[[D:\CNWei\CNW\Rust\]]}, -- 5. nil 或空字符串(解析为空参数列表) empty_alias = nil, blank = " " } } "#, ) .unwrap(); assert_eq!( cfg.target.executable_path().unwrap().to_path_buf(), PathBuf::from("C:/tools/git.exe") ); // assert_eq!(cfg.args, vec!["--no-pager", "2"]); assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home")); // PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段 let path = cfg.env.get("PATH").unwrap().to_str().unwrap(); assert!( path == "C:/tools/git/bin;C:/Windows" || path.starts_with("C:/tools/git/bin;C:/Windows;"), "unexpected PATH: {path}" ); // ==================== aliases 断言校验 ==================== // 1. 整体字符串:保留原样(trim 后),不切分空格 assert_eq!( cfg.aliases.get("st").unwrap(), &vec![OsString::from("status"), OsString::from("-s")] ); // 2. 连续数组表:按顺序转为 OsString 列表 assert_eq!( cfg.aliases.get("lg").unwrap(), &vec![ OsString::from("log"), OsString::from("--oneline"), OsString::from("-n"), OsString::from("10") ] ); // 3. 标量数字与布尔值支持 assert_eq!(cfg.aliases.get("v").unwrap(), &vec![OsString::from("1")]); assert_eq!( cfg.aliases.get("quiet").unwrap(), &vec![OsString::from("true")] ); // 4. 嵌套别名组合:加载期递归拓扑打平 // base_log 本身为 "log --graph" assert_eq!( cfg.aliases.get("base_log").unwrap(), &vec![OsString::from("log"), OsString::from("--graph")] ); // all_log 展开 mr:base_log 替换为 "log --graph",追加 "--all" assert_eq!( cfg.aliases.get("all_log").unwrap(), &vec![ OsString::from("log"), OsString::from("--graph"), OsString::from("--all"), OsString::from("D:\\CNWei\\CNW\\Rust"), OsString::from("D:/CNWei/CNW/Rust/"), OsString::from("D:\\CNWei\\CNW\\Rust\\"), ] ); // 5. nil 与纯空白字符串:解析为空 Vec assert_eq!(cfg.aliases.get("blank").unwrap(), &Vec::::new()); // nil 键在遍历表时会被当作空或不存在,不产生 key 或值为空 Vec assert!( cfg.aliases .get("empty_alias") .map_or(true, |v| v.is_empty()) ); } #[test] fn missing_args_and_env_are_empty() { let cfg = parse(r#"return { target = "t.exe" }"#).unwrap(); assert!(cfg.args.is_empty()); assert!(cfg.env.is_empty()); } #[test] fn keeps_empty_args() { let cfg = parse(r#"return { target = "t.exe", args = { "" } }"#).unwrap(); assert_eq!(cfg.args, vec![""]); } #[test] fn keeps_empty_env_value() { let cfg = parse(r#"return { target = "t.exe", env = { FOO = "" } }"#).unwrap(); assert_eq!(cfg.env.get("FOO").unwrap().to_str(), Some("")); } #[test] fn empty_array_clears_env_var() { let cfg = parse(r#"return { target = "t.exe", env = { PATH = {} } }"#).unwrap(); assert_eq!(cfg.env.get("PATH").unwrap().to_str(), Some("")); } #[test] fn expands_nested_env_array() { // get_env("PATH") 现在返回拆分后的段数组,嵌套表应被递归展开 let cfg = parse( r#"return { target = "t.exe", env = { PATH = { "C:/a", { "C:/b", "C:/c" } } } }"#, ) .unwrap(); assert_eq!( cfg.env.get("PATH").unwrap().to_str(), Some("C:/a;C:/b;C:/c") ); } #[test] fn rejects_wrong_env_type() { assert!(parse(r#"return { target = "t.exe", env = "PATH=C:/x" }"#).is_err()); } #[test] fn rejects_sparse_env_array() { assert!(parse(r#"return { target = "t.exe", env = { P = { "a", nil, "b" } } }"#).is_err()); } #[test] fn rejects_mixed_key_env_array() { assert!(parse(r#"return { target = "t.exe", env = { P = { a = "b" } } }"#).is_err()); } #[test] fn rejects_invalid_env_key() { assert!(parse(r#"return { target = "t.exe", env = { ["FOO=1"] = "x" } }"#).is_err()); assert!(parse(r#"return { target = "t.exe", env = { [""] = "x" } }"#).is_err()); } #[test] fn rejects_nul_in_env_value() { assert!(parse(r#"return { target = "t.exe", env = { P = { string.char(0) } } }"#).is_err()); } #[test] fn rejects_quote_in_env_value() { // Windows 的 join_paths 对含双引号的路径元素返回错误 assert!( parse(r#"return { target = "t.exe", env = { P = { string.char(34) } } }"#).is_err() ); } #[test] fn rejects_unsupported_env_value_type() { assert!(parse(r#"return { target = "t.exe", env = { F = function() end } }"#).is_err()); } #[test] fn missing_target_is_error() { assert!(parse(r#"return { args = { "x" } }"#).is_err()); } #[test] fn rejects_non_string_target() { assert!(parse(r#"return { target = 123 }"#).is_err()); assert!(parse(r#"return { target = false }"#).is_err()); } #[test] fn rejects_non_string_args_element() { // ValueShunt::Args 允许基础标量(数字/布尔)转字符串,仅禁止嵌套表 let cfg = parse(r#"return { target = "t.exe", args = { 1, true } }"#).unwrap(); assert_eq!(cfg.args, vec!["1", "true"]); } #[test] fn rejects_sparse_args() { assert!(parse(r#"return { target = "t.exe", args = { "a", nil, "b" } }"#).is_err()); } #[test] fn rejects_non_table_args() { assert!(parse(r#"return { target = "t.exe", args = "-B" }"#).is_err()); } }