构建 WSL-UI:模拟模式和假发行版
Source: Dev.to
请提供您想要翻译的具体文本内容,我将为您翻译成简体中文。
架构决策:WSL‑UI 的 Mock 模式
我在 WSL‑UI 做的首批架构决策之一,就是构建一个完整的 mock 模式。
它不仅用于自动化测试——虽然这很关键——还用于日常开发。
为什么?
- 我不想在调试时不小心删除真实的 WSL 发行版。
- 我需要测试一些用真实发行版难以复现的场景(例如网络超时、注册表条目损坏)。
关键的洞见来自 领域驱动设计:防腐层(Anti‑Corruption Layer)模式。
我没有在命令处理器中直接调用 wsl.exe,而是创建了一个抽象层,可以在运行时进行替换。
特性定义(Rust)
pub trait WslCommandExecutor: Send + Sync {
fn list_distributions(&self) -> Result<(), WslError>;
fn start_distribution(&self, name: &str) -> Result<(), WslError>;
fn stop_distribution(&self, name: &str) -> Result<(), WslError>;
fn terminate_distribution(&self, name: &str) -> Result<(), WslError>;
fn import_distribution(
&self,
name: &str,
path: &Path,
location: &Path,
) -> Result<(), WslError>;
// …more operations
}
pub trait ResourceMonitor: Send + Sync {
fn get_memory_usage(&self, name: &str) -> Result<u64, WslError>;
fn get_cpu_percentage(&self, name: &str) -> Result<f64, WslError>;
fn get_vhdx_size(&self, name: &str) -> Result<u64, WslError>;
fn get_registry_info(&self, name: &str) -> Result<RegistryInfo, WslError>;
}
pub trait TerminalExecutor: Send + Sync {
fn open_terminal(&self, name: &str) -> Result<(), WslError>;
fn execute_command(&self, name: &str, command: &str) -> Result<CommandOutput, WslError>;
}
- 真实实现 调用
wsl.exe、读取 Windows 注册表,并与 Windows Terminal 交互。 - Mock 实现 保持内部状态并返回确定的、可控的响应。
默认 Mock 发行版
当 mock 模式激活时,应用会启动 七个 虚假的发行版:
| 名称 | 版本 | 状态 | 安装来源 |
|---|---|---|---|
| Ubuntu | WSL2 | Running | Microsoft Store |
| Debian | WSL2 | Stopped | LXC Container |
| Alpine | WSL2 | Stopped | Container Import |
| Ubuntu‑22.04 | WSL2 | Running | Download |
| Fedora | WSL2 | Running | Download |
| Arch | WSL2 | Stopped | Download |
| Ubuntu‑legacy | WSL1 | Stopped | Legacy |
每个发行版都有 模拟的资源使用情况:
let (mock_memory, mock_cpu) = match distro {
"Ubuntu" => (512_000_000, 2.5), // ~512 MiB, 2.5 % CPU
"Ubuntu-22.04" => (384_000_000, 1.8), // ~384 MiB, 1.8 % CPU
"Debian" => (256_000_000, 0.5), // ~256 MiB, 0.5 % CPU
"Alpine" => (64_000_000, 0.2), // ~64 MiB, 0.2 % CPU
"Fedora" => (196_000_000, 1.2), // ~196 MiB, 1.2 % CPU
_ => (128_000_000, 0.3), // Default values
};
- 注册表条目 包含真实感的 GUID、路径和软件包信息。
- 磁盘大小 从 500 MiB 到 8 GiB 不等。
- Mock 甚至为磁盘挂载功能模拟了物理磁盘(一个 500 GB SSD 和一个 1 TB HDD,带分区)。
Mock 状态是 动态的:
- 启动发行版会将其状态改为
Running。 - 停止发行版会恢复为
Stopped。 - 创建新发行版会把它加入列表;删除则会将其移除。
Mock 执行器骨架
pub struct MockWslExecutor {
distributions: Arc<Mutex<HashMap<String, MockDistribution>>>,
default_distribution: Arc<Mutex<Option<String>>>,
error_simulation: Arc<Mutex<Option<ErrorSimulation>>>,
}
impl MockWslExecutor {
pub fn new() -> Self {
let mut distros = HashMap::new();
// Initialise with the 7 default distributions
distros.insert(
"Ubuntu".to_string(),
M
Source: …
ockDistribution {
guid: generate_guid(),
name: "Ubuntu".to_string(),
state: DistroState::Running,
version: WslVersion::Wsl2,
// …
},
);
// …more distros
Self {
distributions: Arc::new(Mutex::new(distros)),
default_distribution: Arc::new(Mutex::new(Some("Ubuntu".to_string()))),
error_simulation: Arc::new(Mutex::new(None)),
}
}
}
Arc<Mutex<…>> 模式保证了 线程安全 —— Tauri 命令可能会在多个线程中被调用,而模拟状态必须保持一致。
按需模拟错误
测试正常路径(happy‑path)很简单;只有在能够 强制错误 时,错误处理的测试才会变得容易。
pub struct ErrorSimulation {
pub operation: String,
pub error_type: ErrorType,
pub delay_ms: u64,
}
pub enum ErrorType {
Timeout,
CommandFailed,
NotFound,
Cancelled,
}
在前端(或 E2E 测试)中,你可以安排一次失败:
await invoke('set_mock_error', {
operation: 'start_distribution',
errorType: 'timeout',
delayMs: 5000,
});
// 下一次 start_distribution 调用将在 5 秒后超时
await invoke('start_distribution', { name: 'Ubuntu' });
这让我们能够验证:
- 缓慢操作期间的进度对话框
- 错误通知 UI
- 重试逻辑
- 优雅降级
启用 Mock 模式
Mock 模式通过环境变量进行切换:
# 任意一个都可以激活 mock 模式
WSL_MOCK=1
WSL_UI_MOCK_MODE=1
启动时,应用会检查该标志并加载相应的实现:
fn init_executors() {
if crate::utils::is_mock_mode() {
// Mock 实现
let wsl_mock = Arc::new(MockWslExecutor::new());
WSL_EXECUTOR.get_or_init(|| wsl_mock.clone());
let resource_mock = MockResourceMonitor::with_wsl_mock(wsl_mock.clone());
RESOURCE_MONITOR.get_or_init(|| Arc::new(resource_mock));
let terminal_mock = MockTerminalExecutor::new();
TERMINAL_EXECUTOR.get_or_init(|| Arc::new(terminal_mock));
} else {
// 真正的实现
WSL_EXECUTOR.get_or_init(|| Arc::new(RealWslExecutor));
RESOURCE_MONITOR.get_or_init(|| Arc::new(RealResourceMonitor));
TERMINAL_EXECUTOR.get_or_init(|| Arc::new(RealTerminalExecutor));
}
}
小结
- Mock 模式 在保护真实 WSL 安装的同时,提供了丰富且可控的测试环境。
- 防腐层(traits + 具体实现)使得在真实代码与 Mock 代码之间切换变得轻而易举。
- 通过 有状态的 Mock、错误模拟 与 基于环境变量的激活,开发、手动测试以及 CI 流水线都变得更快、更安全、更可靠。
前端 Mock 支持
Mock 模式不仅限于后端。前端在 E2E 测试期间会将 Zustand store 暴露在 window 对象上:
// 在开发/测试模式下
if (import.meta.env.DEV || import.meta.env.MODE === 'test') {
(window as any).__distroStore = useDistroStore;
(window as any).__notificationStore = useNotificationStore;
}
这让 E2E 测试可以直接检查和操作应用状态:
// 在 WebdriverIO 测试中
const store = await browser.execute(() => window.__distroStore.getState());
expect(store.distributions).toHaveLength(7);
// 在测试之间重置为初始状态
await browser.execute(() => window.__distroStore.getState().reset());
这项工作值得吗?
绝对值得。 构建 Mock 模式耗费了相当多的精力——大约占项目总工时的 15‑20 %——但它在多方面带来了回报:
- 更快的开发 —— 不必等待真实的 WSL 操作;启动发行版只需毫秒级而非秒级。
- 安全的实验 —— 破坏性操作(删除、格式化)可以在不冒风险的情况下进行测试。
- 可复现的测试 —— E2E 测试能够在受控的模拟环境中稳定运行。
tests run against an identical initial state every time.
- 离线开发 – 无需安装实际的 WSL 发行版。
- 边缘情况覆盖 – 轻松测试诸如“如果用户拥有 50 个发行版?”之类的场景。
- CI/CD 友好 – 测试在 GitHub Actions 的干净 Windows 运行器上运行,无需 WSL 设置。
经验教训
如果我再做一次,我会更早地启动模拟模式。抽象层在整个开发过程中都有收益,而不仅仅是在测试时。
我会做的不同之处
- 更真实的时序 – 模拟运行得太快。真实的 WSL 操作有明显的延迟。添加可配置的延迟可以让开发体验更具代表性。
- 持久化选项 – 目前模拟在应用重启时会重置。提供将模拟状态持久化到文件的选项,对更长的测试会话会很有帮助。
- 模糊测试 – 随机操作序列以发现边缘情况。基础设施已经就绪,我只需要编写相应的测试。
接下来是什么?
本系列的下一篇文章将深入探讨 重命名发行版 的细节,包括使其正常工作所需的 Windows 注册表更改。
获取 WSL‑UI
- Microsoft Store – 搜索“WSL UI”或访问商店页面。
- Direct Download – 官方网站。
- GitHub – https://github.com/octasoft-ltd/wsl-ui