Files
Unlucky-Launcher/crates/cli/tests/parse_test.rs
T
AskaEth 5f8382565e
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Unlucky Launcher v0.1.0
单提交快照:main 只保留已发布版本的代码树,开发史在 dev。
2026-11-01 20:02:49 +08:00

339 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 子命令解析:手写解析器的语法契约。
#[path = "support/mod.rs"]
mod support;
use unlucky_cli::cli::{parse, AccountsAddKind, Command, Globals};
use unlucky_cli::error::ErrorKind;
fn invoke(args: &[&str]) -> unlucky_cli::cli::Invocation {
let owned: Vec<String> = args.iter().map(|arg| arg.to_string()).collect();
parse(&owned).unwrap_or_else(|error| panic!("解析失败:{error}"))
}
#[test]
fn no_args_is_welcome() {
let invocation = invoke(&[]);
assert_eq!(invocation.command, Command::Welcome);
assert_eq!(invocation.globals, Globals::default());
}
#[test]
fn version_flags() {
assert_eq!(invoke(&["--version"]).command, Command::Version);
assert_eq!(invoke(&["-V"]).command, Command::Version);
assert_eq!(invoke(&["version"]).command, Command::Version);
}
#[test]
fn help_takes_topic_from_positional() {
assert_eq!(invoke(&["--help"]).command, Command::Help(None));
assert_eq!(
invoke(&["--help", "launch"]).command,
Command::Help(Some("launch".to_string()))
);
assert_eq!(
invoke(&["help", "config"]).command,
Command::Help(Some("config".to_string()))
);
}
#[test]
fn globals_are_position_independent() {
let invocation = invoke(&["instances", "list", "--json", "--quiet"]);
assert_eq!(invocation.command, Command::InstancesList);
assert!(invocation.globals.json);
assert!(invocation.globals.quiet);
let before = invoke(&["--json", "instances", "list"]);
assert!(before.globals.json);
assert_eq!(before.command, Command::InstancesList);
}
#[test]
fn data_dir_accepts_both_forms() {
assert_eq!(
invoke(&["--data-dir", "/tmp/ull", "instances", "list"])
.globals
.data_dir,
Some(std::path::PathBuf::from("/tmp/ull"))
);
assert_eq!(
invoke(&["instances", "list", "--data-dir=/tmp/ull"])
.globals
.data_dir,
Some(std::path::PathBuf::from("/tmp/ull"))
);
}
#[test]
fn install_flags() {
let Command::Install(args) =
invoke(&["install", "1.20.1", "--loader", "vanilla", "--name=x"]).command
else {
panic!("应为 install");
};
assert_eq!(args.version, "1.20.1");
assert_eq!(args.loader.as_deref(), Some("vanilla"));
assert_eq!(args.name.as_deref(), Some("x"));
assert!(!args.dry_run);
assert!(args.progress, "人类模式默认打实时进度");
}
#[test]
fn install_loader_version_flag() {
let Command::Install(args) = invoke(&[
"install",
"1.20.1",
"--loader",
"fabric",
"--loader-version",
"0.15.11",
])
.command
else {
panic!("应为 install");
};
assert_eq!(args.loader.as_deref(), Some("fabric"));
assert_eq!(args.loader_version.as_deref(), Some("0.15.11"));
// 不带该 flag 时为 None(由 core 取官方默认版本)。
let Command::Install(default) = invoke(&["install", "1.20.1"]).command else {
panic!("应为 install");
};
assert!(default.loader_version.is_none());
}
#[test]
fn install_dry_run_flag_and_progress_suppression() {
let Command::Install(args) = invoke(&["install", "1.20.1", "--dry-run"]).command else {
panic!("应为 install");
};
assert!(args.dry_run);
assert!(args.progress);
assert!(!args.json_progress, "人类模式打文本进度");
// `--json`:stderr 换成 JSONL 进度(stdout 仍是单个信封)。
let Command::Install(args) = invoke(&["--json", "install", "1.20.1"]).command else {
panic!("应为 install");
};
assert!(args.progress);
assert!(args.json_progress);
// `--quiet`:一律不打进度。
let Command::Install(args) = invoke(&["install", "1.20.1", "-q"]).command else {
panic!("应为 install");
};
assert!(!args.progress);
assert!(!args.dry_run);
}
#[test]
fn launch_flags_and_defaults() {
let Command::Launch(args) = invoke(&["launch", "生存服"]).command else {
panic!("应为 launch");
};
assert_eq!(args.instance, "生存服");
assert!(!args.dry_run);
assert_eq!(args.java, None);
assert_eq!(args.max_memory, None);
let Command::Launch(args) = invoke(&[
"launch",
"生存服",
"--dry-run",
"--java",
"/opt/jdk/bin/java",
"--username",
"Steve",
"--max-memory",
"4096",
])
.command
else {
panic!("应为 launch");
};
assert!(args.dry_run);
assert_eq!(args.username.as_deref(), Some("Steve"));
assert_eq!(args.max_memory, Some(4096));
}
#[test]
fn accounts_subcommands() {
assert_eq!(invoke(&["accounts"]).command, Command::AccountsList);
assert_eq!(invoke(&["accounts", "list"]).command, Command::AccountsList);
let Command::AccountsAdd(args) = invoke(&["accounts", "add", "offline", "Steve"]).command
else {
panic!("应为 accounts add offline");
};
assert_eq!(args.kind, AccountsAddKind::Offline);
assert_eq!(args.name.as_deref(), Some("Steve"));
let Command::AccountsAdd(args) = invoke(&["accounts", "add", "msa"]).command else {
panic!("应为 accounts add msa");
};
assert_eq!(args.kind, AccountsAddKind::Microsoft);
assert_eq!(args.name, None);
let Command::AccountsRemove { name } = invoke(&["accounts", "remove", "Steve"]).command else {
panic!("应为 accounts remove");
};
assert_eq!(name, "Steve");
}
#[test]
fn launch_account_flag() {
let Command::Launch(args) = invoke(&["launch", "生存服", "--account", "Steve"]).command
else {
panic!("应为 launch");
};
assert_eq!(args.account.as_deref(), Some("Steve"));
assert_eq!(args.username, None);
}
#[test]
fn logs_and_diag_default_tails() {
let Command::Logs(args) = invoke(&["logs", "abc"]).command else {
panic!("应为 logs");
};
assert_eq!(args.tail, 200);
let Command::Diag(args) = invoke(&["diag", "abc", "--tail", "10"]).command else {
panic!("应为 diag");
};
assert_eq!(args.tail, 10);
}
#[test]
fn config_get_set_forms() {
let Command::ConfigGet(args) = invoke(&["config", "get"]).command else {
panic!("应为 config get");
};
assert_eq!(args.key, None);
assert!(!args.show_secret);
let Command::ConfigGet(args) =
invoke(&["config", "get", "ui.language", "--show-secret"]).command
else {
panic!("应为 config get");
};
assert_eq!(args.key.as_deref(), Some("ui.language"));
assert!(args.show_secret);
let Command::ConfigSet(args) = invoke(&["config", "set", "ui.language", "en_us"]).command
else {
panic!("应为 config set");
};
assert_eq!(args.key, "ui.language");
assert_eq!(args.value, "en_us");
}
#[test]
fn java_subcommands() {
assert_eq!(invoke(&["java"]).command, Command::JavaList);
assert_eq!(invoke(&["java", "list"]).command, Command::JavaList);
assert_eq!(
invoke(&["java", "install", "21"]).command,
Command::JavaInstall {
major: 21,
vendor: None
}
);
// D23:`--vendor` 钉死发行版(别名也认),`java vendors` 列出四家。
assert_eq!(
invoke(&["java", "install", "21", "--vendor", "oracle"]).command,
Command::JavaInstall {
major: 21,
vendor: Some(unlucky_core::game::java::JdkVendor::Oracle),
}
);
assert_eq!(
invoke(&["java", "install", "17", "--vendor", "adoptium"]).command,
Command::JavaInstall {
major: 17,
vendor: Some(unlucky_core::game::java::JdkVendor::Temurin),
}
);
assert_eq!(invoke(&["java", "vendors"]).command, Command::JavaVendors);
}
#[test]
fn usage_errors_are_user_errors() {
let cases: &[&[&str]] = &[
&["frobnicate"],
&["install"],
&["launch"],
&["launch", "a", "b"],
&["instances", "list", "--nope"],
&["install", "1.20.1", "--dry-run=1"],
&["config", "set", "only-key"],
&["java", "install", "abc"],
&["--data-dir"],
&["sessions", "watch"],
&["accounts", "add"],
&["accounts", "add", "offline"],
&["accounts", "add", "authlib"],
&["accounts", "remove"],
&["accounts", "frobnicate"],
];
for args in cases {
let owned: Vec<String> = args.iter().map(|arg| arg.to_string()).collect();
let error = parse(&owned).unwrap_err();
assert_eq!(error.kind(), ErrorKind::User, "args={args:?}");
assert_eq!(error.exit_code(), 1, "args={args:?}");
}
}
#[test]
fn value_flags_do_not_swallow_the_next_flag() {
// P2-2:漏了值的写法必须在解析期报用法错误,而不是把 `--dry-run` 当成 Java 路径。
let cases: &[&[&str]] = &[
&["launch", "x", "--java", "--dry-run"],
&["launch", "x", "--username", "--json"],
&["install", "1.20.1", "--name", "--loader", "fabric"],
&["install", "1.20.1", "--loader", "--name", "x"],
&["logs", "abc", "--tail", "--json"],
&["--data-dir", "--json", "instances", "list"],
];
for args in cases {
let owned: Vec<String> = args.iter().map(|arg| arg.to_string()).collect();
let error = parse(&owned).expect_err(&format!("args={args:?} 应当报用法错误"));
assert_eq!(error.code(), "usage", "args={args:?}");
assert!(
error.message().contains("需要一个值"),
"args={args:?} message={}",
error.message()
);
}
// 负数值与正常值不受影响(只拒 `--` 开头的 token)。
let Command::Launch(args) = invoke(&["launch", "x", "--java", "/opt/jdk/bin/java"]).command
else {
panic!("应为 launch");
};
assert_eq!(
args.java.as_deref(),
Some(std::path::Path::new("/opt/jdk/bin/java"))
);
}
#[test]
fn help_text_covers_every_command() {
for topic in [
"instances",
"install",
"launch",
"sessions",
"logs",
"diag",
"config",
"java",
] {
assert!(
unlucky_cli::cli::help::topic(topic).is_some(),
"缺少 {topic} 的帮助"
);
}
assert!(unlucky_cli::cli::help::topic("nope").is_none());
}