use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Version {
Semver(semver::Version),
Numeric(Vec<u64>),
Pep440(Pep440),
Deb {
epoch: u64,
upstream: String,
revision: String,
},
Rpm {
epoch: u64,
version: String,
release: String,
},
Maven(String),
Opaque(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pep440 {
pub epoch: u64,
pub release: Vec<u64>,
pub pre: Option<(PreRelease, u64)>,
pub post: Option<u64>,
pub dev: Option<u64>,
pub local: Vec<LocalSegment>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PreRelease {
Alpha,
Beta,
Rc,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum LocalSegment {
Str(String),
Num(u64),
}
impl Version {
pub fn parse_lenient(s: &str) -> Self {
let stripped = strip_v_prefix(s);
if let Ok(v) = semver::Version::parse(stripped) {
return Version::Semver(v);
}
let parts: Vec<&str> = stripped.splitn(3, '.').collect();
let padded = match parts.len() {
1 => Some(format!("{}.0.0", parts[0])),
2 => Some(format!("{}.{}.0", parts[0], parts[1])),
_ => None,
};
if let Some(ref padded) = padded {
if let Ok(v) = semver::Version::parse(padded) {
return Version::Semver(v);
}
}
if let Some(segments) = parse_numeric(stripped) {
return Version::Numeric(segments);
}
if let Some(pep) = parse_pep440(stripped) {
if pep.epoch > 0 || pep.pre.is_some() || pep.post.is_some() || pep.dev.is_some() {
return Version::Pep440(pep);
}
}
if let Some(deb) = parse_deb(stripped) {
return deb;
}
Version::Opaque(s.to_string())
}
pub fn parse_for_ecosystem(ecosystem: Option<&str>, s: &str) -> Self {
match Scheme::for_ecosystem(ecosystem) {
Scheme::Infer => Version::parse_lenient(s),
Scheme::Deb => parse_deb(s)
.or_else(|| parse_deb(strip_v_prefix(s)))
.unwrap_or_else(|| Version::Opaque(s.to_string())),
Scheme::Rpm => parse_rpm(s)
.or_else(|| parse_rpm(strip_v_prefix(s)))
.unwrap_or_else(|| Version::Opaque(s.to_string())),
Scheme::Maven => parse_maven(s)
.or_else(|| parse_maven(strip_v_prefix(s)))
.unwrap_or_else(|| Version::Opaque(s.to_string())),
}
}
pub fn partial_cmp_lenient(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Version::Semver(a), Version::Semver(b)) => Some(a.cmp_precedence(b)),
(Version::Numeric(a), Version::Numeric(b)) => Some(numeric_cmp(a, b)),
(Version::Semver(a), Version::Numeric(b)) => {
Some(numeric_cmp(&[a.major, a.minor, a.patch], b))
}
(Version::Numeric(a), Version::Semver(b)) => {
Some(numeric_cmp(a, &[b.major, b.minor, b.patch]))
}
(
Version::Deb {
epoch: ae,
upstream: au,
revision: arev,
},
Version::Deb {
epoch: be,
upstream: bu,
revision: brev,
},
) => Some(deb_cmp((*ae, au, arev), (*be, bu, brev))),
(
Version::Rpm {
epoch: ae,
version: av,
release: arel,
},
Version::Rpm {
epoch: be,
version: bv,
release: brel,
},
) => Some(rpm_cmp((*ae, av, arel), (*be, bv, brel))),
(Version::Maven(a), Version::Maven(b)) => maven_cmp(a, b),
(Version::Pep440(_), _) | (_, Version::Pep440(_)) => {
Some(pep440_cmp(&as_pep440(self)?, &as_pep440(other)?))
}
_ => None,
}
}
pub fn is_downgrade(&self, new: &Self) -> bool {
self.partial_cmp_lenient(new) == Some(Ordering::Greater)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Scheme {
Infer,
Deb,
Rpm,
Maven,
}
impl Scheme {
fn for_ecosystem(ecosystem: Option<&str>) -> Self {
match ecosystem {
Some(e) if e.eq_ignore_ascii_case("deb") => Scheme::Deb,
Some(e) if e.eq_ignore_ascii_case("rpm") => Scheme::Rpm,
Some(e) if e.eq_ignore_ascii_case("maven") => Scheme::Maven,
_ => Scheme::Infer,
}
}
}
fn numeric_cmp(a: &[u64], b: &[u64]) -> Ordering {
let max_len = a.len().max(b.len());
for i in 0..max_len {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
if x != y {
return x.cmp(&y);
}
}
Ordering::Equal
}
fn parse_numeric(stripped: &str) -> Option<Vec<u64>> {
let mut segments = Vec::new();
for part in stripped.split('.') {
segments.push(part.parse::<u64>().ok()?);
}
if segments.is_empty() {
None
} else {
Some(segments)
}
}
const PRE_ALIASES: [(&str, PreRelease); 8] = [
("alpha", PreRelease::Alpha),
("beta", PreRelease::Beta),
("preview", PreRelease::Rc),
("pre", PreRelease::Rc),
("rc", PreRelease::Rc),
("a", PreRelease::Alpha),
("b", PreRelease::Beta),
("c", PreRelease::Rc),
];
const POST_ALIASES: [(&str, ()); 3] = [("post", ()), ("rev", ()), ("r", ())];
const DEV_ALIASES: [(&str, ()); 1] = [("dev", ())];
fn parse_pep440(s: &str) -> Option<Pep440> {
let lower = s.to_ascii_lowercase();
let (head, local) = match lower.split_once('+') {
Some((head, tail)) => (head, parse_local(tail)?),
None => (lower.as_str(), Vec::new()),
};
let (epoch, mut rest) = match head.split_once('!') {
Some((epoch, tail)) => (epoch.parse::<u64>().ok()?, tail),
None => (0, head),
};
let mut release = Vec::new();
loop {
let end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
if end == 0 {
return None;
}
release.push(rest[..end].parse::<u64>().ok()?);
rest = &rest[end..];
match rest.strip_prefix('.') {
Some(next) if next.starts_with(|c: char| c.is_ascii_digit()) => rest = next,
_ => break,
}
}
let (pre, rest) = match take_segment(rest, &PRE_ALIASES) {
Some((kind, n, rest)) => (Some((kind, n)), rest),
None => (None, rest),
};
let (post, rest) = match take_segment(rest, &POST_ALIASES) {
Some((_, n, rest)) => (Some(n), rest),
None => (None, rest),
};
let (dev, rest) = match take_segment(rest, &DEV_ALIASES) {
Some((_, n, rest)) => (Some(n), rest),
None => (None, rest),
};
if !rest.is_empty() {
return None;
}
Some(Pep440 {
epoch,
release,
pre,
post,
dev,
local,
})
}
fn take_segment<'a, T: Copy>(s: &'a str, aliases: &[(&str, T)]) -> Option<(T, u64, &'a str)> {
let body = s.strip_prefix(['-', '_', '.']).unwrap_or(s);
let (name, tag, rest) = aliases
.iter()
.find_map(|(name, tag)| Some((*name, *tag, body.strip_prefix(*name)?)))?;
let digits = rest.strip_prefix(['-', '_', '.']).unwrap_or(rest);
let end = digits
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(digits.len());
let n = match end {
0 if name.len() == 1 => return None,
0 => 0,
_ => digits[..end].parse::<u64>().ok()?,
};
Some((tag, n, &digits[end..]))
}
fn parse_local(s: &str) -> Option<Vec<LocalSegment>> {
let mut segments = Vec::new();
for part in s.split(['-', '_', '.']) {
if part.is_empty() || !part.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
segments.push(match part.parse::<u64>() {
Ok(n) => LocalSegment::Num(n),
Err(_) => LocalSegment::Str(part.to_string()),
});
}
Some(segments)
}
fn as_pep440(v: &Version) -> Option<Pep440> {
let plain = |release| Pep440 {
epoch: 0,
release,
pre: None,
post: None,
dev: None,
local: Vec::new(),
};
match v {
Version::Pep440(p) => Some(p.clone()),
Version::Numeric(segments) => Some(plain(segments.clone())),
Version::Semver(s) if s.pre.is_empty() => Some(plain(vec![s.major, s.minor, s.patch])),
Version::Semver(s) => {
parse_pep440(&format!("{}.{}.{}-{}", s.major, s.minor, s.patch, s.pre))
}
Version::Deb { .. } | Version::Rpm { .. } | Version::Maven(_) | Version::Opaque(_) => None,
}
}
fn pep440_cmp(a: &Pep440, b: &Pep440) -> Ordering {
a.epoch
.cmp(&b.epoch)
.then_with(|| numeric_cmp(&a.release, &b.release))
.then_with(|| pre_key(a).cmp(&pre_key(b)))
.then_with(|| a.post.cmp(&b.post))
.then_with(|| dev_key(a).cmp(&dev_key(b)))
.then_with(|| a.local.cmp(&b.local))
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum PreKey {
BeforeAll,
Pre(PreRelease, u64),
AfterAll,
}
fn pre_key(v: &Pep440) -> PreKey {
match v.pre {
Some((kind, n)) => PreKey::Pre(kind, n),
None if v.post.is_none() && v.dev.is_some() => PreKey::BeforeAll,
None => PreKey::AfterAll,
}
}
fn dev_key(v: &Pep440) -> (bool, u64) {
(v.dev.is_none(), v.dev.unwrap_or(0))
}
fn strip_v_prefix(s: &str) -> &str {
s.strip_prefix('v')
.or_else(|| s.strip_prefix('V'))
.unwrap_or(s)
}
fn parse_deb(s: &str) -> Option<Version> {
let (epoch, rest) = split_epoch(s, &[':', '!']);
if !rest.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
if !rest.chars().all(is_deb_char) {
return None;
}
let (upstream, revision) = match rest.rfind('-') {
Some(idx) => (rest[..idx].to_string(), rest[idx + 1..].to_string()),
None => (rest.to_string(), String::new()),
};
Some(Version::Deb {
epoch,
upstream,
revision,
})
}
fn split_epoch<'a>(s: &'a str, seps: &[char]) -> (u64, &'a str) {
if let Some(idx) = s.find(seps) {
let (head, tail) = s.split_at(idx);
if !head.is_empty() && head.bytes().all(|b| b.is_ascii_digit()) {
if let Ok(epoch) = head.parse::<u64>() {
return (epoch, &tail[1..]);
}
}
}
(0, s)
}
fn is_deb_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':')
}
fn deb_cmp(a: (u64, &str, &str), b: (u64, &str, &str)) -> Ordering {
a.0.cmp(&b.0)
.then_with(|| verrevcmp(a.1, b.1))
.then_with(|| verrevcmp(a.2, b.2))
}
fn verrevcmp(a: &str, b: &str) -> Ordering {
let a = a.as_bytes();
let b = b.as_bytes();
let mut i = 0;
let mut j = 0;
while i < a.len() || j < b.len() {
while (i < a.len() && !a[i].is_ascii_digit()) || (j < b.len() && !b[j].is_ascii_digit()) {
let ac = a.get(i).map_or(0, |&c| deb_order(c));
let bc = b.get(j).map_or(0, |&c| deb_order(c));
if ac != bc {
return ac.cmp(&bc);
}
i += 1;
j += 1;
}
while i < a.len() && a[i] == b'0' {
i += 1;
}
while j < b.len() && b[j] == b'0' {
j += 1;
}
let mut first_diff = 0i32;
while i < a.len() && a[i].is_ascii_digit() && j < b.len() && b[j].is_ascii_digit() {
if first_diff == 0 {
first_diff = i32::from(a[i]) - i32::from(b[j]);
}
i += 1;
j += 1;
}
if i < a.len() && a[i].is_ascii_digit() {
return Ordering::Greater;
}
if j < b.len() && b[j].is_ascii_digit() {
return Ordering::Less;
}
if first_diff != 0 {
return first_diff.cmp(&0);
}
}
Ordering::Equal
}
fn deb_order(c: u8) -> i32 {
if c.is_ascii_digit() {
0
} else if c.is_ascii_alphabetic() {
i32::from(c)
} else if c == b'~' {
-1
} else {
i32::from(c) + 256
}
}
fn parse_rpm(s: &str) -> Option<Version> {
let (epoch, rest) = split_epoch(s, &[':']);
if !rest.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
if !rest.chars().all(is_rpm_char) {
return None;
}
let (version, release) = match rest.rfind('-') {
Some(idx) => (rest[..idx].to_string(), rest[idx + 1..].to_string()),
None => (rest.to_string(), String::new()),
};
Some(Version::Rpm {
epoch,
version,
release,
})
}
fn is_rpm_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':' | '_' | '^')
}
fn rpm_cmp(a: (u64, &str, &str), b: (u64, &str, &str)) -> Ordering {
a.0.cmp(&b.0)
.then_with(|| rpmvercmp(a.1, b.1))
.then_with(|| rpmvercmp(a.2, b.2))
}
fn rpmvercmp(a: &str, b: &str) -> Ordering {
let a = a.as_bytes();
let b = b.as_bytes();
let mut i = 0;
let mut j = 0;
while i < a.len() || j < b.len() {
while i < a.len() && is_rpm_separator(a[i]) {
i += 1;
}
while j < b.len() && is_rpm_separator(b[j]) {
j += 1;
}
if a.get(i) == Some(&b'~') || b.get(j) == Some(&b'~') {
if a.get(i) != Some(&b'~') {
return Ordering::Greater;
}
if b.get(j) != Some(&b'~') {
return Ordering::Less;
}
i += 1;
j += 1;
continue;
}
if a.get(i) == Some(&b'^') || b.get(j) == Some(&b'^') {
if i == a.len() {
return Ordering::Less;
}
if j == b.len() {
return Ordering::Greater;
}
if a.get(i) != Some(&b'^') {
return Ordering::Greater;
}
if b.get(j) != Some(&b'^') {
return Ordering::Less;
}
i += 1;
j += 1;
continue;
}
if i == a.len() || j == b.len() {
break;
}
let numeric = a[i].is_ascii_digit();
let a_end = run_end(a, i, numeric);
let b_end = run_end(b, j, numeric);
if b_end == j {
return if numeric {
Ordering::Greater
} else {
Ordering::Less
};
}
let mut x = &a[i..a_end];
let mut y = &b[j..b_end];
if numeric {
x = strip_leading_zeros(x);
y = strip_leading_zeros(y);
if x.len() != y.len() {
return x.len().cmp(&y.len());
}
}
match x.cmp(y) {
Ordering::Equal => {}
ord => return ord,
}
i = a_end;
j = b_end;
}
match (i == a.len(), j == b.len()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
_ => Ordering::Greater,
}
}
fn is_rpm_separator(c: u8) -> bool {
!(c.is_ascii_alphanumeric() || matches!(c, b'~' | b'^'))
}
fn run_end(s: &[u8], from: usize, numeric: bool) -> usize {
let in_run = |c: &&u8| {
if numeric {
c.is_ascii_digit()
} else {
c.is_ascii_alphabetic()
}
};
from + s[from..].iter().take_while(in_run).count()
}
fn strip_leading_zeros(s: &[u8]) -> &[u8] {
let zeros = s.iter().take_while(|&&c| c == b'0').count();
&s[zeros..]
}
fn parse_maven(s: &str) -> Option<Version> {
if !s.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
if !s.chars().all(is_maven_char) {
return None;
}
maven_parse(s)?;
Some(Version::Maven(s.to_string()))
}
fn is_maven_char(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '.' | '-' | '_' | '+')
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum MavenItem {
Num(String),
Qual(String),
List(Vec<MavenItem>),
}
impl MavenItem {
fn is_null(&self) -> bool {
match self {
MavenItem::Num(digits) => digits.is_empty(),
MavenItem::Qual(value) => value.is_empty(),
MavenItem::List(items) => items.is_empty(),
}
}
}
const MAVEN_MAX_DEPTH: usize = 64;
fn maven_cmp(a: &str, b: &str) -> Option<Ordering> {
Some(maven_list_cmp(&maven_parse(a)?, &maven_parse(b)?))
}
fn maven_parse(s: &str) -> Option<Vec<MavenItem>> {
let s = s.to_lowercase();
let mut stack: Vec<Vec<MavenItem>> = vec![Vec::new()];
let mut digits = false;
let mut start = 0;
for (i, c) in s.char_indices() {
if matches!(c, '.' | '-' | '_') {
let item = if i == start {
MavenItem::Num(String::new())
} else {
maven_item(digits, &s[start..i])
};
stack.last_mut().expect("stack is never emptied").push(item);
start = i + c.len_utf8();
if c != '.' {
maven_open(&mut stack)?;
}
continue;
}
let is_digit = c.is_ascii_digit();
if i > start && is_digit && !digits {
if !stack.last().expect("stack is never emptied").is_empty() {
maven_open(&mut stack)?;
}
let qualifier = MavenItem::Qual(maven_qualifier(&s[start..i], true));
stack
.last_mut()
.expect("stack is never emptied")
.push(qualifier);
start = i;
maven_open(&mut stack)?;
} else if i > start && !is_digit && digits {
let number = maven_item(true, &s[start..i]);
stack
.last_mut()
.expect("stack is never emptied")
.push(number);
start = i;
maven_open(&mut stack)?;
}
digits = is_digit;
}
if s.len() > start {
if !digits && !stack.last().expect("stack is never emptied").is_empty() {
maven_open(&mut stack)?;
}
let item = maven_item(digits, &s[start..]);
stack.last_mut().expect("stack is never emptied").push(item);
}
while stack.len() > 1 {
let mut child = stack.pop().expect("length is above one");
maven_normalize(&mut child);
stack
.last_mut()
.expect("stack is never emptied")
.push(MavenItem::List(child));
}
let mut items = stack.pop().expect("stack is never emptied");
maven_normalize(&mut items);
Some(items)
}
fn maven_open(stack: &mut Vec<Vec<MavenItem>>) -> Option<()> {
if stack.len() >= MAVEN_MAX_DEPTH {
return None;
}
stack.push(Vec::new());
Some(())
}
fn maven_item(digits: bool, text: &str) -> MavenItem {
if digits {
MavenItem::Num(text.trim_start_matches('0').to_string())
} else {
MavenItem::Qual(maven_qualifier(text, false))
}
}
fn maven_qualifier(text: &str, followed_by_digit: bool) -> String {
let lower = text.to_lowercase();
if followed_by_digit {
match lower.as_str() {
"a" => return "alpha".to_string(),
"b" => return "beta".to_string(),
"m" => return "milestone".to_string(),
_ => {}
}
}
match lower.as_str() {
"ga" | "final" | "release" => String::new(),
"cr" => "rc".to_string(),
_ => lower,
}
}
fn maven_normalize(items: &mut Vec<MavenItem>) {
let mut i = items.len();
while i > 0 {
i -= 1;
if items[i].is_null() {
items.remove(i);
} else if !matches!(items[i], MavenItem::List(_)) {
break;
}
}
}
fn maven_list_cmp(a: &[MavenItem], b: &[MavenItem]) -> Ordering {
for i in 0..a.len().max(b.len()) {
let ord = match (a.get(i), b.get(i)) {
(Some(x), Some(y)) => maven_item_cmp(x, y),
(Some(x), None) => maven_null_cmp(x),
(None, Some(y)) => maven_null_cmp(y).reverse(),
(None, None) => Ordering::Equal,
};
if ord != Ordering::Equal {
return ord;
}
}
Ordering::Equal
}
fn maven_item_cmp(a: &MavenItem, b: &MavenItem) -> Ordering {
match (a, b) {
(MavenItem::Num(x), MavenItem::Num(y)) => x.len().cmp(&y.len()).then_with(|| x.cmp(y)),
(MavenItem::Qual(x), MavenItem::Qual(y)) => {
maven_qualifier_rank(x).cmp(&maven_qualifier_rank(y))
}
(MavenItem::List(x), MavenItem::List(y)) => maven_list_cmp(x, y),
(MavenItem::Num(_), _) => Ordering::Greater,
(_, MavenItem::Num(_)) => Ordering::Less,
(MavenItem::List(_), MavenItem::Qual(_)) => Ordering::Greater,
(MavenItem::Qual(_), MavenItem::List(_)) => Ordering::Less,
}
}
fn maven_null_cmp(item: &MavenItem) -> Ordering {
match item {
MavenItem::Num(digits) => {
if digits.is_empty() {
Ordering::Equal
} else {
Ordering::Greater
}
}
MavenItem::Qual(value) => maven_qualifier_rank(value).cmp(&maven_qualifier_rank("")),
MavenItem::List(items) => items
.iter()
.map(maven_null_cmp)
.find(|ord| *ord != Ordering::Equal)
.unwrap_or(Ordering::Equal),
}
}
fn maven_qualifier_rank(q: &str) -> (usize, &str) {
const KNOWN: [&str; 7] = ["alpha", "beta", "milestone", "rc", "snapshot", "", "sp"];
match KNOWN.iter().position(|known| *known == q) {
Some(i) => (i, ""),
None => (KNOWN.len(), q),
}
}
pub fn is_version_downgrade(old_ver: &str, new_ver: &str) -> bool {
Version::parse_lenient(old_ver).is_downgrade(&Version::parse_lenient(new_ver))
}
pub fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
Version::parse_lenient(a).partial_cmp_lenient(&Version::parse_lenient(b))
}
pub fn is_version_downgrade_for_ecosystem(
ecosystem: Option<&str>,
old_ver: &str,
new_ver: &str,
) -> bool {
Version::parse_for_ecosystem(ecosystem, old_ver)
.is_downgrade(&Version::parse_for_ecosystem(ecosystem, new_ver))
}
pub fn compare_versions_for_ecosystem(
ecosystem: Option<&str>,
a: &str,
b: &str,
) -> Option<Ordering> {
Version::parse_for_ecosystem(ecosystem, a)
.partial_cmp_lenient(&Version::parse_for_ecosystem(ecosystem, b))
}
#[cfg(test)]
mod tests {
use super::*;
fn maven_cmp(a: &str, b: &str) -> Ordering {
super::maven_cmp(a, b).unwrap_or_else(|| panic!("{a} vs {b} exceeds the depth cap"))
}
#[test]
fn parse_standard_semver() {
let v = Version::parse_lenient("1.2.3");
assert_eq!(v, Version::Semver(semver::Version::new(1, 2, 3)));
}
#[test]
fn parse_v_prefix() {
assert_eq!(
Version::parse_lenient("v1.2.3"),
Version::Semver(semver::Version::new(1, 2, 3))
);
assert_eq!(
Version::parse_lenient("V1.2.3"),
Version::Semver(semver::Version::new(1, 2, 3))
);
}
#[test]
fn parse_two_parts() {
assert_eq!(
Version::parse_lenient("1.2"),
Version::Semver(semver::Version::new(1, 2, 0))
);
}
#[test]
fn parse_single_part() {
assert_eq!(
Version::parse_lenient("42"),
Version::Semver(semver::Version::new(42, 0, 0))
);
}
#[test]
fn parse_prerelease() {
let v = Version::parse_lenient("1.2.3-beta.1");
match v {
Version::Semver(sv) => {
assert_eq!(sv.major, 1);
assert_eq!(sv.minor, 2);
assert_eq!(sv.patch, 3);
assert!(!sv.pre.is_empty());
}
other => panic!("expected Semver, got {:?}", other),
}
}
#[test]
fn parse_build_metadata() {
let v = Version::parse_lenient("1.2.3+build.456");
match v {
Version::Semver(sv) => {
assert_eq!((sv.major, sv.minor, sv.patch), (1, 2, 3));
assert!(!sv.build.is_empty());
}
other => panic!("expected Semver, got {:?}", other),
}
}
#[test]
fn parse_prerelease_and_build() {
let v = Version::parse_lenient("1.0.0-alpha.1+build.789");
match v {
Version::Semver(sv) => {
assert_eq!(sv.major, 1);
assert!(!sv.pre.is_empty());
assert!(!sv.build.is_empty());
}
other => panic!("expected Semver, got {:?}", other),
}
}
#[test]
fn parse_v_prefix_two_parts() {
assert_eq!(
Version::parse_lenient("v1.2"),
Version::Semver(semver::Version::new(1, 2, 0))
);
}
#[test]
fn parse_v_prefix_single_part() {
assert_eq!(
Version::parse_lenient("v5"),
Version::Semver(semver::Version::new(5, 0, 0))
);
}
#[test]
fn parse_v_prefix_prerelease() {
let v = Version::parse_lenient("v2.0.0-rc.1");
match v {
Version::Semver(sv) => {
assert_eq!(sv.major, 2);
assert!(!sv.pre.is_empty());
}
other => panic!("expected Semver, got {:?}", other),
}
}
#[test]
fn parse_zero_version() {
assert_eq!(
Version::parse_lenient("0.0.0"),
Version::Semver(semver::Version::new(0, 0, 0))
);
}
#[test]
fn parse_large_numbers() {
assert_eq!(
Version::parse_lenient("999.888.777"),
Version::Semver(semver::Version::new(999, 888, 777))
);
}
#[test]
fn parse_single_zero() {
assert_eq!(
Version::parse_lenient("0"),
Version::Semver(semver::Version::new(0, 0, 0))
);
}
#[test]
fn parse_four_part_is_numeric() {
assert_eq!(
Version::parse_lenient("1.2.3.4"),
Version::Numeric(vec![1, 2, 3, 4])
);
}
#[test]
fn parse_date_based_is_numeric() {
assert_eq!(
Version::parse_lenient("2024.01.15"),
Version::Numeric(vec![2024, 1, 15])
);
}
#[test]
fn parse_v_prefix_four_part_is_numeric() {
assert_eq!(
Version::parse_lenient("v1.2.3.4"),
Version::Numeric(vec![1, 2, 3, 4])
);
assert_eq!(
Version::parse_lenient("V1.2.3.4"),
Version::Numeric(vec![1, 2, 3, 4])
);
}
#[test]
fn parse_v_prefix_date_based_is_numeric() {
assert_eq!(
Version::parse_lenient("v2024.01.15"),
Version::Numeric(vec![2024, 1, 15])
);
}
#[test]
fn parse_leading_zeros_is_numeric() {
assert_eq!(
Version::parse_lenient("01.02.03"),
Version::Numeric(vec![1, 2, 3])
);
}
#[test]
fn parse_non_numeric_is_opaque() {
assert_eq!(Version::parse_lenient("abc"), Version::Opaque("abc".into()));
assert_eq!(
Version::parse_lenient("foo.bar.baz"),
Version::Opaque("foo.bar.baz".into())
);
}
#[test]
fn parse_whitespace_is_opaque() {
assert!(matches!(
Version::parse_lenient(" 1.2.3"),
Version::Opaque(_)
));
assert!(matches!(
Version::parse_lenient("1.2.3 "),
Version::Opaque(_)
));
}
#[test]
fn parse_empty_string_is_opaque() {
assert!(matches!(Version::parse_lenient(""), Version::Opaque(_)));
}
#[test]
fn downgrade_semver() {
assert!(is_version_downgrade("2.0.0", "1.5.0"));
assert!(is_version_downgrade("1.1.0", "1.0.0"));
assert!(is_version_downgrade("1.0.1", "1.0.0"));
}
#[test]
fn upgrade_semver_not_flagged() {
assert!(!is_version_downgrade("1.0.0", "1.1.0"));
assert!(!is_version_downgrade("1.0.0", "2.0.0"));
assert!(!is_version_downgrade("1.0.0", "1.0.1"));
}
#[test]
fn equal_semver_not_flagged() {
assert!(!is_version_downgrade("1.0.0", "1.0.0"));
}
#[test]
fn downgrade_v_prefix() {
assert!(is_version_downgrade("v2.0.0", "v1.0.0"));
assert!(!is_version_downgrade("v1.0.0", "v2.0.0"));
}
#[test]
fn downgrade_prerelease() {
assert!(is_version_downgrade("1.0.0", "1.0.0-rc1"));
assert!(!is_version_downgrade("1.0.0-rc1", "1.0.0"));
}
#[test]
fn downgrade_build_metadata() {
assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.2"));
assert!(!is_version_downgrade("1.0.0+build.2", "1.0.0+build.1"));
assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.1"));
assert!(!is_version_downgrade("1.0.0+c144a98", "1.0.0+bc17664"));
assert!(!is_version_downgrade("1.0.0+build.10", "1.0.0+build.9"));
}
#[test]
fn downgrade_mixed_v_prefix() {
assert!(is_version_downgrade("v2.0.0", "1.0.0"));
assert!(is_version_downgrade("2.0.0", "v1.0.0"));
assert!(!is_version_downgrade("v1.0.0", "2.0.0"));
assert!(!is_version_downgrade("1.0.0", "v2.0.0"));
}
#[test]
fn downgrade_prerelease_ordering() {
assert!(is_version_downgrade("1.0.0-beta.1", "1.0.0-alpha.1"));
assert!(is_version_downgrade("1.0.0-rc.1", "1.0.0-beta.1"));
assert!(!is_version_downgrade("1.0.0-alpha.1", "1.0.0-beta.1"));
assert!(!is_version_downgrade("1.0.0-beta.1", "1.0.0-rc.1"));
}
#[test]
fn downgrade_prerelease_numeric_ordering() {
assert!(is_version_downgrade("1.0.0-rc.2", "1.0.0-rc.1"));
assert!(!is_version_downgrade("1.0.0-rc.1", "1.0.0-rc.2"));
}
#[test]
fn downgrade_equal_with_v_prefix() {
assert!(!is_version_downgrade("v1.0.0", "v1.0.0"));
}
#[test]
fn downgrade_padded_two_part() {
assert!(is_version_downgrade("1.2", "1.1"));
assert!(!is_version_downgrade("1.1", "1.2"));
assert!(!is_version_downgrade("1.2", "1.2"));
}
#[test]
fn downgrade_padded_single_part() {
assert!(is_version_downgrade("2", "1"));
assert!(!is_version_downgrade("1", "2"));
assert!(!is_version_downgrade("5", "5"));
}
#[test]
fn downgrade_mixed_part_counts_semver() {
assert!(is_version_downgrade("2.0", "1.9.9"));
assert!(!is_version_downgrade("1.9.9", "2.0"));
}
#[test]
fn downgrade_v_prefix_two_part() {
assert!(is_version_downgrade("v2.0", "v1.0"));
assert!(!is_version_downgrade("v1.0", "v2.0"));
}
#[test]
fn downgrade_four_part() {
assert!(is_version_downgrade("1.2.3.4", "1.2.3.3"));
assert!(!is_version_downgrade("1.2.3.3", "1.2.3.4"));
assert!(!is_version_downgrade("1.2.3.4", "1.2.3.4"));
}
#[test]
fn downgrade_date_based() {
assert!(is_version_downgrade("2024.01.15", "2023.12.01"));
assert!(!is_version_downgrade("2023.12.01", "2024.01.15"));
}
#[test]
fn downgrade_v_prefix_four_part() {
assert!(is_version_downgrade("v1.2.3.4", "v1.2.3.3"));
assert!(!is_version_downgrade("v1.2.3.3", "v1.2.3.4"));
assert!(!is_version_downgrade("v1.2.3.4", "v1.2.3.4"));
}
#[test]
fn downgrade_v_prefix_date_based() {
assert!(is_version_downgrade("v2024.01.15", "v2023.12.01"));
assert!(!is_version_downgrade("v2023.12.01", "v2024.01.15"));
}
#[test]
fn downgrade_non_numeric_not_flagged() {
assert!(!is_version_downgrade("abc", "def"));
assert!(!is_version_downgrade("foo.bar", "foo.baz"));
}
#[test]
fn downgrade_numeric_unequal_length() {
assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
}
#[test]
fn downgrade_large_major_numeric_equal() {
assert!(!is_version_downgrade("2024.1.15", "2024.1.15"));
}
#[test]
fn downgrade_semver_vs_four_part() {
assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
}
#[test]
fn downgrade_v_prefix_vs_four_part() {
assert!(!is_version_downgrade("v1.2.3", "1.2.3.4"));
assert!(is_version_downgrade("1.2.3.4", "v1.2.3"));
}
#[test]
fn downgrade_empty_strings() {
assert!(!is_version_downgrade("", "1.0.0"));
assert!(!is_version_downgrade("1.0.0", ""));
assert!(!is_version_downgrade("", ""));
}
#[test]
fn parse_epoch_is_deb() {
assert!(matches!(
Version::parse_lenient("2:1.0"),
Version::Deb { .. }
));
assert!(matches!(
Version::parse_lenient("1:9.0"),
Version::Deb { .. }
));
}
#[test]
fn parse_revision_is_deb() {
assert!(matches!(
Version::parse_lenient("5.1-3"),
Version::Deb { .. }
));
}
#[test]
fn parse_deb_fields() {
match Version::parse_lenient("2:1.2.3-4") {
Version::Deb {
epoch,
upstream,
revision,
} => {
assert_eq!(epoch, 2);
assert_eq!(upstream, "1.2.3");
assert_eq!(revision, "4");
}
other => panic!("expected Deb, got {:?}", other),
}
}
#[test]
fn parse_deb_revision_splits_at_last_hyphen() {
match Version::parse_lenient("1.2-2-1") {
Version::Deb {
epoch,
upstream,
revision,
} => {
assert_eq!(epoch, 0);
assert_eq!(upstream, "1.2-2");
assert_eq!(revision, "1");
}
other => panic!("expected Deb, got {:?}", other),
}
}
#[test]
fn parse_pep440_epoch_is_pep440() {
match Version::parse_lenient("1!2.0") {
Version::Pep440(p) => {
assert_eq!(p.epoch, 1);
assert_eq!(p.release, vec![2, 0]);
assert_eq!(p.pre, None);
}
other => panic!("expected Pep440, got {:?}", other),
}
}
#[test]
fn parse_deb_keeps_epoch_bang_forms_it_declines() {
match Version::parse_lenient("1!2.0-3") {
Version::Deb {
epoch,
upstream,
revision,
} => {
assert_eq!(epoch, 1);
assert_eq!(upstream, "2.0");
assert_eq!(revision, "3");
}
other => panic!("expected Deb, got {:?}", other),
}
}
#[test]
fn parse_tilde_prerelease_is_deb() {
assert!(matches!(
Version::parse_lenient("1.0.0~rc1"),
Version::Deb { .. }
));
}
#[test]
fn parse_codename_stays_opaque() {
assert!(matches!(
Version::parse_lenient("focal-1"),
Version::Opaque(_)
));
assert!(matches!(
Version::parse_lenient("stable"),
Version::Opaque(_)
));
assert!(matches!(
Version::parse_lenient("1:stable"),
Version::Opaque(_)
));
}
#[test]
fn downgrade_epoch() {
assert!(is_version_downgrade("2:1.0", "1:9.0"));
assert!(!is_version_downgrade("1:9.0", "2:1.0"));
assert!(!is_version_downgrade("1:1.0", "2:0.1"));
assert!(is_version_downgrade("2:0.1", "1:1.0"));
}
#[test]
fn downgrade_epoch_equal_upstream() {
assert!(is_version_downgrade("1:2.0", "1:1.0"));
assert!(!is_version_downgrade("1:1.0", "1:2.0"));
assert!(!is_version_downgrade("1:1.0", "1:1.0"));
}
#[test]
fn downgrade_implicit_epoch_zero() {
assert!(!is_version_downgrade("5.1-1", "1:0.1-1"));
assert!(is_version_downgrade("1:0.1-1", "0:0.1-1"));
}
#[test]
fn downgrade_revision() {
assert!(is_version_downgrade("5.1-3", "5.1-2"));
assert!(!is_version_downgrade("5.1-2", "5.1-3"));
assert!(!is_version_downgrade("5.1-2", "5.1-2"));
}
#[test]
fn downgrade_upstream_trumps_revision() {
assert!(is_version_downgrade("1:5.2-1", "1:5.1-1"));
assert!(!is_version_downgrade("1:5.1-9", "1:5.2-1"));
}
#[test]
fn downgrade_absent_revision_equals_zero() {
assert!(is_version_downgrade("1:2.0-1", "1:2.0"));
assert!(!is_version_downgrade("1:2.0", "1:2.0-1"));
}
#[test]
fn downgrade_rpm_release_with_epoch() {
assert!(is_version_downgrade("1:1.2.3-2.el8", "1:1.2.3-1.el8"));
assert!(!is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-2.el8"));
assert!(is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-1.el7"));
assert!(!is_version_downgrade("1:1.2.3-1.el7", "1:1.2.3-1.el8"));
}
#[test]
fn downgrade_deb_numeric_not_lexical() {
assert!(is_version_downgrade("1.10-1", "1.9-1"));
assert!(!is_version_downgrade("1.9-1", "1.10-1"));
}
#[test]
fn downgrade_deb_tilde_prerelease() {
assert!(is_version_downgrade("1.0.0~rc2", "1.0.0~rc1"));
assert!(!is_version_downgrade("1.0.0~rc1", "1.0.0~rc2"));
assert!(is_version_downgrade("1:1.0~rc1", "1:1.0~beta1"));
}
#[test]
fn downgrade_real_world_deb() {
assert!(is_version_downgrade(
"1:1.1.1f-1ubuntu2.16",
"1:1.1.1f-1ubuntu2.15"
));
assert!(!is_version_downgrade(
"1:1.1.1f-1ubuntu2.15",
"1:1.1.1f-1ubuntu2.16"
));
}
#[test]
fn downgrade_deb_opaque_not_flagged() {
assert!(!is_version_downgrade("focal", "bionic"));
assert!(!is_version_downgrade("1:stable", "1:oldstable"));
}
#[test]
fn downgrade_deb_vs_semver_not_flagged() {
assert!(!is_version_downgrade("2:1.0", "1.0.0"));
assert!(!is_version_downgrade("1.0.0", "2:1.0"));
}
#[test]
fn deb_canonical_ordering_vectors() {
use Ordering::{Equal, Greater, Less};
let cases = [
("1:1.0~~", "1:1.0~~a", Less),
("1:1.0~~a", "1:1.0~", Less),
("1:1.0~", "1:1.0", Less),
("1:1.0", "1:1.0a", Less),
("1:1.0~rc1", "1:1.0", Less),
("1:1.0~rc1", "1:1.0~rc2", Less),
("1:1.10", "1:1.9", Greater),
("1:1.0", "1:1.00", Equal),
("1:1.01", "1:1.1", Equal),
("1:1.a", "1:1.1", Greater),
("1:1.0a", "1:1.01", Less),
("2:0.1", "1:9.9", Greater),
("1:5.2-1", "1:5.1-9", Greater),
("1:2.0", "1:2.0-0", Equal),
("1:2.0-10", "1:2.0-9", Greater),
];
for (a, b, expected) in cases {
assert!(
matches!(Version::parse_lenient(a), Version::Deb { .. }),
"{a} no longer parses as Deb"
);
assert!(
matches!(Version::parse_lenient(b), Version::Deb { .. }),
"{b} no longer parses as Deb"
);
match expected {
Less => {
assert!(is_version_downgrade(b, a), "expected {a} < {b}");
assert!(!is_version_downgrade(a, b), "expected {a} < {b}");
}
Greater => {
assert!(is_version_downgrade(a, b), "expected {a} > {b}");
assert!(!is_version_downgrade(b, a), "expected {a} > {b}");
}
Equal => {
assert!(!is_version_downgrade(a, b), "expected {a} == {b}");
assert!(!is_version_downgrade(b, a), "expected {a} == {b}");
}
}
}
}
#[test]
fn compare_orders_comparable_variant_pairs() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("2.0.0", "1.5.0", Greater),
("1.0.0", "1.0.0", Equal),
("1.2.3.4", "1.2.3.3", Greater),
("1.2.3", "1.2.3.4", Less),
("2:1.0-3", "1:9.0-1", Greater),
("5.1-3", "5.1-3", Equal),
] {
assert_eq!(compare_versions(a, b), Some(expected), "{a} vs {b}");
assert_eq!(
compare_versions(b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
}
#[test]
fn compare_leaves_opaque_and_mixed_variants_unordered() {
for (a, b) in [
("deadbeef", "1.0.0"),
("deadbeef", "cafebabe"),
("deadbeef", "deadbeef"),
("2:1.0-3", "1.0.0"),
("5.1-3", "5.1.0.0"),
] {
assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
assert_eq!(compare_versions(b, a), None, "{b} vs {a}");
}
}
#[test]
fn parse_pep440_suffixes() {
for s in [
"1.0rc1",
"1.0a1",
"1.0b1",
"1.0.dev1",
"1.0.post1",
"4.2.0rc1",
"1!1.0",
"1.0alpha1",
"1.0-rc-1",
"1.0_beta_2",
"1.0.RC1",
"2.0.post2.dev3",
"1.0rc1+ubuntu.1",
] {
assert!(
matches!(Version::parse_lenient(s), Version::Pep440(_)),
"{s} should parse as Pep440"
);
}
}
#[test]
fn parse_pep440_fields() {
match Version::parse_lenient("2!4.2.0.post3.dev7") {
Version::Pep440(p) => {
assert_eq!(p.epoch, 2);
assert_eq!(p.release, vec![4, 2, 0]);
assert_eq!(p.pre, None);
assert_eq!(p.post, Some(3));
assert_eq!(p.dev, Some(7));
assert!(p.local.is_empty());
}
other => panic!("expected Pep440, got {:?}", other),
}
}
#[test]
fn parse_pep440_leaves_other_formats_alone() {
for s in [
"1.2.3",
"v1.2",
"42",
"1.2.3-beta.1",
"1.0.0-alpha.1+build.789",
"0.0.0",
] {
assert!(
matches!(Version::parse_lenient(s), Version::Semver(_)),
"{s} should still be Semver"
);
}
for s in ["1.2.3.4", "2024.01.15", "01.02.03", "v1.2.3.4"] {
assert!(
matches!(Version::parse_lenient(s), Version::Numeric(_)),
"{s} should still be Numeric"
);
}
for s in [
"2:1.0",
"5.1-3",
"2:1.2.3-4",
"1.2-2-1",
"1.0.0~rc1",
"1:1.1.1f-1ubuntu2.16",
"1.0+ubuntu.1",
] {
assert!(
matches!(Version::parse_lenient(s), Version::Deb { .. }),
"{s} should still be Deb"
);
}
for s in ["abc", "foo.bar.baz", "focal-1", "stable", "1:stable", ""] {
assert!(
matches!(Version::parse_lenient(s), Version::Opaque(_)),
"{s} should still be Opaque"
);
}
}
fn assert_ascending(versions: &[&str]) {
for (i, a) in versions.iter().enumerate() {
for b in &versions[i + 1..] {
assert_eq!(
compare_versions(a, b),
Some(Ordering::Less),
"expected {a} < {b}"
);
assert_eq!(
compare_versions(b, a),
Some(Ordering::Greater),
"expected {b} > {a}"
);
assert!(is_version_downgrade(b, a), "expected {b} -> {a} downgrade");
assert!(!is_version_downgrade(a, b), "expected {a} -> {b} upgrade");
}
}
}
#[test]
fn pep440_release_cycle_ordering() {
assert_ascending(&[
"1.0.dev1",
"1.0a1",
"1.0a2",
"1.0b1",
"1.0rc1",
"1.0",
"1.0.post1",
"1.0.1",
]);
}
#[test]
fn pep440_dev_ordering_within_segments() {
assert_ascending(&["1.0.dev1", "1.0a1.dev1", "1.0a1", "1.0"]);
assert_ascending(&["1.0", "1.0.post1.dev1", "1.0.post1"]);
}
#[test]
fn pep440_epoch_ordering() {
assert_ascending(&["2.0", "1!1.0", "1!2.0", "2!0.1"]);
}
#[test]
fn pep440_spelling_aliases() {
for (canonical, aliases) in [
("1.0a1", ["1.0alpha1", "1.0.ALPHA.1", "1.0-a-1"]),
("1.0b1", ["1.0beta1", "1.0.BETA.1", "1.0_b_1"]),
("1.0rc1", ["1.0c1", "1.0pre1", "1.0preview1"]),
("1.0.post1", ["1.0rev1", "1.0r1", "1.0-POST-1"]),
] {
for alias in aliases {
assert_eq!(
compare_versions(canonical, alias),
Some(Ordering::Equal),
"{alias} should normalize to {canonical}"
);
}
}
assert_eq!(compare_versions("1.0rc", "1.0rc0"), Some(Ordering::Equal));
assert_eq!(compare_versions("1.0rc", "1.0rc1"), Some(Ordering::Less));
}
#[test]
fn pep440_compares_against_semver_and_numeric() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("4.2.0rc1", "4.2.0", Less),
("1.0rc1", "1.0", Less),
("1.0.dev1", "1.0", Less),
("1.0", "1.0.post1", Less),
("1!1.0", "2.0", Greater),
("1.0.post1", "1.0.1", Less),
("1.0.post0", "1.0.0.post0", Equal),
("1.2.3.4rc1", "1.2.3.4", Less),
("1.2.3.4.dev1", "1.2.3.3", Greater),
("1.0.0-rc1", "1.0rc2", Less),
("1.0.0-alpha.1", "1.0b1", Less),
] {
assert_eq!(compare_versions(a, b), Some(expected), "{a} vs {b}");
assert_eq!(
compare_versions(b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
}
#[test]
fn pep440_local_version_ordering() {
assert_ascending(&["1.0rc1", "1.0rc1+ubuntu", "1.0rc1+ubuntu.1"]);
assert_ascending(&["1.0rc1+abc", "1.0rc1+1"]);
assert_ascending(&["1.0rc1+build.9", "1.0rc1+build.10"]);
assert_eq!(
compare_versions("1.0rc1+UBUNTU-1", "1.0rc1+ubuntu.1"),
Some(Ordering::Equal)
);
}
#[test]
fn pep440_stays_uncomparable_against_deb_and_opaque() {
for (a, b) in [
("1.0rc1", "2:1.0"),
("1.0rc1", "1.0.0~rc1"),
("1.0rc1", "deadbeef"),
("1.0.0-foo.bar", "1.0rc1"),
] {
assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
assert_eq!(compare_versions(b, a), None, "{b} vs {a}");
}
}
#[test]
fn downgrade_pep440_gate() {
assert!(!is_version_downgrade("1.0.dev1", "1.0a1"));
assert!(is_version_downgrade("1.0a1", "1.0.dev1"));
assert!(!is_version_downgrade("4.2.0rc1", "4.2.0"));
assert!(is_version_downgrade("4.2.0", "4.2.0rc1"));
assert!(!is_version_downgrade("1.0", "1.0.post1"));
assert!(is_version_downgrade("1.0.post1", "1.0"));
assert!(!is_version_downgrade("1.0rc1", "1.0rc1"));
}
fn letter_suffixed(pattern: &str) -> Vec<String> {
('a'..='z')
.map(|c| pattern.replace('@', &c.to_string()))
.collect()
}
#[test]
fn bare_single_letter_suffix_parses_as_deb() {
for pattern in ["1.0.2@", "2024@", "1.1.1@-r0", "1.0@"] {
for s in letter_suffixed(pattern) {
assert!(
matches!(Version::parse_lenient(&s), Version::Deb { .. }),
"{s} should parse as Deb"
);
}
}
}
#[test]
fn letter_suffixed_versions_order_across_the_alphabet() {
for pattern in ["1.0.2@", "2024@", "1.1.1@-r0"] {
let versions = letter_suffixed(pattern);
let refs: Vec<&str> = versions.iter().map(String::as_str).collect();
assert_ascending(&refs);
}
assert_ascending(&["2024a", "2024h", "2025a", "2025b"]);
assert_ascending(&["1.1.1a-r0", "1.1.1d-r0", "1.1.1d-r1", "1.1.1w-r0"]);
}
#[test]
fn bare_keyword_longer_than_one_letter_stays_pep440() {
for s in [
"1.0rc",
"1.0.dev",
"1.0.post",
"1.0alpha",
"1.0beta",
"1.0pre",
"1.0preview",
"1.0rev",
"2.0.post2.dev3",
] {
assert!(
matches!(Version::parse_lenient(s), Version::Pep440(_)),
"{s} should still parse as Pep440"
);
}
}
#[test]
fn single_letter_alias_with_a_number_stays_pep440() {
for s in [
"1.0a1",
"1.0b1",
"1.0c1",
"1.0r1",
"1.0a0",
"1.0-a-1",
"1.0_b_2",
"1.0.c.3",
"1!2.0a1",
"1.0a1+ubuntu.1",
] {
assert!(
matches!(Version::parse_lenient(s), Version::Pep440(_)),
"{s} should still parse as Pep440"
);
}
}
#[test]
fn downgrade_letter_suffix_gate() {
assert!(is_version_downgrade("1.0.2d", "1.0.2c"));
assert!(!is_version_downgrade("1.0.2c", "1.0.2d"));
assert!(is_version_downgrade("2025a", "2024h"));
assert!(!is_version_downgrade("2024h", "2025a"));
assert!(is_version_downgrade("1.1.1d-r0", "1.1.1a-r0"));
assert!(!is_version_downgrade("1.1.1a-r0", "1.1.1d-r0"));
assert!(!is_version_downgrade("1.0.2a", "1.0.2a"));
assert_eq!(compare_versions("1.0.2", "1.0.2a"), None);
assert!(!is_version_downgrade("1.0.2", "1.0.2a"));
assert!(!is_version_downgrade("1.0.2a", "1.0.2"));
}
#[test]
fn downgrade_agrees_with_compare() {
use Ordering::Greater;
for (a, b) in [
("2.0.0", "1.5.0"),
("1.0.0", "2.0.0"),
("1.0.0", "1.0.0"),
("2024.01.15", "2024.01.14"),
("2:1.0-3", "1:9.0-1"),
("1.0.0+build.10", "1.0.0+build.9"),
("deadbeef", "1.0.0"),
] {
assert_eq!(
is_version_downgrade(a, b),
compare_versions(a, b) == Some(Greater),
"{a} -> {b}"
);
}
}
const ECOSYSTEM_CORPUS: &[&str] = &[
"1",
"1.0",
"1.0.0",
"1.2.3",
"v1.2.3",
"2024.01.15",
"1.2.3.4",
"1.0.0-alpha.1",
"1.0.0-alpha.2",
"1.0.0-rc.1",
"1.0.0+build.9",
"1.0.0-foo.bar",
"4.2.0rc1",
"1.0.dev1",
"1.0a1",
"1!1.0",
"1.0.post1",
"1.0.2a",
"1.2.3-1",
"1.2.3-2",
"1.2.3-1ubuntu2",
"1.2.3-1build1",
"1.2.3-1+deb11u1",
"1.0~rc1",
"2:1.0-3",
"4.4.2-2.el7_9",
"deadbeef",
"",
];
#[test]
fn unknown_ecosystem_parses_exactly_like_parse_lenient() {
for eco in [None, Some("npm"), Some("cargo"), Some("golang")] {
for s in ECOSYSTEM_CORPUS {
assert_eq!(
Version::parse_for_ecosystem(eco, s),
Version::parse_lenient(s),
"{eco:?} / {s}"
);
}
}
}
#[test]
fn unknown_ecosystem_orders_exactly_like_the_string_only_path() {
for eco in [None, Some("npm"), Some("cargo"), Some("golang")] {
for a in ECOSYSTEM_CORPUS {
for b in ECOSYSTEM_CORPUS {
assert_eq!(
compare_versions_for_ecosystem(eco, a, b),
compare_versions(a, b),
"{eco:?} / {a} vs {b}"
);
assert_eq!(
is_version_downgrade_for_ecosystem(eco, a, b),
is_version_downgrade(a, b),
"{eco:?} / {a} -> {b}"
);
}
}
}
}
#[test]
fn semver_prereleases_keep_their_semver_reading() {
for eco in [None, Some("npm"), Some("cargo")] {
assert_eq!(
compare_versions_for_ecosystem(eco, "1.0.0-alpha.1", "1.0.0"),
Some(Ordering::Less),
"{eco:?}"
);
assert!(!is_version_downgrade_for_ecosystem(
eco,
"1.0.0-alpha.1",
"1.0.0"
));
assert!(is_version_downgrade_for_ecosystem(
eco,
"1.0.0",
"1.0.0-alpha.1"
));
}
}
#[test]
fn deb_ecosystem_parses_as_deb() {
assert_eq!(
Version::parse_for_ecosystem(Some("deb"), "1.2.3-1ubuntu2"),
Version::Deb {
epoch: 0,
upstream: "1.2.3".into(),
revision: "1ubuntu2".into(),
}
);
assert!(matches!(
Version::parse_for_ecosystem(Some("deb"), "1.2.3"),
Version::Deb { .. }
));
assert!(matches!(
Version::parse_for_ecosystem(Some("deb"), "1.0.0-alpha.1"),
Version::Deb { .. }
));
}
#[test]
fn deb_ecosystem_match_ignores_case() {
assert_eq!(
Version::parse_for_ecosystem(Some("DEB"), "1.2.3-1ubuntu2"),
Version::parse_for_ecosystem(Some("deb"), "1.2.3-1ubuntu2")
);
}
#[test]
fn deb_ecosystem_does_not_retry_a_non_deb_string_as_semver() {
assert_eq!(
Version::parse_for_ecosystem(Some("deb"), "4.4.2-2.el7_9"),
Version::Opaque("4.4.2-2.el7_9".into())
);
assert!(matches!(
Version::parse_for_ecosystem(Some("deb"), "v1.2.3-1ubuntu2"),
Version::Deb { .. }
));
}
#[test]
fn deb_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
use Ordering::{Greater, Less};
for (a, b, expected) in [
("v1.2.3", "v1.2.4", Less),
("v1.2.10", "v1.2.9", Greater),
("V1.2.3", "V1.2.4", Less),
("v1.2.3-1ubuntu2", "v1.2.3-2", Less),
] {
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), a, b),
Some(expected),
"{a} vs {b}"
);
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
assert!(is_version_downgrade_for_ecosystem(
Some("deb"),
"v1.2.4",
"v1.2.3"
));
}
#[test]
fn deb_ecosystem_orders_revisions_the_way_dpkg_does() {
use Ordering::{Greater, Less};
for (a, b, expected) in [
("1.2.3-1ubuntu2", "1.2.3-2", Less),
("1.2.3-1build1", "1.2.3-2", Less),
("1.2.3-2ubuntu0.1", "1.2.3-3", Less),
("1.2.3-1+deb11u1", "1.2.3-2", Less),
("1.2.3-1+deb11u1", "1.2.3-1+deb11u2", Less),
("1.2.3-1ubuntu2", "1.2.3-1ubuntu1", Greater),
("1.2.3-1", "1.2.3-10", Less),
("1.2.3", "1.2.3-1", Less),
("1.0-1", "1.0", Greater),
("1.0~rc1", "1.0", Less),
("1.0~rc1-1", "1.0-1", Less),
("1.2.3-1~bpo11+1", "1.2.3-1", Less),
("2:1.0-1", "10.0-1", Greater),
("1.1.1n-0+deb11u5", "1.1.1o-1", Less),
] {
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), a, b),
Some(expected),
"{a} vs {b}"
);
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
}
#[test]
fn deb_ecosystem_clears_the_false_downgrade_the_string_only_path_reports() {
assert!(!is_version_downgrade_for_ecosystem(
Some("deb"),
"1.2.3-1ubuntu2",
"1.2.3-2"
));
assert!(is_version_downgrade("1.2.3-1ubuntu2", "1.2.3-2"));
assert!(is_version_downgrade_for_ecosystem(
Some("deb"),
"1.2.3-2",
"1.2.3-1ubuntu2"
));
}
#[test]
fn deb_ecosystem_catches_the_downgrades_the_string_only_path_passed() {
for (old, new) in [
("1.2.3-2", "1.2.3-1ubuntu2"),
("1.2.3-3", "1.2.3-2ubuntu0.1"),
("1.2.3-1+deb11u2", "1.2.3-1+deb11u1"),
] {
assert!(!is_version_downgrade(old, new), "{old} -> {new}");
assert!(
is_version_downgrade_for_ecosystem(Some("deb"), old, new),
"{old} -> {new}"
);
}
}
#[test]
fn deb_ecosystem_orders_plus_revisions_the_string_only_path_read_as_equal() {
assert_eq!(
compare_versions("1.2.3-1+deb11u1", "1.2.3-1+deb11u2"),
Some(Ordering::Equal)
);
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), "1.2.3-1+deb11u1", "1.2.3-1+deb11u2"),
Some(Ordering::Less)
);
}
#[test]
fn deb_ecosystem_makes_previously_uncomparable_pairs_comparable() {
for (a, b) in [("1.0-1", "1.0"), ("1.0~rc1", "1.0"), ("1.0.2a", "1.0.2")] {
assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
assert!(
compare_versions_for_ecosystem(Some("deb"), a, b).is_some(),
"{a} vs {b}"
);
}
}
#[test]
fn rpmvercmp_upstream_vectors() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("1.0", "1.0", Equal),
("1.0", "2.0", Less),
("2.0", "1.0", Greater),
("2.0.1", "2.0.1", Equal),
("2.0", "2.0.1", Less),
("2.0.1", "2.0", Greater),
("2.0.1a", "2.0.1a", Equal),
("2.0.1a", "2.0.1", Greater),
("2.0.1", "2.0.1a", Less),
("5.5p1", "5.5p1", Equal),
("5.5p1", "5.5p2", Less),
("5.5p2", "5.5p1", Greater),
("5.5p10", "5.5p10", Equal),
("5.5p1", "5.5p10", Less),
("5.5p10", "5.5p1", Greater),
("10xyz", "10.1xyz", Less),
("10.1xyz", "10xyz", Greater),
("xyz10", "xyz10", Equal),
("xyz10", "xyz10.1", Less),
("xyz10.1", "xyz10", Greater),
("xyz.4", "xyz.4", Equal),
("xyz.4", "8", Less),
("8", "xyz.4", Greater),
("xyz.4", "2", Less),
("2", "xyz.4", Greater),
("5.5p2", "5.6p1", Less),
("5.6p1", "5.5p2", Greater),
("5.6p1", "6.5p1", Less),
("6.5p1", "5.6p1", Greater),
("6.0.rc1", "6.0", Greater),
("6.0", "6.0.rc1", Less),
("10b2", "10a1", Greater),
("10a2", "10b2", Less),
("1.0aa", "1.0aa", Equal),
("1.0a", "1.0aa", Less),
("1.0aa", "1.0a", Greater),
("10.0001", "10.0001", Equal),
("10.0001", "10.1", Equal),
("10.1", "10.0001", Equal),
("10.0001", "10.0039", Less),
("10.0039", "10.0001", Greater),
("4.999.9", "5.0", Less),
("5.0", "4.999.9", Greater),
("20101121", "20101121", Equal),
("20101121", "20101122", Less),
("20101122", "20101121", Greater),
("2_0", "2_0", Equal),
("2.0", "2_0", Equal),
("2_0", "2.0", Equal),
("a", "a", Equal),
("a+", "a+", Equal),
("a+", "a_", Equal),
("a_", "a+", Equal),
("+a", "+a", Equal),
("+a", "_a", Equal),
("_a", "+a", Equal),
("+_", "+_", Equal),
("_+", "+_", Equal),
("_+", "_+", Equal),
("+", "_", Equal),
("_", "+", Equal),
("1.0~rc1", "1.0~rc1", Equal),
("1.0~rc1", "1.0", Less),
("1.0", "1.0~rc1", Greater),
("1.0~rc1", "1.0~rc2", Less),
("1.0~rc2", "1.0~rc1", Greater),
("1.0~rc1~git123", "1.0~rc1~git123", Equal),
("1.0~rc1~git123", "1.0~rc1", Less),
("1.0~rc1", "1.0~rc1~git123", Greater),
("1.0^", "1.0^", Equal),
("1.0^", "1.0", Greater),
("1.0", "1.0^", Less),
("1.0^git1", "1.0^git1", Equal),
("1.0^git1", "1.0", Greater),
("1.0", "1.0^git1", Less),
("1.0^git1", "1.0^git2", Less),
("1.0^git2", "1.0^git1", Greater),
("1.0^git1", "1.01", Less),
("1.01", "1.0^git1", Greater),
("1.0^20160101", "1.0^20160101", Equal),
("1.0^20160101", "1.0.1", Less),
("1.0.1", "1.0^20160101", Greater),
("1.0^20160101^git1", "1.0^20160101^git1", Equal),
("1.0^20160102", "1.0^20160101^git1", Greater),
("1.0^20160101^git1", "1.0^20160102", Less),
("1.0~rc1^git1", "1.0~rc1^git1", Equal),
("1.0~rc1^git1", "1.0~rc1", Greater),
("1.0~rc1", "1.0~rc1^git1", Less),
("1.0^git1~pre", "1.0^git1~pre", Equal),
("1.0^git1", "1.0^git1~pre", Greater),
("1.0^git1~pre", "1.0^git1", Less),
("1b.fc17", "1b.fc17", Equal),
("1b.fc17", "1.fc17", Less),
("1.fc17", "1b.fc17", Greater),
("1g.fc17", "1g.fc17", Equal),
("1g.fc17", "1.fc17", Greater),
("1.fc17", "1g.fc17", Less),
("1.1.α", "1.1.α", Equal),
("1.1.α", "1.1.β", Equal),
("1.1.β", "1.1.α", Equal),
("1.1.αα", "1.1.α", Equal),
("1.1.α", "1.1.ββ", Equal),
("1.1.ββ", "1.1.αα", Equal),
] {
assert_eq!(rpmvercmp(a, b), expected, "{a} vs {b}");
}
}
#[test]
fn rpm_ecosystem_orders_epoch_then_version_then_release() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("2:1.0-1", "1:9.9-9", Greater),
("1.0-1", "0:1.0-1", Equal),
("1.1-1", "1.0-9", Greater),
("1.0-2", "1.0-10", Less),
("1.0-1.el8", "1.0-1.el9", Less),
("1.0-0", "1.0-1", Less),
("1.0", "1.0-0", Less),
] {
assert_eq!(
compare_versions_for_ecosystem(Some("rpm"), a, b),
Some(expected),
"{a} vs {b}"
);
assert_eq!(
compare_versions_for_ecosystem(Some("rpm"), b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
}
#[test]
fn rpm_and_deb_disagree_on_ordinary_versions() {
use Ordering::{Equal, Greater, Less};
for (a, b, deb, rpm) in [
("1.a", "1.1", Some(Greater), Some(Less)),
("1.fc35", "1.1", Some(Greater), Some(Less)),
("1.0", "1_0", None, Some(Equal)),
("1.0^20200101gitabc", "1.0", None, Some(Greater)),
("4.4.2-2.el7_9", "4.4.2-3.el7_9", None, Some(Less)),
] {
assert_eq!(
compare_versions_for_ecosystem(Some("deb"), a, b),
deb,
"deb: {a} vs {b}"
);
assert_eq!(
compare_versions_for_ecosystem(Some("rpm"), a, b),
rpm,
"rpm: {a} vs {b}"
);
}
}
#[test]
fn rpm_ecosystem_reverses_a_gate_the_deb_rules_fire_backwards() {
assert!(is_version_downgrade_for_ecosystem(
Some("deb"),
"1.a",
"1.1"
));
assert!(!is_version_downgrade_for_ecosystem(
Some("rpm"),
"1.a",
"1.1"
));
assert!(is_version_downgrade_for_ecosystem(
Some("rpm"),
"1.1",
"1.a"
));
}
#[test]
fn rpm_ecosystem_parses_as_rpm() {
assert_eq!(
Version::parse_for_ecosystem(Some("rpm"), "5.1.8-2.fc35"),
Version::Rpm {
epoch: 0,
version: "5.1.8".into(),
release: "2.fc35".into(),
}
);
assert_eq!(
Version::parse_for_ecosystem(Some("rpm"), "1:2.36.1-2.fc35"),
Version::Rpm {
epoch: 1,
version: "2.36.1".into(),
release: "2.fc35".into(),
}
);
assert_eq!(
Version::parse_for_ecosystem(Some("rpm"), "4.4.2-2.el7_9"),
Version::Rpm {
epoch: 0,
version: "4.4.2".into(),
release: "2.el7_9".into(),
}
);
assert!(matches!(
Version::parse_for_ecosystem(Some("rpm"), "1.2.3"),
Version::Rpm { .. }
));
assert!(matches!(
Version::parse_for_ecosystem(Some("rpm"), "1.0.0-alpha.1"),
Version::Rpm { .. }
));
}
#[test]
fn rpm_ecosystem_match_ignores_case() {
assert_eq!(
Version::parse_for_ecosystem(Some("RPM"), "5.1.8-2.fc35"),
Version::parse_for_ecosystem(Some("rpm"), "5.1.8-2.fc35")
);
}
#[test]
fn rpm_ecosystem_keeps_codenames_and_hashes_opaque() {
for s in ["deadbeef", "focal", "", "stable", "1.0 "] {
assert_eq!(
Version::parse_for_ecosystem(Some("rpm"), s),
Version::Opaque(s.to_string()),
"{s}"
);
}
}
#[test]
fn rpm_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
assert!(matches!(
Version::parse_for_ecosystem(Some("rpm"), "v1.2.3-1"),
Version::Rpm { .. }
));
assert_eq!(
compare_versions_for_ecosystem(Some("rpm"), "v1.2.3", "v1.2.4"),
Some(Ordering::Less)
);
}
#[test]
fn rpm_stays_uncomparable_against_every_other_parse_result() {
let rpm = Version::parse_for_ecosystem(Some("rpm"), "1.2.3-1");
for other in [
Version::parse_for_ecosystem(Some("deb"), "1.2.3-1"),
Version::parse_lenient("1.2.3"),
Version::parse_lenient("2024.01.15"),
Version::parse_lenient("4.2.0rc1"),
Version::parse_lenient("deadbeef"),
] {
assert_eq!(rpm.partial_cmp_lenient(&other), None, "{other:?}");
assert_eq!(other.partial_cmp_lenient(&rpm), None, "{other:?}");
}
}
#[test]
fn parse_lenient_never_produces_the_rpm_variant() {
for s in
ECOSYSTEM_CORPUS
.iter()
.copied()
.chain(["1.0^20200101", "1_0", "1.a", "5.1.8-2.fc35"])
{
assert!(
!matches!(Version::parse_lenient(s), Version::Rpm { .. }),
"{s}"
);
}
}
#[test]
fn maven_documented_ordering_examples() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("1", "1.1", Less),
("1-snapshot", "1", Less),
("1", "1-sp", Less),
("1-foo2", "1-foo10", Less),
("1.foo", "1-foo", Equal),
("1-foo", "1-1", Less),
("1-1", "1.1", Less),
("1.ga", "1-ga", Equal),
("1-ga", "1-0", Equal),
("1-0", "1_0", Equal),
("1_0", "1.0", Equal),
("1.0", "1", Equal),
("1-sp", "1-ga", Greater),
("1-sp.1", "1-ga.1", Greater),
("1-sp-1", "1-ga-1", Less),
("1-a1", "1-alpha-1", Equal),
("1.0-alpha1", "1.0-ALPHA1", Equal),
("1.7", "1.K", Greater),
("5.zebra", "5.aardvark", Greater),
("1.α", "1.b", Greater),
] {
assert_eq!(maven_cmp(a, b), expected, "{a} vs {b}");
assert_eq!(maven_cmp(b, a), expected.reverse(), "{b} vs {a}");
}
}
#[test]
fn maven_documented_splitting_and_trimming_examples() {
for (version, reduced) in [
("1-1.foo-bar1baz-.1", "1-1.foo-bar-1-baz-0.1"),
("1.0.0", "1"),
("1.ga", "1"),
("1.final", "1"),
("1.0", "1"),
("1.", "1"),
("1-", "1"),
("1_", "1"),
("1.0.0-foo.0.0", "1-foo"),
("1.0.0-0.0.0", "1"),
] {
assert_eq!(maven_parse(version), maven_parse(reduced), "{version}");
}
}
#[test]
fn maven_ranks_qualifiers_in_the_documented_order() {
let ascending = [
"1-alpha",
"1-beta",
"1-milestone",
"1-rc",
"1-snapshot",
"1",
"1-sp",
];
for (i, a) in ascending.iter().enumerate() {
for b in &ascending[i + 1..] {
assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
}
assert_eq!(maven_cmp(a, "1-zzz"), Ordering::Less, "{a} vs 1-zzz");
}
assert_eq!(maven_cmp("1-zzz", "1-aaa"), Ordering::Greater);
}
#[test]
fn maven_folds_qualifier_aliases() {
for (a, b) in [
("1-cr", "1-rc"),
("1-cr1", "1-rc1"),
("1-ga", "1"),
("1-final", "1"),
("1-release", "1"),
("1-a1", "1-alpha1"),
("1-b2", "1-beta2"),
("1-m3", "1-milestone3"),
("1-RC1", "1-rc1"),
] {
assert_eq!(maven_cmp(a, b), Ordering::Equal, "{a} vs {b}");
}
assert_eq!(maven_cmp("1-a", "1-alpha"), Ordering::Greater);
assert_eq!(maven_cmp("1-a.1", "1-alpha.1"), Ordering::Greater);
}
#[test]
fn maven_folds_a_dotted_qualifier_to_the_hyphenated_form() {
for (a, b) in [
("1.0.0.CR1", "1.0.0-RC1"),
("1.0.0.Final", "1.0.0"),
("1.0.0.GA", "1.0.0-ga"),
("2.0.0.Final", "2.0.0-Final"),
("1.0.0.Alpha1", "1.0.0-a1"),
("3.1.0.RELEASE", "3.1.0"),
] {
assert_eq!(maven_cmp(a, b), Ordering::Equal, "{a} vs {b}");
}
for (a, b) in [
("1.0.0.CR1", "1.0.0-CR2"),
("1.0.0.Alpha1", "1.0.0-RC1"),
("1.0.0.CR1", "1.0.0"),
("2.0.a", "2-1"),
("3.1.0.M1", "3.1.0-RC1"),
("1.0.0.Beta1", "1.0.0.CR1"),
] {
assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
}
}
#[test]
fn maven_orders_the_published_dotted_ladders() {
for ladder in [
&[
"1.0.0.Alpha1",
"1.0.0-Beta1",
"1.0.0.CR1",
"1.0.0-CR2",
"1.0.0.Final",
][..],
&[
"3.1.0.M1",
"3.1.0-M2",
"3.1.0.RC1",
"3.1.0-RELEASE",
"3.1.1.RELEASE",
][..],
&["5.4.2.Final", "5.4.10.Final", "5.5.0.Alpha1", "5.5.0.Final"][..],
&["4.1.9.Final", "4.1.65.Final", "4.1.65.1.Final"][..],
] {
for (i, a) in ladder.iter().enumerate() {
for b in &ladder[i + 1..] {
assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
}
}
}
}
#[test]
fn maven_nests_a_qualifier_reached_past_an_item() {
assert_eq!(maven_cmp("1-0.alpha", "1-alpha"), Ordering::Greater);
assert_eq!(maven_cmp("1-0.beta", "1-0.alpha"), Ordering::Greater);
assert_eq!(maven_cmp("1-0.alpha", "1-1"), Ordering::Less);
assert_eq!(maven_cmp("1-0.alpha", "1"), Ordering::Less);
}
#[test]
fn maven_declines_a_version_nested_past_the_depth_cap() {
let ok = format!("1{}", "-1".repeat(MAVEN_MAX_DEPTH - 2));
let deep = format!("1{}", "-1".repeat(MAVEN_MAX_DEPTH));
assert!(super::maven_parse(&ok).is_some());
assert!(super::maven_parse(&deep).is_none());
assert_eq!(
Version::parse_for_ecosystem(Some("maven"), &ok),
Version::Maven(ok)
);
assert_eq!(
Version::parse_for_ecosystem(Some("maven"), &deep),
Version::Opaque(deep)
);
}
#[test]
fn maven_declines_a_version_that_would_overflow_the_stack() {
for deep in [format!("1{}", "-1".repeat(200_000)), "1a".repeat(200_000)] {
assert_eq!(
Version::parse_for_ecosystem(Some("maven"), &deep),
Version::Opaque(deep.clone())
);
assert_eq!(
Version::Maven(deep.clone()).partial_cmp_lenient(&Version::Maven(deep)),
None
);
}
}
#[test]
fn maven_ordering_is_antisymmetric_and_transitive_on_the_documented_vectors() {
const CORPUS: &[&str] = &[
"1",
"1.0",
"1.1",
"1-1",
"1.foo",
"1-foo",
"1.bar",
"1-bar",
"1-alpha",
"1-a1",
"1-beta",
"1-milestone",
"1-rc",
"1-cr",
"1-snapshot",
"1-ga",
"1-sp",
"1-sp.1",
"1-sp-1",
"1-ga-1",
"1.0.0-foo.0.0",
"1_0",
"2",
"1.0.1",
"1.0-alpha1",
];
for a in CORPUS {
for b in CORPUS {
assert_eq!(
maven_cmp(a, b),
maven_cmp(b, a).reverse(),
"asymmetric: {a} vs {b}"
);
for c in CORPUS {
let (ab, bc) = (maven_cmp(a, b), maven_cmp(b, c));
if ab == bc || bc == Ordering::Equal {
assert_eq!(maven_cmp(a, c), ab, "intransitive: {a}, {b}, {c}");
}
}
}
}
}
#[test]
fn maven_compares_numeric_tokens_beyond_u64() {
assert_eq!(
maven_cmp("1.99999999999999999999999", "1.99999999999999999999998"),
Ordering::Greater
);
assert_eq!(maven_cmp("1.0000000000000000000001", "1.2"), Ordering::Less);
}
#[test]
fn maven_ecosystem_orders_a_snapshot_against_its_release() {
assert_eq!(compare_versions("1.0-SNAPSHOT", "1.0"), None);
assert_eq!(
compare_versions_for_ecosystem(Some("maven"), "1.0-SNAPSHOT", "1.0"),
Some(Ordering::Less)
);
assert!(!is_version_downgrade_for_ecosystem(
Some("maven"),
"1.0-SNAPSHOT",
"1.0"
));
assert!(is_version_downgrade_for_ecosystem(
Some("maven"),
"1.0",
"1.0-SNAPSHOT"
));
}
#[test]
fn maven_ecosystem_makes_previously_uncomparable_pairs_comparable() {
use Ordering::{Equal, Greater, Less};
for (a, b, expected) in [
("1.0-SNAPSHOT", "1.0", Less),
("1.0-M1", "1.0", Less),
("1.0-sp1", "1.0", Greater),
("2.0.0.Final", "2.0.0", Equal),
("1.0-cr1", "1.0-rc1", Equal),
("1.7.0_80", "1.7.0_79", Greater),
] {
assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
assert_eq!(
compare_versions_for_ecosystem(Some("maven"), a, b),
Some(expected),
"{a} vs {b}"
);
assert_eq!(
compare_versions_for_ecosystem(Some("maven"), b, a),
Some(expected.reverse()),
"{b} vs {a}"
);
}
}
#[test]
fn maven_ecosystem_reverses_a_gate_the_string_only_path_fires_backwards() {
assert_eq!(
compare_versions("1.0-Final", "1.0-SNAPSHOT"),
Some(Ordering::Less)
);
assert!(is_version_downgrade("1.0-SNAPSHOT", "1.0-Final"));
assert!(!is_version_downgrade("1.0-Final", "1.0-SNAPSHOT"));
assert_eq!(
compare_versions_for_ecosystem(Some("maven"), "1.0-Final", "1.0-SNAPSHOT"),
Some(Ordering::Greater)
);
assert!(!is_version_downgrade_for_ecosystem(
Some("maven"),
"1.0-SNAPSHOT",
"1.0-Final"
));
assert!(is_version_downgrade_for_ecosystem(
Some("maven"),
"1.0-Final",
"1.0-SNAPSHOT"
));
}
#[test]
fn maven_ecosystem_parses_as_maven() {
assert_eq!(
Version::parse_for_ecosystem(Some("maven"), "1.0-SNAPSHOT"),
Version::Maven("1.0-SNAPSHOT".into())
);
for s in [
"1.2.3",
"1.0.0-alpha.1",
"2.0.0.Final",
"1.7.0_80",
"1.0+b1",
] {
assert!(
matches!(
Version::parse_for_ecosystem(Some("maven"), s),
Version::Maven(_)
),
"{s}"
);
}
}
#[test]
fn maven_ecosystem_match_ignores_case() {
assert_eq!(
Version::parse_for_ecosystem(Some("MAVEN"), "1.0-SNAPSHOT"),
Version::parse_for_ecosystem(Some("maven"), "1.0-SNAPSHOT")
);
}
#[test]
fn maven_ecosystem_keeps_codenames_and_hashes_opaque() {
for s in [
"deadbeef",
"RELEASE",
"LATEST",
"",
"master-SNAPSHOT",
"1.0 ",
] {
assert_eq!(
Version::parse_for_ecosystem(Some("maven"), s),
Version::Opaque(s.to_string()),
"{s}"
);
}
}
#[test]
fn maven_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
assert!(matches!(
Version::parse_for_ecosystem(Some("maven"), "v1.2.3"),
Version::Maven(_)
));
assert_eq!(
compare_versions_for_ecosystem(Some("maven"), "v1.2.3", "v1.2.4"),
Some(Ordering::Less)
);
}
#[test]
fn maven_stays_uncomparable_against_every_other_parse_result() {
let maven = Version::parse_for_ecosystem(Some("maven"), "1.2.3-1");
for other in [
Version::parse_for_ecosystem(Some("deb"), "1.2.3-1"),
Version::parse_for_ecosystem(Some("rpm"), "1.2.3-1"),
Version::parse_lenient("1.2.3"),
Version::parse_lenient("2024.01.15"),
Version::parse_lenient("4.2.0rc1"),
Version::parse_lenient("deadbeef"),
] {
assert_eq!(maven.partial_cmp_lenient(&other), None, "{other:?}");
assert_eq!(other.partial_cmp_lenient(&maven), None, "{other:?}");
}
}
#[test]
fn parse_lenient_never_produces_the_maven_variant() {
for s in ECOSYSTEM_CORPUS.iter().copied().chain([
"1.0-SNAPSHOT",
"2.0.0.Final",
"1.7.0_80",
"1-sp",
]) {
assert!(
!matches!(Version::parse_lenient(s), Version::Maven(_)),
"{s}"
);
}
}
}