use std::{
cmp::Ordering,
error::Error,
fmt,
process::{ExitCode, Termination},
};
use clap::ValueEnum;
use indexmap::IndexMap;
use rand::prelude::*;
use regex::Regex;
use semver::{BuildMetadata, Prerelease, Version, VersionReq};
use serde::Serialize;
use super::regex::{SEMVER_REGEX, generate_any_valid_semver, generate_u64_safe_semver};
macro_rules! impl_success_termination {
($($ty:ty),* $(,)?) => {
$(impl Termination for $ty {
fn report(self) -> ExitCode {
ExitCode::SUCCESS
}
})*
};
}
fn writeln_items<T: fmt::Display>(items: &[T], f: &mut fmt::Formatter<'_>) -> fmt::Result {
for item in items {
writeln!(f, "{item}")?;
}
Ok(())
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SemverComponent {
Major,
Minor,
Patch,
PreRelease,
BuildMetadata,
}
#[derive(Serialize, PartialEq)]
pub(crate) struct ValidateResult {
valid: bool,
}
impl ValidateResult {
pub(crate) fn validate(semantic_version: String, small: bool) -> Self {
let pass = if small {
Version::parse(&semantic_version).is_ok()
} else {
Regex::new(super::regex::SEMVER_REGEX)
.unwrap()
.is_match(&semantic_version)
};
Self { valid: pass }
}
}
impl fmt::Display for ValidateResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "valid: {}", self.valid)?;
Ok(())
}
}
impl Termination for ValidateResult {
fn report(self) -> std::process::ExitCode {
if self.valid {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct FilterTestResult {
pass: bool,
}
impl FilterTestResult {
pub(crate) fn filter_test(filter: &VersionReq, semantic_version: &Version) -> FilterTestResult {
filter.matches(semantic_version).into()
}
}
impl Termination for FilterTestResult {
fn report(self) -> std::process::ExitCode {
if self.pass {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
}
impl From<bool> for FilterTestResult {
fn from(value: bool) -> Self {
Self { pass: value }
}
}
impl fmt::Display for FilterTestResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "pass: {}", self.pass)?;
Ok(())
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct SelectResult {
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
#[serde(skip)]
fail_if_not_found: bool,
}
#[derive(Debug)]
struct SelectParseError(String);
impl fmt::Display for SelectParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SelectParseError {}
impl SelectResult {
pub(crate) fn select(
version: &str,
component: SemverComponent,
small: bool,
fail_if_not_found: bool,
) -> Result<Self, Box<dyn Error>> {
let value = if small {
let v = Version::parse(version).map_err(|e| {
SelectParseError(format!("unable to parse semantic version: {version}: {e}"))
})?;
Self::extract_from_version(&v, component)
} else {
let parsed = parse_semver_components(version).ok_or_else(|| {
SelectParseError(format!(
"version string did not match semver regex: {version}"
))
})?;
Self::extract_from_regex(parsed, component)
};
Ok(Self {
value,
fail_if_not_found,
})
}
fn extract_from_version(v: &Version, component: SemverComponent) -> Option<String> {
match component {
SemverComponent::Major => Some(v.major.to_string()),
SemverComponent::Minor => Some(v.minor.to_string()),
SemverComponent::Patch => Some(v.patch.to_string()),
SemverComponent::PreRelease => {
let s = v.pre.as_str();
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
SemverComponent::BuildMetadata => {
let s = v.build.as_str();
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
}
}
fn extract_from_regex(
(major, minor, patch, pre, build): SemverRegexCaptures,
component: SemverComponent,
) -> Option<String> {
match component {
SemverComponent::Major => Some(major),
SemverComponent::Minor => Some(minor),
SemverComponent::Patch => Some(patch),
SemverComponent::PreRelease => pre,
SemverComponent::BuildMetadata => build,
}
}
}
type SemverRegexCaptures = (String, String, String, Option<String>, Option<String>);
fn parse_semver_components(s: &str) -> Option<SemverRegexCaptures> {
let re = Regex::new(SEMVER_REGEX).ok()?;
let cap = re.captures(s)?;
let major = cap.get(1)?.as_str().to_string();
let minor = cap.get(2)?.as_str().to_string();
let patch = cap.get(3)?.as_str().to_string();
let pre = cap
.get(4)
.map(|m| m.as_str().to_string())
.filter(|s| !s.is_empty());
let build = cap
.get(5)
.map(|m| m.as_str().to_string())
.filter(|s| !s.is_empty());
Some((major, minor, patch, pre, build))
}
impl fmt::Display for SelectResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref v) = self.value {
writeln!(f, "{v}")?;
}
Ok(())
}
}
impl Termination for SelectResult {
fn report(self) -> std::process::ExitCode {
if self.fail_if_not_found && self.value.is_none() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
pub(crate) enum SegmentType {
Numeric,
Ascii,
}
impl fmt::Display for SegmentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SegmentType::Numeric => write!(f, "Numeric"),
SegmentType::Ascii => write!(f, "Ascii"),
}
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
pub(crate) struct PreMetaSegment {
kind: SegmentType,
value: String,
}
impl fmt::Display for PreMetaSegment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.value, self.kind)
}
}
impl From<&str> for PreMetaSegment {
fn from(value: &str) -> Self {
PreMetaSegment {
kind: if value.chars().all(|c| c.is_ascii_digit()) {
SegmentType::Numeric
} else {
SegmentType::Ascii
},
value: value.to_string(),
}
}
}
#[derive(Clone, Serialize, PartialEq)]
pub(crate) struct VersionExplanation {
major: u64,
minor: u64,
patch: u64,
#[serde(skip_serializing_if = "Option::is_none")]
prerelease_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename(serialize = "prerelease"))]
prerelease: Option<Vec<PreMetaSegment>>,
#[serde(skip_serializing_if = "Option::is_none")]
build_metadata_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename(serialize = "build-metadata"))]
build_metadata: Option<Vec<PreMetaSegment>>,
}
impl From<&Version> for VersionExplanation {
fn from(value: &Version) -> Self {
let prerelease_string = value.pre.as_str();
let (prerelease_string, prerelease) = if prerelease_string.is_empty() {
(None, None)
} else {
(
Some(prerelease_string.to_string()),
Some(
prerelease_string
.split('.')
.map(PreMetaSegment::from)
.collect(),
),
)
};
let build_metadata_string = value.build.as_str();
let (build_metadata_string, build_metadata) = if build_metadata_string.is_empty() {
(None, None)
} else {
(
Some(build_metadata_string.to_string()),
Some(
build_metadata_string
.split('.')
.map(PreMetaSegment::from)
.collect(),
),
)
};
Self {
major: value.major,
minor: value.minor,
patch: value.patch,
prerelease,
prerelease_string,
build_metadata,
build_metadata_string,
}
}
}
impl fmt::Display for VersionExplanation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Major: {}", self.major)?;
writeln!(f, "Minor: {}", self.minor)?;
writeln!(f, "Patch: {}", self.patch)?;
if let (Some(pr_str), Some(pr)) = (&self.prerelease_string, &self.prerelease) {
writeln!(f, "PreRelease: {}", pr_str)?;
for i in pr.iter() {
writeln!(f, "- {i}")?;
}
}
if let (Some(bm_str), Some(bm)) = (&self.build_metadata_string, &self.build_metadata) {
writeln!(f, "Build Metadata: {}", bm_str)?;
for i in bm.iter() {
writeln!(f, "- {i}")?;
}
}
Ok(())
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct FlatVersionsList {
versions: Vec<Version>,
potentially_ambiguous: bool,
}
impl From<&mut OrderedVersionMap> for FlatVersionsList {
fn from(value: &mut OrderedVersionMap) -> Self {
let mut flat: Vec<Version> = Vec::new();
value.inner.iter_mut().for_each(|vv| flat.append(vv.1));
Self {
versions: flat,
potentially_ambiguous: value.potentially_ambiguous,
}
}
}
impl fmt::Display for FlatVersionsList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln_items(&self.versions, f)
}
}
#[derive(Serialize)]
pub(crate) struct OrderedVersionMap {
#[serde(rename(serialize = "versions"))]
inner: IndexMap<Version, Vec<Version>>,
potentially_ambiguous: bool,
}
impl OrderedVersionMap {
pub(crate) fn new(
versions: &mut Vec<Version>,
filter: &Option<VersionReq>,
lexical_sorting: bool,
reverse: bool,
stable: bool,
) -> Self {
if let Some(filter) = filter {
versions.retain(|v| filter.matches(v));
}
if stable {
versions.retain(|v| v.pre.is_empty());
}
if reverse {
versions.sort_by(|a, b| b.cmp(a));
} else {
versions.sort();
}
let mut ordered_version_map: IndexMap<Version, Vec<Version>> = IndexMap::new();
for version in versions {
let key = version_without_build_metadata(version);
match ordered_version_map.get_mut(&key) {
Some(v) => v.push(version.clone()),
None => {
let new_value = vec![version.clone()];
let map_response = ordered_version_map.insert(key, new_value);
if map_response.is_some() {
panic!("should not have gotten a map response for an empty key")
}
}
}
}
let mut potentially_ambiguous = false;
for (_, v) in ordered_version_map.iter_mut() {
if lexical_sorting {
if reverse {
v.sort_by(|a, b| b.cmp(a));
} else {
v.sort();
}
} else {
v.shuffle(&mut rand::rng());
}
if v.len() > 1 {
potentially_ambiguous = true
}
}
Self {
inner: ordered_version_map,
potentially_ambiguous,
}
}
pub(crate) fn potentially_ambiguous(&self) -> bool {
self.potentially_ambiguous
}
}
impl fmt::Display for OrderedVersionMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for key in self.inner.keys() {
writeln!(f, "{key}:")?;
if let Some(vals) = self.inner.get(key) {
for val in vals {
writeln!(f, "\t- {val}")?;
}
}
}
Ok(())
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct ComparisonStatement {
semantic_ordering: SerializableOrdering,
lexical_ordering: SerializableOrdering,
}
impl ComparisonStatement {
pub(crate) fn new(a: &Version, b: &Version) -> Self {
let a_no_build = version_without_build_metadata(a);
let b_no_build = version_without_build_metadata(b);
Self {
semantic_ordering: a_no_build.cmp(&b_no_build).into(),
lexical_ordering: a.cmp(b).into(),
}
}
pub(crate) fn semantic_ordering(&self) -> &SerializableOrdering {
&self.semantic_ordering
}
#[allow(dead_code)]
pub(crate) fn lexical_ordering(&self) -> &SerializableOrdering {
&self.lexical_ordering
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct GenerateResult {
#[serde(rename(serialize = "versions"))]
inner: Vec<String>,
}
impl GenerateResult {
pub(crate) fn new(small: bool, count: usize) -> Self {
let inner = if small {
generate_u64_safe_semver(count)
} else {
generate_any_valid_semver(count)
};
GenerateResult { inner }
}
#[cfg(test)]
pub(crate) fn into_inner(self) -> Vec<String> {
self.inner
}
}
impl fmt::Display for GenerateResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln_items(&self.inner, f)
}
}
#[derive(Serialize, PartialEq)]
pub(crate) struct VersionMutationResult {
pub(crate) mutated_version: Version,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum BoundaryKind {
Min,
Max,
}
#[derive(Serialize, PartialEq)]
pub(crate) struct BoundaryVersionResult {
pub(crate) versions: Vec<Version>,
pub(crate) potentially_ambiguous: bool,
pub(crate) lexical_tiebreak_used: bool,
pub(crate) stable_filter_applied: bool,
}
impl BoundaryVersionResult {
pub(crate) fn boundary_versions(
map: &OrderedVersionMap,
kind: BoundaryKind,
allow_ambiguous: bool,
lexical_sorting: bool,
stable_filter_applied: bool,
) -> Result<Self, super::misc::ApplicationError> {
if map.inner.is_empty() {
return Err(super::misc::ApplicationError::FailedRequirementError {
err: "no versions remaining after filters".to_string(),
});
}
let (_key, group) = match kind {
BoundaryKind::Max => map
.inner
.iter()
.max_by(|a, b| a.0.cmp(b.0))
.expect("non-empty map"),
BoundaryKind::Min => map
.inner
.iter()
.min_by(|a, b| a.0.cmp(b.0))
.expect("non-empty map"),
};
let potentially_ambiguous = group.len() > 1;
if potentially_ambiguous && !allow_ambiguous && !lexical_sorting {
return Err(super::misc::ApplicationError::FailedRequirementError {
err: "ambiguous boundary (same precedence, differing build metadata)".to_string(),
});
}
let versions = if potentially_ambiguous && allow_ambiguous {
group.clone()
} else if potentially_ambiguous && lexical_sorting {
let picked = match kind {
BoundaryKind::Max => group.last().expect("non-empty group"),
BoundaryKind::Min => group.first().expect("non-empty group"),
};
vec![picked.clone()]
} else {
vec![group[0].clone()]
};
Ok(Self {
versions,
potentially_ambiguous,
lexical_tiebreak_used: potentially_ambiguous && lexical_sorting && !allow_ambiguous,
stable_filter_applied,
})
}
}
impl fmt::Display for BoundaryVersionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln_items(&self.versions, f)
}
}
impl VersionMutationResult {
pub(crate) fn set(
version: &Version,
major: Option<u64>,
minor: Option<u64>,
patch: Option<u64>,
pre_release: Option<String>,
build_metadata: Option<String>,
) -> Result<VersionMutationResult, Box<dyn Error>> {
let mut response = version.clone();
if let Some(major) = major {
response.major = major
}
if let Some(minor) = minor {
response.minor = minor
}
if let Some(patch) = patch {
response.patch = patch
}
if let Some(pre_release) = pre_release {
response.pre = match Prerelease::new(pre_release.as_str()) {
Ok(p) => p,
Err(e) => return Err(format!("unable to use string as pre-release: {}", e).into()),
};
}
if let Some(build_metadata) = build_metadata {
response.build = match BuildMetadata::new(build_metadata.as_str()) {
Ok(b) => b,
Err(e) => {
return Err(format!("unable to use string as build metadata: {}", e).into());
}
};
}
Ok(VersionMutationResult {
mutated_version: response,
})
}
pub(crate) fn bump(
version: &Version,
major: Option<u64>,
minor: Option<u64>,
patch: Option<u64>,
) -> Result<VersionMutationResult, Box<dyn Error>> {
let mut response = version.clone();
if let Some(major) = major {
response.major = match response.major.checked_add(major) {
Some(m) => m,
None => {
return Err(format!(
"major bump ({} + {}) overflows u64",
response.major, major
)
.into());
}
}
}
if let Some(minor) = minor {
response.minor = match response.minor.checked_add(minor) {
Some(m) => m,
None => {
return Err(format!(
"minor bump ({} + {}) overflows u64",
response.minor, minor
)
.into());
}
}
}
if let Some(patch) = patch {
response.patch = match response.patch.checked_add(patch) {
Some(m) => m,
None => {
return Err(format!(
"patch bump ({} + {}) overflows u64",
response.patch, patch
)
.into());
}
}
}
Ok(VersionMutationResult {
mutated_version: response,
})
}
pub(crate) fn bump_reset(
version: &Version,
major_reset: bool,
clear_pre_release: bool,
clear_build_metadata: bool,
normal_version_only: bool,
) -> Result<VersionMutationResult, Box<dyn Error>> {
let mut response = version.clone();
if major_reset {
response.major = match response.major.checked_add(1) {
Some(m) => m,
None => {
return Err(
format!("major bump-reset ({} + 1) overflows u64", response.major).into(),
);
}
};
response.minor = 0;
response.patch = 0;
} else {
response.minor = match response.minor.checked_add(1) {
Some(m) => m,
None => {
return Err(
format!("minor bump-reset ({} + 1) overflows u64", response.minor).into(),
);
}
};
response.patch = 0;
}
let clear_pre = normal_version_only || clear_pre_release;
let clear_build = normal_version_only || clear_build_metadata;
if clear_pre {
response.pre = Prerelease::EMPTY;
}
if clear_build {
response.build = BuildMetadata::EMPTY;
}
Ok(VersionMutationResult {
mutated_version: response,
})
}
}
impl fmt::Display for VersionMutationResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.mutated_version)
}
}
impl Termination for ComparisonStatement {
fn report(self) -> ExitCode {
match (self.semantic_ordering, self.lexical_ordering) {
(SerializableOrdering::Equal, SerializableOrdering::Equal) => ExitCode::SUCCESS,
(sem, lex) => ExitCode::from(simplify_exit_code(sem, lex)),
}
}
}
fn simplify_exit_code(sem: SerializableOrdering, lex: SerializableOrdering) -> u8 {
100 + (Into::<u8>::into(sem) * 10) + Into::<u8>::into(lex)
}
impl fmt::Display for ComparisonStatement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Semantically: {:?}\nLexically: {:?}\n",
self.semantic_ordering, self.lexical_ordering
)
}
}
#[derive(Debug, Serialize, PartialEq)]
pub(crate) enum SerializableOrdering {
Less,
Greater,
Equal,
}
impl From<SerializableOrdering> for u8 {
fn from(value: SerializableOrdering) -> Self {
match value {
SerializableOrdering::Less => 0,
SerializableOrdering::Equal => 1,
SerializableOrdering::Greater => 2,
}
}
}
impl From<Ordering> for SerializableOrdering {
fn from(value: Ordering) -> Self {
match value {
Ordering::Less => SerializableOrdering::Less,
Ordering::Equal => SerializableOrdering::Equal,
Ordering::Greater => SerializableOrdering::Greater,
}
}
}
pub fn version_without_build_metadata(version: &Version) -> Version {
Version {
major: version.major,
minor: version.minor,
patch: version.patch,
pre: version.pre.clone(),
build: BuildMetadata::EMPTY,
}
}
pub fn compare_exit_code(a: &Version, b: &Version) -> i32 {
let a_no_build = version_without_build_metadata(a);
let b_no_build = version_without_build_metadata(b);
ordering_pair_to_exit_code(a_no_build.cmp(&b_no_build), a.cmp(b))
}
pub fn ordering_pair_to_exit_code(sem: Ordering, lex: Ordering) -> i32 {
match (
SerializableOrdering::from(sem),
SerializableOrdering::from(lex),
) {
(SerializableOrdering::Equal, SerializableOrdering::Equal) => 0,
(s, l) => simplify_exit_code(s, l) as i32,
}
}
impl_success_termination!(
VersionExplanation,
FlatVersionsList,
OrderedVersionMap,
GenerateResult,
BoundaryVersionResult,
VersionMutationResult,
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_without_build_metadata() {
assert_eq!(
version_without_build_metadata(&Version::parse("0.0.0-123.123+123.123").unwrap()).build,
BuildMetadata::EMPTY
);
}
#[test]
fn test_pre_meta_segment() {
assert_eq!(
PreMetaSegment::from("00000aaaa00000"),
PreMetaSegment {
kind: SegmentType::Ascii,
value: "00000aaaa00000".to_string()
}
);
assert_eq!(
PreMetaSegment::from("00000"),
PreMetaSegment {
kind: SegmentType::Numeric,
value: "00000".to_string()
}
);
assert_eq!(
PreMetaSegment::from("00001"),
PreMetaSegment {
kind: SegmentType::Numeric,
value: "00001".to_string()
}
);
assert_eq!(
PreMetaSegment::from("-00001"),
PreMetaSegment {
kind: SegmentType::Ascii,
value: "-00001".to_string()
}
);
}
#[test]
fn test_ordered_version_map() {
let mut scaffold1 = ["99.0.0", "100.0.0", "0.0.1"]
.iter()
.map(|v| Version::parse(v).unwrap())
.collect();
let test = OrderedVersionMap::new(&mut scaffold1, &None, false, false, false);
assert!(test.inner.contains_key(&Version::parse("99.0.0").unwrap()));
assert!(test.inner.contains_key(&Version::parse("100.0.0").unwrap()));
assert!(test.inner.contains_key(&Version::parse("0.0.1").unwrap()));
assert!(!test.potentially_ambiguous);
let mut scaffold2: Vec<Version> = vec![
"0.0.0-alpha.0+metadata",
"0.0.0-alpha.0+other.metadata",
"0.0.0-alpha.0+other.metadata.3",
"0.0.1",
"0.0.2",
"0.2.0",
"0.2.99",
"1.0.0-rc.1",
"1.0.0-rc-1",
"1.0.0-rc-2+aaaaaa",
"1.0.0-rc-2+bbbbbb",
"1.0.0-rc-2+cccccc",
"1.0.0-rc-2+dddddd",
"1.0.0-rc-2.0+dddddd",
"1.0.0-rc-2.1+dddddd",
"1.0.0-rc-2+dddddd.0",
"1.0.0-rc-2+dddddd.1",
"1.0.0-rc-2+eeeeee",
"1.0.0+aaaaaa",
"1.0.0",
"99.99.0-rc1.0",
]
.iter()
.map(|v| Version::parse(v).unwrap())
.collect();
let test = OrderedVersionMap::new(&mut scaffold2, &None, false, false, false);
let test_keys: Vec<Version> = test.inner.keys().cloned().collect();
assert!(test_keys.len() == 12);
assert!(test_keys[0] == Version::parse("0.0.0-alpha.0").unwrap());
assert!(test_keys[test_keys.len() - 1] == Version::parse("99.99.0-rc1.0").unwrap());
assert!(test.potentially_ambiguous);
let test = OrderedVersionMap::new(&mut scaffold2, &None, false, true, false);
let test_keys: Vec<Version> = test.inner.keys().cloned().collect();
assert!(test_keys.len() == 12);
assert!(test_keys[test_keys.len() - 1] == Version::parse("0.0.0-alpha.0").unwrap());
assert!(test_keys[0] == Version::parse("99.99.0-rc1.0").unwrap());
assert!(test.potentially_ambiguous);
let test = OrderedVersionMap::new(
&mut scaffold2,
&Some(VersionReq::parse("*").unwrap()),
false,
false,
false,
);
let test_keys: Vec<Version> = test.inner.keys().cloned().collect();
assert!(test_keys.len() == 5);
assert!(test_keys[0] == Version::parse("0.0.1").unwrap());
assert!(test_keys[test_keys.len() - 1] == Version::parse("1.0.0").unwrap());
let _ = format!("{}", test);
}
#[test]
fn flat_version_list() {
let mut scaffold: Vec<Version> = vec![
"0.0.0-alpha.0+metadata",
"0.0.0-alpha.0+other.metadata",
"0.0.0-alpha.0+other.metadata.3",
"0.0.1",
"0.0.2",
"0.2.0",
"0.2.99",
"1.0.0-rc.1",
"1.0.0-rc-1",
"1.0.0-rc-2+aaaaaa",
"1.0.0-rc-2+bbbbbb",
"1.0.0-rc-2+cccccc",
"1.0.0-rc-2+dddddd",
"1.0.0-rc-2.0+dddddd",
"1.0.0-rc-2.1+dddddd",
"1.0.0-rc-2+dddddd.0",
"1.0.0-rc-2+dddddd.1",
"1.0.0-rc-2+eeeeee",
"1.0.0+aaaaaa",
"1.0.0",
"99.99.0-rc1.0",
]
.iter()
.map(|v| Version::parse(v).unwrap())
.collect();
let mut test = OrderedVersionMap::new(&mut scaffold, &None, true, false, false);
let test = FlatVersionsList::from(&mut test);
assert!(test.versions.len() == 21);
assert!(test.versions[0] == Version::parse("0.0.0-alpha.0+metadata").unwrap());
assert!(test.versions[test.versions.len() - 1] == Version::parse("99.99.0-rc1.0").unwrap());
let mut test = OrderedVersionMap::new(&mut scaffold, &None, true, true, false);
let test = FlatVersionsList::from(&mut test);
assert!(test.versions.len() == 21);
assert!(
test.versions[test.versions.len() - 1]
== Version::parse("0.0.0-alpha.0+metadata").unwrap()
);
assert!(test.versions[0] == Version::parse("99.99.0-rc1.0").unwrap());
let _ = format!("{}", test);
}
#[test]
fn test_version_explanation() {
let test =
VersionExplanation::from(&Version::parse("0.0.0-0.a.b.c.4+0.-1.a.b0.3").unwrap());
assert!(test.major == 0);
assert!(test.minor == 0);
assert!(test.patch == 0);
assert!(test.prerelease.is_some());
let test_prerelease = test.prerelease.as_ref().unwrap();
assert!(test_prerelease.len() == 5);
assert!(test_prerelease[1].kind == SegmentType::Ascii);
assert!(test_prerelease[1].value == "a");
assert!(test_prerelease[test_prerelease.len() - 1].kind == SegmentType::Numeric);
assert!(test_prerelease[test_prerelease.len() - 1].value == "4");
assert!(test.prerelease_string.as_ref().unwrap() == "0.a.b.c.4");
assert!(test.build_metadata.is_some());
let test_build_metadata = test.build_metadata.as_ref().unwrap();
assert!(test_build_metadata.len() == 5);
assert!(test_build_metadata[1].kind == SegmentType::Ascii);
assert!(test_build_metadata[1].value == "-1");
assert!(test_build_metadata[test_build_metadata.len() - 1].kind == SegmentType::Numeric);
assert!(test_build_metadata[test_build_metadata.len() - 1].value == "3");
assert!(test.build_metadata_string.as_ref().unwrap() == "0.-1.a.b0.3");
let _ = format!("{}", test);
}
#[test]
fn test_filter_test_result() {
let test = FilterTestResult::filter_test(
&VersionReq::parse(">1").unwrap(),
&Version::parse("0.0.0").unwrap(),
);
assert!(!test.pass);
assert_eq!(test.report(), ExitCode::FAILURE);
let test = FilterTestResult::filter_test(
&VersionReq::parse(">1").unwrap(),
&Version::parse("2.0.0").unwrap(),
);
assert!(test.pass);
assert_eq!(test.report(), ExitCode::SUCCESS);
let test = FilterTestResult::filter_test(
&VersionReq::parse(">=1").unwrap(),
&Version::parse("1.0.0").unwrap(),
);
assert!(test.pass);
assert_eq!(test.report(), ExitCode::SUCCESS);
let test = FilterTestResult::filter_test(
&VersionReq::parse(">=1").unwrap(),
&Version::parse("1.0.0").unwrap(),
);
let _ = format!("{}", test);
}
#[test]
fn test_validate() {
let test = ValidateResult::validate("0.0.0-x+b".to_string(), true);
assert!(test.valid);
let test = ValidateResult::validate("0.0.0-x+b".to_string(), false);
assert!(test.valid);
let test = ValidateResult::validate("18446744073709551616.0.0-x+b".to_string(), true);
assert!(!test.valid);
let test = ValidateResult::validate("18446744073709551616.0.0-x+b".to_string(), false);
assert!(test.valid);
let test = ValidateResult::validate("0.0.0a".to_string(), true);
assert!(!test.valid);
let test = ValidateResult::validate("0.0.0a".to_string(), false);
assert!(!test.valid);
let _ = format!("{}", test);
}
#[test]
fn test_generate() {
let test = GenerateResult::new(false, 10);
assert_eq!(test.into_inner().len(), 10);
let test = GenerateResult::new(true, 10);
for s in test.into_inner() {
assert!(Version::parse(&s).is_ok())
}
let test = GenerateResult::new(true, 1);
let _ = format!("{}", test);
}
#[test]
fn test_bump_and_set() {
let base_version = Version::new(1, 1, 1);
let set_version = VersionMutationResult::set(
&base_version,
Some(2),
Some(3),
Some(4),
Some("a.b.c".to_string()),
Some("x.y.z".to_string()),
)
.unwrap();
let bmp_version =
VersionMutationResult::bump(&base_version, Some(2), Some(3), Some(4)).unwrap();
assert_eq!(set_version.mutated_version.major, 2);
assert_eq!(bmp_version.mutated_version.major, 3);
assert_eq!(set_version.mutated_version.minor, 3);
assert_eq!(bmp_version.mutated_version.minor, 4);
assert_eq!(set_version.mutated_version.patch, 4);
assert_eq!(bmp_version.mutated_version.patch, 5);
assert_eq!(set_version.mutated_version.pre.as_str(), "a.b.c");
assert_eq!(bmp_version.mutated_version.pre, Prerelease::EMPTY);
assert_eq!(set_version.mutated_version.build.as_str(), "x.y.z");
assert_eq!(bmp_version.mutated_version.build, BuildMetadata::EMPTY);
assert!(
VersionMutationResult::bump(&base_version, Some(u64::MAX), Some(3), Some(4)).is_err()
);
assert!(
VersionMutationResult::bump(&base_version, Some(2), Some(u64::MAX), Some(4)).is_err()
);
assert!(
VersionMutationResult::bump(&base_version, Some(2), Some(3), Some(u64::MAX)).is_err()
);
}
#[test]
fn test_bump_reset() {
let v = Version::parse("1.2.3-rc.1+ci.42").unwrap();
let minor = VersionMutationResult::bump_reset(&v, false, false, false, false).unwrap();
assert_eq!(minor.mutated_version.to_string(), "1.3.0-rc.1+ci.42");
let major = VersionMutationResult::bump_reset(&v, true, false, false, false).unwrap();
assert_eq!(major.mutated_version.to_string(), "2.0.0-rc.1+ci.42");
let cleared = VersionMutationResult::bump_reset(&v, false, true, true, false).unwrap();
assert_eq!(cleared.mutated_version.to_string(), "1.3.0");
let normal = VersionMutationResult::bump_reset(&v, false, false, false, true).unwrap();
assert_eq!(normal.mutated_version.to_string(), "1.3.0");
}
#[test]
fn test_boundary_versions() {
let mut versions: Vec<Version> = ["1.0.0", "2.0.0+bm", "2.0.0+bm2"]
.iter()
.map(|s| Version::parse(s).unwrap())
.collect();
let map = OrderedVersionMap::new(&mut versions, &None, false, false, false);
let max =
BoundaryVersionResult::boundary_versions(&map, BoundaryKind::Max, false, false, false);
assert!(max.is_err());
let max_lex =
BoundaryVersionResult::boundary_versions(&map, BoundaryKind::Max, false, true, false)
.unwrap();
assert_eq!(max_lex.versions.len(), 1);
assert!(max_lex.lexical_tiebreak_used);
let min =
BoundaryVersionResult::boundary_versions(&map, BoundaryKind::Min, false, false, false)
.unwrap();
assert_eq!(min.versions[0].to_string(), "1.0.0");
}
#[test]
fn test_ordered_version_map_stable() {
let mut versions: Vec<Version> = ["1.0.0-alpha", "1.0.0", "2.0.0"]
.iter()
.map(|s| Version::parse(s).unwrap())
.collect();
let map = OrderedVersionMap::new(&mut versions, &None, false, false, true);
assert_eq!(map.inner.len(), 2);
for key in map.inner.keys() {
assert!(key.pre.is_empty());
}
}
use proptest::prelude::*;
use proptest_semver::*;
use std::collections::HashMap;
fn prop_version_without_build(v: &Version) -> Version {
Version {
major: v.major,
minor: v.minor,
patch: v.patch,
pre: v.pre.clone(),
build: BuildMetadata::EMPTY,
}
}
fn prop_boundary_precedence_key(versions: &[Version], kind_max: bool) -> Version {
versions
.iter()
.map(prop_version_without_build)
.min_by(|a, b| if kind_max { b.cmp(a) } else { a.cmp(b) })
.expect("non-empty")
}
fn prop_boundary_group_at_key(filtered: &[Version], kind_max: bool) -> Vec<Version> {
let key = prop_boundary_precedence_key(filtered, kind_max);
filtered
.iter()
.filter(|v| prop_version_without_build(v) == key)
.cloned()
.collect()
}
fn prop_expected_lexical_pick(filtered: &[Version], kind_max: bool, reverse: bool) -> Version {
let mut group = prop_boundary_group_at_key(filtered, kind_max);
if reverse {
group.sort_by(|a, b| b.cmp(a));
} else {
group.sort();
}
if kind_max {
group.last().expect("non-empty group").clone()
} else {
group.first().expect("non-empty group").clone()
}
}
fn prop_boundary_ambiguous(versions: &[Version], kind_max: bool) -> bool {
let mut groups: HashMap<Version, usize> = HashMap::new();
for v in versions {
*groups.entry(prop_version_without_build(v)).or_insert(0) += 1;
}
let key = if kind_max {
groups.keys().max().cloned()
} else {
groups.keys().min().cloned()
};
key.and_then(|k| groups.get(&k).copied())
.map(|c| c > 1)
.unwrap_or(false)
}
proptest! {
#![proptest_config(ProptestConfig {
fork: true,
cases: 256,
.. ProptestConfig::default()
})]
#[test]
fn prop_boundary_versions(
stable: bool,
reverse: bool,
lexical_sorting: bool,
allow_ambiguous: bool,
kind_max: bool,
filter in arb_optional_version_req(0.5, 2),
mut versions in arb_vec_versions(16),
) {
let map = OrderedVersionMap::new(
&mut versions,
&filter,
lexical_sorting,
reverse,
stable,
);
let kind = if kind_max {
BoundaryKind::Max
} else {
BoundaryKind::Min
};
let result = BoundaryVersionResult::boundary_versions(
&map,
kind,
allow_ambiguous,
lexical_sorting,
stable,
);
if map.inner.is_empty() {
prop_assert!(result.is_err());
} else {
let filtered: Vec<Version> = map
.inner
.values()
.flat_map(|group| group.iter().cloned())
.collect();
let ambiguous = prop_boundary_ambiguous(&filtered, kind_max);
let expected_key = prop_boundary_precedence_key(&filtered, kind_max);
if ambiguous && !allow_ambiguous && !lexical_sorting {
prop_assert!(result.is_err());
} else {
let ok = result.expect("expected boundary success");
if allow_ambiguous && ambiguous {
prop_assert!(ok.versions.len() > 1);
for v in &ok.versions {
prop_assert_eq!(prop_version_without_build(v), expected_key.clone());
}
} else if lexical_sorting && ambiguous {
prop_assert_eq!(ok.versions.len(), 1);
prop_assert_eq!(
ok.versions[0].clone(),
prop_expected_lexical_pick(&filtered, kind_max, reverse)
);
} else {
prop_assert_eq!(ok.versions.len(), 1);
prop_assert_eq!(
prop_version_without_build(&ok.versions[0]),
expected_key
);
}
}
}
}
}
#[test]
fn test_comparison_statement() {
let test = ComparisonStatement::new(
&Version::parse("0.0.0").unwrap(),
&Version::parse("2.0.0").unwrap(),
);
assert_eq!(test.semantic_ordering, SerializableOrdering::Less);
assert_eq!(test.lexical_ordering, SerializableOrdering::Less);
assert_eq!(test.report(), 100.into());
let test = ComparisonStatement::new(
&Version::parse("2.0.0+100").unwrap(),
&Version::parse("2.0.0").unwrap(),
);
assert_eq!(test.semantic_ordering, SerializableOrdering::Equal);
assert_eq!(test.lexical_ordering, SerializableOrdering::Greater);
assert_eq!(test.report(), 112.into());
let test = ComparisonStatement::new(
&Version::parse("2.0.0").unwrap(),
&Version::parse("2.0.0-rc1").unwrap(),
);
assert_eq!(test.semantic_ordering, SerializableOrdering::Greater);
assert_eq!(test.lexical_ordering, SerializableOrdering::Greater);
assert_eq!(test.report(), 122.into());
let test = ComparisonStatement::new(
&Version::parse("2.4.2").unwrap(),
&Version::parse("2.4.2").unwrap(),
);
assert_eq!(test.semantic_ordering, SerializableOrdering::Equal);
assert_eq!(test.lexical_ordering, SerializableOrdering::Equal);
assert_eq!(test.report(), ExitCode::SUCCESS);
let test = ComparisonStatement::new(
&Version::parse("2.4.2").unwrap(),
&Version::parse("2.4.2").unwrap(),
);
let _ = format!("{}", test);
}
}