use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolchainInfo {
pub channel: String,
pub date: Option<String>,
pub full_version: String,
}
impl ToolchainInfo {
pub fn new(
channel: impl Into<String>,
date: Option<String>,
full_version: impl Into<String>,
) -> Self {
Self {
channel: channel.into(),
date,
full_version: full_version.into(),
}
}
pub fn rustup_toolchain(&self) -> String {
match &self.date {
Some(date) => format!("{}-{}", self.channel, date),
None => self.channel.clone(),
}
}
pub fn is_nightly(&self) -> bool {
self.channel == "nightly"
}
pub fn is_beta(&self) -> bool {
self.channel == "beta"
}
pub fn is_stable(&self) -> bool {
self.channel == "stable"
}
}
impl std::fmt::Display for ToolchainInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.rustup_toolchain())
}
}
pub fn wrap_command_with_toolchain(command: &str, toolchain: Option<&ToolchainInfo>) -> String {
match toolchain {
Some(tc) => format!("rustup run {} {}", tc.rustup_toolchain(), command),
None => command.to_string(),
}
}
use crate::types::ColorMode;
pub fn wrap_command_with_color(command: &str, color_mode: ColorMode) -> String {
match color_mode {
ColorMode::Always => {
format!(
"env -u NO_COLOR CARGO_TERM_COLOR=always RUST_LOG_STYLE=always CLICOLOR_FORCE=1 FORCE_COLOR=1 {}",
command
)
}
ColorMode::Auto => {
command.to_string()
}
ColorMode::Never => {
format!("env NO_COLOR=1 CARGO_TERM_COLOR=never {}", command)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_toolchain_info_new() {
let tc = ToolchainInfo::new(
"nightly",
Some("2024-01-15".to_string()),
"rustc 1.76.0-nightly",
);
assert_eq!(tc.channel, "nightly");
assert_eq!(tc.date, Some("2024-01-15".to_string()));
}
#[test]
fn test_rustup_toolchain_with_date() {
let tc = ToolchainInfo {
channel: "nightly".to_string(),
date: Some("2024-01-15".to_string()),
full_version: "".to_string(),
};
assert_eq!(tc.rustup_toolchain(), "nightly-2024-01-15");
}
#[test]
fn test_rustup_toolchain_without_date() {
let tc = ToolchainInfo {
channel: "stable".to_string(),
date: None,
full_version: "".to_string(),
};
assert_eq!(tc.rustup_toolchain(), "stable");
}
#[test]
fn test_toolchain_info_display() {
let tc = ToolchainInfo {
channel: "nightly".to_string(),
date: Some("2024-01-15".to_string()),
full_version: "".to_string(),
};
assert_eq!(tc.to_string(), "nightly-2024-01-15");
}
#[test]
fn test_is_nightly() {
let tc = ToolchainInfo::new("nightly", None, "");
assert!(tc.is_nightly());
assert!(!tc.is_stable());
assert!(!tc.is_beta());
}
#[test]
fn test_is_stable() {
let tc = ToolchainInfo::new("stable", None, "");
assert!(tc.is_stable());
assert!(!tc.is_nightly());
assert!(!tc.is_beta());
}
#[test]
fn test_serialization_roundtrip() {
let tc = ToolchainInfo {
channel: "nightly".to_string(),
date: Some("2024-01-15".to_string()),
full_version: "rustc 1.76.0-nightly (abc123 2024-01-15)".to_string(),
};
let json = serde_json::to_string(&tc).unwrap();
let parsed: ToolchainInfo = serde_json::from_str(&json).unwrap();
assert_eq!(tc, parsed);
}
#[test]
fn test_wrap_command_with_toolchain() {
let tc = ToolchainInfo::new("nightly", Some("2024-01-15".to_string()), "");
let wrapped = wrap_command_with_toolchain("cargo build", Some(&tc));
assert_eq!(wrapped, "rustup run nightly-2024-01-15 cargo build");
}
#[test]
fn test_wrap_command_no_toolchain() {
let wrapped = wrap_command_with_toolchain("cargo build", None);
assert_eq!(wrapped, "cargo build");
}
#[test]
fn test_wrap_command_stable_toolchain() {
let tc = ToolchainInfo::new("stable", None, "");
let wrapped = wrap_command_with_toolchain("cargo test", Some(&tc));
assert_eq!(wrapped, "rustup run stable cargo test");
}
#[test]
fn test_is_beta() {
let tc = ToolchainInfo::new("beta", None, "");
assert!(tc.is_beta());
assert!(!tc.is_stable());
assert!(!tc.is_nightly());
}
#[test]
fn test_wrap_command_beta_with_date() {
let tc = ToolchainInfo::new("beta", Some("2024-02-01".to_string()), "");
let wrapped = wrap_command_with_toolchain("cargo check", Some(&tc));
assert_eq!(wrapped, "rustup run beta-2024-02-01 cargo check");
}
#[test]
fn test_wrap_command_specific_version() {
let tc = ToolchainInfo::new("1.75.0", None, "rustc 1.75.0");
let wrapped = wrap_command_with_toolchain("cargo build --release", Some(&tc));
assert_eq!(wrapped, "rustup run 1.75.0 cargo build --release");
}
#[test]
fn test_wrap_command_with_complex_args() {
let tc = ToolchainInfo::new("nightly", None, "");
let cmd = "cargo build --features \"feature1,feature2\" --target x86_64-unknown-linux-gnu";
let wrapped = wrap_command_with_toolchain(cmd, Some(&tc));
assert_eq!(
wrapped,
"rustup run nightly cargo build --features \"feature1,feature2\" --target x86_64-unknown-linux-gnu"
);
}
#[test]
fn test_wrap_command_with_path_spaces() {
let tc = ToolchainInfo::new("stable", None, "");
let cmd = "cargo build --manifest-path \"/path/with spaces/Cargo.toml\"";
let wrapped = wrap_command_with_toolchain(cmd, Some(&tc));
assert_eq!(
wrapped,
"rustup run stable cargo build --manifest-path \"/path/with spaces/Cargo.toml\""
);
}
#[test]
fn test_wrap_command_empty_string() {
let tc = ToolchainInfo::new("stable", None, "");
let wrapped = wrap_command_with_toolchain("", Some(&tc));
assert_eq!(wrapped, "rustup run stable ");
}
#[test]
fn test_wrap_command_empty_string_no_toolchain() {
let wrapped = wrap_command_with_toolchain("", None);
assert_eq!(wrapped, "");
}
#[test]
fn test_wrap_command_clippy() {
let tc = ToolchainInfo::new("nightly", Some("2024-03-15".to_string()), "");
let wrapped = wrap_command_with_toolchain("cargo clippy -- -D warnings", Some(&tc));
assert_eq!(
wrapped,
"rustup run nightly-2024-03-15 cargo clippy -- -D warnings"
);
}
#[test]
fn test_wrap_command_rustfmt() {
let tc = ToolchainInfo::new("nightly", None, "");
let wrapped = wrap_command_with_toolchain("cargo fmt --check", Some(&tc));
assert_eq!(wrapped, "rustup run nightly cargo fmt --check");
}
#[test]
fn test_wrap_command_test_with_filter() {
let tc = ToolchainInfo::new("stable", None, "");
let wrapped = wrap_command_with_toolchain("cargo test test_name -- --nocapture", Some(&tc));
assert_eq!(
wrapped,
"rustup run stable cargo test test_name -- --nocapture"
);
}
#[test]
fn test_rustup_toolchain_beta_with_date() {
let tc = ToolchainInfo {
channel: "beta".to_string(),
date: Some("2024-02-01".to_string()),
full_version: "".to_string(),
};
assert_eq!(tc.rustup_toolchain(), "beta-2024-02-01");
}
#[test]
fn test_rustup_toolchain_specific_version() {
let tc = ToolchainInfo {
channel: "1.75.0".to_string(),
date: None,
full_version: "rustc 1.75.0".to_string(),
};
assert_eq!(tc.rustup_toolchain(), "1.75.0");
}
#[test]
fn test_display_stable() {
let tc = ToolchainInfo::new("stable", None, "");
assert_eq!(tc.to_string(), "stable");
}
#[test]
fn test_display_beta_with_date() {
let tc = ToolchainInfo::new("beta", Some("2024-02-01".to_string()), "");
assert_eq!(tc.to_string(), "beta-2024-02-01");
}
#[test]
fn test_serialization_stable() {
let tc = ToolchainInfo::new("stable", None, "rustc 1.75.0 (hash 2024-01-01)");
let json = serde_json::to_string(&tc).unwrap();
assert!(json.contains("\"channel\":\"stable\""));
assert!(json.contains("\"date\":null"));
let parsed: ToolchainInfo = serde_json::from_str(&json).unwrap();
assert_eq!(tc, parsed);
}
#[test]
fn test_serialization_beta_with_date() {
let tc = ToolchainInfo::new("beta", Some("2024-02-01".to_string()), "rustc 1.76.0-beta");
let json = serde_json::to_string(&tc).unwrap();
let parsed: ToolchainInfo = serde_json::from_str(&json).unwrap();
assert_eq!(tc, parsed);
assert_eq!(parsed.date, Some("2024-02-01".to_string()));
}
#[test]
fn test_toolchain_info_clone() {
let tc = ToolchainInfo::new("nightly", Some("2024-01-15".to_string()), "rustc 1.76.0");
let cloned = tc.clone();
assert_eq!(tc, cloned);
assert_eq!(tc.channel, cloned.channel);
assert_eq!(tc.date, cloned.date);
assert_eq!(tc.full_version, cloned.full_version);
}
#[test]
fn test_toolchain_info_equality() {
let tc1 = ToolchainInfo::new("nightly", Some("2024-01-15".to_string()), "rustc 1.76.0");
let tc2 = ToolchainInfo::new("nightly", Some("2024-01-15".to_string()), "rustc 1.76.0");
let tc3 = ToolchainInfo::new("nightly", Some("2024-01-16".to_string()), "rustc 1.76.0");
assert_eq!(tc1, tc2);
assert_ne!(tc1, tc3);
}
#[test]
fn test_channel_detection_methods_consistency() {
for channel in &["stable", "beta", "nightly", "1.75.0", "custom"] {
let tc = ToolchainInfo::new(*channel, None, "");
let count = [tc.is_stable(), tc.is_beta(), tc.is_nightly()]
.iter()
.filter(|&&b| b)
.count();
assert!(count <= 1, "Multiple channel types for: {}", channel);
}
}
#[test]
fn test_wrap_command_with_color_always() {
let wrapped = wrap_command_with_color("cargo test", ColorMode::Always);
assert!(wrapped.contains("CARGO_TERM_COLOR=always"));
assert!(wrapped.contains("RUST_LOG_STYLE=always"));
assert!(wrapped.contains("CLICOLOR_FORCE=1"));
assert!(wrapped.contains("FORCE_COLOR=1"));
assert!(wrapped.contains("env -u NO_COLOR"));
assert!(wrapped.contains("cargo test"));
}
#[test]
fn test_wrap_command_with_color_auto() {
let wrapped = wrap_command_with_color("cargo test", ColorMode::Auto);
assert_eq!(wrapped, "cargo test");
}
#[test]
fn test_wrap_command_with_color_never() {
let wrapped = wrap_command_with_color("cargo test", ColorMode::Never);
assert!(wrapped.starts_with("env "));
assert!(wrapped.contains("NO_COLOR=1"));
assert!(wrapped.contains("CARGO_TERM_COLOR=never"));
assert!(wrapped.contains("cargo test"));
}
#[test]
fn test_wrap_command_with_color_never_is_timeout_safe() {
let wrapped = wrap_command_with_color("sh -c 'exit 43'", ColorMode::Never);
let status = std::process::Command::new("sh")
.arg("-c")
.arg(format!(
"timeout --foreground --preserve-status 5 {}",
wrapped
))
.status()
.expect("timeout-wrapped color-disabled command should execute");
assert_eq!(
status.code(),
Some(43),
"timeout must execute env, not an environment assignment argv"
);
}
#[test]
fn test_wrap_command_with_color_preserves_complex_command() {
let cmd = "cargo test --release -- --test-threads=4";
let wrapped = wrap_command_with_color(cmd, ColorMode::Always);
assert!(wrapped.contains(cmd));
}
#[test]
fn test_wrap_command_with_color_bun_commands() {
let wrapped = wrap_command_with_color("bun test", ColorMode::Always);
assert!(wrapped.contains("FORCE_COLOR=1"));
assert!(wrapped.contains("bun test"));
}
#[test]
fn test_color_mode_display() {
assert_eq!(ColorMode::Always.to_string(), "always");
assert_eq!(ColorMode::Auto.to_string(), "auto");
assert_eq!(ColorMode::Never.to_string(), "never");
}
#[test]
fn test_color_mode_from_str() {
assert_eq!("always".parse::<ColorMode>(), Ok(ColorMode::Always));
assert_eq!("force".parse::<ColorMode>(), Ok(ColorMode::Always));
assert_eq!("auto".parse::<ColorMode>(), Ok(ColorMode::Auto));
assert_eq!("never".parse::<ColorMode>(), Ok(ColorMode::Never));
assert_eq!("no".parse::<ColorMode>(), Ok(ColorMode::Never));
assert!("invalid".parse::<ColorMode>().is_err());
}
#[test]
fn test_color_mode_default() {
assert_eq!(ColorMode::default(), ColorMode::Always);
}
}