tracel-xtask 4.17.1

Reusable and Extensible xtask commands to manage repositories.
Documentation
use std::collections::HashMap;

use anyhow::Result;
use clap::ValueEnum;
use log::info;
use strum::IntoEnumIterator;
use tracel_xtask_utils::{
    endgroup,
    environment::{Environment, EnvironmentName},
    group,
    process::{run_process_for_package, run_process_for_workspace},
    rustup::{is_current_toolchain_nightly, rustup_add_component},
    workspace::{WorkspaceMember, WorkspaceMemberType, get_workspace_members},
};

use crate::{
    commands::{CARGO_NIGHTLY_MSG, WARN_IGNORED_ONLY_ARGS},
    context::Context,
};

use super::Target;

#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MiriMode {
    #[default]
    All,
    UbOnly,
}

impl MiriMode {
    pub fn envs(self) -> Option<HashMap<&'static str, &'static str>> {
        match self {
            Self::All => None,
            Self::UbOnly => Some(HashMap::from([("MIRIFLAGS", "-Zmiri-ignore-leaks")])),
        }
    }

    pub fn description(self) -> &'static str {
        match self {
            Self::All => "Undefined Behavior + Memory Leaks",
            Self::UbOnly => "Undefined Behavior Only",
        }
    }

    pub fn print_enabled(self) {
        info!("Miri is enabled");
    }

    pub fn print_mode_info(self) {
        info!("Miri mode: {}", self.description());
    }
}

#[tracel_xtask_macros::declare_command_args(Target, TestSubCommand)]
pub struct TestCmdArgs {}

pub fn handle_command(args: TestCmdArgs, env: Environment, _ctx: Context) -> anyhow::Result<()> {
    if args.target == Target::Workspace && !args.only.is_empty() {
        log::warn!("{WARN_IGNORED_ONLY_ARGS}");
    }
    if !check_environment(&args, &env) {
        std::process::exit(1);
    }
    match args.get_command() {
        TestSubCommand::Unit => run_unit(&args.target, &args),
        TestSubCommand::Integration => run_integration(&args.target, &args),
        TestSubCommand::All => TestSubCommand::iter()
            .filter(|command| *command != TestSubCommand::All)
            .try_for_each(|command| {
                handle_command(
                    TestCmdArgs {
                        command: Some(command),
                        target: args.target.clone(),
                        exclude: args.exclude.clone(),
                        only: args.only.clone(),
                        threads: args.threads,
                        test: args.test.clone(),
                        jobs: args.jobs,
                        force: args.force,
                        features: args.features.clone(),
                        no_default_features: args.no_default_features,
                        no_capture: args.no_capture,
                        miri: args.miri,
                        release: args.release,
                    },
                    env.clone(),
                    _ctx.clone(),
                )
            }),
    }
}

/// Return true if the environment is OK.
/// Prevents from running tests in production unless the `force` flag is set.
pub fn check_environment(args: &TestCmdArgs, env: &Environment) -> bool {
    if env.name == EnvironmentName::Production {
        if args.force {
            log::warn!("Force running tests in production (--force argument is set)");
            true
        } else {
            info!("Abort tests to avoid running them in production!");
            false
        }
    } else {
        true
    }
}

fn push_test_command_prefix(cmd_args: &mut Vec<String>, args: &TestCmdArgs) {
    if args.miri.is_some() {
        cmd_args.push("miri".to_string());
    }
    cmd_args.push("test".to_string());
}

fn push_cargo_optional_args(cmd_args: &mut Vec<String>, args: &TestCmdArgs) {
    if let Some(jobs) = args.jobs {
        cmd_args.extend(["--jobs".to_string(), jobs.to_string()]);
    }

    if let Some(features) = &args.features {
        if !features.is_empty() {
            cmd_args.extend(["--features".to_string(), features.join(",")]);
        }
    }

    if args.release {
        cmd_args.push("--release".to_string());
    }

    if args.no_default_features {
        cmd_args.push("--no-default-features".to_string());
    }
}

fn push_test_harness_args(cmd_args: &mut Vec<String>, args: &TestCmdArgs) {
    cmd_args.extend(["--".to_string(), "--color=always".to_string()]);

    if let Some(threads) = args.threads {
        cmd_args.extend(["--test-threads".to_string(), threads.to_string()]);
    }

    if args.no_capture {
        cmd_args.push("--nocapture".to_string());
    }
}

