639 lines
22 KiB
Rust
639 lines
22 KiB
Rust
//! Runtime version constraint matching
|
|
//!
|
|
//! Provides utilities for parsing and evaluating semver version constraints
|
|
//! against available runtime versions. Used by the worker to select the
|
|
//! appropriate runtime version when an action or sensor declares a
|
|
//! `runtime_version_constraint`.
|
|
//!
|
|
//! # Constraint Syntax
|
|
//!
|
|
//! Constraints follow standard semver range syntax:
|
|
//!
|
|
//! | Constraint | Meaning |
|
|
//! |-----------------|----------------------------------------|
|
|
//! | `3.12` | Exactly 3.12.x (any patch) |
|
|
//! | `=3.12.1` | Exactly 3.12.1 |
|
|
//! | `>=3.12` | 3.12.0 or newer |
|
|
//! | `>=3.12,<4.0` | 3.12.0 or newer, but below 4.0.0 |
|
|
//! | `~3.12` | Compatible with 3.12.x (>=3.12.0, <3.13.0) |
|
|
//! | `^3.12` | Compatible with 3.x.x (>=3.12.0, <4.0.0) |
|
|
//!
|
|
//! Multiple constraints can be separated by commas (AND logic).
|
|
//!
|
|
//! # Lenient Parsing
|
|
//!
|
|
//! Version strings are parsed leniently to handle real-world formats:
|
|
//! - `3.12` → `3.12.0`
|
|
//! - `3` → `3.0.0`
|
|
//! - `v3.12.1` → `3.12.1` (leading 'v' stripped)
|
|
//! - `3.12.1-beta.1` → parsed with pre-release info
|
|
//!
|
|
//! # Examples
|
|
//!
|
|
//! ```
|
|
//! use attune_common::version_matching::{parse_version, matches_constraint, select_best_version};
|
|
//! use attune_common::models::RuntimeVersion;
|
|
//!
|
|
//! // Simple constraint matching
|
|
//! assert!(matches_constraint("3.12.1", ">=3.12").unwrap());
|
|
//! assert!(!matches_constraint("3.11.0", ">=3.12").unwrap());
|
|
//!
|
|
//! // Range constraints
|
|
//! assert!(matches_constraint("3.12.5", ">=3.12,<4.0").unwrap());
|
|
//! assert!(!matches_constraint("4.0.0", ">=3.12,<4.0").unwrap());
|
|
//!
|
|
//! // Tilde (patch-level compatibility)
|
|
//! assert!(matches_constraint("3.12.5", "~3.12").unwrap());
|
|
//! assert!(!matches_constraint("3.13.0", "~3.12").unwrap());
|
|
//!
|
|
//! // Caret (minor-level compatibility)
|
|
//! assert!(matches_constraint("3.15.0", "^3.12").unwrap());
|
|
//! assert!(!matches_constraint("4.0.0", "^3.12").unwrap());
|
|
//! ```
|
|
|
|
use semver::{Version, VersionReq};
|
|
use tracing::{debug, warn};
|
|
|
|
use crate::models::RuntimeVersion;
|
|
|
|
/// Error type for version matching operations.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum VersionError {
|
|
#[error("Invalid version string '{0}': {1}")]
|
|
InvalidVersion(String, String),
|
|
|
|
#[error("Invalid version constraint '{0}': {1}")]
|
|
InvalidConstraint(String, String),
|
|
}
|
|
|
|
/// Result type for version matching operations.
|
|
pub type VersionResult<T> = std::result::Result<T, VersionError>;
|
|
|
|
/// Parse a version string leniently into a [`semver::Version`].
|
|
///
|
|
/// Handles common real-world formats:
|
|
/// - `"3.12"` → `Version { major: 3, minor: 12, patch: 0 }`
|
|
/// - `"3"` → `Version { major: 3, minor: 0, patch: 0 }`
|
|
/// - `"v3.12.1"` → `Version { major: 3, minor: 12, patch: 1 }`
|
|
/// - `"3.12.1"` → `Version { major: 3, minor: 12, patch: 1 }`
|
|
pub fn parse_version(version_str: &str) -> VersionResult<Version> {
|
|
let trimmed = version_str.trim();
|
|
|
|
// Strip leading 'v' or 'V'
|
|
let stripped = trimmed
|
|
.strip_prefix('v')
|
|
.or_else(|| trimmed.strip_prefix('V'))
|
|
.unwrap_or(trimmed);
|
|
|
|
// Try direct parse first (handles full semver like "3.12.1" and pre-release)
|
|
if let Ok(v) = Version::parse(stripped) {
|
|
return Ok(v);
|
|
}
|
|
|
|
// Try adding missing components
|
|
let parts: Vec<&str> = stripped.split('.').collect();
|
|
let padded = match parts.len() {
|
|
1 => format!("{}.0.0", parts[0]),
|
|
2 => format!("{}.{}.0", parts[0], parts[1]),
|
|
_ => {
|
|
// More than 3 parts or other issues — try joining first 3
|
|
if parts.len() >= 3 {
|
|
format!("{}.{}.{}", parts[0], parts[1], parts[2])
|
|
} else {
|
|
stripped.to_string()
|
|
}
|
|
}
|
|
};
|
|
|
|
Version::parse(&padded)
|
|
.map_err(|e| VersionError::InvalidVersion(version_str.to_string(), e.to_string()))
|
|
}
|
|
|
|
/// Parse a version constraint string into a [`semver::VersionReq`].
|
|
///
|
|
/// Handles comma-separated constraints (AND logic) and the standard
|
|
/// semver operators: `=`, `>=`, `<=`, `>`, `<`, `~`, `^`.
|
|
///
|
|
/// If a bare version is given (no operator), it is treated as a
|
|
/// compatibility constraint: `"3.12"` becomes `">=3.12.0, <3.13.0"` (tilde behavior).
|
|
///
|
|
/// Note: The `semver` crate's `VersionReq` natively handles comma-separated
|
|
/// constraints and all standard operators.
|
|
pub fn parse_constraint(constraint_str: &str) -> VersionResult<VersionReq> {
|
|
let trimmed = constraint_str.trim();
|
|
|
|
if trimmed.is_empty() {
|
|
// Empty constraint matches everything
|
|
return Ok(VersionReq::STAR);
|
|
}
|
|
|
|
// Preprocess each comma-separated part to handle lenient input.
|
|
// For each part, if it looks like a bare version (no operator prefix),
|
|
// we treat it as a tilde constraint so "3.12" means "~3.12".
|
|
let parts: Vec<String> = trimmed
|
|
.split(',')
|
|
.map(|part| {
|
|
let p = part.trim();
|
|
if p.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
// Check if the first character is an operator
|
|
let first_char = p.chars().next().unwrap_or(' ');
|
|
if first_char.is_ascii_digit() || first_char == 'v' || first_char == 'V' {
|
|
// Bare version — treat as tilde range (compatible within minor)
|
|
let stripped = p
|
|
.strip_prefix('v')
|
|
.or_else(|| p.strip_prefix('V'))
|
|
.unwrap_or(p);
|
|
|
|
// Pad to at least major.minor for tilde semantics
|
|
let dot_count = stripped.chars().filter(|c| *c == '.').count();
|
|
let padded = match dot_count {
|
|
0 => format!("{}.0", stripped),
|
|
_ => stripped.to_string(),
|
|
};
|
|
|
|
format!("~{}", padded)
|
|
} else {
|
|
// Has operator prefix — normalize version part if needed
|
|
// Find where the version number starts
|
|
let version_start = p.find(|c: char| c.is_ascii_digit()).unwrap_or(p.len());
|
|
|
|
let (op, ver) = p.split_at(version_start);
|
|
let ver = ver
|
|
.strip_prefix('v')
|
|
.or_else(|| ver.strip_prefix('V'))
|
|
.unwrap_or(ver);
|
|
|
|
// Pad version if needed
|
|
let dot_count = ver.chars().filter(|c| *c == '.').count();
|
|
let padded = match dot_count {
|
|
0 if !ver.is_empty() => format!("{}.0.0", ver),
|
|
1 => format!("{}.0", ver),
|
|
_ => ver.to_string(),
|
|
};
|
|
|
|
format!("{}{}", op.trim(), padded)
|
|
}
|
|
})
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
if parts.is_empty() {
|
|
return Ok(VersionReq::STAR);
|
|
}
|
|
|
|
let normalized = parts.join(", ");
|
|
|
|
VersionReq::parse(&normalized)
|
|
.map_err(|e| VersionError::InvalidConstraint(constraint_str.to_string(), e.to_string()))
|
|
}
|
|
|
|
/// Check whether a version string satisfies a constraint string.
|
|
///
|
|
/// Returns `true` if the version matches the constraint.
|
|
/// Returns an error if either the version or constraint cannot be parsed.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use attune_common::version_matching::matches_constraint;
|
|
///
|
|
/// assert!(matches_constraint("3.12.1", ">=3.12").unwrap());
|
|
/// assert!(!matches_constraint("3.11.0", ">=3.12").unwrap());
|
|
/// assert!(matches_constraint("3.12.5", ">=3.12,<4.0").unwrap());
|
|
/// ```
|
|
pub fn matches_constraint(version_str: &str, constraint_str: &str) -> VersionResult<bool> {
|
|
let version = parse_version(version_str)?;
|
|
let constraint = parse_constraint(constraint_str)?;
|
|
Ok(constraint.matches(&version))
|
|
}
|
|
|
|
/// Select the best matching runtime version from a list of candidates.
|
|
///
|
|
/// "Best" is defined as the highest version that satisfies the constraint
|
|
/// and is marked as available. If no constraint is given, the default version
|
|
/// is preferred; if no default exists, the highest available version is returned.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `versions` - All registered versions for a runtime (any order)
|
|
/// * `constraint` - Optional version constraint string (e.g., `">=3.12"`)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The best matching `RuntimeVersion`, or `None` if no version matches.
|
|
pub fn select_best_version<'a>(
|
|
versions: &'a [RuntimeVersion],
|
|
constraint: Option<&str>,
|
|
) -> Option<&'a RuntimeVersion> {
|
|
if versions.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Only consider available versions
|
|
let available: Vec<&RuntimeVersion> = versions.iter().filter(|v| v.available).collect();
|
|
|
|
if available.is_empty() {
|
|
debug!("No available versions found");
|
|
return None;
|
|
}
|
|
|
|
match constraint {
|
|
Some(constraint_str) if !constraint_str.trim().is_empty() => {
|
|
let req = match parse_constraint(constraint_str) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
warn!("Invalid version constraint '{}': {}", constraint_str, e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// Filter to versions that match the constraint, then pick the highest
|
|
let mut matching: Vec<(&RuntimeVersion, Version)> = available
|
|
.iter()
|
|
.filter_map(|rv| match parse_version(&rv.version) {
|
|
Ok(v) if req.matches(&v) => Some((*rv, v)),
|
|
Ok(_) => {
|
|
debug!(
|
|
"Version {} does not match constraint '{}'",
|
|
rv.version, constraint_str
|
|
);
|
|
None
|
|
}
|
|
Err(e) => {
|
|
warn!("Cannot parse version '{}' for matching: {}", rv.version, e);
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
if matching.is_empty() {
|
|
debug!(
|
|
"No available versions match constraint '{}'",
|
|
constraint_str
|
|
);
|
|
return None;
|
|
}
|
|
|
|
// Sort by semver descending — highest version first
|
|
matching.sort_by(|a, b| b.1.cmp(&a.1));
|
|
|
|
Some(matching[0].0)
|
|
}
|
|
|
|
_ => {
|
|
// No constraint — prefer the default version, else the highest available
|
|
if let Some(default) = available.iter().find(|v| v.is_default) {
|
|
return Some(default);
|
|
}
|
|
|
|
// Pick highest available version
|
|
let mut with_parsed: Vec<(&RuntimeVersion, Version)> = available
|
|
.iter()
|
|
.filter_map(|rv| parse_version(&rv.version).ok().map(|v| (*rv, v)))
|
|
.collect();
|
|
|
|
with_parsed.sort_by(|a, b| b.1.cmp(&a.1));
|
|
with_parsed.first().map(|(rv, _)| *rv)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extract semver components from a version string.
|
|
///
|
|
/// Returns `(major, minor, patch)` as `Option<i32>` values.
|
|
/// Useful for populating the `version_major`, `version_minor`, `version_patch`
|
|
/// columns in the `runtime_version` table.
|
|
pub fn extract_version_components(version_str: &str) -> (Option<i32>, Option<i32>, Option<i32>) {
|
|
match parse_version(version_str) {
|
|
Ok(v) => (
|
|
i32::try_from(v.major).ok(),
|
|
i32::try_from(v.minor).ok(),
|
|
i32::try_from(v.patch).ok(),
|
|
),
|
|
Err(_) => (None, None, None),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
// ========================================================================
|
|
// parse_version tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_parse_version_full() {
|
|
let v = parse_version("3.12.1").unwrap();
|
|
assert_eq!(v, Version::new(3, 12, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_two_parts() {
|
|
let v = parse_version("3.12").unwrap();
|
|
assert_eq!(v, Version::new(3, 12, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_one_part() {
|
|
let v = parse_version("3").unwrap();
|
|
assert_eq!(v, Version::new(3, 0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_leading_v() {
|
|
let v = parse_version("v3.12.1").unwrap();
|
|
assert_eq!(v, Version::new(3, 12, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_leading_v_uppercase() {
|
|
let v = parse_version("V20.11.0").unwrap();
|
|
assert_eq!(v, Version::new(20, 11, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_with_whitespace() {
|
|
let v = parse_version(" 3.12.1 ").unwrap();
|
|
assert_eq!(v, Version::new(3, 12, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_version_invalid() {
|
|
assert!(parse_version("not-a-version").is_err());
|
|
}
|
|
|
|
// ========================================================================
|
|
// parse_constraint tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_parse_constraint_gte() {
|
|
let req = parse_constraint(">=3.12").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 13, 0)));
|
|
assert!(req.matches(&Version::new(4, 0, 0)));
|
|
assert!(!req.matches(&Version::new(3, 11, 9)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_exact_with_eq() {
|
|
let req = parse_constraint("=3.12.1").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 1)));
|
|
assert!(!req.matches(&Version::new(3, 12, 2)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_bare_version() {
|
|
// Bare "3.12" is treated as ~3.12 → >=3.12.0, <3.13.0
|
|
let req = parse_constraint("3.12").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 12, 9)));
|
|
assert!(!req.matches(&Version::new(3, 13, 0)));
|
|
assert!(!req.matches(&Version::new(3, 11, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_tilde() {
|
|
let req = parse_constraint("~3.12").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 12, 99)));
|
|
assert!(!req.matches(&Version::new(3, 13, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_caret() {
|
|
let req = parse_constraint("^3.12").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 99, 0)));
|
|
assert!(!req.matches(&Version::new(4, 0, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_range() {
|
|
let req = parse_constraint(">=3.12,<4.0").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 99, 0)));
|
|
assert!(!req.matches(&Version::new(4, 0, 0)));
|
|
assert!(!req.matches(&Version::new(3, 11, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_empty() {
|
|
let req = parse_constraint("").unwrap();
|
|
assert!(req.matches(&Version::new(0, 0, 1)));
|
|
assert!(req.matches(&Version::new(999, 0, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_lt() {
|
|
let req = parse_constraint("<4.0").unwrap();
|
|
assert!(req.matches(&Version::new(3, 99, 99)));
|
|
assert!(!req.matches(&Version::new(4, 0, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_lte() {
|
|
let req = parse_constraint("<=3.12").unwrap();
|
|
assert!(req.matches(&Version::new(3, 12, 0)));
|
|
// Note: semver <=3.12.0 means exactly ≤3.12.0
|
|
assert!(!req.matches(&Version::new(3, 12, 1)));
|
|
assert!(!req.matches(&Version::new(3, 13, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_constraint_gt() {
|
|
let req = parse_constraint(">3.12").unwrap();
|
|
assert!(!req.matches(&Version::new(3, 12, 0)));
|
|
assert!(req.matches(&Version::new(3, 12, 1)));
|
|
assert!(req.matches(&Version::new(3, 13, 0)));
|
|
}
|
|
|
|
// ========================================================================
|
|
// matches_constraint tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_matches_constraint_basic() {
|
|
assert!(matches_constraint("3.12.1", ">=3.12").unwrap());
|
|
assert!(!matches_constraint("3.11.0", ">=3.12").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_constraint_range() {
|
|
assert!(matches_constraint("3.12.5", ">=3.12,<4.0").unwrap());
|
|
assert!(!matches_constraint("4.0.0", ">=3.12,<4.0").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_constraint_tilde() {
|
|
assert!(matches_constraint("3.12.5", "~3.12").unwrap());
|
|
assert!(!matches_constraint("3.13.0", "~3.12").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_constraint_caret() {
|
|
assert!(matches_constraint("3.15.0", "^3.12").unwrap());
|
|
assert!(!matches_constraint("4.0.0", "^3.12").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_matches_constraint_node_versions() {
|
|
assert!(matches_constraint("20.11.0", ">=18").unwrap());
|
|
assert!(matches_constraint("18.0.0", ">=18").unwrap());
|
|
assert!(!matches_constraint("16.20.0", ">=18").unwrap());
|
|
}
|
|
|
|
// ========================================================================
|
|
// extract_version_components tests
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn test_extract_components_full() {
|
|
let (maj, min, pat) = extract_version_components("3.12.1");
|
|
assert_eq!(maj, Some(3));
|
|
assert_eq!(min, Some(12));
|
|
assert_eq!(pat, Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_components_partial() {
|
|
let (maj, min, pat) = extract_version_components("20.11");
|
|
assert_eq!(maj, Some(20));
|
|
assert_eq!(min, Some(11));
|
|
assert_eq!(pat, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_components_invalid() {
|
|
let (maj, min, pat) = extract_version_components("not-a-version");
|
|
assert_eq!(maj, None);
|
|
assert_eq!(min, None);
|
|
assert_eq!(pat, None);
|
|
}
|
|
|
|
// ========================================================================
|
|
// select_best_version tests
|
|
// ========================================================================
|
|
|
|
fn make_version(
|
|
id: i64,
|
|
runtime: i64,
|
|
version: &str,
|
|
is_default: bool,
|
|
available: bool,
|
|
) -> RuntimeVersion {
|
|
let (major, minor, patch) = extract_version_components(version);
|
|
RuntimeVersion {
|
|
id,
|
|
runtime,
|
|
runtime_ref: "core.python".to_string(),
|
|
version: version.to_string(),
|
|
version_major: major,
|
|
version_minor: minor,
|
|
version_patch: patch,
|
|
execution_config: json!({}),
|
|
distributions: json!({}),
|
|
is_default,
|
|
available,
|
|
verified_at: None,
|
|
meta: json!({}),
|
|
created: chrono::Utc::now(),
|
|
updated: chrono::Utc::now(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_no_constraint_prefers_default() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.11.0", false, true),
|
|
make_version(2, 1, "3.12.0", true, true), // default
|
|
make_version(3, 1, "3.14.0", false, true),
|
|
];
|
|
|
|
let best = select_best_version(&versions, None).unwrap();
|
|
assert_eq!(best.id, 2); // default version
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_no_constraint_no_default_picks_highest() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.11.0", false, true),
|
|
make_version(2, 1, "3.12.0", false, true),
|
|
make_version(3, 1, "3.14.0", false, true),
|
|
];
|
|
|
|
let best = select_best_version(&versions, None).unwrap();
|
|
assert_eq!(best.id, 3); // highest version
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_with_constraint() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.11.0", false, true),
|
|
make_version(2, 1, "3.12.0", false, true),
|
|
make_version(3, 1, "3.14.0", false, true),
|
|
];
|
|
|
|
// >=3.12,<3.14 should pick 3.12.0 (3.14.0 is excluded)
|
|
let best = select_best_version(&versions, Some(">=3.12,<3.14")).unwrap();
|
|
assert_eq!(best.id, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_with_constraint_picks_highest_match() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.11.0", false, true),
|
|
make_version(2, 1, "3.12.0", false, true),
|
|
make_version(3, 1, "3.12.5", false, true),
|
|
make_version(4, 1, "3.13.0", false, true),
|
|
];
|
|
|
|
// ~3.12 → >=3.12.0, <3.13.0 → should pick 3.12.5
|
|
let best = select_best_version(&versions, Some("~3.12")).unwrap();
|
|
assert_eq!(best.id, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_skips_unavailable() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.12.0", false, true),
|
|
make_version(2, 1, "3.14.0", false, false), // not available
|
|
];
|
|
|
|
let best = select_best_version(&versions, Some(">=3.12")).unwrap();
|
|
assert_eq!(best.id, 1); // 3.14 is unavailable
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_no_match() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.11.0", false, true),
|
|
make_version(2, 1, "3.12.0", false, true),
|
|
];
|
|
|
|
let best = select_best_version(&versions, Some(">=4.0"));
|
|
assert!(best.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_empty_versions() {
|
|
let versions: Vec<RuntimeVersion> = vec![];
|
|
assert!(select_best_version(&versions, None).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_select_best_all_unavailable() {
|
|
let versions = vec![
|
|
make_version(1, 1, "3.12.0", false, false),
|
|
make_version(2, 1, "3.14.0", false, false),
|
|
];
|
|
|
|
assert!(select_best_version(&versions, None).is_none());
|
|
}
|
|
}
|