use crate::bound::Bound;
use crate::version::{
CmpOp, Suffix, Version, VersionKind, is_branch_alias as composer_semver_is_branch_alias,
};
use regex::Regex;
use std::sync::OnceLock;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Constraint {
Any,
Op {
op: CmpOp,
version: Version,
explicit_lower_bound: bool,
},
And(Vec<Constraint>),
Or(Vec<Constraint>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Invalid(String),
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Invalid(s) => write!(f, "invalid constraint: {s:?}"),
}
}
}
impl std::error::Error for ParseError {}
impl Constraint {
pub fn parse(input: &str) -> Result<Self, ParseError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ParseError::Invalid(input.to_owned()));
}
let alts: Vec<&str> = re_or_split()
.split(trimmed)
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if alts.is_empty() {
return Err(ParseError::Invalid(input.to_owned()));
}
let parsed: Vec<Constraint> = alts
.into_iter()
.map(parse_intersection)
.collect::<Result<_, _>>()?;
Ok(if parsed.len() == 1 {
parsed.into_iter().next().unwrap()
} else {
Self::Or(parsed)
})
}
pub fn matches(&self, version: &Version) -> bool {
match self {
Self::Any => true,
Self::Op {
op,
version: target,
explicit_lower_bound,
} => matches_atom(version, *op, target, *explicit_lower_bound),
Self::And(items) => items.iter().all(|c| c.matches(version)),
Self::Or(items) => items.iter().any(|c| c.matches(version)),
}
}
#[must_use]
pub fn lower_bound(&self) -> Bound {
match self {
Self::Any => Bound::zero(),
Self::Op { op, version, .. } => {
if version.normalized.starts_with("dev-") {
return Bound::zero();
}
match op {
CmpOp::Eq | CmpOp::Ge => Bound::new(version.normalized.clone(), true),
CmpOp::Gt => Bound::new(version.normalized.clone(), false),
CmpOp::Lt | CmpOp::Le | CmpOp::Ne => Bound::zero(),
}
}
Self::And(items) => fold_bound(items, true, Constraint::lower_bound),
Self::Or(items) => fold_bound(items, false, Constraint::lower_bound),
}
}
#[must_use]
pub fn upper_bound(&self) -> Bound {
match self {
Self::Any => Bound::positive_infinity(),
Self::Op { op, version, .. } => {
if version.normalized.starts_with("dev-") {
return Bound::positive_infinity();
}
match op {
CmpOp::Eq | CmpOp::Le => Bound::new(version.normalized.clone(), true),
CmpOp::Lt => Bound::new(version.normalized.clone(), false),
CmpOp::Gt | CmpOp::Ge | CmpOp::Ne => Bound::positive_infinity(),
}
}
Self::And(items) => fold_bound(items, false, Constraint::upper_bound),
Self::Or(items) => fold_bound(items, true, Constraint::upper_bound),
}
}
#[must_use]
pub fn intersects(&self, other: &Constraint) -> bool {
match (self, other) {
(Self::Or(items), _) => items.iter().any(|c| c.intersects(other)),
(_, Self::Or(items)) => items.iter().any(|c| self.intersects(c)),
(Self::Any, _) | (_, Self::Any) => true,
_ => intervals_overlap(self, other),
}
}
}
fn fold_bound(
items: &[Constraint],
keep_greater: bool,
extract: fn(&Constraint) -> Bound,
) -> Bound {
let mut acc: Option<Bound> = None;
for c in items {
let b = extract(c);
match &acc {
None => acc = Some(b),
Some(cur) => {
if b.compare_to(cur, keep_greater) {
acc = Some(b);
}
}
}
}
acc.unwrap_or_else(Bound::zero)
}
fn intervals_overlap(a: &Constraint, b: &Constraint) -> bool {
let la = a.lower_bound();
let lb = b.lower_bound();
let ua = a.upper_bound();
let ub = b.upper_bound();
let lower = if la.compare_to(&lb, true) { la } else { lb };
let upper = if ua.compare_to(&ub, false) { ua } else { ub };
if lower.is_zero() || upper.is_positive_infinity() {
return true;
}
if lower.is_positive_infinity() {
return false;
}
match lower.version_cmp(&upper) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Greater => false,
std::cmp::Ordering::Equal => lower.is_inclusive() && upper.is_inclusive(),
}
}
fn matches_atom(candidate: &Version, op: CmpOp, target: &Version, explicit_lower: bool) -> bool {
if same_numeric_prerelease_vs_stable(candidate, target) {
let admit_as_equal = match op {
CmpOp::Lt | CmpOp::Le | CmpOp::Eq | CmpOp::Ne => true,
CmpOp::Gt | CmpOp::Ge => explicit_lower,
};
if admit_as_equal {
return matches!(op, CmpOp::Eq | CmpOp::Ge | CmpOp::Le);
}
}
candidate.compare(op, target)
}
fn same_numeric_prerelease_vs_stable(a: &Version, b: &Version) -> bool {
let (
VersionKind::Numeric {
segments_raw: sa,
suffix: suf_a,
},
VersionKind::Numeric {
segments_raw: sb,
suffix: suf_b,
},
) = (&a.kind, &b.kind)
else {
return false;
};
if !segments_equal_numerically(sa, sb) {
return false;
}
is_prerelease(suf_a) ^ is_prerelease(suf_b)
}
fn is_prerelease(s: &Suffix) -> bool {
matches!(
s,
Suffix::Prerelease { .. }
| Suffix::PrereleaseDev { .. }
| Suffix::PatchDev(_)
| Suffix::Dev
)
}
fn segments_equal_numerically(a: &[String], b: &[String]) -> bool {
let len = a.len().max(b.len());
for i in 0..len {
let va: u64 = a.get(i).and_then(|s| s.parse().ok()).unwrap_or(0);
let vb: u64 = b.get(i).and_then(|s| s.parse().ok()).unwrap_or(0);
if va != vb {
return false;
}
}
true
}
fn re_or_split() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"\s*\|\|?\s*").unwrap())
}
fn parse_intersection(s: &str) -> Result<Constraint, ParseError> {
if let Some(range) = parse_hyphen_range(s)? {
return Ok(range);
}
let tokens = tokenize_intersection(s);
let parsed: Vec<Constraint> = tokens
.iter()
.map(|t| parse_atom(t))
.collect::<Result<_, _>>()?;
if parsed.is_empty() {
return Err(ParseError::Invalid(s.to_owned()));
}
Ok(if parsed.len() == 1 {
parsed.into_iter().next().unwrap()
} else {
Constraint::And(parsed)
})
}
fn tokenize_intersection(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == ',' || ch.is_whitespace() {
if !cur.is_empty() && is_pending_operator(&cur) {
while let Some(&peek) = chars.peek() {
if peek.is_whitespace() {
chars.next();
} else {
break;
}
}
continue;
}
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
continue;
}
cur.push(ch);
}
if !cur.is_empty() {
out.push(cur);
}
out
}
fn is_pending_operator(s: &str) -> bool {
matches!(s, ">" | ">=" | "<" | "<=" | "=" | "==" | "!=" | "<>")
}
fn re_hyphen() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(r"^(?P<lo>[^\s]+)\s+-\s+(?P<hi>[^\s]+)$").unwrap()
})
}
fn parse_hyphen_range(s: &str) -> Result<Option<Constraint>, ParseError> {
let Some(caps) = re_hyphen().captures(s.trim()) else {
return Ok(None);
};
let lo_str = caps.name("lo").unwrap().as_str();
let hi_str = caps.name("hi").unwrap().as_str();
let lo = expand_partial_lower(lo_str)?;
let hi = expand_partial_upper(hi_str)?;
Ok(Some(Constraint::And(vec![
Constraint::Op {
op: CmpOp::Ge,
version: lo,
explicit_lower_bound: false,
},
Constraint::Op {
op: CmpOp::Le,
version: hi,
explicit_lower_bound: false,
},
])))
}
fn expand_partial_lower(s: &str) -> Result<Version, ParseError> {
let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
let padded = pad_partial(cleaned);
Version::parse(&padded).map_err(|e| ParseError::Invalid(e.to_string()))
}
fn expand_partial_upper(s: &str) -> Result<Version, ParseError> {
let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
let segs = split_partial_segments(cleaned);
if segs.is_empty() {
return Err(ParseError::Invalid(s.to_owned()));
}
let padded = pad_segments_with(segs, "9999999", 4);
let candidate = padded.join(".");
Version::parse(&candidate).map_err(|e| ParseError::Invalid(e.to_string()))
}
fn parse_atom(token: &str) -> Result<Constraint, ParseError> {
let s = token.trim();
if s.is_empty() {
return Err(ParseError::Invalid(token.to_owned()));
}
let s = s.split_once('#').map_or(s, |(left, _)| left).trim_end();
if matches!(s, "*" | "x" | "X") {
return Ok(Constraint::Any);
}
let is_dev_branch = s.len() >= 4 && s.as_bytes()[..4].eq_ignore_ascii_case(b"dev-");
if composer_semver_is_branch_alias(s) || is_dev_branch {
let v = Version::parse(s).map_err(|e| ParseError::Invalid(format!("{token}: {e}")))?;
return Ok(Constraint::Op {
op: CmpOp::Eq,
version: v,
explicit_lower_bound: true,
});
}
if let Some(rest) = s.strip_prefix('^') {
return parse_caret(rest.trim());
}
if let Some(rest) = s.strip_prefix('~') {
return parse_tilde(rest.trim());
}
for prefix in &[">=", "<=", "==", "!=", "<>", ">", "<", "="] {
if let Some(rest) = s.strip_prefix(*prefix) {
let op = match *prefix {
">=" => CmpOp::Ge,
"<=" => CmpOp::Le,
">" => CmpOp::Gt,
"<" => CmpOp::Lt,
"==" | "=" => CmpOp::Eq,
"!=" | "<>" => CmpOp::Ne,
_ => unreachable!(),
};
let v = Version::parse(rest.trim())
.map_err(|e| ParseError::Invalid(format!("{token}: {e}")))?;
let explicit_lower_bound =
matches!(op, CmpOp::Ge | CmpOp::Gt) && wrote_full_version(rest.trim());
return Ok(Constraint::Op {
op,
version: v,
explicit_lower_bound,
});
}
}
if contains_wildcard(s) {
return parse_wildcard(s);
}
match parse_partial_or_exact(s) {
Ok(c) => Ok(c),
Err(e) => {
if let Some(body) = strip_dev_suffix(s) {
if is_name_safe(body) {
if let Ok(v) = Version::parse(&format!("dev-{body}")) {
return Ok(Constraint::Op {
op: CmpOp::Eq,
version: v,
explicit_lower_bound: true,
});
}
}
}
Err(e)
}
}
}
fn strip_dev_suffix(s: &str) -> Option<&str> {
if s.len() >= 5 && s.as_bytes()[s.len() - 4..].eq_ignore_ascii_case(b"-dev") {
Some(&s[..s.len() - 4])
} else {
None
}
}
fn is_name_safe(s: &str) -> bool {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'/'))
}
fn contains_wildcard(s: &str) -> bool {
s.split('.').any(|p| matches!(p, "*" | "x" | "X"))
}
fn parse_caret(rest: &str) -> Result<Constraint, ParseError> {
let segs = split_partial_numeric(rest)?;
if segs.is_empty() {
return Err(ParseError::Invalid(rest.to_owned()));
}
let major: u32 = segs[0].parse().unwrap_or(0);
let minor: u32 = segs.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let minor_specified = segs.len() >= 2;
let patch_specified = segs.len() >= 3;
let lower = Version::parse(rest)
.or_else(|_| Version::parse(&pad_partial(rest)))
.map_err(|e| ParseError::Invalid(format!("^{rest}: {e}")))?;
let upper_str = if major > 0 || !minor_specified {
format!("{}.0.0.0", major + 1)
} else if minor > 0 || !patch_specified {
format!("0.{}.0.0", minor + 1)
} else {
let patch: u32 = segs.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
format!("0.0.{}.0", patch + 1)
};
let upper = Version::parse(&upper_str)
.map_err(|e| ParseError::Invalid(format!("^{rest} upper: {e}")))?;
let explicit_lower = wrote_full_version(rest);
Ok(Constraint::And(vec![
Constraint::Op {
op: CmpOp::Ge,
version: lower,
explicit_lower_bound: explicit_lower,
},
Constraint::Op {
op: CmpOp::Lt,
version: upper,
explicit_lower_bound: false,
},
]))
}
fn parse_tilde(rest: &str) -> Result<Constraint, ParseError> {
let segs = split_partial_numeric(rest)?;
if segs.is_empty() {
return Err(ParseError::Invalid(rest.to_owned()));
}
let lower = Version::parse(rest)
.or_else(|_| Version::parse(&pad_partial(rest)))
.map_err(|e| ParseError::Invalid(format!("~{rest}: {e}")))?;
let upper_str = match segs.len() {
1 => {
let major: u32 = segs[0].parse().unwrap_or(0);
format!("{}.0.0.0", major + 1)
}
2 => {
let major: u32 = segs[0].parse().unwrap_or(0);
format!("{}.0.0.0", major + 1)
}
_ => {
let major: u32 = segs[0].parse().unwrap_or(0);
let minor: u32 = segs[1].parse().unwrap_or(0);
format!("{}.{}.0.0", major, minor + 1)
}
};
let upper = Version::parse(&upper_str)
.map_err(|e| ParseError::Invalid(format!("~{rest} upper: {e}")))?;
let explicit_lower = wrote_full_version(rest);
Ok(Constraint::And(vec![
Constraint::Op {
op: CmpOp::Ge,
version: lower,
explicit_lower_bound: explicit_lower,
},
Constraint::Op {
op: CmpOp::Lt,
version: upper,
explicit_lower_bound: false,
},
]))
}
fn parse_wildcard(s: &str) -> Result<Constraint, ParseError> {
let parts: Vec<&str> = s.split('.').collect();
let first_wild = parts
.iter()
.position(|p| matches!(*p, "*" | "x" | "X"))
.ok_or_else(|| ParseError::Invalid(s.to_owned()))?;
let numeric_prefix: Vec<u32> = parts[..first_wild]
.iter()
.map(|p| p.parse().unwrap_or(0))
.collect();
let lower_segs: Vec<String> = numeric_prefix
.iter()
.map(u32::to_string)
.chain(std::iter::repeat_n(
"0".to_owned(),
4usize.saturating_sub(first_wild),
))
.collect();
let lower = Version::parse(&lower_segs.join("."))
.map_err(|e| ParseError::Invalid(format!("{s} lower: {e}")))?;
let mut upper_segs = numeric_prefix.clone();
if first_wild == 0 {
return Ok(Constraint::Any);
}
*upper_segs.last_mut().unwrap() += 1;
let upper_str: String = upper_segs
.iter()
.map(u32::to_string)
.chain(std::iter::repeat_n(
"0".to_owned(),
4usize.saturating_sub(upper_segs.len()),
))
.collect::<Vec<_>>()
.join(".");
let upper =
Version::parse(&upper_str).map_err(|e| ParseError::Invalid(format!("{s} upper: {e}")))?;
Ok(Constraint::And(vec![
Constraint::Op {
op: CmpOp::Ge,
version: lower,
explicit_lower_bound: false,
},
Constraint::Op {
op: CmpOp::Lt,
version: upper,
explicit_lower_bound: false,
},
]))
}
fn parse_partial_or_exact(s: &str) -> Result<Constraint, ParseError> {
let cleaned = s.split_once('+').map_or(s, |(left, _)| left);
let segs = split_partial_numeric(cleaned)?;
if segs.len() >= 3 {
let v = Version::parse(cleaned).map_err(|e| ParseError::Invalid(format!("{s}: {e}")))?;
return Ok(Constraint::Op {
op: CmpOp::Eq,
version: v,
explicit_lower_bound: true,
});
}
let major: u32 = segs[0].parse().unwrap_or(0);
let lower_str = pad_partial(cleaned);
let lower =
Version::parse(&lower_str).map_err(|e| ParseError::Invalid(format!("{s} lower: {e}")))?;
let upper_str = if segs.len() == 1 {
format!("{}.0.0.0", major + 1)
} else {
let minor: u32 = segs[1].parse().unwrap_or(0);
format!("{}.{}.0.0", major, minor + 1)
};
let upper =
Version::parse(&upper_str).map_err(|e| ParseError::Invalid(format!("{s} upper: {e}")))?;
Ok(Constraint::And(vec![
Constraint::Op {
op: CmpOp::Ge,
version: lower,
explicit_lower_bound: false,
},
Constraint::Op {
op: CmpOp::Lt,
version: upper,
explicit_lower_bound: false,
},
]))
}
fn wrote_full_version(s: &str) -> bool {
let s = s.trim();
let s = s.strip_prefix(['v', 'V']).unwrap_or(s);
let body = s.split_once(['-', '+']).map_or(s, |(left, _)| left);
let segs: Vec<&str> = body.split('.').filter(|p| !p.is_empty()).collect();
segs.len() >= 3 && segs.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()))
}
fn split_partial_numeric(s: &str) -> Result<Vec<String>, ParseError> {
let cleaned = s
.strip_prefix(['v', 'V'])
.unwrap_or(s)
.split_once('-')
.map_or(s.strip_prefix(['v', 'V']).unwrap_or(s), |(left, _)| left);
let segs: Vec<String> = cleaned
.split('.')
.filter(|p| !p.is_empty())
.map(str::to_owned)
.collect();
if segs.is_empty() {
return Err(ParseError::Invalid(s.to_owned()));
}
if !segs[0].chars().all(|c| c.is_ascii_digit()) {
return Err(ParseError::Invalid(s.to_owned()));
}
Ok(segs)
}
fn split_partial_segments(s: &str) -> Vec<String> {
let stripped = s.strip_prefix(['v', 'V']).unwrap_or(s);
let cleaned = stripped.split_once('-').map_or(stripped, |(left, _)| left);
cleaned
.split('.')
.filter(|p| !p.is_empty())
.map(str::to_owned)
.collect()
}
fn pad_partial(s: &str) -> String {
let stripped = s.strip_prefix(['v', 'V']).unwrap_or(s);
let (body, tail) = match stripped.split_once('-') {
Some((b, t)) => (b, format!("-{t}")),
None => (stripped, String::new()),
};
let segs: Vec<&str> = body.split('.').filter(|p| !p.is_empty()).collect();
let mut padded: Vec<String> = segs.iter().map(|p| (*p).to_owned()).collect();
while padded.len() < 4 {
padded.push("0".to_owned());
}
format!("{}{}", padded.join("."), tail)
}
fn pad_segments_with(mut segs: Vec<String>, pad: &str, target: usize) -> Vec<String> {
while segs.len() < target {
segs.push(pad.to_owned());
}
segs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caret_zero_partial_versions_match_composer() {
let c = Constraint::parse("^0").unwrap();
assert!(c.matches(&Version::parse("0.0.1").unwrap()));
assert!(c.matches(&Version::parse("0.19.2").unwrap()));
assert!(!c.matches(&Version::parse("1.0.0").unwrap()));
let c = Constraint::parse("^0.0").unwrap();
assert!(c.matches(&Version::parse("0.0.9").unwrap()));
assert!(!c.matches(&Version::parse("0.1.0").unwrap()));
let c = Constraint::parse("^0.3").unwrap();
assert!(c.matches(&Version::parse("0.3.7").unwrap()));
assert!(!c.matches(&Version::parse("0.4.0").unwrap()));
let c = Constraint::parse("^0.0.3").unwrap();
assert!(c.matches(&Version::parse("0.0.3").unwrap()));
assert!(!c.matches(&Version::parse("0.0.4").unwrap()));
let c = Constraint::parse("^1").unwrap();
assert!(c.matches(&Version::parse("1.9.9").unwrap()));
assert!(!c.matches(&Version::parse("2.0.0").unwrap()));
}
#[test]
fn nx_dev_constraint_matches_same_branch_version() {
let c = Constraint::parse("3.x-dev").unwrap();
let v = Version::parse("3.x-dev").unwrap();
assert!(c.matches(&v), "got constraint {c:?}");
}
#[test]
fn nx_dev_constraint_rejects_other_major_branch() {
let c = Constraint::parse("3.x-dev").unwrap();
let other = Version::parse("2.x-dev").unwrap();
assert!(!c.matches(&other), "got constraint {c:?}");
}
#[test]
fn nx_dev_constraint_rejects_stable_release() {
let c = Constraint::parse("3.x-dev").unwrap();
let stable = Version::parse("3.0.0").unwrap();
assert!(!c.matches(&stable), "got constraint {c:?}");
}
#[test]
fn n_dot_x_dev_handles_two_segment_form() {
let c = Constraint::parse("1.x-dev").unwrap();
let v = Version::parse("1.x-dev").unwrap();
assert!(c.matches(&v));
}
#[test]
fn n_dot_m_dot_x_dev_handles_three_segment_form() {
let c = Constraint::parse("1.0.x-dev").unwrap();
let v = Version::parse("1.0.x-dev").unwrap();
assert!(c.matches(&v));
}
#[test]
fn dev_branch_constraint_matches_named_branch() {
let c = Constraint::parse("dev-main").unwrap();
let v = Version::parse("dev-main").unwrap();
assert!(c.matches(&v), "constraint {c:?} should match {v:?}");
}
#[test]
fn dev_branch_constraint_rejects_other_branches() {
let c = Constraint::parse("dev-main").unwrap();
let other = Version::parse("dev-feature-x").unwrap();
assert!(!c.matches(&other));
}
#[test]
fn name_dash_dev_suffix_is_synonym_for_dev_dash_name() {
let c = Constraint::parse("master-dev").unwrap();
let v = Version::parse("dev-master").unwrap();
assert!(c.matches(&v), "constraint {c:?} should match {v:?}");
}
#[test]
fn name_dash_dev_rejects_unsafe_chars() {
assert!(Constraint::parse("foo bar-dev").is_err());
assert!(Constraint::parse("1.0.0<1.0.5-dev").is_err());
}
#[test]
fn name_dash_dev_does_not_swallow_numeric_dev_versions() {
let c = Constraint::parse("1.0.0-dev").unwrap();
let v = Version::parse("1.0.0-dev").unwrap();
assert!(c.matches(&v));
}
fn lb(s: &str) -> (String, bool) {
let b = Constraint::parse(s).unwrap().lower_bound();
(b.version().to_owned(), b.is_inclusive())
}
#[test]
fn lower_bound_atomic_ops() {
assert_eq!(lb(">=8.1"), ("8.1.0.0".to_owned(), true));
assert_eq!(lb(">8.0"), ("8.0.0.0".to_owned(), false));
assert_eq!(lb("8.1.2"), ("8.1.2.0".to_owned(), true)); assert!(Constraint::parse("<8.0").unwrap().lower_bound().is_zero());
assert!(Constraint::parse("!=8.0").unwrap().lower_bound().is_zero());
assert!(Constraint::parse("*").unwrap().lower_bound().is_zero());
}
#[test]
fn lower_bound_caret_tilde_take_the_floor() {
assert_eq!(lb("^8.1"), ("8.1.0.0".to_owned(), true));
assert_eq!(lb("~8.2.3"), ("8.2.3.0".to_owned(), true));
assert_eq!(lb("^8"), ("8.0.0.0".to_owned(), true));
}
#[test]
fn lower_bound_conjunction_takes_the_greater() {
assert_eq!(lb(">=7.4 <8.3"), ("7.4.0.0".to_owned(), true));
}
#[test]
fn lower_bound_disjunction_takes_the_least() {
assert_eq!(lb("^7.4 || ^8.0"), ("7.4.0.0".to_owned(), true));
}
#[test]
fn intersects_any_provider_matches_everything() {
let provided = Constraint::parse("*").unwrap();
assert!(provided.intersects(&Constraint::parse("^2.0").unwrap()));
assert!(provided.intersects(&Constraint::parse("*").unwrap()));
}
#[test]
fn intersects_overlapping_and_disjoint_ranges() {
let p = Constraint::parse("^1.0").unwrap();
assert!(p.intersects(&Constraint::parse(">=1.5").unwrap())); assert!(!p.intersects(&Constraint::parse("^2.0").unwrap())); assert!(!p.intersects(&Constraint::parse(">=2.0").unwrap()));
}
#[test]
fn intersects_disjunction_member_overlap() {
let provided = Constraint::parse("^1.0 || ^3.0").unwrap();
assert!(provided.intersects(&Constraint::parse("^3.1").unwrap()));
assert!(!provided.intersects(&Constraint::parse("^2.0").unwrap()));
}
#[test]
fn dev_branch_constraint_handles_slashed_branch_name() {
let c = Constraint::parse("dev-fix/some-bug").unwrap();
let v = Version::parse("dev-fix/some-bug").unwrap();
assert!(c.matches(&v));
}
}