whitaker-installer 0.2.7

Installer CLI for Whitaker Dylint lint libraries
Documentation
//! Test helpers for toolchain tests.

use super::*;
use std::cell::RefCell;
use std::process::ExitStatus;

#[cfg(unix)]
pub fn exit_status(code: i32) -> ExitStatus {
    use std::os::unix::process::ExitStatusExt;

    ExitStatusExt::from_raw(code << 8)
}

#[cfg(windows)]
pub fn exit_status(code: i32) -> ExitStatus {
    use std::os::windows::process::ExitStatusExt;

    ExitStatusExt::from_raw(code as u32)
}

pub fn output_with_status(code: i32) -> Output {
    Output {
        status: exit_status(code),
        stdout: Vec::new(),
        stderr: Vec::new(),
    }
}

pub fn output_with_stderr(code: i32, stderr: &str) -> Output {
    Output {
        status: exit_status(code),
        stdout: Vec::new(),
        stderr: stderr.as_bytes().to_vec(),
    }
}

pub fn test_toolchain(channel: &str) -> Toolchain {
    Toolchain {
        channel: channel.to_owned(),
        workspace_root: Utf8PathBuf::from("."),
    }
}

/// Captures command invocations and returns a preconfigured `Output`.
pub(crate) struct CapturingCommandRunner {
    calls: RefCell<Vec<(String, Vec<String>)>>,
    output: Output,
}

impl CapturingCommandRunner {
    /// Creates a capture runner that clones `output` for every executed command.
    pub(crate) fn new(output: Output) -> Self {
        Self {
            calls: RefCell::new(Vec::new()),
            output,
        }
    }

    /// Returns a cloned list of recorded `(program, args)` pairs without mutating state.
    pub(crate) fn recorded_calls(&self) -> Vec<(String, Vec<String>)> {
        self.calls.borrow().clone()
    }
}

impl CommandRunner for CapturingCommandRunner {
    fn run(&self, program: &str, args: &[&str]) -> std::io::Result<Output> {
        self.calls.borrow_mut().push((
            program.to_owned(),
            args.iter().map(|arg| (*arg).to_owned()).collect(),
        ));
        Ok(self.output.clone())
    }
}

/// Common expectation fields for rustup commands.
pub struct RustupExpectation<'a> {
    pub exit_code: i32,
    pub stderr: Option<&'a str>,
}

// Generic helper to expect a rustup command with custom validation.
fn expect_rustup_command<F>(
    runner: &mut MockCommandRunner,
    seq: &mut mockall::Sequence,
    expectation: RustupExpectation<'_>,
    matcher: F,
) where
    F: Fn(&str, &[&str]) -> bool + Send + 'static,
{
    let stderr = expectation.stderr.map(str::to_owned);
    let exit_code = expectation.exit_code;
    runner
        .expect_run()
        .withf(matcher)
        .times(1)
        .in_sequence(seq)
        .returning(move |_, _| {
            let output = match stderr.as_deref() {
                Some(message) => output_with_stderr(exit_code, message),
                None => output_with_status(exit_code),
            };
            Ok(output)
        });
}

pub struct ToolchainInstallExpectation<'a> {
    pub channel: &'a str,
    pub exit_code: i32,
    pub stderr: Option<&'a str>,
}

pub fn expect_rustc_version(
    runner: &mut MockCommandRunner,
    seq: &mut mockall::Sequence,
    channel: &str,
    exit_code: i32,
) {
    let channel = channel.to_owned();
    runner
        .expect_run()
        .withf(move |program, args| {
            program == "rustup"
                && args.len() == 4
                && args[0] == "run"
                && args[1] == channel
                && args[2] == "rustc"
                && args[3] == "--version"
        })
        .times(1)
        .in_sequence(seq)
        .returning(move |_, _| Ok(output_with_status(exit_code)));
}

pub fn expect_toolchain_install(
    runner: &mut MockCommandRunner,
    seq: &mut mockall::Sequence,
    expectation: ToolchainInstallExpectation<'_>,
) {
    let channel = expectation.channel.to_owned();
    expect_rustup_command(
        runner,
        seq,
        RustupExpectation {
            exit_code: expectation.exit_code,
            stderr: expectation.stderr,
        },
        move |program, args| {
            program == "rustup"
                && args.len() == 3
                && args[0] == "toolchain"
                && args[1] == "install"
                && args[2] == channel
        },
    );
}

/// Helper to test that ensure_installed fails with the expected error.
pub fn assert_install_fails_with<F, I, E>(
    toolchain: Toolchain,
    setup_mocks: F,
    install: I,
    error_matcher: E,
) where
    F: FnOnce(&mut MockCommandRunner, &mut mockall::Sequence),
    I: FnOnce(&Toolchain, &MockCommandRunner) -> Result<ToolchainInstallStatus>,
    E: FnOnce(InstallerError),
{
    let mut runner = MockCommandRunner::new();
    let mut seq = mockall::Sequence::new();

    setup_mocks(&mut runner, &mut seq);

    let err = install(&toolchain, &runner).expect_err("expected installation failure");

    error_matcher(err);
}

/// Returns a predicate that matches a rustup component add command with multiple components.
pub fn matches_multi_component_add(
    channel: &str,
    components: &[&str],
) -> impl Fn(&str, &[&str]) -> bool + use<> {
    let channel = channel.to_owned();
    let components: Vec<String> = components.iter().map(|s| (*s).to_owned()).collect();
    move |program, args| {
        let Some(actual_components) = args.get(4..) else {
            return false;
        };
        program == "rustup"
            && args[0] == "component"
            && args[1] == "add"
            && args[2] == "--toolchain"
            && args[3] == channel
            && actual_components.len() == components.len()
            && actual_components
                .iter()
                .zip(&components)
                .all(|(a, b)| *a == b)
    }
}