diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a029c6b2..f9fb115d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,17 @@ jobs: env: RUSTFLAGS: --cfg test_node_semver ${{env.RUSTFLAGS}} + mirror_node_matches_prerelease: + name: mirror_node_matches_prerelease + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --package semver --test test_matches_prerelease --features "mirror_node_matches_prerelease" + minimal: name: Minimal versions needs: pre_ci diff --git a/Cargo.toml b/Cargo.toml index 12b7404f..c8dea82a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ rust-version = "1.31" [features] default = ["std"] +mirror_node_matches_prerelease = [] std = [] [dependencies] diff --git a/src/eval.rs b/src/eval.rs index e6e38949..b7024d78 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,13 +1,17 @@ -use crate::{Comparator, Op, Version, VersionReq}; +use crate::{eval_ext, Comparator, Op, Version, VersionReq}; -pub(crate) fn matches_req(req: &VersionReq, ver: &Version) -> bool { +pub(crate) fn matches_req(req: &VersionReq, ver: &Version, prerelease_matches: bool) -> bool { for cmp in &req.comparators { - if !matches_impl(cmp, ver) { + if prerelease_matches { + if !eval_ext::matches_prerelease_impl(cmp, ver) { + return false; + } + } else if !matches_impl(cmp, ver) { return false; } } - if ver.pre.is_empty() { + if ver.pre.is_empty() || prerelease_matches { return true; } @@ -41,7 +45,7 @@ fn matches_impl(cmp: &Comparator, ver: &Version) -> bool { } } -fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { +pub(super) fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return false; } @@ -61,7 +65,7 @@ fn matches_exact(cmp: &Comparator, ver: &Version) -> bool { ver.pre == cmp.pre } -fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { +pub(super) fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return ver.major > cmp.major; } @@ -87,7 +91,7 @@ fn matches_greater(cmp: &Comparator, ver: &Version) -> bool { ver.pre > cmp.pre } -fn matches_less(cmp: &Comparator, ver: &Version) -> bool { +pub(super) fn matches_less(cmp: &Comparator, ver: &Version) -> bool { if ver.major != cmp.major { return ver.major < cmp.major; } diff --git a/src/eval_ext.rs b/src/eval_ext.rs new file mode 100644 index 00000000..1e9b07c4 --- /dev/null +++ b/src/eval_ext.rs @@ -0,0 +1,269 @@ +use super::eval::{matches_exact, matches_greater, matches_less}; +use crate::{Comparator, Op, Prerelease, Version}; + +pub(super) fn matches_prerelease_impl(cmp: &Comparator, ver: &Version) -> bool { + match cmp.op { + Op::Exact | Op::Wildcard => matches_exact_prerelease(cmp, ver), + Op::Greater => matches_greater(cmp, ver), + Op::GreaterEq => { + if matches_exact_prerelease(cmp, ver) { + return true; + } + matches_greater(cmp, ver) + } + Op::Less => matches_less(&fill_partial_req(cmp), ver), + Op::LessEq => { + if matches_exact_prerelease(cmp, ver) { + return true; + } + matches_less(&fill_partial_req(cmp), ver) + } + Op::Tilde => matches_tilde_prerelease(cmp, ver), + Op::Caret => matches_caret_prerelease(cmp, ver), + #[cfg(no_non_exhaustive)] + Op::__NonExhaustive => unreachable!(), + } +} + +#[cfg(not(feature = "mirror_node_matches_prerelease"))] +fn fill_partial_req(cmp: &Comparator) -> Comparator { + let mut cmp = cmp.clone(); + if cmp.minor.is_none() { + cmp.minor = Some(0); + cmp.patch = Some(0); + } else if cmp.patch.is_none() { + cmp.patch = Some(0); + } + cmp +} + +#[cfg(not(feature = "mirror_node_matches_prerelease"))] +fn matches_exact_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + // If the comparator has a prerelease tag like =3.0.0-alpha.24, + // then it shoud be only exactly match 3.0.0-alpha.24. + if !cmp.pre.is_empty() { + return false; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match (upper.minor.is_some(), upper.patch.is_some()) { + (true, true) => { + upper.patch = Some(upper.patch.unwrap() + 1); + } + (true, false) => { + // Partial Exact VersionReq eg. =0.24 + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (false, false) => { + // Partial Exact VersionReq eg. =0 + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + _ => {} + } + + matches_less(&upper, ver) +} + +fn matches_tilde_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match (upper.minor.is_some(), upper.patch.is_some()) { + (true, _) => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (false, false) => { + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + _ => {} + } + + matches_less(&upper, ver) +} + +#[cfg(not(feature = "mirror_node_matches_prerelease"))] +fn matches_caret_prerelease(cmp: &Comparator, ver: &Version) -> bool { + if matches_exact(cmp, ver) { + return true; + } + + if !matches_greater(&fill_partial_req(cmp), ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match ( + upper.major > 0, + upper.minor.is_some(), + upper.patch.is_some(), + ) { + (true, _, _) | (_, false, false) => { + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + (_, true, false) => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() > 0 => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() == 0 => { + if upper.patch.is_none() { + upper.patch = Some(1); + } else { + upper.patch = Some(upper.patch.unwrap() + 1); + } + } + _ => {} + } + + matches_less(&upper, ver) +} + +#[cfg(feature = "mirror_node_matches_prerelease")] +fn fill_partial_req(cmp: &Comparator) -> Comparator { + let mut cmp = cmp.clone(); + if cmp.minor.is_none() { + cmp.minor = Some(0); + cmp.patch = Some(0); + cmp.pre = Prerelease::new("0").unwrap(); + } else if cmp.patch.is_none() { + cmp.patch = Some(0); + cmp.pre = Prerelease::new("0").unwrap(); + } + cmp +} + +#[cfg(feature = "mirror_node_matches_prerelease")] +fn matches_exact_prerelease(cmp: &Comparator, ver: &Version) -> bool { + let lower = fill_partial_req(cmp); + if matches_exact(&lower, ver) { + return true; + } + + // If the comparator has a prerelease tag like =3.0.0-alpha.24, + // then it shoud be only exactly match 3.0.0-alpha.24. + if !cmp.pre.is_empty() { + return false; + } + + if !matches_greater(&lower, ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match (upper.minor.is_some(), upper.patch.is_some()) { + (true, true) => { + upper.patch = Some(upper.patch.unwrap() + 1); + } + (true, false) => { + // Partial Exact VersionReq eg. =0.24 + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (false, false) => { + // Partial Exact VersionReq eg. =0 + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + _ => {} + } + + matches_less(&upper, ver) +} + +#[cfg(feature = "mirror_node_matches_prerelease")] +fn matches_caret_prerelease(cmp: &Comparator, ver: &Version) -> bool { + let mut lower = fill_partial_req(cmp); + if lower.major == 0 && lower.pre.is_empty() { + lower.pre = Prerelease::new("0").unwrap(); + } + + if matches_exact(&lower, ver) { + return true; + } + + if !matches_greater(&lower, ver) { + return false; + } + + let mut upper = Comparator { + op: Op::Less, + pre: Prerelease::new("0").unwrap(), + ..cmp.clone() + }; + + match ( + upper.major > 0, + upper.minor.is_some(), + upper.patch.is_some(), + ) { + (true, _, _) | (_, false, false) => { + upper.major += 1; + upper.minor = Some(0); + upper.patch = Some(0); + } + (_, true, false) => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() > 0 => { + upper.minor = Some(upper.minor.unwrap() + 1); + upper.patch = Some(0); + } + (_, true, _) if upper.minor.unwrap() == 0 => { + if upper.patch.is_none() { + upper.patch = Some(1); + } else { + upper.patch = Some(upper.patch.unwrap() + 1); + } + } + _ => {} + } + + matches_less(&upper, ver) +} diff --git a/src/lib.rs b/src/lib.rs index de7b6906..e7d0d883 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,7 @@ mod backport; mod display; mod error; mod eval; +mod eval_ext; mod identifier; mod impls; mod parse; @@ -521,7 +522,12 @@ impl VersionReq { /// Evaluate whether the given `Version` satisfies the version requirement /// described by `self`. pub fn matches(&self, version: &Version) -> bool { - eval::matches_req(self, version) + eval::matches_req(self, version, false) + } + + /// Simliar to [`Self::matches`], it allows to match any "Semver-compatible" pre-release version. + pub fn matches_prerelease(&self, version: &Version) -> bool { + eval::matches_req(self, version, true) } } diff --git a/tests/node/mod.rs b/tests/node/mod.rs index eb50673d..b3817738 100644 --- a/tests/node/mod.rs +++ b/tests/node/mod.rs @@ -8,8 +8,10 @@ use std::process::Command; pub(super) struct VersionReq(semver::VersionReq); impl VersionReq { + #[allow(dead_code)] pub(super) const STAR: Self = VersionReq(semver::VersionReq::STAR); + #[allow(dead_code)] pub(super) fn matches(&self, version: &Version) -> bool { let out = Command::new("node") .arg("-e") @@ -29,6 +31,27 @@ impl VersionReq { panic!("unexpected output: {}", s); } } + + #[allow(dead_code)] + pub(super) fn matches_prerelease(&self, version: &Version) -> bool { + let out = Command::new("node") + .arg("-e") + .arg(format!( + "console.log(require('semver').satisfies('{}', '{}', {{includePrerelease: true}}))", + version, + self.to_string().replace(',', ""), + )) + .output() + .unwrap(); + if out.stdout == b"true\n" { + true + } else if out.stdout == b"false\n" { + false + } else { + let s = String::from_utf8_lossy(&out.stdout) + String::from_utf8_lossy(&out.stderr); + panic!("unexpected output: {}", s); + } + } } impl Display for VersionReq { diff --git a/tests/test_matches_prerelease.rs b/tests/test_matches_prerelease.rs new file mode 100644 index 00000000..161ee030 --- /dev/null +++ b/tests/test_matches_prerelease.rs @@ -0,0 +1,458 @@ +#![allow( + clippy::missing_panics_doc, + clippy::shadow_unrelated, + clippy::toplevel_ref_arg, + clippy::wildcard_imports +)] + +mod node; +mod util; +use crate::util::*; +#[cfg(test_node_semver)] +use node::{req, VersionReq}; +#[cfg(not(test_node_semver))] +use semver::VersionReq; +#[cfg_attr(not(no_track_caller), track_caller)] +fn assert_prerelease_match_all(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = version(string); + assert!( + req.matches_prerelease(&parsed), + "{} did not match {}", + req, + string, + ); + } +} + +#[cfg_attr(not(no_track_caller), track_caller)] +fn assert_prerelease_match_none(req: &VersionReq, versions: &[&str]) { + for string in versions { + let parsed = version(string); + assert!( + !req.matches_prerelease(&parsed), + "{} matched {}", + req, + string + ); + } +} + +#[test] +#[cfg(not(any(feature = "mirror_node_matches_prerelease", test_node_semver)))] +fn test_exact() { + // =I.J.K-pre only match I.J.K-pre + let ref r = req("=4.2.1-0"); + // Only exactly match 4.2.1-0 + assert_prerelease_match_all(r, &["4.2.1-0"]); + // Not match others + assert_prerelease_match_none(r, &["1.2.3", "4.2.0", "4.2.1-1", "4.2.2"]); + + // =I.J.K equivalent to >=I.J.K, =4.2.1, <4.2.2-0")] { + assert_prerelease_match_all(r, &["4.2.1"]); + assert_prerelease_match_none(r, &["1.2.3", "4.2.1-0", "4.2.2-0", "4.2.2"]); + } + + // =I.J equivalent to >=I.J.0, =4.2.0, <4.3.0-0")] { + assert_prerelease_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]); + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]); + assert_prerelease_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0-0", "5.0.0"]); + } + + // =I equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("=4"), req(">=4.0.0, <5.0.0-0")] { + assert_prerelease_match_all(r, &["4.0.0", "4.2.1", "4.2.4-0", "4.9.9"]); + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]); + assert_prerelease_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } +} + +#[test] +#[cfg(not(any(feature = "mirror_node_matches_prerelease", test_node_semver)))] +fn test_greater_eq() { + // >=I.J.K-0 + let ref r = req(">=4.2.1-0"); + assert_prerelease_match_all(r, &["4.2.1-0", "4.2.1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "1.2.3"]); + + // >=I.J.K + let ref r = req(">=4.2.1"); + assert_prerelease_match_all(r, &["4.2.1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1-0"]); + + // >=I.J equivalent to >=I.J.0 + for r in &[req(">=4.2"), req(">=4.2.0")] { + assert_prerelease_match_all(r, &["4.2.1-0", "4.2.0", "4.3.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.1.1", "4.2.0-0"]); + } + + // >=I equivalent to >=I.0.0 + for r in &[req(">=4"), req(">=4.0.0")] { + assert_prerelease_match_all(r, &["4.0.0", "4.1.0-1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "1.2.3", "4.0.0-0"]); + } +} + +#[test] +#[cfg(not(any(feature = "mirror_node_matches_prerelease", test_node_semver)))] +fn test_less() { + // 0) — equivalent to >=I.J.K-0, <(I+1).0.0-0 + for r in &[req("^1.2.3-0"), req(">=1.2.3-0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.3-0", "1.2.3-1", "1.2.3", "1.9.9"]); + assert_prerelease_match_none(r, &["0.0.9", "1.1.1-0", "2.0.0-0", "2.1.1"]); + } + + // ^I.J.K (for I>0) — equivalent to >=I.J.K, <(I+1).0.0-0 + for r in &[req("^1.2.3"), req(">=1.2.3, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.3", "1.9.9"]); + assert_prerelease_match_none( + r, + &["0.0.9", "1.1.1-0", "1.2.3-0", "1.2.3-1", "2.0.0-0", "2.1.1"], + ); + } + + // ^0.J.K-0 (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + for r in &[req("^0.2.3-0"), req(">=0.2.3-0, <0.3.0-0")] { + assert_prerelease_match_all(r, &["0.2.3-0", "0.2.3", "0.2.9-0", "0.2.9"]); + assert_prerelease_match_none(r, &["0.0.9", "0.3.0-0", "0.3.11", "1.1.1"]); + } + + // ^0.J.K (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + for r in &[req("^0.2.3"), req(">=0.2.3, <0.3.0-0")] { + assert_prerelease_match_all(r, &["0.2.3", "0.2.9-0", "0.2.9"]); + assert_prerelease_match_none(r, &["0.0.9", "0.2.3-0", "0.3.0-0", "0.3.11", "1.1.1"]); + } + + // ^0.0.K-0 — equivalent to >=0.0.K-0, <0.0.(K+1)-0 + for r in &[req("^0.0.3-0"), req(">=0.0.3-0, <0.1.0-0")] { + assert_prerelease_match_all(r, &["0.0.3-0", "0.0.3-1", "0.0.3"]); + assert_prerelease_match_none(r, &["0.0.1", "0.3.0-0", "0.4.0-0", "1.1.1"]); + } + + // ^0.0.K — equivalent to >=0.0.K, <0.0.(K+1)-0 + for r in &[req("^0.0.3"), req(">=0.0.3, <0.1.0-0")] { + assert_prerelease_match_all(r, &["0.0.3"]); + assert_prerelease_match_none( + r, + &["0.0.1", "0.0.3-0", "0.3.0-0", "0.0.3-1", "0.4.0-0", "1.1.1"], + ); + } + + // ^I.J (for I>0 or J>0) — equivalent to >=I.J.0, <(I+1).0.0-0) + for r in &[req("^1.2"), req(">=1.2.0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.0", "1.9.0-0", "1.9.9"]); + assert_prerelease_match_none(r, &["0.0.1", "0.0.4-0", "1.2.0-0", "2.0.0-0", "4.0.1"]); + } + + // ^0.0 — equivalent to >=0.0.0, <0.1.0-0 + for r in &[req("^0.0"), req(">=0.0.0, <0.1.0-0")] { + assert_prerelease_match_all(r, &["0.0.0", "0.0.1", "0.0.4-0"]); + assert_prerelease_match_none(r, &["0.0.0-0", "0.1.0-0", "0.1.0", "1.1.1"]); + } + + // ^I — equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("^1"), req(">=1.0.0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.0.0", "1.0.1"]); + assert_prerelease_match_none(r, &["0.1.0-0", "0.1.0", "1.0.0-0", "2.0.0-0", "3.1.2"]); + } +} + +#[test] +#[cfg(not(any(feature = "mirror_node_matches_prerelease", test_node_semver)))] +fn test_wildcard() { + // I.J.* — equivalent to =I.J + // + // =I.J equivalent to >=I.J.0, = 4.2.0, < 4.3.0-0 + assert_prerelease_match_all(r, &["4.2.0", "4.2.1", "4.2.9"]); + // Not Match < 4.2.0 + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0", "4.2.0-0"]); + // Not Match >= 4.3.0-0 + assert_prerelease_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0", "5.0.1"]); + } + + // I.* or I.*.* — equivalent to =I + // + // =I equivalent to >=I.0.0, <(I+1).0.0-0 + for r in &[req("4.*"), req("4.*.*"), req("=4")] { + // Match >= 4.0.0, < 5.0.0-0 + assert_prerelease_match_all(r, &["4.0.0", "4.2.1", "4.9.9"]); + // Not Match < 4.0.0 + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0", "4.0.0-0"]); + // Not Match >= 5.0.0-0 + assert_prerelease_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } +} + +// +// These tests below can pass in both implementations +// + +#[test] +fn test_greater() { + // >I.J.K-0 + let ref r = req(">4.2.1-0"); + assert_prerelease_match_all(r, &["4.2.1", "4.2.2", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1-0"]); + + // >I.J.K + let ref r = req(">4.2.1"); + assert_prerelease_match_all(r, &["4.2.2", "5.0.0-0", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1-0", "4.2.1"]); + + // >I.J equivalent to >=I.(J+1).0-0 + for r in &[req(">4.2"), req(">=4.3.0-0")] { + assert_prerelease_match_all(r, &["4.3.0-0", "4.3.0", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1"]); + } + + // >I equivalent to >=(I+1).0.0-0 + for r in &[req(">4"), req(">=5.0.0-0")] { + assert_prerelease_match_all(r, &["5.0.0-0", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1"]); + } +} + +#[test] +fn test_less_eq() { + // <=I.J.K + let ref r = req("<=4.2.1"); + assert_prerelease_match_all(r, &["0.0.0", "4.2.1-0", "4.2.1"]); + assert_prerelease_match_none(r, &["4.2.2", "5.0.0-0", "5.0.0"]); + // <=I.J.K-0 + let ref r = req("<=4.2.1-0"); + assert_prerelease_match_all(r, &["0.0.0", "4.2.1-0"]); + assert_prerelease_match_none(r, &["4.2.1", "4.2.2", "5.0.0-0", "5.0.0"]); + + // <=I.J equivalent to =I.J.K-0, = 1.2.3-0, < 1.3.0-0")] { + assert_prerelease_match_all(r, &["1.2.3-0", "1.2.3", "1.2.4-0", "1.2.4"]); + assert_prerelease_match_none(r, &["0.0.1", "1.1.0-0"]); + assert_prerelease_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]); + } + + // ~I.J.K — equivalent to >=I.J.K, = 1.2.3, < 1.3.0-0")] { + assert_prerelease_match_all(r, &["1.2.3", "1.2.4-0", "1.2.4"]); + assert_prerelease_match_none(r, &["0.0.1", "1.1.0-0", "1.2.3-0"]); + assert_prerelease_match_none(r, &["1.3.0-0", "1.3.0", "1.3.1", "2.0.0"]); + } + + // ~I.J — equivalent to >=I.J.0, =0.24.0, <0.25.0-0")] { + assert_prerelease_match_all(r, &["0.24.0", "0.24.1-0", "0.24.1", "0.24.9"]); + assert_prerelease_match_none(r, &["0.0.1", "0.9.9", "0.24.0-0"]); + assert_prerelease_match_none(r, &["0.25.0-0", "1.1.0", "1.2.3", "2.0.0"]); + } + + // ~I — >=I.0.0, <(I+1).0.0-0 + for r in &[req("~1"), req(">=1.0.0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.0.0", "1.1.0-0", "1.1.0"]); + assert_prerelease_match_none(r, &["0.0.1", "0.9.9", "1.0.0-0"]); + assert_prerelease_match_none(r, &["2.0.0-0", "2.0.0", "2.0.1"]); + } +} + +// +// These tests below are for node semver compatibility test. (with includePrerelease=true, see https://github.com/npm/node-semver?tab=readme-ov-file#functions) +// + +#[test] +#[cfg(any(feature = "mirror_node_matches_prerelease", test_node_semver))] +fn test_exact() { + // =I.J.K-pre only match I.J.K-pre + let ref r = req("=4.2.1-0"); + // Only exactly match 4.2.1-0 + assert_prerelease_match_all(r, &["4.2.1-0"]); + // Not match others + assert_prerelease_match_none(r, &["1.2.3", "4.2.0", "4.2.1-1", "4.2.2"]); + + // =I.J.K equivalent to >=I.J.K, =4.2.1, <4.2.2-0")] { + assert_prerelease_match_all(r, &["4.2.1"]); + assert_prerelease_match_none(r, &["1.2.3", "4.2.1-0", "4.2.2-0", "4.2.2"]); + } + + // =I.J equivalent to >=I.J.0-0, =4.2.0-0, <4.3.0-0")] { + assert_prerelease_match_all(r, &["4.2.0-0", "4.2.0", "4.2.1", "4.2.9"]); + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0"]); + assert_prerelease_match_none(r, &["4.3.0-0", "4.3.0", "5.0.0-0", "5.0.0"]); + } + + // =I equivalent to >=I.0.0-0, <(I+1).0.0-0 + for r in &[req("=4"), req(">=4.0.0-0, <5.0.0-0")] { + assert_prerelease_match_all(r, &["4.0.0-0", "4.0.0", "4.2.1", "4.2.4-0", "4.9.9"]); + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0"]); + assert_prerelease_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } +} + +#[test] +#[cfg(any(feature = "mirror_node_matches_prerelease", test_node_semver))] +fn test_greater_eq() { + // >=I.J.K-0 + let ref r = req(">=4.2.1-0"); + assert_prerelease_match_all(r, &["4.2.1-0", "4.2.1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "1.2.3"]); + + // >=I.J.K + let ref r = req(">=4.2.1"); + assert_prerelease_match_all(r, &["4.2.1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.2.1-0"]); + + // >=I.J equivalent to >=I.J.0-0 + for r in &[req(">=4.2"), req(">=4.2.0-0")] { + assert_prerelease_match_all(r, &["4.2.0-0", "4.2.0", "4.2.1-0", "4.3.0"]); + assert_prerelease_match_none(r, &["0.0.0", "4.1.1"]); + } + + // >=I equivalent to >=I.0.0-0 + for r in &[req(">=4"), req(">=4.0.0-0")] { + assert_prerelease_match_all(r, &["4.0.0-0", "4.0.0", "4.1.0-1", "5.0.0"]); + assert_prerelease_match_none(r, &["0.0.0", "1.2.3"]); + } +} + +#[test] +#[cfg(any(feature = "mirror_node_matches_prerelease", test_node_semver))] +fn test_less() { + // 0) — equivalent to >=I.J.K-0, <(I+1).0.0-0 + for r in &[req("^1.2.3-0"), req(">=1.2.3-0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.3-0", "1.2.3-1", "1.2.3", "1.9.9"]); + assert_prerelease_match_none(r, &["0.0.9", "1.1.1-0", "2.0.0-0", "2.1.1"]); + } + + // ^I.J.K (for I>0) — equivalent to >=I.J.K, <(I+1).0.0-0 + for r in &[req("^1.2.3"), req(">=1.2.3, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.3", "1.9.9"]); + assert_prerelease_match_none( + r, + &["0.0.9", "1.1.1-0", "1.2.3-0", "1.2.3-1", "2.0.0-0", "2.1.1"], + ); + } + + // ^0.J.K-0 (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + // ^0.J.K (for J>0) — equivalent to >=0.J.K-0, <0.(J+1).0-0 + for r in &[req("^0.2.3-0"), req("^0.2.3"), req(">=0.2.3-0, <0.3.0-0")] { + assert_prerelease_match_all(r, &["0.2.3-0", "0.2.3", "0.2.9-0", "0.2.9"]); + assert_prerelease_match_none(r, &["0.0.9", "0.3.0-0", "0.3.11", "1.1.1"]); + } + + // ^0.0.K-0 — equivalent to >=0.0.K-0, <0.0.(K+1)-0 + // ^0.0.K — equivalent to >=0.0.K-0, <0.0.(K+1)-0 + for r in &[req("^0.0.3-0"), req("^0.0.3"), req(">=0.0.3-0, <0.1.0-0")] { + assert_prerelease_match_all(r, &["0.0.3-0", "0.0.3-1", "0.0.3"]); + assert_prerelease_match_none(r, &["0.0.1", "0.3.0-0", "0.4.0-0", "1.1.1"]); + } + + // ^I.J (for I>0 or J>0) — equivalent to >=I.J.0-0, <(I+1).0.0-0) + for r in &[req("^1.2"), req(">=1.2.0-0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.2.0-0", "1.2.0", "1.9.0-0", "1.9.9"]); + assert_prerelease_match_none(r, &["0.0.1", "0.0.4-0", "2.0.0-0", "4.0.1"]); + } + + // ^0.0 — equivalent to >=0.0.0-0, <0.1.0-0 + for r in &[req("^0.0"), req(">=0.0.0-0, <0.1.0-0")] { + assert_prerelease_match_all(r, &["0.0.0-0", "0.0.0", "0.0.1", "0.0.4-0"]); + assert_prerelease_match_none(r, &["0.1.0-0", "0.1.0", "1.1.1"]); + } + + // ^I — equivalent to >=I.0.0-0, <(I+1).0.0-0 + for r in &[req("^1"), req(">=1.0.0-0, <2.0.0-0")] { + assert_prerelease_match_all(r, &["1.0.0-0", "1.0.0", "1.0.1"]); + assert_prerelease_match_none(r, &["0.1.0-0", "0.1.0", "2.0.0-0", "3.1.2"]); + } +} + +#[test] +#[cfg(any(feature = "mirror_node_matches_prerelease", test_node_semver))] +fn test_wildcard() { + // I.J.* — equivalent to =I.J + // + // =I.J equivalent to >=I.J.0-0, =I.0.0-0, <(I+1).0.0-0 + for r in &[req("4.*"), req("4.*.*"), req("=4")] { + assert_prerelease_match_all(r, &["4.0.0-0", "4.0.0", "4.2.1", "4.9.9"]); + assert_prerelease_match_none(r, &["0.0.1", "2.1.2-0"]); + assert_prerelease_match_none(r, &["5.0.0-0", "5.0.0", "5.0.1"]); + } +}