pub fn run_unit(target: &Target, args: &TestCmdArgs) -> Result<()> {
    if let Some(mode) = args.miri {
        ensure_miri_ready()?;
        mode.print_enabled();
        mode.print_mode_info();
    }

    match target {
        Target::Workspace => {
            info!("Workspace Unit Tests");

            let test = args.test.as_deref().unwrap_or("");
            let mut cmd_args = Vec::new();
            push_test_command_prefix(&mut cmd_args, args);
            cmd_args.extend([
                "--workspace".to_string(),
                "--lib".to_string(),
                "--bins".to_string(),
                "--examples".to_string(),
            ]);
            if !test.is_empty() {
                cmd_args.push(test.to_string());
            }
            push_cargo_optional_args(&mut cmd_args, args);
            push_test_harness_args(&mut cmd_args, args);

            run_process_for_workspace(
                "cargo",
                &cmd_args.iter().map(String::as_str).collect::<Vec<_>>(),
                args.miri.and_then(MiriMode::envs),
                &args.exclude,
                Some(r".*target/[^/]+/deps/([^-\s]+)"),
                Some("Unit Tests"),
                "Workspace Unit Tests failed",
                Some("no library targets found"),
                Some("No library found to test for in workspace."),
            )?;
        }
        Target::Crates | Target::Examples => {
            let members = match target {
                Target::Crates => get_workspace_members(WorkspaceMemberType::Crate),
                Target::Examples => get_workspace_members(WorkspaceMemberType::Example),
                _ => unreachable!(),
            };

            for member in members {
                run_unit_test(&member, args)?;
            }
        }
        Target::AllPackages => {
            Target::iter()
                .filter(|target| *target != Target::AllPackages && *target != Target::Workspace)
                .try_for_each(|target| run_unit(&target, args))?;
        }
    }

    Ok(())
}

pub fn run_unit_test(member: &WorkspaceMember, args: &TestCmdArgs) -> Result<()> {
    group!("Unit Tests: {}", member.name);

    let test = args.test.as_deref().unwrap_or("");
    let mut cmd_args = Vec::new();

    push_test_command_prefix(&mut cmd_args, args);

    if !test.is_empty() {
        cmd_args.push(test.to_string());
    }

    cmd_args.extend([
        "--lib".to_string(),
        "--bins".to_string(),
        "--examples".to_string(),
        "-p".to_string(),
        member.name.clone(),
    ]);

    push_cargo_optional_args(&mut cmd_args, args);
    push_test_harness_args(&mut cmd_args, args);

    run_process_for_package(
        "cargo",
        &member.name,
        &cmd_args.iter().map(String::as_str).collect::<Vec<_>>(),
        args.miri.and_then(MiriMode::envs),
        &args.exclude,
        &args.only,
        &format!("Failed to execute unit test for '{}'", member.name),
        Some("no library targets found"),
        Some(&format!(
            "No library found to test for in the crate '{}'.",
            member.name
        )),
    )?;

    endgroup!();
    Ok(())
}

pub fn run_integration(target: &Target, args: &TestCmdArgs) -> Result<()> {
    if let Some(mode) = args.miri {
        ensure_miri_ready()?;
        mode.print_enabled();
        mode.print_mode_info();
    }

    match target {
        Target::Workspace => {
            info!("Workspace Integration Tests");

            let test = args.test.as_deref().unwrap_or("*");
            let mut cmd_args = Vec::new();

            push_test_command_prefix(&mut cmd_args, args);
            cmd_args.extend([
                "--workspace".to_string(),
                "--test".to_string(),
                test.to_string(),
            ]);

            push_cargo_optional_args(&mut cmd_args, args);
            push_test_harness_args(&mut cmd_args, args);

            run_process_for_workspace(
                "cargo",
                &cmd_args.iter().map(String::as_str).collect::<Vec<_>>(),
                args.miri.and_then(MiriMode::envs),
                &args.exclude,
                Some(r".*target/[^/]+/deps/([^-\s]+)"),
                Some("Integration Tests"),
                "Workspace Integration Tests failed",
                Some("no test target matches pattern"),
                Some("No tests found matching the pattern `test_*` in workspace."),
            )?;
        }
        Target::Crates | Target::Examples => {
            let members = match target {
                Target::Crates => get_workspace_members(WorkspaceMemberType::Crate),
                Target::Examples => get_workspace_members(WorkspaceMemberType::Example),
                _ => unreachable!(),
            };

            for member in members {
                run_integration_test(&member, args)?;
            }
        }
        Target::AllPackages => {
            Target::iter()
                .filter(|target| *target != Target::AllPackages && *target != Target::Workspace)
                .try_for_each(|target| run_integration(&target, args))?;
        }
    }

    Ok(())
}

fn run_integration_test(member: &WorkspaceMember, args: &TestCmdArgs) -> Result<()> {
    group!("Integration Tests: {}", member.name);

    let mut cmd_args = Vec::new();

    push_test_command_prefix(&mut cmd_args, args);
    cmd_args.extend([
        "--test".to_string(),
        "*".to_string(),
        "-p".to_string(),
        member.name.clone(),
    ]);

    push_cargo_optional_args(&mut cmd_args, args);
    push_test_harness_args(&mut cmd_args, args);

    run_process_for_package(
        "cargo",
        &member.name,
        &cmd_args.iter().map(String::as_str).collect::<Vec<_>>(),
        args.miri.and_then(MiriMode::envs),
        &args.exclude,
        &args.only,
        &format!("Failed to execute integration test for '{}'", member.name),
        Some("no test target matches pattern"),
        Some(&format!(
            "No integration tests found for '{}'.",
            member.name
        )),
    )?;

    endgroup!();
    Ok(())
}

fn ensure_miri_ready() -> Result<()> {
    if !is_current_toolchain_nightly() {
        anyhow::bail!("{CARGO_NIGHTLY_MSG}");
    }

    rustup_add_component("miri")?;
    rustup_add_component("rust-src")?;
    Ok(())
}