Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,136 @@
|
||||
//! Linux 探测:`/etc/os-release`、内核 release、会话类型、glibc 版本。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
const UBUNTU_OS_RELEASE: &str = "\
|
||||
PRETTY_NAME=\"Ubuntu 24.04.1 LTS\"
|
||||
NAME=\"Ubuntu\"
|
||||
VERSION_ID=\"24.04\"
|
||||
ID=ubuntu
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn os_release_prefers_pretty_name() {
|
||||
assert_eq!(
|
||||
linux_caps::parse_os_release(UBUNTU_OS_RELEASE),
|
||||
Some("Ubuntu 24.04.1 LTS".to_string())
|
||||
);
|
||||
// 没有 PRETTY_NAME 的裁剪发行版:NAME + VERSION_ID 拼一个(不够精确但够日志用)。
|
||||
let minimal = "NAME=\"Alpine Linux\"\nVERSION_ID=3.19\n";
|
||||
assert_eq!(
|
||||
linux_caps::parse_os_release(minimal),
|
||||
Some("Alpine Linux 3.19".to_string())
|
||||
);
|
||||
assert_eq!(linux_caps::parse_os_release(""), None);
|
||||
assert_eq!(linux_caps::parse_os_release("ID=arch\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_release_yields_major_minor() {
|
||||
assert_eq!(
|
||||
linux_caps::parse_kernel_version("6.8.0-45-generic"),
|
||||
Some((6, 8))
|
||||
);
|
||||
assert_eq!(linux_caps::parse_kernel_version("5.15.0"), Some((5, 15)));
|
||||
assert_eq!(linux_caps::parse_kernel_version("3.2"), Some((3, 2)));
|
||||
assert_eq!(linux_caps::parse_kernel_version("6.8"), Some((6, 8)));
|
||||
assert_eq!(linux_caps::parse_kernel_version(""), None);
|
||||
assert_eq!(linux_caps::parse_kernel_version("not-a-version"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_detection_order_is_wayland_then_x11_then_headless() {
|
||||
fn detect(pairs: &[(&str, &str)]) -> SessionKind {
|
||||
let map: HashMap<String, String> = pairs
|
||||
.iter()
|
||||
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
|
||||
.collect();
|
||||
linux_caps::session_from_env(|key| map.get(key).cloned())
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
detect(&[("WAYLAND_DISPLAY", "wayland-0")]),
|
||||
SessionKind::Wayland
|
||||
);
|
||||
assert_eq!(
|
||||
detect(&[("XDG_SESSION_TYPE", "wayland"), ("DISPLAY", ":0")]),
|
||||
SessionKind::Wayland
|
||||
);
|
||||
assert_eq!(
|
||||
detect(&[("XDG_SESSION_TYPE", "x11"), ("DISPLAY", ":0")]),
|
||||
SessionKind::X11
|
||||
);
|
||||
assert_eq!(detect(&[("DISPLAY", ":0")]), SessionKind::X11);
|
||||
// 空值不算有会话(SSH 转发里 `WAYLAND_DISPLAY=` 是常见的空串形态)。
|
||||
assert_eq!(
|
||||
detect(&[("WAYLAND_DISPLAY", " "), ("DISPLAY", ":0")]),
|
||||
SessionKind::X11
|
||||
);
|
||||
assert_eq!(detect(&[]), SessionKind::Headless);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_kind_helpers() {
|
||||
assert!(SessionKind::Wayland.has_display());
|
||||
assert!(SessionKind::X11.has_display());
|
||||
assert!(!SessionKind::Headless.has_display());
|
||||
assert!(!SessionKind::Unknown.has_display());
|
||||
assert_eq!(SessionKind::Wayland.as_str(), "wayland");
|
||||
assert_eq!(SessionKind::Headless.as_str(), "headless");
|
||||
assert_eq!(GlProbe::Unknown.as_str(), "unknown");
|
||||
assert_eq!(GlProbe::Missing.as_str(), "missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glibc_version_comes_from_the_first_ldd_line() {
|
||||
assert_eq!(
|
||||
linux_caps::parse_glibc_version(
|
||||
"ldd (Ubuntu GLIBC 2.39-0ubuntu8.3) 2.39\nCopyright (C) 2024\n"
|
||||
),
|
||||
Some((2, 39))
|
||||
);
|
||||
assert_eq!(
|
||||
linux_caps::parse_glibc_version("ldd (GNU libc) 2.31"),
|
||||
Some((2, 31))
|
||||
);
|
||||
assert_eq!(
|
||||
linux_caps::parse_glibc_version("ldd (Debian GLIBC 2.36-9+deb12u4) 2.36"),
|
||||
Some((2, 36))
|
||||
);
|
||||
// musl / 命令不存在:不能瞎猜,交给上层「不阻塞」。
|
||||
assert_eq!(linux_caps::parse_glibc_version("ldd: not found"), None);
|
||||
assert_eq!(linux_caps::parse_glibc_version(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_version_is_dotted() {
|
||||
assert_eq!(linux_caps::format_version((2, 31)), "2.31");
|
||||
assert_eq!(linux_caps::format_version((6, 8)), "6.8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gl_probe_is_a_hook_returning_unknown_for_now() {
|
||||
// P1 只留钩子:真探测要 dlopen(FFI),core 禁 unsafe,故恒为 Unknown 且不阻塞。
|
||||
assert_eq!(linux_caps::probe_gl(), GlProbe::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_probe_matches_the_host() {
|
||||
let caps: LinuxCaps = linux_caps::probe();
|
||||
if cfg!(target_os = "linux") {
|
||||
// 容器里可能全空,但会话类型至少是「探过了」。
|
||||
assert_ne!(caps.session, SessionKind::Unknown);
|
||||
if let Some(kernel) = &caps.kernel {
|
||||
assert!(
|
||||
caps.kernel_version.is_some(),
|
||||
"内核串 {kernel} 应能取 major.minor"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
assert_eq!(caps.session, SessionKind::Unknown);
|
||||
assert!(caps.kernel.is_none());
|
||||
assert!(caps.distro.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! macOS 探测:`sw_vers -productVersion` 与 `sysctl hw.optional.arm64`。
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn product_version_parses_sw_vers_output() {
|
||||
assert_eq!(
|
||||
macos_version::parse_product_version("12.3.1\n"),
|
||||
Some((12, 3, 1))
|
||||
);
|
||||
assert_eq!(
|
||||
macos_version::parse_product_version("12.3"),
|
||||
Some((12, 3, 0))
|
||||
);
|
||||
assert_eq!(macos_version::parse_product_version("12"), Some((12, 0, 0)));
|
||||
assert_eq!(
|
||||
macos_version::parse_product_version(" 14.2.1 "),
|
||||
Some((14, 2, 1))
|
||||
);
|
||||
assert_eq!(macos_version::parse_product_version(""), None);
|
||||
assert_eq!(macos_version::parse_product_version("ProductVersion"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_arm64_flag() {
|
||||
assert!(macos_version::parse_sysctl_bool("1\n"));
|
||||
assert!(!macos_version::parse_sysctl_bool("0\n"));
|
||||
// Intel 机器上 sysctl 以非零码退出 → stdout 为空 → 不是 arm64。
|
||||
assert!(!macos_version::parse_sysctl_bool(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_strings_and_runtime_arch() {
|
||||
let apple_silicon = MacOsVersion::new(12, 3, 1, true);
|
||||
assert_eq!(apple_silicon.version_string(), "12.3.1");
|
||||
assert_eq!(apple_silicon.runtime_arch(), "arm64");
|
||||
assert_eq!(apple_silicon.display_name(), "macOS 12.3.1 (arm64)");
|
||||
|
||||
// Rosetta 2 下编译目标是 x86_64,但真实架构仍是 arm64:
|
||||
// 探到 arm64 时 runtime_arch 必须是 arm64(MC 原生库与渲染路径都按它选)。
|
||||
let rosetta = MacOsVersion::new(12, 3, 1, true);
|
||||
assert_eq!(rosetta.runtime_arch(), "arm64");
|
||||
|
||||
let intel = MacOsVersion::new(15, 0, 0, false);
|
||||
assert_eq!(intel.runtime_arch(), "x86_64");
|
||||
assert_eq!(intel.display_name(), "macOS 15.0.0 (x86_64)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_matches_the_host() {
|
||||
let probed = macos_version::probe();
|
||||
if cfg!(target_os = "macos") {
|
||||
let version = probed.expect("macOS 宿主必须能探到产品版本");
|
||||
assert!(version.major >= 10, "探到 {version:?}");
|
||||
} else {
|
||||
assert!(probed.is_none(), "非 macOS 平台不应探到 macOS 版本");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! platform 模块集成测试入口(`cargo test -p unlucky-core --test platform`)。
|
||||
//!
|
||||
//! 覆盖 D19(`PlanDoc/11-os-detection.md`)的运行期探测:
|
||||
//! - 等级映射 / 文案:注入式假数据,不改机器真实版本;
|
||||
//! - 各平台探测的**解析**(Windows 注册表与 `ver`、Linux `/etc/os-release`+内核+glibc、
|
||||
//! macOS `sw_vers`+`sysctl`):解析函数跨平台编译,所以能在任意宿主上单测;
|
||||
//! - `rules.os.version` 用真实探测值求值的接线;
|
||||
//! - 系统主题探测(`theme_probe`:注册表 / `defaults` / `gsettings` 解析 + 三态契约)。
|
||||
|
||||
mod linux_caps_cases;
|
||||
mod macos_cases;
|
||||
mod os_info_cases;
|
||||
mod prelude;
|
||||
mod rules_wiring;
|
||||
mod theme_cases;
|
||||
mod vk_cases;
|
||||
mod windows_version_cases;
|
||||
@@ -0,0 +1,299 @@
|
||||
//! 支持等级映射与双语文案:全部用注入式假数据,不改机器真实版本。
|
||||
//!
|
||||
//! 基线(用户拍板):Windows 10+ / macOS 12+ / Linux 达标 → 一等,其余一律不支持;
|
||||
//! Win8.1/8/7 也不承诺 CLI,所以没有中间档。
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
fn windows_info(major_minor: Option<(u32, u32)>) -> OsInfo {
|
||||
let product = match major_minor {
|
||||
Some(version) if version >= os_info::MIN_WINDOWS => "Windows 10",
|
||||
Some(_) => "Windows 7",
|
||||
None => "Windows",
|
||||
};
|
||||
OsInfo {
|
||||
family: OsFamily::Windows,
|
||||
arch: "x86_64".to_string(),
|
||||
major_minor,
|
||||
build: Some(19_045),
|
||||
product: product.to_string(),
|
||||
version_string: "10.0.19045".to_string(),
|
||||
session: SessionKind::Unknown,
|
||||
gl: GlProbe::Unknown,
|
||||
vk: VkProbe::Unknown,
|
||||
kernel: None,
|
||||
glibc: None,
|
||||
support_level: os_info::support_level_for(OsFamily::Windows, major_minor, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_info(kernel: Option<(u32, u32)>, glibc: Option<(u32, u32)>) -> OsInfo {
|
||||
OsInfo {
|
||||
family: OsFamily::Linux,
|
||||
arch: "x86_64".to_string(),
|
||||
major_minor: kernel,
|
||||
build: None,
|
||||
product: "Ubuntu 24.04.1 LTS".to_string(),
|
||||
version_string: "6.8.0-45-generic".to_string(),
|
||||
session: SessionKind::Wayland,
|
||||
gl: GlProbe::Unknown,
|
||||
vk: VkProbe::Unknown,
|
||||
kernel,
|
||||
glibc,
|
||||
support_level: os_info::support_level_for(OsFamily::Linux, kernel, kernel, glibc),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_levels_require_windows_10() {
|
||||
// Win10 / Win11(都报 10.0,靠 build 区分产品名)→ 一等。
|
||||
assert_eq!(os_info::windows_level(Some((10, 0))), SupportLevel::One);
|
||||
// Win8.1 / Win8 / Win7:低于基线,GUI 与 CLI 都不承诺。
|
||||
assert_eq!(
|
||||
os_info::windows_level(Some((6, 3))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::windows_level(Some((6, 2))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::windows_level(Some((6, 1))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
// Vista / XP。
|
||||
assert_eq!(
|
||||
os_info::windows_level(Some((6, 0))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::windows_level(Some((5, 1))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
// 探测失败 → 保守(无法确认 Win10+ 就不放行)。
|
||||
assert_eq!(os_info::windows_level(None), SupportLevel::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_levels_use_twelve_as_the_floor() {
|
||||
assert_eq!(os_info::macos_level(Some(12)), SupportLevel::One);
|
||||
assert_eq!(os_info::macos_level(Some(15)), SupportLevel::One);
|
||||
assert_eq!(os_info::macos_level(Some(11)), SupportLevel::Unsupported);
|
||||
assert_eq!(os_info::macos_level(None), SupportLevel::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_levels_gate_on_kernel_and_glibc() {
|
||||
assert_eq!(
|
||||
os_info::linux_level(Some((6, 8)), Some((2, 39))),
|
||||
SupportLevel::One
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::linux_level(Some((2, 6)), Some((2, 39))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::linux_level(Some((6, 8)), Some((2, 16))),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
// musl / Termux / 容器里取不到 glibc 与内核 → 不阻塞。
|
||||
assert_eq!(os_info::linux_level(None, None), SupportLevel::One);
|
||||
assert_eq!(os_info::linux_level(Some((3, 2)), None), SupportLevel::One);
|
||||
// 恰好等于下限也算支持。
|
||||
assert_eq!(
|
||||
os_info::linux_level(Some((6, 8)), Some(os_info::MIN_GLIBC)),
|
||||
SupportLevel::One
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn support_level_for_dispatches_by_family() {
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::Windows, Some((10, 0)), None, None),
|
||||
SupportLevel::One
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::Windows, Some((6, 1)), None, None),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::MacOs, Some((12, 3)), None, None),
|
||||
SupportLevel::One
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::MacOs, Some((11, 7)), None, None),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
// Linux 的 major_minor 不参与判定,看 kernel/glibc。
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::Linux, Some((10, 0)), Some((2, 6)), None),
|
||||
SupportLevel::Unsupported
|
||||
);
|
||||
assert_eq!(
|
||||
os_info::support_level_for(OsFamily::Linux, None, Some((6, 8)), Some((2, 39))),
|
||||
SupportLevel::One
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_7_is_reported_as_unsupported_with_the_new_baseline() {
|
||||
let info = windows_info(Some((6, 1)));
|
||||
assert_eq!(info.support_level, SupportLevel::Unsupported);
|
||||
let text = info.describe();
|
||||
assert!(text.zh.contains("不支持"), "zh: {}", text.zh);
|
||||
assert!(text.zh.contains("Windows 10"), "zh: {}", text.zh);
|
||||
// 不再有「受限模式 / HeadlessOnly」文案。
|
||||
assert!(!text.zh.contains("受限模式"), "zh: {}", text.zh);
|
||||
assert!(text.en.contains("Unsupported"), "en: {}", text.en);
|
||||
assert!(text.en.contains("Windows 10 or newer"), "en: {}", text.en);
|
||||
// combined() 是 CLI 打印形态:中文在前、英文在后。
|
||||
let combined = text.combined();
|
||||
let (zh, en) = combined.split_once('\n').expect("两行");
|
||||
assert!(zh.contains("不支持"));
|
||||
assert!(en.contains("Unsupported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_macos_and_linux_name_their_own_minimums() {
|
||||
let mac = OsInfo {
|
||||
family: OsFamily::MacOs,
|
||||
arch: "arm64".to_string(),
|
||||
major_minor: Some((11, 7)),
|
||||
build: None,
|
||||
product: "macOS 11.7.0 (arm64)".to_string(),
|
||||
version_string: "11.7.0".to_string(),
|
||||
session: SessionKind::Unknown,
|
||||
gl: GlProbe::Unknown,
|
||||
vk: VkProbe::Unknown,
|
||||
kernel: None,
|
||||
glibc: None,
|
||||
support_level: SupportLevel::Unsupported,
|
||||
};
|
||||
let text = mac.describe();
|
||||
assert!(text.zh.contains("macOS 12"), "zh: {}", text.zh);
|
||||
assert!(text.en.contains("macOS 12"), "en: {}", text.en);
|
||||
assert!(!text.zh.contains("Windows"), "zh: {}", text.zh);
|
||||
|
||||
let linux = linux_info(Some((2, 6)), None);
|
||||
assert_eq!(linux.support_level, SupportLevel::Unsupported);
|
||||
let text = linux.describe();
|
||||
assert!(text.zh.contains("内核"), "zh: {}", text.zh);
|
||||
assert!(text.en.contains("kernel"), "en: {}", text.en);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_windows_version_is_unsupported_and_says_so() {
|
||||
let info = windows_info(None);
|
||||
assert_eq!(info.support_level, SupportLevel::Unsupported);
|
||||
let text = info.describe();
|
||||
assert!(text.zh.contains("未能探测到系统版本"), "zh: {}", text.zh);
|
||||
assert!(text.zh.contains("Windows 10"), "zh: {}", text.zh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_linux_without_display_adds_a_hint() {
|
||||
let mut info = linux_info(Some((6, 8)), Some((2, 39)));
|
||||
info.session = SessionKind::Headless;
|
||||
let text = info.describe();
|
||||
assert!(text.zh.contains("图形会话"), "zh: {}", text.zh);
|
||||
assert!(text.en.contains("graphical session"), "en: {}", text.en);
|
||||
|
||||
// 有显示服务器时不加提示。
|
||||
let text = linux_info(Some((6, 8)), Some((2, 39))).describe();
|
||||
assert!(!text.zh.contains("图形会话"), "zh: {}", text.zh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_line_carries_the_diagnostic_fields() {
|
||||
let line = linux_info(Some((6, 8)), Some((2, 39))).summary_line();
|
||||
assert!(line.contains("os=linux"), "{line}");
|
||||
assert!(line.contains("version=6.8.0-45-generic"), "{line}");
|
||||
assert!(line.contains("arch=x86_64"), "{line}");
|
||||
assert!(line.contains("level=one"), "{line}");
|
||||
assert!(line.contains("session=wayland"), "{line}");
|
||||
assert!(line.contains("gl=unknown"), "{line}");
|
||||
assert!(line.contains("vk=unknown"), "{line}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_line_reports_the_vulkan_probe() {
|
||||
let mut info = linux_info(Some((6, 8)), Some((2, 39)));
|
||||
info.vk = VkProbe::LoaderOnly;
|
||||
assert!(info.summary_line().contains("vk=loader_only"));
|
||||
info.vk = VkProbe::Available;
|
||||
assert!(info.summary_line().contains("vk=available"));
|
||||
info.vk = VkProbe::Missing;
|
||||
assert!(info.summary_line().contains("vk=missing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_line_marks_missing_version_as_unknown() {
|
||||
let mut info = windows_info(None);
|
||||
info.version_string = String::new();
|
||||
let line = info.summary_line();
|
||||
assert!(line.contains("version=unknown"), "{line}");
|
||||
assert!(line.contains("level=unsupported"), "{line}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn support_level_helpers() {
|
||||
assert_eq!(SupportLevel::One.as_str(), "one");
|
||||
assert_eq!(SupportLevel::Unsupported.as_str(), "unsupported");
|
||||
assert!(SupportLevel::One.is_usable());
|
||||
assert!(!SupportLevel::Unsupported.is_usable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_family_facts_and_launch_reexport_are_the_same_type() {
|
||||
// launch::platform 只是把 OsFamily 从 platform::os_info 再导出(避免两套枚举)。
|
||||
let family: unlucky_core::platform::os_info::OsFamily = OsFamily::Windows;
|
||||
assert_eq!(family, OsFamily::Windows);
|
||||
assert_eq!(OsFamily::Windows.manifest_name(), "windows");
|
||||
assert_eq!(OsFamily::MacOs.manifest_name(), "osx");
|
||||
assert_eq!(OsFamily::Linux.manifest_name(), "linux");
|
||||
assert!(!OsFamily::Windows.is_posix());
|
||||
assert!(OsFamily::Linux.is_posix());
|
||||
assert!(!OsFamily::current().manifest_arch().is_empty());
|
||||
assert_eq!(
|
||||
OsFamily::current().manifest_arch(),
|
||||
OsFamily::Linux.manifest_arch()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_reports_the_host_it_runs_on() {
|
||||
let info = os_info::detect();
|
||||
// 家族与当前编译目标一致。
|
||||
assert_eq!(info.family, OsFamily::current());
|
||||
assert!(!info.arch.is_empty());
|
||||
assert!(!info.product.is_empty());
|
||||
// 缓存入口返回同一份事实。
|
||||
let cached = os_info::current();
|
||||
assert_eq!(cached, &info);
|
||||
assert_eq!(cached.family, info.family);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_host_probes_a_real_version() {
|
||||
if !cfg!(target_os = "windows") {
|
||||
return;
|
||||
}
|
||||
let info = os_info::detect();
|
||||
let (major, _minor) = info.major_minor.expect("Windows 上必须探到 major.minor");
|
||||
assert!(major >= 6, "探到的版本不合理: {:?}", info.major_minor);
|
||||
assert!(!info.os_version().is_empty(), "os_version 不能是空串");
|
||||
assert!(info.build.is_some(), "Windows 上应有 build 号");
|
||||
// 非 Windows 分支的事实不应泄漏进来。
|
||||
assert!(info.kernel.is_none());
|
||||
assert!(info.glibc.is_none());
|
||||
assert_eq!(info.session, SessionKind::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_returns_level_message_and_os_info() {
|
||||
let check: SupportCheck = os_info::check();
|
||||
assert_eq!(check.level, os_info::current().support_level);
|
||||
assert_eq!(check.is_usable(), check.level.is_usable());
|
||||
assert!(check.message.contains('\n'), "文案应为中英双语多行");
|
||||
assert_eq!(check.message, check.os.describe().combined());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! 测试共用设施(crate 名只出现在这里)。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use unlucky_core::launch::args_table::assemble;
|
||||
pub use unlucky_core::launch::argument::{ArgumentEntry, OsRule, Rule, RuleContext};
|
||||
pub use unlucky_core::launch::game_version::GameVersion;
|
||||
pub use unlucky_core::launch::platform::{OsFamily, PlatformCtx};
|
||||
pub use unlucky_core::launch::request::*;
|
||||
pub use unlucky_core::platform::linux_caps::{self, GlProbe, LinuxCaps, SessionKind};
|
||||
pub use unlucky_core::platform::macos_version::{self, MacOsVersion};
|
||||
pub use unlucky_core::platform::os_info::{self, OsInfo, SupportCheck, SupportLevel};
|
||||
pub use unlucky_core::platform::vk_probe::{
|
||||
classify, count_icd_manifests, env_icd_path, ldconfig_has_libvulkan, loader_candidates,
|
||||
probe_vk, registry_has_driver_entry, VkEvidence, VkProbe, ICD_DIRS, ICD_ENV_VARS,
|
||||
VULKAN_DRIVERS_KEY,
|
||||
};
|
||||
pub use unlucky_core::platform::windows_version::{self, WindowsVersion};
|
||||
|
||||
/// 一个最小可组装请求:现代清单,游戏参数由调用方给。
|
||||
pub fn request_with_game(game: Vec<ArgumentEntry>) -> LaunchRequest {
|
||||
LaunchRequest {
|
||||
instance: InstanceInfo {
|
||||
id: "inst-1".to_string(),
|
||||
name: "1.20.1".to_string(),
|
||||
version: GameVersion::parse("1.20.1"),
|
||||
version_type: "release".to_string(),
|
||||
},
|
||||
launcher: LauncherInfo {
|
||||
name: "UnluckyLauncher".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
java: JavaSelection::new("C:/java/bin/java.exe", 17, 64),
|
||||
manifest: ManifestArgs {
|
||||
main_class: Some("net.minecraft.client.main.Main".to_string()),
|
||||
game,
|
||||
..ManifestArgs::default()
|
||||
},
|
||||
classpath: vec![PathBuf::from("C:/mc/libraries/a.jar")],
|
||||
..LaunchRequest::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 一条带 `rules.os.version` 的游戏参数项。
|
||||
pub fn version_ruled_entry(pattern: &str, value: &str) -> ArgumentEntry {
|
||||
let mut rule = Rule::allow();
|
||||
rule.os = Some(OsRule {
|
||||
name: None,
|
||||
arch: None,
|
||||
version: Some(pattern.to_string()),
|
||||
});
|
||||
ArgumentEntry::ruled(vec![rule], [value])
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! 接线:`rules.os.version` 用真实探测值求值(D19 §2 —— 之前 `os_version` 是空串,
|
||||
//! 带版本规则的参数项永远命中不了)。
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
fn argv_of(entries: Vec<ArgumentEntry>, ctx: &PlatformCtx) -> Vec<String> {
|
||||
let request = request_with_game(entries);
|
||||
assemble(&request, ctx).expect("组装失败").argv().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_10_rule_hits_with_detected_version() {
|
||||
let ctx = PlatformCtx::new(OsFamily::Windows, "x86_64").with_os_version("10.0.19045");
|
||||
let entries = vec![version_ruled_entry(r"^10\.", "--demo-windows10")];
|
||||
let argv = argv_of(entries, &ctx);
|
||||
assert!(
|
||||
argv.iter().any(|arg| arg == "--demo-windows10"),
|
||||
"argv 应包含带版本规则的参数: {argv:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_rule_misses_when_version_is_empty() {
|
||||
// 空版本串是修复前的常态:规则求值走保守分支(不放参数)。
|
||||
let ctx = PlatformCtx::new(OsFamily::Windows, "x86_64");
|
||||
assert!(ctx.os_version.is_empty());
|
||||
let entries = vec![version_ruled_entry(r"^10\.", "--demo-windows10")];
|
||||
let argv = argv_of(entries, &ctx);
|
||||
assert!(
|
||||
!argv.iter().any(|arg| arg == "--demo-windows10"),
|
||||
"空版本串不该命中: {argv:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_dot_patterns_match_real_versions() {
|
||||
// `^10\.15` 是 macOS 上常见的写法:反转义后按 `10.15` 前缀比较。
|
||||
let ctx = PlatformCtx::new(OsFamily::MacOs, "x86_64").with_os_version("10.15.7");
|
||||
let entries = vec![version_ruled_entry(r"^10\.15", "--demo-catalina")];
|
||||
let argv = argv_of(entries, &ctx);
|
||||
assert!(argv.iter().any(|arg| arg == "--demo-catalina"), "{argv:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_shaped_patterns_stay_conservative() {
|
||||
let ctx = PlatformCtx::new(OsFamily::Windows, "x86_64").with_os_version("10.0.19045");
|
||||
// 需要真正则的模式:宁可少加参数,也不猜。
|
||||
let entries = vec![version_ruled_entry(r"^10\.[0-9]+", "--demo-regex")];
|
||||
let argv = argv_of(entries, &ctx);
|
||||
assert!(!argv.iter().any(|arg| arg == "--demo-regex"), "{argv:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_context_uses_the_injected_version() {
|
||||
let features = FeatureSet::default();
|
||||
let entry = version_ruled_entry(r"^6\.1\.", "--demo-win7");
|
||||
assert!(entry.applies(&RuleContext::new(
|
||||
"windows", "x86_64", "6.1.7601", &features
|
||||
)));
|
||||
assert!(!entry.applies(&RuleContext::new(
|
||||
"windows",
|
||||
"x86_64",
|
||||
"10.0.19045",
|
||||
&features
|
||||
)));
|
||||
// 空版本串:任何版本规则都不命中。
|
||||
assert!(!entry.applies(&RuleContext::new("windows", "x86_64", "", &features)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_ctx_current_carries_the_probed_version() {
|
||||
let ctx = PlatformCtx::current();
|
||||
let info = os_info::current();
|
||||
assert_eq!(ctx.os, info.family);
|
||||
assert_eq!(ctx.arch, info.arch);
|
||||
assert_eq!(ctx.os_version, info.os_version());
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(!ctx.os_version.is_empty(), "Windows 上版本串不能为空");
|
||||
// 版本串应能被规则前缀匹配到(形如 10.0.19045)。
|
||||
assert!(
|
||||
ctx.os_version.starts_with(|c: char| c.is_ascii_digit()),
|
||||
"版本串形态异常: {}",
|
||||
ctx.os_version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_matches_current() {
|
||||
let ctx = PlatformCtx::default();
|
||||
assert_eq!(ctx.os_version, PlatformCtx::current().os_version);
|
||||
assert_eq!(
|
||||
ctx.path_separator(),
|
||||
PlatformCtx::current().path_separator()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! 系统主题探测的解析与判定(`core::platform::theme_probe`)。
|
||||
//!
|
||||
//! 解析函数跨平台编译,所以本用例在任意宿主上都能跑;真实探测只是把同一份解析套到
|
||||
//! `reg query` / `defaults` / `gsettings` 的输出上(`probe()` 的非确定性不进断言)。
|
||||
|
||||
use unlucky_core::platform::theme_probe::{
|
||||
self, parse_apps_use_light_theme, parse_gsettings_color_scheme, parse_gtk_theme,
|
||||
parse_macos_interface_style, SystemTheme, WINDOWS_PERSONALIZE_KEY,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn windows_registry_apps_use_light_theme_maps_dword() {
|
||||
// 真实 `reg query` 输出(REG_DWORD 以 0x 十六进制打印)。
|
||||
let light = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\n \
|
||||
AppsUseLightTheme REG_DWORD 0x1\n SystemUsesLightTheme REG_DWORD 0x0\n";
|
||||
assert_eq!(parse_apps_use_light_theme(light), Some(SystemTheme::Light));
|
||||
|
||||
let dark = " AppsUseLightTheme REG_DWORD 0x0\n";
|
||||
assert_eq!(parse_apps_use_light_theme(dark), Some(SystemTheme::Dark));
|
||||
|
||||
// 十进制形式的输出(某些区域设置/工具会把 DWORD 打成十进制)。
|
||||
assert_eq!(
|
||||
parse_apps_use_light_theme(" AppsUseLightTheme REG_DWORD 1"),
|
||||
Some(SystemTheme::Light)
|
||||
);
|
||||
|
||||
// 键不存在(老系统 / 被策略删掉)与别的值都不猜。
|
||||
assert_eq!(parse_apps_use_light_theme(""), None);
|
||||
assert_eq!(
|
||||
parse_apps_use_light_theme(" SystemUsesLightTheme REG_DWORD 0x0"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
parse_apps_use_light_theme(" AppsUseLightTheme REG_SZ yes"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_key_points_at_personalize_hive() {
|
||||
assert!(WINDOWS_PERSONALIZE_KEY.contains("Themes\\Personalize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_interface_style_maps_dark_and_empty() {
|
||||
assert_eq!(
|
||||
parse_macos_interface_style("Dark\n"),
|
||||
Some(SystemTheme::Dark)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_macos_interface_style("Light\n"),
|
||||
Some(SystemTheme::Light)
|
||||
);
|
||||
// 浅色时 `AppleInterfaceStyle` 键不存在,defaults 无输出。
|
||||
assert_eq!(parse_macos_interface_style(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gsettings_color_scheme_prefers_explicit_preference() {
|
||||
assert_eq!(
|
||||
parse_gsettings_color_scheme("'prefer-dark'\n"),
|
||||
Some(SystemTheme::Dark)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_gsettings_color_scheme("'prefer-light'\n"),
|
||||
Some(SystemTheme::Light)
|
||||
);
|
||||
// `default` = 未表态,交给 gtk-theme 名字兜底。
|
||||
assert_eq!(parse_gsettings_color_scheme("'default'\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gtk_theme_name_falls_back_to_dark_suffix() {
|
||||
assert_eq!(parse_gtk_theme("'Yaru-dark'\n"), Some(SystemTheme::Dark));
|
||||
assert_eq!(parse_gtk_theme("'Adwaita-dark'\n"), Some(SystemTheme::Dark));
|
||||
assert_eq!(parse_gtk_theme("'Yaru'\n"), Some(SystemTheme::Light));
|
||||
assert_eq!(parse_gtk_theme("''\n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_theme_tristate_contract() {
|
||||
assert_eq!(SystemTheme::Light.is_dark(), Some(false));
|
||||
assert_eq!(SystemTheme::Dark.is_dark(), Some(true));
|
||||
assert_eq!(
|
||||
SystemTheme::Unknown.is_dark(),
|
||||
None,
|
||||
"Unknown 必须让调用方自己决定回落(不假装浅色)"
|
||||
);
|
||||
assert_eq!(SystemTheme::Light.as_str(), "light");
|
||||
assert_eq!(SystemTheme::Dark.as_str(), "dark");
|
||||
assert_eq!(SystemTheme::Unknown.as_str(), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_never_panics_and_reports_one_of_three_states() {
|
||||
// 真实探测(Windows 读注册表,其余平台问 DE);不断言具体值,只要求落在三态内。
|
||||
let probed = theme_probe::probe();
|
||||
assert!(matches!(
|
||||
probed,
|
||||
SystemTheme::Light | SystemTheme::Dark | SystemTheme::Unknown
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//! Vulkan 能力探测(`platform::vk_probe`)的三平台注入样本。
|
||||
//!
|
||||
//! 全部用 [`VkEvidence`] 注入,不读真机、不起子进程:
|
||||
//! 三平台解析函数是纯函数且跨平台编译,任意宿主都能跑这几个用例。
|
||||
//!
|
||||
//! 关键语义(`research/05-mc-vulkan.md` §4.3 的真机证据):
|
||||
//! **Windows 注册表 `HKLM\SOFTWARE\Khronos\Vulkan\Drivers` 缺失不等于没有 Vulkan**
|
||||
//! (实测机 Vulkan 1.3.217 可用但该键不存在),所以只能降级到 `LoaderOnly`,不能判 `Missing`。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn windows_loader_without_registry_key_is_only_loader_only() {
|
||||
// 真机证据的复刻:vulkan-1.dll 在,注册表键缺失 → 只能说「loader 在、驱动未确认」。
|
||||
let probe = classify(VkEvidence::new(OsFamily::Windows, true, false));
|
||||
assert_eq!(probe, VkProbe::LoaderOnly);
|
||||
assert!(probe.has_loader());
|
||||
assert!(!probe.is_missing(), "注册表缺失被误判成没有 Vulkan");
|
||||
assert!(!probe.is_available(), "没有驱动信号不该承诺可用");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_loader_plus_registry_entry_is_available() {
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::Windows, true, true)),
|
||||
VkProbe::Available
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_without_loader_is_missing() {
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::Windows, false, true)),
|
||||
VkProbe::Missing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_loader_with_icd_manifests_is_available() {
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::Linux, true, true)),
|
||||
VkProbe::Available
|
||||
);
|
||||
// 只有 loader、没有 ICD 目录/环境变量:同样不否定(驱动可能由 loader 自寻)。
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::Linux, true, false)),
|
||||
VkProbe::LoaderOnly
|
||||
);
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::Linux, false, false)),
|
||||
VkProbe::Missing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_molten_vk_needs_no_separate_icd_signal() {
|
||||
// macOS 没有系统 Vulkan:MoltenVK 既是 loader 又是 ICD,没有 LoaderOnly 这一档。
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::MacOs, true, false)),
|
||||
VkProbe::Available
|
||||
);
|
||||
assert_eq!(
|
||||
classify(VkEvidence::new(OsFamily::MacOs, false, true)),
|
||||
VkProbe::Missing
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_names_are_stable_and_lower_snake() {
|
||||
assert_eq!(VkProbe::Unknown.as_str(), "unknown");
|
||||
assert_eq!(VkProbe::LoaderOnly.as_str(), "loader_only");
|
||||
assert_eq!(VkProbe::Available.as_str(), "available");
|
||||
assert_eq!(VkProbe::Missing.as_str(), "missing");
|
||||
assert_eq!(VkProbe::default(), VkProbe::Unknown);
|
||||
assert!(!VkProbe::Unknown.has_loader());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_candidates_cover_each_platform_signature() {
|
||||
let windows = loader_candidates(OsFamily::Windows);
|
||||
assert!(
|
||||
windows
|
||||
.iter()
|
||||
.any(|path| path.ends_with("System32/vulkan-1.dll")
|
||||
|| path.ends_with(r"System32\vulkan-1.dll")),
|
||||
"Windows 判据应是 System32\\vulkan-1.dll:{windows:?}"
|
||||
);
|
||||
|
||||
let linux = loader_candidates(OsFamily::Linux);
|
||||
assert!(
|
||||
linux
|
||||
.iter()
|
||||
.any(|path| path.to_string_lossy().contains("libvulkan.so.1")),
|
||||
"Linux 判据应含 libvulkan.so.1:{linux:?}"
|
||||
);
|
||||
|
||||
let macos = loader_candidates(OsFamily::MacOs);
|
||||
let macos_names = macos
|
||||
.iter()
|
||||
.map(|path| {
|
||||
path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
macos_names.iter().any(|name| name == "libvulkan.1.dylib"),
|
||||
"macOS 判据应含 libvulkan.1.dylib:{macos_names:?}"
|
||||
);
|
||||
assert!(
|
||||
macos_names.iter().any(|name| name == "libMoltenVK.dylib"),
|
||||
"macOS 判据应含 MoltenVK:{macos_names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_query_output_is_parsed_for_driver_entries() {
|
||||
// 键不存在:只有本地化错误行(中英各一例,真机输出)。
|
||||
let missing_en =
|
||||
"ERROR: The system was unable to find the specified registry key or value.\r\n";
|
||||
let missing_zh = "\r\n错误: 系统找不到指定的注册表项或值。\r\n";
|
||||
assert!(!registry_has_driver_entry(missing_en));
|
||||
assert!(!registry_has_driver_entry(missing_zh));
|
||||
assert!(!registry_has_driver_entry(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_query_real_format_is_recognised() {
|
||||
// 真实 `reg query KEY`(不带 /s)格式,本机实测:
|
||||
// 空行 → 键头行(顶格、完整 hive 名、无尾反斜杠)→ 缩进的值行
|
||||
// ` <值名> REG_DWORD 0x0`
|
||||
// Vulkan 的 ICD 注册是「值名 = manifest 绝对路径」,名字里可能有空格,所以列宽不定。
|
||||
let present = "\r\n\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Khronos\\Vulkan\\Drivers\r\n \
|
||||
C:\\Windows\\System32\\DriverStore\\FileRepository\\amdvlk64.inf_amd64_1b2c\\amdvlk64.json REG_DWORD 0x0\r\n \
|
||||
C:\\vendor a\\vk_vendor_a.json REG_DWORD 0x0\r\n";
|
||||
assert!(registry_has_driver_entry(present));
|
||||
|
||||
// 值可能是 REG_SZ(第三方 loader/层注册的写法)。
|
||||
let reg_sz = "\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Khronos\\Vulkan\\Drivers\r\n \
|
||||
C:\\vendor\\vk.json REG_SZ \r\n";
|
||||
assert!(registry_has_driver_entry(reg_sz));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_key_without_values_is_not_a_driver_signal() {
|
||||
// 键存在但没有任何值项:只有键头行。
|
||||
let header_only = "\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Khronos\\Vulkan\\Drivers\r\n";
|
||||
assert!(!registry_has_driver_entry(header_only));
|
||||
|
||||
// 键存在但只有子键(无 /s 时子键也缩进打印,且不带 REG_ 类型标记)。
|
||||
let subkeys_only = "\r\n\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Khronos\\Vulkan\\Drivers\r\n \
|
||||
LayerOne\r\n LayerTwo\r\n";
|
||||
assert!(!registry_has_driver_entry(subkeys_only));
|
||||
|
||||
// 键头是**别的**键时,其值行不能算数(防止把上层/邻居键的值当成 ICD 注册)。
|
||||
let other_key = "\r\n\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Khronos\\Vulkan\r\n \
|
||||
ImplicitLayers REG_DWORD 0x0\r\n";
|
||||
assert!(!registry_has_driver_entry(other_key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ldconfig_output_is_parsed_for_the_loader() {
|
||||
let with_loader =
|
||||
"\tlibvulkan.so.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libvulkan.so.1\n\
|
||||
\tlibvulkan.so (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libvulkan.so\n";
|
||||
assert!(ldconfig_has_libvulkan(with_loader));
|
||||
assert!(!ldconfig_has_libvulkan(
|
||||
"\tlibGL.so.1 (libc6,x86-64) => /usr/lib/libGL.so.1\n"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icd_env_prefers_the_legacy_variable_and_skips_blanks() {
|
||||
let vars = |pairs: &'static [(&'static str, &'static str)]| {
|
||||
env_icd_path(move |key| {
|
||||
pairs
|
||||
.iter()
|
||||
.find(|(name, _)| *name == key)
|
||||
.map(|(_, value)| (*value).to_string())
|
||||
})
|
||||
};
|
||||
assert_eq!(
|
||||
vars(&[("VK_ICD_FILENAMES", "/a/nvidia_icd.json")]).as_deref(),
|
||||
Some("/a/nvidia_icd.json")
|
||||
);
|
||||
assert_eq!(
|
||||
vars(&[("VK_DRIVER_FILES", "/b/intel_icd.json")]).as_deref(),
|
||||
Some("/b/intel_icd.json")
|
||||
);
|
||||
// 旧名优先(两者都设时 Khronos loader 也是旧名优先)。
|
||||
assert_eq!(
|
||||
vars(&[
|
||||
("VK_ICD_FILENAMES", "/a/one.json"),
|
||||
("VK_DRIVER_FILES", "/b/two.json"),
|
||||
])
|
||||
.as_deref(),
|
||||
Some("/a/one.json")
|
||||
);
|
||||
// 空白值当作没设。
|
||||
assert_eq!(vars(&[("VK_ICD_FILENAMES", " ")]), None);
|
||||
assert_eq!(vars(&[]), None);
|
||||
assert_eq!(ICD_ENV_VARS.len(), 2);
|
||||
assert_eq!(ICD_DIRS.len(), 3);
|
||||
assert!(VULKAN_DRIVERS_KEY.contains("Khronos"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icd_manifest_count_ignores_non_json_and_missing_dirs() {
|
||||
let dir = temp_dir("vk-icd");
|
||||
assert_eq!(count_icd_manifests(&dir), 0);
|
||||
std::fs::write(dir.join("nvidia_icd.json"), "{}").expect("写 json");
|
||||
std::fs::write(dir.join("intel_icd.json"), "{}").expect("写 json");
|
||||
std::fs::write(dir.join("README"), "x").expect("写非 json");
|
||||
assert_eq!(count_icd_manifests(&dir), 2);
|
||||
assert_eq!(count_icd_manifests(&dir.join("nope")), 0);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_vk_on_this_host_is_not_unknown() {
|
||||
// 真探一次:只断言「给了确定结论」(本机有没有 Vulkan 都合法,不许恒 Unknown)。
|
||||
assert_ne!(probe_vk(), VkProbe::Unknown);
|
||||
}
|
||||
|
||||
/// 唯一临时目录(不引 tempfile 依赖)。
|
||||
fn temp_dir(tag: &str) -> PathBuf {
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
let index = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("ull-vk-{tag}-{}-{index}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("创建临时目录");
|
||||
dir
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Windows 版本探测:注册表 / `ver` 输出的解析 + 本机真实探测。
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Win11(`CurrentMajorVersionNumber` 是 REG_DWORD,`reg query` 输出 0x..)。
|
||||
const WIN11_REG: &str = "\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion
|
||||
CurrentBuildNumber REG_SZ 22631
|
||||
CurrentMajorVersionNumber REG_DWORD 0xa
|
||||
CurrentMinorVersionNumber REG_DWORD 0x0
|
||||
CurrentVersion REG_SZ 6.3
|
||||
ProductName REG_SZ Windows 10 Pro
|
||||
";
|
||||
|
||||
/// Win10:数字键存在,`CurrentVersion` 是过期的 6.3,不能拿它当版本。
|
||||
const WIN10_REG: &str = "\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion
|
||||
CurrentBuildNumber REG_SZ 19045
|
||||
CurrentMajorVersionNumber REG_DWORD 0xa
|
||||
CurrentMinorVersionNumber REG_DWORD 0x0
|
||||
CurrentVersion REG_SZ 6.3
|
||||
";
|
||||
|
||||
/// Win7:没有数字键,只有 `CurrentVersion` 字符串(`CurrentBuildNumber` Win7 也有)。
|
||||
const WIN7_REG: &str = "\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion
|
||||
CurrentBuildNumber REG_SZ 7601
|
||||
CurrentVersion REG_SZ 6.1
|
||||
ProductName REG_SZ Windows 7 Ultimate
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn reg_query_parses_windows_11() {
|
||||
let version = windows_version::parse_reg_query(WIN11_REG).expect("解析失败");
|
||||
assert_eq!(version, WindowsVersion::new(10, 0, 22631));
|
||||
assert_eq!(version.version_string(), "10.0.22631");
|
||||
assert_eq!(version.display_name(), "Windows 11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reg_query_prefers_numeric_keys_over_stale_current_version() {
|
||||
let version = windows_version::parse_reg_query(WIN10_REG).expect("解析失败");
|
||||
assert_eq!(version, WindowsVersion::new(10, 0, 19045));
|
||||
assert_eq!(version.display_name(), "Windows 10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reg_query_falls_back_to_current_version_on_windows_7() {
|
||||
let version = windows_version::parse_reg_query(WIN7_REG).expect("解析失败");
|
||||
assert_eq!(version, WindowsVersion::new(6, 1, 7601));
|
||||
assert_eq!(version.major_minor(), (6, 1));
|
||||
assert_eq!(version.display_name(), "Windows 7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reg_query_without_build_number_is_unparsable() {
|
||||
let text = " CurrentVersion REG_SZ 6.1\n";
|
||||
assert_eq!(windows_version::parse_reg_query(text), None);
|
||||
// 空输出(reg 不可用 / 被策略禁用)也不能崩。
|
||||
assert_eq!(windows_version::parse_reg_query(""), None);
|
||||
assert_eq!(
|
||||
windows_version::parse_reg_query("ERROR: 找不到指定的注册表项"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ver_output_parses_english_and_chinese() {
|
||||
let english =
|
||||
windows_version::parse_ver_output("Microsoft Windows [Version 10.0.19045.4046]\r\n")
|
||||
.expect("解析失败");
|
||||
assert_eq!(english, WindowsVersion::new(10, 0, 19045));
|
||||
|
||||
let chinese = windows_version::parse_ver_output(
|
||||
"Microsoft Windows [\u{7248}\u{672c} 10.0.19045.4046]\r\n",
|
||||
)
|
||||
.expect("解析失败");
|
||||
assert_eq!(chinese, english);
|
||||
|
||||
let win7 = windows_version::parse_ver_output("Microsoft Windows [Version 6.1.7601]")
|
||||
.expect("解析失败");
|
||||
assert_eq!(win7, WindowsVersion::new(6, 1, 7601));
|
||||
assert_eq!(win7.display_name(), "Windows 7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ver_output_rejects_garbage() {
|
||||
assert_eq!(windows_version::parse_ver_output(""), None);
|
||||
assert_eq!(windows_version::parse_ver_output("no brackets here"), None);
|
||||
assert_eq!(
|
||||
windows_version::parse_ver_output("Microsoft Windows [Version ]"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_number_maps_to_version_families() {
|
||||
// 快路径:单值查询只给 build,major.minor 由 build 族反推。
|
||||
assert_eq!(
|
||||
windows_version::from_build(7601),
|
||||
Some(WindowsVersion::new(6, 1, 7601))
|
||||
);
|
||||
assert_eq!(
|
||||
windows_version::from_build(9200).map(|v| v.major_minor()),
|
||||
Some((6, 2))
|
||||
);
|
||||
assert_eq!(
|
||||
windows_version::from_build(9600).map(|v| v.major_minor()),
|
||||
Some((6, 3))
|
||||
);
|
||||
assert_eq!(
|
||||
windows_version::from_build(19045).map(|v| v.major_minor()),
|
||||
Some((10, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
windows_version::from_build(22631).map(|v| v.display_name()),
|
||||
Some("Windows 11")
|
||||
);
|
||||
assert_eq!(
|
||||
windows_version::from_build(26100).map(|v| v.major_minor()),
|
||||
Some((10, 0))
|
||||
);
|
||||
// Vista 之前不猜:交给整键/`ver` 解析兜底。
|
||||
assert_eq!(windows_version::from_build(3790), None);
|
||||
assert_eq!(windows_version::from_build(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_value_query_yields_build_number() {
|
||||
let text = "\
|
||||
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion
|
||||
CurrentBuildNumber REG_SZ 26100
|
||||
";
|
||||
assert_eq!(windows_version::parse_build_number(text), Some(26100));
|
||||
assert_eq!(
|
||||
windows_version::parse_build_number("ERROR: \u{627e}\u{4e0d}\u{5230}\u{6307}\u{5b9a}\u{7684}\u{6ce8}\u{518c}\u{8868}\u{9879}"),
|
||||
None
|
||||
);
|
||||
assert_eq!(windows_version::parse_build_number(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_name_covers_the_matrix() {
|
||||
assert_eq!(
|
||||
WindowsVersion::new(10, 0, 22_000).display_name(),
|
||||
"Windows 11"
|
||||
);
|
||||
assert_eq!(
|
||||
WindowsVersion::new(10, 0, 21_999).display_name(),
|
||||
"Windows 10"
|
||||
);
|
||||
assert_eq!(
|
||||
WindowsVersion::new(6, 3, 9600).display_name(),
|
||||
"Windows 8.1"
|
||||
);
|
||||
assert_eq!(WindowsVersion::new(6, 2, 9200).display_name(), "Windows 8");
|
||||
assert_eq!(WindowsVersion::new(6, 1, 7601).display_name(), "Windows 7");
|
||||
assert_eq!(
|
||||
WindowsVersion::new(6, 0, 6002).display_name(),
|
||||
"Windows Vista"
|
||||
);
|
||||
assert_eq!(WindowsVersion::new(5, 1, 2600).display_name(), "Windows XP");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_matches_the_host() {
|
||||
let probed = windows_version::probe();
|
||||
if cfg!(target_os = "windows") {
|
||||
let version = probed.expect("Windows 宿主必须能探到真实版本");
|
||||
// 真实的 Windows 至少是 6.x(XP 是 5.x,不会被这个仓库支持)。
|
||||
assert!(version.major >= 6, "探到 {version:?}");
|
||||
assert!(!version.display_name().is_empty());
|
||||
// 重新探一次结果稳定(reg 与 ver 两条路径应给同一个 major.minor)。
|
||||
let again = windows_version::probe().expect("二次探测失败");
|
||||
assert_eq!(again.major_minor(), version.major_minor());
|
||||
} else {
|
||||
assert!(probed.is_none(), "非 Windows 平台不应探到 Windows 版本");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user