diff --git a/.justfile b/.justfile index 11b3df7..18370b8 100644 --- a/.justfile +++ b/.justfile @@ -4,4 +4,16 @@ default: # Generate coverage/lcov.info coverage: - cargo tarpaulin --engine ptrace -o lcov --output-dir coverage \ No newline at end of file + cargo tarpaulin --engine ptrace -o lcov --output-dir coverage + +# For getting ptrace as html on macos +docker-coverage: + docker image pull pkgxdev/pkgx + docker run \ + --name semverator \ + --rm \ + --volume .:/volume \ + --security-opt seccomp=unconfined \ + --platform linux/amd64 \ + xd009642/tarpaulin \ + cargo tarpaulin --engine llvm -o html --all --all-targets --all-features --output-dir /volume/coverage diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 155838b..f1b1d69 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -17,3 +17,6 @@ crate-type = ["cdylib", "rlib"] anyhow = "1.0.75" lazy_static = "1.5.0" regex = "1.9.5" + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/lib/src/range/mod.rs b/lib/src/range/mod.rs index 3b1a963..467d41b 100644 --- a/lib/src/range/mod.rs +++ b/lib/src/range/mod.rs @@ -1,5 +1,8 @@ use crate::semver::Semver; -use std::hash::{Hash, Hasher}; +use std::{ + fmt, + hash::{Hash, Hasher}, +}; pub mod intersect; pub mod max; @@ -39,7 +42,79 @@ impl Range { } impl Hash for Semver { + #[cfg(not(tarpaulin_include))] fn hash(&self, state: &mut H) { self.components.hash(state); } } + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let str = self + .set + .iter() + .map(|v| match v { + Constraint::Any => "*".to_string(), + Constraint::Single(v) => format!("={}", v), + Constraint::Contiguous(v1, v2) => { + let v1_chomp = v1.raw.trim_end_matches(".0").to_string(); + let v2_chomp = v2.raw.trim_end_matches(".0").to_string(); + if v2.major == v1.major + 1 && v2.minor == 0 && v2.patch == 0 { + if v1.major == 0 { + if v1.components.len() == 1 { + "^0".to_string() + } else { + format!(">={}<1", v1_chomp) + } + } else { + format!("^{}", v1_chomp) + } + } else if v2.major == v1.major && v2.minor == v1.minor + 1 && v2.patch == 0 { + format!("~{}", v1_chomp) + } else if v2.major == usize::MAX { + format!(">={}", v1_chomp) + } else if at(&v1.clone(), &v2.clone()) { + format!("@{}", v1) + } else { + format!(">={}<{}", v1_chomp, v2_chomp) + } + } + }) + .collect::>() + .join(","); + write!(f, "{}", str) + } +} + +/// checks @ syntax, eg. node@22.1 +/// `@` is `=`, as long as there's 3 components +fn at(left: &Semver, right: &Semver) -> bool { + let mut cc1 = left.components.clone(); + let cc2 = &right.components; + + // helper function to get the last element of a slice + fn last(arr: &[usize]) -> usize { + *arr.last().unwrap() + } + + if cc1.len() > cc2.len() { + return false; + } + + // Ensure cc1 and cc2 have the same length by appending 0s to cc1 + while cc1.len() < cc2.len() { + cc1.push(0); + } + + if last(&cc1) + 1 != last(cc2) { + return false; + } + + for i in 0..cc1.len() - 1 { + if cc1[i] != cc2[i] { + return false; + } + } + + true +} diff --git a/lib/src/range/parse.rs b/lib/src/range/parse.rs index 907b7e8..98751e7 100644 --- a/lib/src/range/parse.rs +++ b/lib/src/range/parse.rs @@ -9,7 +9,7 @@ lazy_static! { static ref RANGE_REGEX: Regex = Regex::new(r"\s*(,|\|\|)\s*").unwrap(); static ref CONSTRAINT_REGEX_RANGE: Regex = Regex::new(r"^>=((\d+\.)*\d+)\s*(<((\d+\.)*\d+))?$").unwrap(); - static ref CONSTRAINT_REGEX_SIMPLE: Regex = Regex::new(r"^([~=<^])(.+)$").unwrap(); + static ref CONSTRAINT_REGEX_SIMPLE: Regex = Regex::new(r"^([~=<^@])(.+)$").unwrap(); } impl Range { @@ -91,6 +91,20 @@ impl Constraint { let v2 = Semver::parse(cap.get(2).context("invalid description")?.as_str())?; Ok(Constraint::Contiguous(v1, v2)) } + "@" => { + let v1 = Semver::parse(cap.get(2).context("invalid description")?.as_str())?; + let mut parts = v1.components.clone(); + let last = parts.last_mut().context("version too short")?; + *last += 1; + let v2 = Semver::parse( + &parts + .iter() + .map(|c| c.to_string()) + .collect::>() + .join("."), + )?; + Ok(Constraint::Contiguous(v1, v2)) + } "=" => Ok(Constraint::Single(Semver::parse( cap.get(2).context("invalid description")?.as_str(), )?)), diff --git a/lib/src/semver/mod.rs b/lib/src/semver/mod.rs index 367bd9e..e1ea6d6 100644 --- a/lib/src/semver/mod.rs +++ b/lib/src/semver/mod.rs @@ -1,3 +1,5 @@ +use std::fmt; + pub mod bump; pub mod compare; pub mod parse; @@ -28,3 +30,40 @@ impl Semver { } } } + +impl fmt::Display for Semver { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + self.components + .iter() + .map(|c| c.to_string()) + .collect::>() + .join(".") + )?; + if !self.prerelease.is_empty() { + write!( + f, + "-{}", + self.prerelease + .iter() + .map(|c| c.to_string()) + .collect::>() + .join(".") + )?; + } + if !self.build.is_empty() { + write!( + f, + "+{}", + self.build + .iter() + .map(|c| c.to_string()) + .collect::>() + .join(".") + )?; + } + Ok(()) + } +} diff --git a/lib/src/tests/range.rs b/lib/src/tests/range.rs index 4fa90d3..7097d03 100644 --- a/lib/src/tests/range.rs +++ b/lib/src/tests/range.rs @@ -16,6 +16,10 @@ fn test_parse() -> Result<()> { let j = Range::parse("Your mom"); let k = Range::parse(""); let l = Range::parse(">=12"); + let m = Range::parse("@1"); + let n = Range::parse("@1.1"); + let o = Range::parse("@1.1.1"); + let p = Range::parse("@1.1.1.1"); assert!(a.is_ok()); assert!(b.is_ok()); @@ -29,6 +33,10 @@ fn test_parse() -> Result<()> { assert!(j.is_err()); assert!(k.is_err()); assert!(l.is_ok()); + assert!(m.is_ok()); + assert!(n.is_ok()); + assert!(o.is_ok()); + assert!(p.is_ok()); assert_eq!(f?.set.len(), 5); @@ -203,3 +211,71 @@ fn test_intersect() -> Result<()> { Ok(()) } + +#[test] +fn test_at() -> Result<()> { + let ra = Range::parse(">=1.0<1.1")?; + let rb = Range::parse("=1.1")?; + + assert_eq!(format!("{ra}"), "~1"); + assert_eq!(format!("{rb}"), "=1.1"); + + let rc = Range::parse(">=1.1.0<1.1.1")?; + let rd = Range::parse("=1.1.1")?; + + assert_eq!(format!("{rc}"), "@1.1.0"); + assert_eq!(format!("{rd}"), "=1.1.1"); + + let re = Range::parse(">=1.1.1.0<1.1.1.1")?; + let rf = Range::parse("=1.1.1.0")?; + + assert_eq!(format!("{re}"), "@1.1.1.0"); + assert_eq!(format!("{rf}"), "=1.1.1.0"); + + let rg = Range::parse(">=1.1<1.1.1.1.1")?; + let rh = Range::parse(">=1.1.1<1.1.3")?; + let ri = Range::parse(">=1.1.1<1.2.2")?; + + assert_eq!(format!("{rg}"), ">=1.1<1.1.1.1.1"); + assert_eq!(format!("{rh}"), ">=1.1.1<1.1.3"); + assert_eq!(format!("{ri}"), ">=1.1.1<1.2.2"); + + let rj = Range::parse("@1")?; + let rk = Range::parse("@1.1")?; + let rl = Range::parse("@1.1.1")?; + let rm = Range::parse("@1.1.1.1")?; + + assert_eq!(format!("{rj}"), "^1"); + assert_eq!(format!("{rk}"), "~1.1"); + assert_eq!(format!("{rl}"), "@1.1.1"); + assert_eq!(format!("{rm}"), "@1.1.1.1"); + + Ok(()) +} + +#[test] +fn test_display() -> Result<()> { + let ra = Range::parse("^3.7")?; + let rb = Range::parse("=3.11")?; + let rc = Range::parse("^3.9")?; + let rd = Range::parse("*")?; + let re = Range::parse(">=0<1")?; + let rf = Range::parse(">=0.1<1")?; + let rg = Range::parse(">=0.1<0.2")?; + let rh = Range::parse(">=0.1.1<0.2")?; + let ri = Range::parse(">=0.1.1")?; + let rj = Range::parse(">=0.1.1<3")?; + + assert_eq!(ra.to_string(), "^3.7"); + assert_eq!(rb.to_string(), "=3.11"); + assert_eq!(rc.to_string(), "^3.9"); + assert_eq!(rd.to_string(), "*"); + assert_eq!(re.to_string(), "^0"); + assert_eq!(rf.to_string(), ">=0.1<1"); + assert_eq!(rg.to_string(), "~0.1"); + assert_eq!(rh.to_string(), "~0.1.1"); + assert_eq!(ri.to_string(), ">=0.1.1"); + assert_eq!(rj.to_string(), ">=0.1.1<3"); + + Ok(()) +} diff --git a/lib/src/tests/semver.rs b/lib/src/tests/semver.rs index e3cbf9d..24fbbde 100644 --- a/lib/src/tests/semver.rs +++ b/lib/src/tests/semver.rs @@ -81,16 +81,20 @@ fn test_compare() -> Result<()> { let j = Semver::parse("1.2.3-alpha.1+7ec0834")?; let k = Semver::parse("1.2.3-alpha.2+7ec0834")?; - assert!(e.lt(&f)); - assert!(f.eq(&f)); + assert!(e.lt(&f)); // 1.2.3-alpha < 1.2.3-alpha.1 + assert!(f.eq(&f)); // 1.2.3-alpha.1 == 1.2.3-alpha.1 assert!(f.lt(&a)); // 1.2.3-alpha.1 < 1.2.3 - assert!(f.lt(&g)); - assert!(f.lt(&g)); - assert!(g.lt(&h)); - assert!(f.lt(&i)); - assert!(i.gt(&j)); - assert!(i.lt(&k)); - assert!(j.lt(&k)); + assert!(a.gt(&f)); // 1.2.3 > 1.2.3-alpha.1 + assert!(f.lt(&g)); // 1.2.3-alpha.1 < 1.2.3-alpha.2 + assert!(g.gt(&f)); // 1.2.3-alpha.2 > 1.2.3-alpha.1 + assert!(g.lt(&h)); // 1.2.3-alpha.2 < 1.2.3-beta.1 + assert!(f.lt(&i)); // 1.2.3-alpha.1 < 1.2.3-alpha.1+8ec0834 + assert!(i.gt(&f)); // 1.2.3-alpha.1+8ec0834 > 1.2.3-alpha.1 + assert!(i.eq(&i)); // 1.2.3-alpha.1+8ec0834 == 1.2.3-alpha.1+8ec0834 + assert!(i.gt(&j)); // 1.2.3-alpha.1+8ec0834 > 1.2.3-alpha.1+7ec0834 + assert!(j.lt(&i)); // 1.2.3-alpha.1+7ec0834 < 1.2.3-alpha.1+8ec0834 + assert!(i.lt(&k)); // 1.2.3-alpha.1+8ec0834 < 1.2.3-alpha.2+7ec0834 + assert!(j.lt(&k)); // 1.2.3-alpha.1+7ec0834 < 1.2.3-alpha.2+7ec0834 Ok(()) } @@ -156,3 +160,24 @@ fn test_infinty() { assert_eq!(inf.components, [usize::MAX, usize::MAX, usize::MAX]); assert_eq!(inf.raw, "Infinity.Infinity.Infinity"); } + +#[test] +fn test_display() -> Result<()> { + let a = Semver::parse("1.2.3")?; + let b = Semver::parse("1.2.3-alpha")?; + let c = Semver::parse("1.2.0")?; + let d = Semver::parse("1.0.0")?; + let e = Semver::parse("1.2.3-alpha.1+b40")?; + let f = Semver::parse("1.2.3-alpha.1+build.40")?; + let g = Semver::parse("1")?; + + assert_eq!(a.to_string(), "1.2.3"); + assert_eq!(b.to_string(), "1.2.3-alpha"); + assert_eq!(c.to_string(), "1.2.0"); + assert_eq!(d.to_string(), "1.0.0"); + assert_eq!(e.to_string(), "1.2.3-alpha.1+b40"); + assert_eq!(f.to_string(), "1.2.3-alpha.1+build.40"); + assert_eq!(g.to_string(), "1"); + + Ok(()) +}