diff --git a/e2e/cli/test_use_latest b/e2e/cli/test_use_latest index b9d6f01ae1..127ad54b2d 100644 --- a/e2e/cli/test_use_latest +++ b/e2e/cli/test_use_latest @@ -16,11 +16,11 @@ assert "mise tool tiny --requested" "latest" touch mise.lock assert "mise use dummy@1 tiny@1" -assert "cat mise.lock" '[tools.dummy] +assert "cat mise.lock" '[[tools.dummy]] version = "1.0.0" backend = "asdf:dummy" -[tools.tiny] +[[tools.tiny]] version = "1.0.0" backend = "asdf:tiny"' assert "mise tool dummy --requested" "1" @@ -29,11 +29,11 @@ assert "mise tool dummy --active" "1.0.0" assert "mise tool tiny --active" "1.0.0" assert "mise use dummy@latest" -assert "cat mise.lock" '[tools.dummy] +assert "cat mise.lock" '[[tools.dummy]] version = "2.0.0" backend = "asdf:dummy" -[tools.tiny] +[[tools.tiny]] version = "1.0.0" backend = "asdf:tiny"' assert "mise tool dummy --requested" "latest" diff --git a/e2e/lockfile/test_lockfile_use b/e2e/lockfile/test_lockfile_use index 2f920dcd39..b088f30d82 100644 --- a/e2e/lockfile/test_lockfile_use +++ b/e2e/lockfile/test_lockfile_use @@ -11,26 +11,26 @@ assert "mise config get -f mise.toml tools.tiny" "1" assert "mise where tiny" "$MISE_DATA_DIR/installs/tiny/1.0.0" assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.0.0" -assert "cat mise.lock" '[tools.tiny] +assert "cat mise.lock" '[[tools.tiny]] version = "1.0.0" backend = "asdf:tiny"' assert "mise use tiny@1" -assert "cat mise.lock" '[tools.tiny] +assert "cat mise.lock" '[[tools.tiny]] version = "1.0.0" backend = "asdf:tiny"' assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.0.0" assert "mise up tiny" -assert "cat mise.lock" '[tools.tiny] +assert "cat mise.lock" '[[tools.tiny]] version = "1.1.0" backend = "asdf:tiny"' assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.1.0" assert "mise up tiny --bump" -assert "cat mise.lock" '[tools.tiny] +assert "cat mise.lock" '[[tools.tiny]] version = "3.1.0" backend = "asdf:tiny"' assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "3" diff --git a/mise.lock b/mise.lock index b9814cd167..3f5039b9ec 100644 --- a/mise.lock +++ b/mise.lock @@ -1,4 +1,4 @@ -[tools.actionlint] +[[tools.actionlint]] version = "1.7.7" backend = "aqua:rhysd/actionlint" @@ -12,7 +12,7 @@ checksum = "sha256:2693315b9093aeacb4ebd91a993fea54fc215057bf0da2659056b4bc03387 size = 1962532 url = "https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_darwin_arm64.tar.gz" -[tools.age] +[[tools.age]] version = "1.2.1" backend = "aqua:FiloSottile/age" @@ -26,7 +26,7 @@ checksum = "blake3:5c7e92baa305e64738b31e6ed9725d6cecdb8915af6e1b6c59bb4d7890efa size = 4758557 url = "https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-darwin-arm64.tar.gz" -[tools.bun] +[[tools.bun]] version = "1.2.21" backend = "core:bun" @@ -38,7 +38,7 @@ size = 39128668 checksum = "blake3:b5824ab4bf0afba1d27d55d4cbec1696c3d1070f6982cbf6b4fa0489892ec931" size = 22056420 -[tools.cargo-binstall] +[[tools.cargo-binstall]] version = "1.15.3" backend = "aqua:cargo-bins/cargo-binstall" @@ -52,31 +52,31 @@ checksum = "blake3:8fb84239a9f54c0107faa2cf6ac12c658572b943356e1291c4cf39f34af6c size = 6004746 url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.15.3/cargo-binstall-aarch64-apple-darwin.zip" -[tools."cargo:cargo-edit"] +[[tools."cargo:cargo-edit"]] version = "0.13.7" backend = "cargo:cargo-edit" -[tools."cargo:cargo-insta"] +[[tools."cargo:cargo-insta"]] version = "1.43.2" backend = "cargo:cargo-insta" -[tools."cargo:cargo-release"] +[[tools."cargo:cargo-release"]] version = "0.25.18" backend = "cargo:cargo-release" -[tools."cargo:git-cliff"] +[[tools."cargo:git-cliff"]] version = "2.10.0" backend = "cargo:git-cliff" -[tools."cargo:toml-cli"] +[[tools."cargo:toml-cli"]] version = "0.2.3" backend = "cargo:toml-cli" -[tools."cargo:usage-cli"] +[[tools."cargo:usage-cli"]] version = "2.2.2" backend = "cargo:usage-cli" -[tools.cosign] +[[tools.cosign]] version = "2.5.3" backend = "aqua:sigstore/cosign" @@ -90,7 +90,7 @@ checksum = "sha256:86e0cad94d0da4c0dab5e26672ede71447a08a0f0d8495b9381c117df27d7 size = 152385074 url = "https://github.com/sigstore/cosign/releases/download/v2.5.3/cosign-darwin-arm64" -[tools.gh] +[[tools.gh]] version = "2.62.0" backend = "aqua:cli/cli" @@ -104,7 +104,7 @@ checksum = "sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92 size = 12793347 url = "https://github.com/cli/cli/releases/download/v2.62.0/gh_2.62.0_macOS_arm64.zip" -[tools.hk] +[[tools.hk]] version = "1.10.7" backend = "aqua:jdx/hk" @@ -118,7 +118,7 @@ checksum = "blake3:2990ec4745178df124c26dcef643a14d362fe8ca24759feba4bdf61e71bf4 size = 5928348 url = "https://github.com/jdx/hk/releases/download/v1.10.7/hk-aarch64-apple-darwin.tar.gz" -[tools.jq] +[[tools.jq]] version = "1.8.1" backend = "aqua:jqlang/jq" @@ -132,19 +132,19 @@ checksum = "sha256:a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263 size = 841408 url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64" -[tools."npm:markdownlint-cli"] +[[tools."npm:markdownlint-cli"]] version = "0.45.0" backend = "npm:markdownlint-cli" -[tools."npm:prettier"] +[[tools."npm:prettier"]] version = "3.6.2" backend = "npm:prettier" -[tools."pipx:toml-sort"] +[[tools."pipx:toml-sort"]] version = "0.24.2" backend = "pipx:toml-sort" -[tools.pkl] +[[tools.pkl]] version = "0.29.1" backend = "aqua:apple/pkl" @@ -158,7 +158,7 @@ checksum = "blake3:5160399295c75f15f6b2b1e2925945a5d42a66793aa4eec821ab2dc60a5ae size = 104874656 url = "https://github.com/apple/pkl/releases/download/0.29.1/pkl-macos-aarch64" -[tools.pre-commit] +[[tools.pre-commit]] version = "4.3.0" backend = "aqua:pre-commit/pre-commit" @@ -172,7 +172,7 @@ checksum = "sha256:f1d50b97e9ca9167aceb76c14e90b07cde8b6789bc199d5005cfd817a7188 size = 8268268 url = "https://github.com/pre-commit/pre-commit/releases/download/v4.3.0/pre-commit-4.3.0.pyz" -[tools.ripgrep] +[[tools.ripgrep]] version = "14.1.1" backend = "aqua:BurntSushi/ripgrep" @@ -186,7 +186,7 @@ checksum = "sha256:24ad76777745fbff131c8fbc466742b011f925bfa4fffa2ded6def23b5b93 size = 1787248 url = "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz" -[tools.shellcheck] +[[tools.shellcheck]] version = "0.11.0" backend = "aqua:koalaman/shellcheck" @@ -195,7 +195,7 @@ checksum = "blake3:0ad9524f3f16ad030f8350e7b970c62c24aeff3d813f2565665aecc5a8b79 size = 2559196 url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" -[tools.shfmt] +[[tools.shfmt]] version = "3.12.0" backend = "aqua:mvdan/sh" @@ -204,7 +204,7 @@ checksum = "sha256:d9fbb2a9c33d13f47e7618cf362a914d029d02a6df124064fff04fd688a74 size = 2916536 url = "https://github.com/mvdan/sh/releases/download/v3.12.0/shfmt_v3.12.0_linux_amd64" -[tools.slsa-verifier] +[[tools.slsa-verifier]] version = "2.7.1" backend = "ubi:slsa-framework/slsa-verifier" @@ -214,7 +214,7 @@ checksum = "blake3:6b7c72ece3e3cbd9db12dd8e261e594bb24721db140692d4f5cbdf508df45 [tools.slsa-verifier.platforms.macos-arm64-slsa-verifier] checksum = "blake3:af93f77462964b7eeb93b9f45ccedeefc641305f6618d9ffc2480d89b0cceed6" -[tools.sops] +[[tools.sops]] version = "3.10.2" backend = "aqua:getsops/sops" @@ -228,7 +228,7 @@ checksum = "sha256:99702df79737162b986641afb8d98251acb16a52e6cab556a6b6f57c608c0 size = 44246082 url = "https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.darwin.arm64" -[tools.taplo] +[[tools.taplo]] version = "0.10.0" backend = "aqua:tamasfe/taplo" @@ -237,7 +237,7 @@ checksum = "blake3:4871fab0e60275a1eb46e7190726e144f56c9a9527f59b0d1da5a042baead size = 5116068 url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-x86_64.gz" -[tools.wait-for-gh-rate-limit] +[[tools.wait-for-gh-rate-limit]] version = "1.0.0" backend = "ubi:jdx/wait-for-gh-rate-limit" diff --git a/src/cli/lock.rs b/src/cli/lock.rs index 792c54c1a6..c6592d0b6f 100644 --- a/src/cli/lock.rs +++ b/src/cli/lock.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use crate::config::Config; use crate::file::display_path; use crate::lockfile::Lockfile; +use crate::platform::Platform; use crate::{cli::args::ToolArg, config::Settings}; use console::style; use eyre::Result; @@ -49,6 +50,21 @@ impl Lock { let config = Config::get().await?; settings.ensure_experimental("lock")?; + // Validate platforms if specified + if !self.platform.is_empty() { + let parsed_platforms = Platform::parse_multiple(&self.platform)?; + miseprintln!( + "{} Validated {} platform(s): {}", + style("→").green(), + parsed_platforms.len(), + parsed_platforms + .iter() + .map(|p| p.to_key()) + .collect::>() + .join(", ") + ); + } + // For Phase 1, just implement lockfile discovery and platform analysis self.analyze_lockfiles(&config).await?; @@ -288,14 +304,30 @@ impl Lock { // If no platforms specified, target all platforms available_platforms.iter().cloned().collect() } else { - // Filter to only specified platforms that exist in lockfile - let specified_platforms: BTreeSet = self.platform.iter().cloned().collect(); - - available_platforms - .iter() - .filter(|platform| specified_platforms.contains(*platform)) - .cloned() - .collect() + // Parse and validate specified platforms first, then filter + match Platform::parse_multiple(&self.platform) { + Ok(parsed_platforms) => { + let specified_platforms: BTreeSet = + parsed_platforms.iter().map(|p| p.to_key()).collect(); + + available_platforms + .iter() + .filter(|platform| specified_platforms.contains(*platform)) + .cloned() + .collect() + } + Err(_) => { + // If parsing fails, fall back to original logic + let specified_platforms: BTreeSet = + self.platform.iter().cloned().collect(); + + available_platforms + .iter() + .filter(|platform| specified_platforms.contains(*platform)) + .cloned() + .collect() + } + } } } } diff --git a/src/lockfile.rs b/src/lockfile.rs index f721c98414..aa5be3a774 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -112,6 +112,7 @@ impl Lockfile { .try_into()?; let mut lockfile = Lockfile::default(); + let mut has_single_version_format = false; for (short, value) in tools { let versions = match value { @@ -119,11 +120,23 @@ impl Lockfile { .into_iter() .map(LockfileTool::try_from) .collect::>>()?, - _ => vec![LockfileTool::try_from(value)?], + _ => { + // Single-Version format detected - will be auto-migrated + has_single_version_format = true; + trace!("Auto-migrating single-version format for tool: {}", short); + vec![LockfileTool::try_from(value)?] + } }; lockfile.tools.insert(short, versions); } + if has_single_version_format { + debug!( + "Auto-migrated lockfile from single-version to multi-version format: {}", + path.display() + ); + } + Ok(lockfile) } @@ -133,16 +146,13 @@ impl Lockfile { } else { let mut tools = toml::Table::new(); for (short, versions) in &self.tools { - let value: toml::Value = if versions.len() == 1 { - versions[0].clone().into_toml_value() - } else { - versions - .iter() - .cloned() - .map(|version| version.into_toml_value()) - .collect::>() - .into() - }; + // Always write Multi-Version format (array format) for consistency + let value: toml::Value = versions + .iter() + .cloned() + .map(|version| version.into_toml_value()) + .collect::>() + .into(); tools.insert(short.clone(), value); } let mut lockfile = toml::Table::new(); @@ -520,3 +530,94 @@ fn format(mut doc: DocumentMut) -> String { doc.to_string() } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn test_multi_version_format_migration() { + // Test that single-version format is read correctly and writes as multi-version + let single_version_toml = r#" +[tools.node] +version = "20.10.0" +backend = "core:node" + +[[tools.python]] +version = "3.11.0" +backend = "core:python" +"#; + + let table: toml::Table = toml::from_str(single_version_toml).unwrap(); + let tools: toml::Table = table.get("tools").unwrap().clone().try_into().unwrap(); + + let mut lockfile = Lockfile::default(); + for (short, value) in tools { + let versions = match value { + toml::Value::Array(arr) => arr + .into_iter() + .map(LockfileTool::try_from) + .collect::>>() + .unwrap(), + _ => vec![LockfileTool::try_from(value).unwrap()], + }; + lockfile.tools.insert(short, versions); + } + + // Verify that we have the expected tools + assert_eq!(lockfile.tools.len(), 2); + assert!(lockfile.tools.contains_key("node")); + assert!(lockfile.tools.contains_key("python")); + + // Verify node was migrated from single-version + let node_versions = &lockfile.tools["node"]; + assert_eq!(node_versions.len(), 1); + assert_eq!(node_versions[0].version, "20.10.0"); + assert_eq!(node_versions[0].backend, Some("core:node".to_string())); + + // Verify python was already multi-version + let python_versions = &lockfile.tools["python"]; + assert_eq!(python_versions.len(), 1); + assert_eq!(python_versions[0].version, "3.11.0"); + } + + #[test] + fn test_save_always_uses_multi_version_format() { + let mut lockfile = Lockfile::default(); + let mut platforms = BTreeMap::new(); + platforms.insert( + "macos-arm64".to_string(), + PlatformInfo { + checksum: Some("sha256:abc123".to_string()), + url: Some("https://example.com/node.tar.gz".to_string()), + size: Some(12345678), + }, + ); + + let tool = LockfileTool { + version: "20.10.0".to_string(), + backend: Some("core:node".to_string()), + platforms, + }; + + lockfile.tools.insert("node".to_string(), vec![tool]); + + // Create a temporary file to test saving + let temp_dir = std::env::temp_dir(); + let test_lockfile = temp_dir.join("test_lockfile.lock"); + + // Save and verify it uses multi-version format + lockfile.save(&test_lockfile).unwrap(); + + let content = std::fs::read_to_string(&test_lockfile).unwrap(); + + // Should use [[tools.node]] array syntax, not [tools.node] single version + assert!(content.contains("[[tools.node]]")); + // Verify it doesn't use single-version format (but allow platforms sections) + assert!(!content.lines().any(|line| line.trim() == "[tools.node]")); + + // Clean up + let _ = std::fs::remove_file(&test_lockfile); + } +} diff --git a/src/main.rs b/src/main.rs index 304839af59..c7695c3b78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,7 @@ mod minisign; pub(crate) mod parallel; mod path; mod path_env; +mod platform; mod plugins; mod rand; mod redactions; diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000000..1364a888c9 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,277 @@ +use crate::config::Settings; +use eyre::{Result, bail}; +use std::fmt; + +/// Represents a target platform for lockfile operations +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Platform { + pub os: String, + pub arch: String, + pub qualifier: Option, +} + +impl Platform { + /// Parse a platform string in the format "os-arch" or "os-arch-qualifier" + pub fn parse(platform_str: &str) -> Result { + let parts: Vec<&str> = platform_str.split('-').collect(); + + match parts.len() { + 2 => Ok(Platform { + os: parts[0].to_string(), + arch: parts[1].to_string(), + qualifier: None, + }), + 3 => Ok(Platform { + os: parts[0].to_string(), + arch: parts[1].to_string(), + qualifier: Some(parts[2].to_string()), + }), + _ => bail!( + "Invalid platform format '{}'. Expected 'os-arch' or 'os-arch-qualifier'", + platform_str + ), + } + } + + /// Get the current platform from system information + pub fn current() -> Self { + let settings = Settings::get(); + Platform { + os: settings.os().to_string(), + arch: settings.arch().to_string(), + qualifier: None, + } + } + + /// Validate that this platform is supported + pub fn validate(&self) -> Result<()> { + // Validate OS + match self.os.as_str() { + "linux" | "macos" | "windows" => {} + _ => bail!( + "Unsupported OS '{}'. Supported: linux, macos, windows", + self.os + ), + } + + // Validate architecture + match self.arch.as_str() { + "x64" | "arm64" | "x86" => {} + _ => bail!( + "Unsupported architecture '{}'. Supported: x64, arm64, x86", + self.arch + ), + } + + // Validate qualifier if present + if let Some(qualifier) = &self.qualifier { + match qualifier.as_str() { + "gnu" | "musl" | "msvc" => {} + _ => bail!( + "Unsupported qualifier '{}'. Supported: gnu, musl, msvc", + qualifier + ), + } + } + + Ok(()) + } + + /// Check if this platform is compatible with the current system + pub fn is_compatible_with_current(&self) -> bool { + let current = Self::current(); + self.os == current.os && self.arch == current.arch + } + + /// Convert to platform key format used in lockfiles + pub fn to_key(&self) -> String { + match &self.qualifier { + Some(qualifier) => format!("{}-{}-{}", self.os, self.arch, qualifier), + None => format!("{}-{}", self.os, self.arch), + } + } + + /// Parse multiple platform strings, validating each one + pub fn parse_multiple(platform_strings: &[String]) -> Result> { + let mut platforms = Vec::new(); + + for platform_str in platform_strings { + let platform = Self::parse(platform_str)?; + platform.validate()?; + platforms.push(platform); + } + + // Remove duplicates and sort + platforms.sort(); + platforms.dedup(); + + Ok(platforms) + } + + /// Get a list of commonly supported platforms + pub fn common_platforms() -> Vec { + vec![ + Platform::parse("linux-x64").unwrap(), + Platform::parse("linux-arm64").unwrap(), + Platform::parse("macos-x64").unwrap(), + Platform::parse("macos-arm64").unwrap(), + Platform::parse("windows-x64").unwrap(), + ] + } + + /// Check if this is a Windows platform + pub fn is_windows(&self) -> bool { + self.os == "windows" + } + + /// Check if this is a macOS platform + pub fn is_macos(&self) -> bool { + self.os == "macos" + } + + /// Check if this is a Linux platform + pub fn is_linux(&self) -> bool { + self.os == "linux" + } + + /// Check if this uses ARM64 architecture + pub fn is_arm64(&self) -> bool { + self.arch == "arm64" + } + + /// Check if this uses x64 architecture + pub fn is_x64(&self) -> bool { + self.arch == "x64" + } +} + +impl fmt::Display for Platform { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_key()) + } +} + +impl From for Platform { + fn from(s: String) -> Self { + Self::parse(&s).unwrap_or_else(|_| { + // Fallback to current platform if parsing fails + Self::current() + }) + } +} + +impl From<&str> for Platform { + fn from(s: &str) -> Self { + Self::parse(s).unwrap_or_else(|_| { + // Fallback to current platform if parsing fails + Self::current() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_platform_parse_basic() { + let platform = Platform::parse("linux-x64").unwrap(); + assert_eq!(platform.os, "linux"); + assert_eq!(platform.arch, "x64"); + assert_eq!(platform.qualifier, None); + } + + #[test] + fn test_platform_parse_with_qualifier() { + let platform = Platform::parse("linux-x64-gnu").unwrap(); + assert_eq!(platform.os, "linux"); + assert_eq!(platform.arch, "x64"); + assert_eq!(platform.qualifier, Some("gnu".to_string())); + } + + #[test] + fn test_platform_parse_invalid() { + assert!(Platform::parse("linux").is_err()); + assert!(Platform::parse("linux-x64-gnu-extra").is_err()); + assert!(Platform::parse("").is_err()); + } + + #[test] + fn test_platform_validation() { + // Valid platforms + assert!(Platform::parse("linux-x64").unwrap().validate().is_ok()); + assert!(Platform::parse("macos-arm64").unwrap().validate().is_ok()); + assert!(Platform::parse("windows-x64").unwrap().validate().is_ok()); + assert!(Platform::parse("linux-x64-gnu").unwrap().validate().is_ok()); + + // Invalid OS + assert!(Platform::parse("invalid-x64").unwrap().validate().is_err()); + + // Invalid arch + assert!( + Platform::parse("linux-invalid") + .unwrap() + .validate() + .is_err() + ); + + // Invalid qualifier + assert!( + Platform::parse("linux-x64-invalid") + .unwrap() + .validate() + .is_err() + ); + } + + #[test] + fn test_platform_to_key() { + let platform1 = Platform::parse("linux-x64").unwrap(); + assert_eq!(platform1.to_key(), "linux-x64"); + + let platform2 = Platform::parse("linux-x64-gnu").unwrap(); + assert_eq!(platform2.to_key(), "linux-x64-gnu"); + } + + #[test] + fn test_platform_multiple_parsing() { + let platform_strings = vec![ + "linux-x64".to_string(), + "macos-arm64".to_string(), + "linux-x64".to_string(), // duplicate should be removed + ]; + + let platforms = Platform::parse_multiple(&platform_strings).unwrap(); + assert_eq!(platforms.len(), 2); + assert_eq!(platforms[0].to_key(), "linux-x64"); + assert_eq!(platforms[1].to_key(), "macos-arm64"); + } + + #[test] + fn test_platform_helpers() { + let linux_platform = Platform::parse("linux-arm64").unwrap(); + assert!(linux_platform.is_linux()); + assert!(linux_platform.is_arm64()); + assert!(!linux_platform.is_windows()); + assert!(!linux_platform.is_x64()); + + let windows_platform = Platform::parse("windows-x64").unwrap(); + assert!(windows_platform.is_windows()); + assert!(windows_platform.is_x64()); + assert!(!windows_platform.is_linux()); + assert!(!windows_platform.is_arm64()); + } + + #[test] + fn test_common_platforms() { + let platforms = Platform::common_platforms(); + assert_eq!(platforms.len(), 5); + + let keys: Vec = platforms.iter().map(|p| p.to_key()).collect(); + assert!(keys.contains(&"linux-x64".to_string())); + assert!(keys.contains(&"linux-arm64".to_string())); + assert!(keys.contains(&"macos-x64".to_string())); + assert!(keys.contains(&"macos-arm64".to_string())); + assert!(keys.contains(&"windows-x64".to_string())); + } +}