- 将 MirrorRegistry MirrorTarget 迁移到 model.rs - TargetConfig 独立实现 FromLua,解耦反序列化与业务校验 - mirror—shim 新增 command.rs 并将 to_command 移动到 command.rs - 其他优化
247 lines
8.3 KiB
Rust
247 lines
8.3 KiB
Rust
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<MirrorTarget> {
|
|
let lua = Lua::new();
|
|
// 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表)
|
|
let get_env = lua
|
|
.create_function(|lua, key: String| -> mlua::Result<Table> {
|
|
let value = std::env::var(key).unwrap_or_default();
|
|
let segments: Vec<String> = 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::<Value>()?;
|
|
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::<OsString>::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());
|
|
}
|
|
}
|