use crate::config::{Config, OutputFormat};
use crate::VERSION;
use std::process::{Command, Stdio};
const RELEASES_URL: &str =
"https://api.github.com/repos/QubeTX/qube-machine-report/releases/latest";
#[cfg(not(windows))]
const SHELL_INSTALLER: &str =
"https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-installer.sh";
#[cfg(windows)]
const PS_INSTALLER: &str =
"https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-installer.ps1";
#[cfg(windows)]
const MSI_GLOBAL_URL: &str = "https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-x86_64-pc-windows-msvc.msi";
#[cfg(windows)]
const MSI_CORPORATE_URL: &str = "https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-x86_64-pc-windows-msvc-corporate.msi";
#[cfg(windows)]
const EXE_GLOBAL_URL: &str = "https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-x86_64-pc-windows-msvc-setup.exe";
#[cfg(windows)]
const EXE_CORPORATE_URL: &str = "https://github.com/QubeTX/qube-machine-report/releases/latest/download/tr300-x86_64-pc-windows-msvc-corporate-setup.exe";
const CRATE_NAME: &str = "tr300";
const MANUAL_INSTALL_URL: &str = "https://github.com/QubeTX/qube-machine-report#installation";
#[cfg_attr(not(windows), allow(dead_code))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UpdateStrategy {
Cargo,
InstallerCurl,
InstallerWget,
InstallerPowerShell,
InstallerPwsh,
MsiGlobal,
MsiCorporate,
ExeGlobal,
ExeCorporate,
}
impl UpdateStrategy {
fn label(self) -> &'static str {
match self {
UpdateStrategy::Cargo => "cargo install",
UpdateStrategy::InstallerCurl => "curl shell installer",
UpdateStrategy::InstallerWget => "wget shell installer",
UpdateStrategy::InstallerPowerShell => "PowerShell installer",
UpdateStrategy::InstallerPwsh => "pwsh installer",
UpdateStrategy::MsiGlobal => "Global MSI installer",
UpdateStrategy::MsiCorporate => "Corporate MSI installer",
UpdateStrategy::ExeGlobal => "Global EXE installer",
UpdateStrategy::ExeCorporate => "Corporate EXE installer",
}
}
fn json_id(self) -> &'static str {
match self {
UpdateStrategy::Cargo => "cargo",
UpdateStrategy::InstallerCurl => "installer_curl",
UpdateStrategy::InstallerWget => "installer_wget",
UpdateStrategy::InstallerPowerShell => "installer_powershell",
UpdateStrategy::InstallerPwsh => "installer_pwsh",
UpdateStrategy::MsiGlobal => "msi_global",
UpdateStrategy::MsiCorporate => "msi_corporate",
UpdateStrategy::ExeGlobal => "exe_global",
UpdateStrategy::ExeCorporate => "exe_corporate",
}
}
fn json_method(self) -> &'static str {
if matches!(self, UpdateStrategy::Cargo) {
"cargo"
} else {
"installer"
}
}
}
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallOrigin {
MsiGlobal,
MsiCorporate,
ExeGlobal,
ExeCorporate,
CargoOrInstaller,
Unknown,
}
#[cfg(windows)]
impl InstallOrigin {
fn json_id(self) -> &'static str {
match self {
InstallOrigin::MsiGlobal => "msi-global",
InstallOrigin::MsiCorporate => "msi-corporate",
InstallOrigin::ExeGlobal => "exe-global",
InstallOrigin::ExeCorporate => "exe-corporate",
InstallOrigin::CargoOrInstaller => "cargo-or-installer",
InstallOrigin::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TargetOs {
Unix,
Windows,
}
#[derive(Debug)]
enum StrategyError {
Preflight(String),
Runtime(String),
}
#[derive(Debug)]
enum AttemptKind {
Skipped,
Failed,
}
#[derive(Debug)]
struct AttemptRecord {
strategy: UpdateStrategy,
kind: AttemptKind,
message: String,
}
#[derive(Debug)]
struct UpdateFailure {
attempts: Vec<AttemptRecord>,
}
fn green(text: &str, config: &Config) -> String {
if config.use_colors {
format!("\x1b[32m{}\x1b[0m", text)
} else {
text.to_string()
}
}
fn red(text: &str, config: &Config) -> String {
if config.use_colors {
format!("\x1b[31m{}\x1b[0m", text)
} else {
text.to_string()
}
}
fn cyan(text: &str, config: &Config) -> String {
if config.use_colors {
format!("\x1b[36m{}\x1b[0m", text)
} else {
text.to_string()
}
}
fn success_icon(config: &Config) -> &'static str {
if config.use_unicode {
"\u{2714}"
} else {
"[OK]"
}
}
fn fail_icon(config: &Config) -> &'static str {
if config.use_unicode {
"\u{2718}"
} else {
"[FAIL]"
}
}
pub fn run(config: &Config) -> i32 {
if config.format == OutputFormat::Json {
return run_json();
}
println!();
println!(" {} Checking for updates...", cyan("*", config));
let latest = match fetch_latest_version() {
Ok(v) => v,
Err(e) => {
println!(
" {} {}",
red(fail_icon(config), config),
red(&format!("Failed to check for updates: {}", e), config),
);
return 2;
}
};
let current = VERSION.to_string();
if !is_newer(¤t, &latest) {
println!(
" {} {}",
green(success_icon(config), config),
green(
&format!("Already on the latest version (v{})", current),
config
),
);
return 0;
}
println!(
" {} Update available: v{} {} v{}",
cyan("*", config),
current,
cyan("->", config),
latest,
);
let strategies = build_strategy_list();
if let Some(strategy) = strategies.first() {
println!(
" {} Updating via {}...",
cyan("*", config),
strategy.label()
);
}
println!();
match execute_update(&strategies) {
Ok(used) => {
println!();
println!(
" {} {}",
green(success_icon(config), config),
green(
&format!("Updated to v{} via {}", latest, used.label()),
config
),
);
0
}
Err(failure) => {
println!();
println!(
" {} {}",
red(fail_icon(config), config),
red("Update failed. Strategies attempted:", config),
);
for record in &failure.attempts {
let kind = match record.kind {
AttemptKind::Skipped => "skipped",
AttemptKind::Failed => "failed",
};
println!(
" · {} — {}: {}",
record.strategy.label(),
kind,
record.message
);
}
println!();
println!(" To update manually, see: {}", MANUAL_INSTALL_URL);
2
}
}
}
fn run_json() -> i32 {
let latest = match fetch_latest_version() {
Ok(v) => v,
Err(e) => {
let mut payload = serde_json::json!({
"action": "update",
"success": false,
"message": format!("Failed to check for updates: {}", e),
"current_version": VERSION,
});
inject_install_origin(&mut payload);
println!("{}", payload);
return 2;
}
};
let current = VERSION.to_string();
let update_available = is_newer(¤t, &latest);
if !update_available {
let mut payload = serde_json::json!({
"action": "update",
"success": true,
"message": "Already on the latest version",
"current_version": current,
"latest_version": latest,
"update_available": false,
});
inject_install_origin(&mut payload);
println!("{}", payload);
return 0;
}
let strategies = build_strategy_list();
match execute_update(&strategies) {
Ok(used) => {
let mut payload = serde_json::json!({
"action": "update",
"success": true,
"message": format!("Updated from v{} to v{}", current, latest),
"current_version": current,
"latest_version": latest,
"update_available": true,
"method": used.json_method(),
"strategy": used.json_id(),
});
inject_install_origin(&mut payload);
println!("{}", payload);
0
}
Err(failure) => {
let attempts: Vec<serde_json::Value> = failure
.attempts
.iter()
.map(|record| {
serde_json::json!({
"strategy": record.strategy.json_id(),
"result": match record.kind {
AttemptKind::Skipped => "skipped",
AttemptKind::Failed => "failed",
},
"message": record.message,
})
})
.collect();
let mut payload = serde_json::json!({
"action": "update",
"success": false,
"message": "Update failed; see attempts",
"current_version": current,
"latest_version": latest,
"update_available": true,
"attempts": attempts,
});
inject_install_origin(&mut payload);
println!("{}", payload);
2
}
}
}
#[cfg(windows)]
fn inject_install_origin(payload: &mut serde_json::Value) {
if let Some(obj) = payload.as_object_mut() {
obj.insert(
"install_origin".to_string(),
serde_json::Value::String(detect_install_origin().json_id().to_string()),
);
}
}
#[cfg(not(windows))]
fn inject_install_origin(_payload: &mut serde_json::Value) {
}
fn fetch_latest_version() -> Result<String, String> {
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(15))
.build();
let resp = agent
.get(RELEASES_URL)
.set("User-Agent", &format!("tr300/{}", VERSION))
.set("Accept", "application/vnd.github+json")
.call()
.map_err(|e| format!("Request failed: {}", e))?;
let body: serde_json::Value = resp
.into_json()
.map_err(|e| format!("Failed to parse response: {}", e))?;
let tag = body["tag_name"]
.as_str()
.ok_or("Missing tag_name in response")?;
Ok(tag.strip_prefix('v').unwrap_or(tag).to_string())
}
fn is_newer(current: &str, latest: &str) -> bool {
let parse =
|v: &str| -> Vec<u64> { v.split('.').filter_map(|s| s.parse::<u64>().ok()).collect() };
let c = parse(current);
let l = parse(latest);
let len = c.len().max(l.len());
for i in 0..len {
let cv = c.get(i).copied().unwrap_or(0);
let lv = l.get(i).copied().unwrap_or(0);
if lv > cv {
return true;
}
if lv < cv {
return false;
}
}
false
}
fn order_strategies(cargo_invokable: bool, os: TargetOs) -> Vec<UpdateStrategy> {
let mut strategies = Vec::new();
if cargo_invokable {
strategies.push(UpdateStrategy::Cargo);
}
match os {
TargetOs::Unix => {
strategies.push(UpdateStrategy::InstallerCurl);
strategies.push(UpdateStrategy::InstallerWget);
}
TargetOs::Windows => {
strategies.push(UpdateStrategy::InstallerPowerShell);
strategies.push(UpdateStrategy::InstallerPwsh);
}
}
strategies
}
fn current_target_os() -> TargetOs {
if cfg!(windows) {
TargetOs::Windows
} else {
TargetOs::Unix
}
}
fn cargo_invokable() -> bool {
tool_exists("cargo")
}
fn tool_exists(tool: &str) -> bool {
Command::new(tool)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn build_strategy_list() -> Vec<UpdateStrategy> {
#[cfg(windows)]
{
match detect_install_origin() {
InstallOrigin::MsiGlobal => return vec![UpdateStrategy::MsiGlobal],
InstallOrigin::MsiCorporate => return vec![UpdateStrategy::MsiCorporate],
InstallOrigin::ExeGlobal => return vec![UpdateStrategy::ExeGlobal],
InstallOrigin::ExeCorporate => return vec![UpdateStrategy::ExeCorporate],
InstallOrigin::CargoOrInstaller | InstallOrigin::Unknown => {
}
}
}
order_strategies(cargo_invokable(), current_target_os())
}
fn rustup_update_stable_best_effort() {
if !tool_exists("rustup") {
return;
}
println!("Updating Rust toolchain (rustup update stable)…");
let _ = Command::new("rustup").args(["update", "stable"]).status();
}
fn execute_update(strategies: &[UpdateStrategy]) -> Result<UpdateStrategy, UpdateFailure> {
let mut attempts = Vec::new();
for &strategy in strategies {
match try_strategy(strategy) {
Ok(()) => return Ok(strategy),
Err(StrategyError::Preflight(message)) => {
eprintln!(" · skipped {}: {}", strategy.label(), message);
attempts.push(AttemptRecord {
strategy,
kind: AttemptKind::Skipped,
message,
});
}
Err(StrategyError::Runtime(message)) => {
eprintln!(" · {} failed: {}", strategy.label(), message);
attempts.push(AttemptRecord {
strategy,
kind: AttemptKind::Failed,
message,
});
}
}
}
Err(UpdateFailure { attempts })
}
fn try_strategy(strategy: UpdateStrategy) -> Result<(), StrategyError> {
match strategy {
UpdateStrategy::Cargo => {
rustup_update_stable_best_effort();
run_command_status("cargo", &["install", CRATE_NAME, "--force"])
}
UpdateStrategy::InstallerCurl => try_installer_curl(),
UpdateStrategy::InstallerWget => try_installer_wget(),
UpdateStrategy::InstallerPowerShell => try_installer_powershell("powershell"),
UpdateStrategy::InstallerPwsh => try_installer_powershell("pwsh"),
UpdateStrategy::MsiGlobal => try_msi_install(msi_global_url()),
UpdateStrategy::MsiCorporate => try_msi_install(msi_corporate_url()),
UpdateStrategy::ExeGlobal => try_exe_install(exe_global_url()),
UpdateStrategy::ExeCorporate => try_exe_install(exe_corporate_url()),
}
}
#[cfg(windows)]
fn msi_global_url() -> &'static str {
MSI_GLOBAL_URL
}
#[cfg(windows)]
fn msi_corporate_url() -> &'static str {
MSI_CORPORATE_URL
}
#[cfg(windows)]
fn exe_global_url() -> &'static str {
EXE_GLOBAL_URL
}
#[cfg(windows)]
fn exe_corporate_url() -> &'static str {
EXE_CORPORATE_URL
}
#[cfg(not(windows))]
fn msi_global_url() -> &'static str {
""
}
#[cfg(not(windows))]
fn msi_corporate_url() -> &'static str {
""
}
#[cfg(not(windows))]
fn exe_global_url() -> &'static str {
""
}
#[cfg(not(windows))]
fn exe_corporate_url() -> &'static str {
""
}
fn run_command_status(launcher: &str, args: &[&str]) -> Result<(), StrategyError> {
match Command::new(launcher).args(args).status() {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(StrategyError::Preflight(
format!("{} not on PATH", launcher),
)),
Err(e) => Err(StrategyError::Preflight(format!(
"Failed to spawn {}: {}",
launcher, e
))),
Ok(status) if status.success() => Ok(()),
Ok(status) => Err(StrategyError::Runtime(format!(
"{} exited with code {}",
launcher,
status.code().unwrap_or(-1)
))),
}
}
#[cfg(unix)]
fn try_installer_curl() -> Result<(), StrategyError> {
if !tool_exists("curl") {
return Err(StrategyError::Preflight("curl not on PATH".into()));
}
if !tool_exists("bash") {
return Err(StrategyError::Preflight("bash not on PATH".into()));
}
let script = format!(
"set -euo pipefail; curl --proto '=https' --tlsv1.2 -fLsS {} | sh",
SHELL_INSTALLER
);
run_command_status("bash", &["-c", &script])
}
#[cfg(not(unix))]
fn try_installer_curl() -> Result<(), StrategyError> {
Err(StrategyError::Preflight(
"curl installer is Unix-only".into(),
))
}
#[cfg(unix)]
fn try_installer_wget() -> Result<(), StrategyError> {
if !tool_exists("wget") {
return Err(StrategyError::Preflight("wget not on PATH".into()));
}
if !tool_exists("bash") {
return Err(StrategyError::Preflight("bash not on PATH".into()));
}
let script = format!("set -euo pipefail; wget -qO- {} | sh", SHELL_INSTALLER);
run_command_status("bash", &["-c", &script])
}
#[cfg(not(unix))]
fn try_installer_wget() -> Result<(), StrategyError> {
Err(StrategyError::Preflight(
"wget installer is Unix-only".into(),
))
}
#[cfg(windows)]
fn try_installer_powershell(launcher: &str) -> Result<(), StrategyError> {
let script = format!("$ErrorActionPreference='Stop'; irm {} | iex", PS_INSTALLER);
run_command_status(
launcher,
&[
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
&script,
],
)
}
#[cfg(not(windows))]
fn try_installer_powershell(_launcher: &str) -> Result<(), StrategyError> {
Err(StrategyError::Preflight(
"PowerShell installer is Windows-only".into(),
))
}
#[cfg(windows)]
fn download_to_file(url: &str, path: &std::path::Path) -> Result<(), String> {
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(120))
.build();
let resp = agent
.get(url)
.set("User-Agent", &format!("tr300/{}", VERSION))
.call()
.map_err(|e| format!("Request failed: {}", e))?;
let mut file = std::fs::File::create(path)
.map_err(|e| format!("Failed to create temp file {}: {}", path.display(), e))?;
let mut reader = resp.into_reader();
std::io::copy(&mut reader, &mut file)
.map_err(|e| format!("Failed to write temp file: {}", e))?;
Ok(())
}
#[cfg(windows)]
fn try_msi_install(url: &str) -> Result<(), StrategyError> {
let temp_path = std::env::temp_dir().join(format!("tr300-update-{}.msi", VERSION));
println!(" Downloading MSI installer...");
download_to_file(url, &temp_path)
.map_err(|e| StrategyError::Runtime(format!("Download failed: {}", e)))?;
println!(" Launching Windows Installer...");
let status = Command::new("msiexec")
.args(["/i", &temp_path.to_string_lossy(), "/passive", "/norestart"])
.status()
.map_err(|e| StrategyError::Preflight(format!("Failed to spawn msiexec: {}", e)))?;
if status.success() {
Ok(())
} else {
Err(StrategyError::Runtime(format!(
"msiexec exited with code {} (likely user cancel, UAC denied, or install error)",
status.code().unwrap_or(-1)
)))
}
}
#[cfg(windows)]
fn try_exe_install(url: &str) -> Result<(), StrategyError> {
let temp_path = std::env::temp_dir().join(format!("tr300-update-{}-setup.exe", VERSION));
println!(" Downloading EXE installer...");
download_to_file(url, &temp_path)
.map_err(|e| StrategyError::Runtime(format!("Download failed: {}", e)))?;
println!(" Launching Inno Setup installer...");
let status = Command::new(&temp_path)
.args(["/SILENT", "/SUPPRESSMSGBOXES", "/NORESTART"])
.status()
.map_err(|e| StrategyError::Preflight(format!("Failed to spawn EXE installer: {}", e)))?;
if status.success() {
Ok(())
} else {
Err(StrategyError::Runtime(format!(
"EXE installer exited with code {} (likely user cancel, UAC denied, or install error)",
status.code().unwrap_or(-1)
)))
}
}
#[cfg(not(windows))]
fn try_msi_install(_url: &str) -> Result<(), StrategyError> {
Err(StrategyError::Preflight(
"MSI installer is Windows-only".into(),
))
}
#[cfg(not(windows))]
fn try_exe_install(_url: &str) -> Result<(), StrategyError> {
Err(StrategyError::Preflight(
"EXE installer is Windows-only".into(),
))
}
#[cfg(windows)]
fn read_install_source_marker() -> Option<InstallOrigin> {
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let key = hkcu.open_subkey("Software\\TR300").ok()?;
let value: String = key.get_value("InstallSource").ok()?;
match value.as_str() {
"msi-global" => Some(InstallOrigin::MsiGlobal),
"msi-corporate" => Some(InstallOrigin::MsiCorporate),
"exe-global" => Some(InstallOrigin::ExeGlobal),
"exe-corporate" => Some(InstallOrigin::ExeCorporate),
_ => None,
}
}
#[cfg(windows)]
fn detect_install_origin() -> InstallOrigin {
if let Some(origin) = read_install_source_marker() {
return origin;
}
let Ok(exe) = std::env::current_exe() else {
return InstallOrigin::Unknown;
};
classify_install_path(&exe.to_string_lossy())
}
#[cfg(windows)]
fn classify_install_path(exe_path: &str) -> InstallOrigin {
let lower = exe_path.to_lowercase();
if lower.contains("\\program files\\tr300\\") {
InstallOrigin::MsiGlobal
} else if lower.contains("\\appdata\\local\\programs\\tr300\\") {
InstallOrigin::MsiCorporate
} else if lower.contains("\\.cargo\\bin\\") {
InstallOrigin::CargoOrInstaller
} else {
InstallOrigin::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unix_with_cargo_orders_cargo_first() {
assert_eq!(
order_strategies(true, TargetOs::Unix),
vec![
UpdateStrategy::Cargo,
UpdateStrategy::InstallerCurl,
UpdateStrategy::InstallerWget,
]
);
}
#[test]
fn unix_without_cargo_prunes_cargo() {
assert_eq!(
order_strategies(false, TargetOs::Unix),
vec![UpdateStrategy::InstallerCurl, UpdateStrategy::InstallerWget,]
);
}
#[test]
fn windows_with_cargo_orders_cargo_first() {
assert_eq!(
order_strategies(true, TargetOs::Windows),
vec![
UpdateStrategy::Cargo,
UpdateStrategy::InstallerPowerShell,
UpdateStrategy::InstallerPwsh,
]
);
}
#[test]
fn windows_without_cargo_prunes_cargo() {
assert_eq!(
order_strategies(false, TargetOs::Windows),
vec![
UpdateStrategy::InstallerPowerShell,
UpdateStrategy::InstallerPwsh,
]
);
}
#[test]
fn json_method_maps_to_legacy_taxonomy() {
assert_eq!(UpdateStrategy::Cargo.json_method(), "cargo");
assert_eq!(UpdateStrategy::InstallerCurl.json_method(), "installer");
assert_eq!(UpdateStrategy::InstallerWget.json_method(), "installer");
assert_eq!(
UpdateStrategy::InstallerPowerShell.json_method(),
"installer"
);
assert_eq!(UpdateStrategy::InstallerPwsh.json_method(), "installer");
assert_eq!(UpdateStrategy::MsiGlobal.json_method(), "installer");
assert_eq!(UpdateStrategy::MsiCorporate.json_method(), "installer");
assert_eq!(UpdateStrategy::ExeGlobal.json_method(), "installer");
assert_eq!(UpdateStrategy::ExeCorporate.json_method(), "installer");
}
#[test]
fn new_strategies_have_stable_json_ids() {
assert_eq!(UpdateStrategy::MsiGlobal.json_id(), "msi_global");
assert_eq!(UpdateStrategy::MsiCorporate.json_id(), "msi_corporate");
assert_eq!(UpdateStrategy::ExeGlobal.json_id(), "exe_global");
assert_eq!(UpdateStrategy::ExeCorporate.json_id(), "exe_corporate");
}
#[test]
fn new_strategies_have_distinct_labels() {
let labels = [
UpdateStrategy::MsiGlobal.label(),
UpdateStrategy::MsiCorporate.label(),
UpdateStrategy::ExeGlobal.label(),
UpdateStrategy::ExeCorporate.label(),
UpdateStrategy::Cargo.label(),
UpdateStrategy::InstallerPowerShell.label(),
UpdateStrategy::InstallerPwsh.label(),
UpdateStrategy::InstallerCurl.label(),
UpdateStrategy::InstallerWget.label(),
];
let unique: std::collections::HashSet<_> = labels.iter().collect();
assert_eq!(
unique.len(),
labels.len(),
"all strategy labels must be unique"
);
}
#[cfg(windows)]
#[test]
fn install_origin_classify_program_files_is_msi_global() {
assert_eq!(
classify_install_path(r"C:\Program Files\tr300\bin\tr300.exe"),
InstallOrigin::MsiGlobal,
);
assert_eq!(
classify_install_path(r"c:\PROGRAM FILES\tr300\BIN\tr300.exe"),
InstallOrigin::MsiGlobal,
);
}
#[cfg(windows)]
#[test]
fn install_origin_classify_localappdata_is_msi_corporate() {
assert_eq!(
classify_install_path(r"C:\Users\alice\AppData\Local\Programs\tr300\bin\tr300.exe"),
InstallOrigin::MsiCorporate,
);
}
#[cfg(windows)]
#[test]
fn install_origin_classify_cargo_bin_is_cargo_or_installer() {
assert_eq!(
classify_install_path(r"C:\Users\alice\.cargo\bin\tr300.exe"),
InstallOrigin::CargoOrInstaller,
);
}
#[cfg(windows)]
#[test]
fn install_origin_classify_random_path_is_unknown() {
assert_eq!(
classify_install_path(r"D:\portable\tr300\tr300.exe"),
InstallOrigin::Unknown,
);
assert_eq!(
classify_install_path(r"C:\Users\alice\Downloads\tr300.exe"),
InstallOrigin::Unknown,
);
}
#[cfg(windows)]
#[test]
fn install_origin_json_ids_are_kebab_case() {
assert_eq!(InstallOrigin::MsiGlobal.json_id(), "msi-global");
assert_eq!(InstallOrigin::MsiCorporate.json_id(), "msi-corporate");
assert_eq!(InstallOrigin::ExeGlobal.json_id(), "exe-global");
assert_eq!(InstallOrigin::ExeCorporate.json_id(), "exe-corporate");
assert_eq!(
InstallOrigin::CargoOrInstaller.json_id(),
"cargo-or-installer"
);
assert_eq!(InstallOrigin::Unknown.json_id(), "unknown");
}
#[test]
fn test_is_newer_basic() {
assert!(is_newer("1.0.0", "2.0.0"));
assert!(is_newer("1.0.0", "1.1.0"));
assert!(is_newer("1.0.0", "1.0.1"));
assert!(!is_newer("2.0.0", "1.0.0"));
assert!(!is_newer("1.0.0", "1.0.0"));
}
#[test]
fn test_is_newer_different_lengths() {
assert!(is_newer("1.0", "1.0.1"));
assert!(!is_newer("1.0.1", "1.0"));
}
#[test]
fn test_is_newer_major_versions() {
assert!(is_newer("3.8.0", "4.0.0"));
assert!(!is_newer("4.0.0", "3.99.99"));
}
}