构建 WSL-UI:模拟模式和假发行版

发布: (2026年1月18日 GMT+8 10:40)
8 min read
原文: Dev.to

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 模式激活时,应用会启动 七个 虚假的发行版:

名称版本状态安装来源
UbuntuWSL2RunningMicrosoft Store
DebianWSL2StoppedLXC Container
AlpineWSL2StoppedContainer Import
Ubuntu‑22.04WSL2RunningDownload
FedoraWSL2RunningDownload
ArchWSL2StoppedDownload
Ubuntu‑legacyWSL1StoppedLegacy

每个发行版都有 模拟的资源使用情况

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 设置。

经验教训

如果我再做一次,我会更早地启动模拟模式。抽象层在整个开发过程中都有收益,而不仅仅是在测试时。

我会做的不同之处

  1. 更真实的时序 – 模拟运行得太快。真实的 WSL 操作有明显的延迟。添加可配置的延迟可以让开发体验更具代表性。
  2. 持久化选项 – 目前模拟在应用重启时会重置。提供将模拟状态持久化到文件的选项,对更长的测试会话会很有帮助。
  3. 模糊测试 – 随机操作序列以发现边缘情况。基础设施已经就绪,我只需要编写相应的测试。

接下来是什么?

本系列的下一篇文章将深入探讨 重命名发行版 的细节,包括使其正常工作所需的 Windows 注册表更改。


获取 WSL‑UI

Back to Blog

相关文章

阅读更多 »

NgRx Toolkit v21

NgRx Toolkit v21 NgRx Toolkit 起源于 SignalStore 甚至还未标记为稳定的时期。 在那些早期,社区对各种 f...

适合初学者的 Rust 入门方式

介绍 Rust 是一种强大的语言,但对初学者来说,入门可能会感到不知所措。当我开始学习 Rust 时,我意识到我需要一个简单的、...

Go 中优雅的领域驱动设计对象

❓ 你如何在 Go 中定义你的领域对象?Go 并不是典型的面向对象语言。当你尝试实现 Domain‑Driven Design(DDD)概念,如 Entity …