From 896344bd76bc2936a39a2b7b1caa830c7a769747 Mon Sep 17 00:00:00 2001 From: discord9 Date: Tue, 24 Feb 2026 18:28:42 +0800 Subject: [PATCH] fix: since/till properly Signed-off-by: discord9 --- tests/compat/README.md | 41 ++++ ...granularity_and_false_positive_rate.result | 22 ++- .../granularity_and_false_positive_rate.sql | 12 +- tests/runner/src/cmd/bare.rs | 5 - tests/runner/src/compatibility_runner.rs | 176 +++++++++++++----- .../runner/src/interceptors/ignore_result.rs | 42 ----- tests/runner/src/interceptors/mod.rs | 1 - tests/runner/src/interceptors/since.rs | 102 ++++++++-- tests/runner/src/interceptors/till.rs | 101 ++++++++-- tests/runner/src/version.rs | 82 +------- 10 files changed, 387 insertions(+), 197 deletions(-) delete mode 100644 tests/runner/src/interceptors/ignore_result.rs diff --git a/tests/compat/README.md b/tests/compat/README.md index 07c4cb494f..75bdfb02e0 100644 --- a/tests/compat/README.md +++ b/tests/compat/README.md @@ -13,6 +13,8 @@ This compatibility test is inspired by [Databend](https://github.com/datafuselab ## Usage +### Legacy script flow + ```shell tests/compat/test-compat.sh ``` @@ -20,6 +22,45 @@ tests/compat/test-compat.sh E.g. `tests/compat/test-compat.sh 0.6.0` tests if the data written by GreptimeDB **v0.6.0** can be read by **current** version of GreptimeDB, and vice-versa. By "current", it's meant the fresh binary built by current codes. +### Sqlness compat command (recommended) + +```shell +cargo sqlness compat --from=1.0.0-beta.1 --to=current --preserve-state +``` + +The compat command runs cases in three phases: + +1. `1.feature` on `--from` +2. `2.verify` on `--to` +3. `3.cleanup` on `--to` + +## Case markers + +Compatibility cases can use sqlness markers: + +- `-- SQLNESS SINCE ` +- `-- SQLNESS TILL ` + +For `since`/`till` skips, runner rewrites the statement before execution to avoid running skipped SQL. + +## Filter behavior + +`--test-filter` still accepts sqlness regex. Compat runner also recognizes optional leading path prefixes: + +- phase prefix: `1.feature/`, `2.verify/`, `3.cleanup/` +- mode prefix: `standalone/`, `distributed/` +- group prefix: `common/`, `only/` + +Examples: + +```shell +# run one standalone case path +cargo sqlness compat --from=1.0.0-beta.1 --to=current --test-filter="standalone/common/show-create-table.sql" + +# same target with explicit phase prefix +cargo sqlness compat --from=1.0.0-beta.1 --to=current --test-filter="2.verify/distributed/common/show-create-table.sql" +``` + ## Prerequisites Current version of GreptimeDB's binaries must reside in `./bins`: diff --git a/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.result b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.result index bafa1f15f3..1433015795 100644 --- a/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.result +++ b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.result @@ -1,4 +1,4 @@ --- SQLNESS ARG since=0.15.0 +-- SQLNESS SINCE 0.15.0 CREATE TABLE granularity_and_false_positive_rate ( ts timestamp time index, val double @@ -9,3 +9,23 @@ CREATE TABLE granularity_and_false_positive_rate ( Affected Rows: 0 +-- SQLNESS SINCE 99.0.0 +SELECT * FROM __sqlness_since_till_should_not_exist__; + +-- SQLNESS_SKIP: target version 1.0.0-beta.1 < 99.0.0 + +-- SQLNESS TILL 0.1.0 +SELECT * FROM __sqlness_since_till_should_not_exist__; + +-- SQLNESS_SKIP: target version 1.0.0-beta.1 > 0.1.0 + +-- SQLNESS SINCE 0.1.0 +-- SQLNESS TILL 99.0.0 +SELECT 1; + ++----------+ +| Int64(1) | ++----------+ +| 1 | ++----------+ + diff --git a/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.sql b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.sql index 5dde1e6b24..69b9fe1304 100644 --- a/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.sql +++ b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.sql @@ -1,4 +1,4 @@ --- SQLNESS ARG since=0.15.0 +-- SQLNESS SINCE 0.15.0 CREATE TABLE granularity_and_false_positive_rate ( ts timestamp time index, val double @@ -6,3 +6,13 @@ CREATE TABLE granularity_and_false_positive_rate ( "index.granularity" = "8192", "index.false_positive_rate" = "0.01" ); + +-- SQLNESS SINCE 99.0.0 +SELECT * FROM __sqlness_since_till_should_not_exist__; + +-- SQLNESS TILL 0.1.0 +SELECT * FROM __sqlness_since_till_should_not_exist__; + +-- SQLNESS SINCE 0.1.0 +-- SQLNESS TILL 99.0.0 +SELECT 1; diff --git a/tests/runner/src/cmd/bare.rs b/tests/runner/src/cmd/bare.rs index 137c4bc0e6..c6029e8640 100644 --- a/tests/runner/src/cmd/bare.rs +++ b/tests/runner/src/cmd/bare.rs @@ -22,7 +22,6 @@ use sqlness::{ConfigBuilder, Runner}; use crate::cmd::SqlnessConfig; use crate::env::bare::{Env, ServiceProvider, StoreConfig, WalConfig}; -use crate::interceptors::ignore_result; use crate::{protocol_interceptor, util}; #[derive(ValueEnum, Debug, Clone)] @@ -123,10 +122,6 @@ impl BareCommand { protocol_interceptor::PREFIX, Arc::new(protocol_interceptor::ProtocolInterceptorFactory), ); - interceptor_registry.register( - ignore_result::PREFIX, - Arc::new(ignore_result::IgnoreResultInterceptorFactory), - ); if let Some(d) = &self.config.case_dir { ensure!(d.is_dir(), "{} is not a directory", d.display()); diff --git a/tests/runner/src/compatibility_runner.rs b/tests/runner/src/compatibility_runner.rs index 2a0129c9ac..7970bfa58e 100644 --- a/tests/runner/src/compatibility_runner.rs +++ b/tests/runner/src/compatibility_runner.rs @@ -21,7 +21,7 @@ use sqlness::{ConfigBuilder, Runner}; use crate::cmd::bare::ServerAddr; use crate::env::bare::{StoreConfig, WalConfig}; use crate::env::compat::Env; -use crate::interceptors::{ignore_result, since, till}; +use crate::interceptors::{since, till}; use crate::version::Version; use crate::{protocol_interceptor, util}; @@ -81,27 +81,27 @@ impl CompatibilityRunner { } pub async fn run(&self) -> anyhow::Result<()> { - self.run_phase(&self.from_version, "1.feature").await?; - self.run_phase(&self.to_version, "2.verify").await?; - self.run_phase(&self.to_version, "3.cleanup").await?; + self.run_phase(&self.from_version, Phase::Feature).await?; + self.run_phase(&self.to_version, Phase::Verify).await?; + self.run_phase(&self.to_version, Phase::Cleanup).await?; Ok(()) } - async fn run_phase(&self, version: &Version, phase: &str) -> anyhow::Result<()> { + async fn run_phase(&self, version: &Version, phase: Phase) -> anyhow::Result<()> { if !self.case_dir.exists() { return Ok(()); } let (mode_filter, remainder) = Self::split_filter(self.test_filter.as_str()); - let case_root = self.case_dir.join(phase); + let case_root = self.case_dir.join(phase.dir_name()); if !case_root.exists() { return Ok(()); } let bins_dir = Self::resolve_bins_dir(version).await?; let mut store_config = self.store_config.clone(); - store_config.setup_etcd = phase == "1.feature"; + store_config.setup_etcd = phase.should_setup_etcd(); let env = Env::new_bare( self.data_dir.clone(), self.server_addr.clone(), @@ -117,10 +117,6 @@ impl CompatibilityRunner { protocol_interceptor::PREFIX, Arc::new(protocol_interceptor::ProtocolInterceptorFactory), ); - interceptor_registry.register( - ignore_result::PREFIX, - Arc::new(ignore_result::IgnoreResultInterceptorFactory), - ); interceptor_registry.register( since::PREFIX, Arc::new(since::SinceInterceptorFactory::new(version.clone())), @@ -138,7 +134,7 @@ impl CompatibilityRunner { .unwrap_or_else(|| "".to_string()); println!( "compat phase={} version={} case_dir={} test_filter={} env_filter={} phase_root={} bins_dir={}", - phase, + phase.dir_name(), version, self.case_dir.display(), self.test_filter, @@ -169,46 +165,76 @@ impl CompatibilityRunner { return (ModeFilter::Any, String::new()); } - let mut normalized = trimmed; + let normalized = Self::trim_anchors(trimmed); + let (mode, mut rest) = Self::extract_mode_and_path(normalized); + rest = Self::trim_case_group_prefixes(rest); + + (mode, rest.to_string()) + } + + fn trim_anchors(filter: &str) -> &str { + let mut normalized = filter; if let Some(rest) = normalized.strip_prefix('^') { normalized = rest; } if let Some(rest) = normalized.strip_suffix('$') { normalized = rest; } - - if let Some(rest) = normalized.strip_prefix("standalone/") { - return (ModeFilter::Standalone, Self::strip_path_prefixes(rest)); - } - if let Some(rest) = normalized.strip_prefix("distributed/") { - return (ModeFilter::Distributed, Self::strip_path_prefixes(rest)); - } - if normalized == "standalone" { - return (ModeFilter::Standalone, String::new()); - } - if normalized == "distributed" { - return (ModeFilter::Distributed, String::new()); - } - - (ModeFilter::Any, Self::strip_path_prefixes(normalized)) + normalized } - fn strip_path_prefixes(input: &str) -> String { - let mut rest = input; - for prefix in [ - "1.feature/", - "2.verify/", - "3.cleanup/", - "standalone/", - "distributed/", - "common/", - "only/", - ] { - if let Some(stripped) = rest.strip_prefix(prefix) { - rest = stripped; + fn extract_mode_and_path(filter: &str) -> (ModeFilter, &str) { + if let Some((mode, rest)) = Self::extract_mode_prefix(filter) { + return (mode, rest); + } + + if let Some(after_phase) = Self::strip_phase_prefix(filter) + && let Some((mode, rest)) = Self::extract_mode_prefix(after_phase) + { + return (mode, rest); + } + + if let Some(after_phase) = Self::strip_phase_prefix(filter) { + return (ModeFilter::Any, after_phase); + } + + (ModeFilter::Any, filter) + } + + fn extract_mode_prefix(filter: &str) -> Option<(ModeFilter, &str)> { + if let Some(rest) = filter.strip_prefix("standalone/") { + return Some((ModeFilter::Standalone, rest)); + } + if let Some(rest) = filter.strip_prefix("distributed/") { + return Some((ModeFilter::Distributed, rest)); + } + if filter == "standalone" { + return Some((ModeFilter::Standalone, "")); + } + if filter == "distributed" { + return Some((ModeFilter::Distributed, "")); + } + + None + } + + fn strip_phase_prefix(filter: &str) -> Option<&str> { + for phase in ["1.feature/", "2.verify/", "3.cleanup/"] { + if let Some(rest) = filter.strip_prefix(phase) { + return Some(rest); } } - rest.to_string() + None + } + + fn trim_case_group_prefixes(mut path: &str) -> &str { + for prefix in ["common/", "only/"] { + if let Some(rest) = path.strip_prefix(prefix) { + path = rest; + } + } + + path } fn env_filter(mode_filter: ModeFilter) -> String { @@ -251,3 +277,69 @@ enum ModeFilter { Standalone, Distributed, } + +#[derive(Copy, Clone)] +enum Phase { + Feature, + Verify, + Cleanup, +} + +impl Phase { + fn dir_name(self) -> &'static str { + match self { + Self::Feature => "1.feature", + Self::Verify => "2.verify", + Self::Cleanup => "3.cleanup", + } + } + + fn should_setup_etcd(self) -> bool { + matches!(self, Self::Feature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_filter_mode_only() { + let (mode, remainder) = CompatibilityRunner::split_filter("standalone"); + assert!(matches!(mode, ModeFilter::Standalone)); + assert!(remainder.is_empty()); + } + + #[test] + fn test_split_filter_mode_and_case_path() { + let (mode, remainder) = CompatibilityRunner::split_filter("distributed/common/show.sql"); + assert!(matches!(mode, ModeFilter::Distributed)); + assert_eq!(remainder, "show.sql"); + } + + #[test] + fn test_split_filter_phase_then_mode() { + let (mode, remainder) = + CompatibilityRunner::split_filter("1.feature/standalone/only/a.sql"); + assert!(matches!(mode, ModeFilter::Standalone)); + assert_eq!(remainder, "a.sql"); + } + + #[test] + fn test_split_filter_regex_anchors() { + let (mode, remainder) = + CompatibilityRunner::split_filter("^2.verify/distributed/common/t.sql$"); + assert!(matches!(mode, ModeFilter::Distributed)); + assert_eq!(remainder, "t.sql"); + } + + #[test] + fn test_split_filter_passthrough_regex_with_colon() { + let (mode, remainder) = CompatibilityRunner::split_filter(".*:foo/bar"); + assert!(matches!(mode, ModeFilter::Any)); + assert_eq!( + CompatibilityRunner::build_test_filter(&remainder), + ".*:foo/bar" + ); + } +} diff --git a/tests/runner/src/interceptors/ignore_result.rs b/tests/runner/src/interceptors/ignore_result.rs deleted file mode 100644 index 38733d639d..0000000000 --- a/tests/runner/src/interceptors/ignore_result.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2023 Greptime Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use sqlness::SqlnessError; -use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; - -pub const PREFIX: &str = "IGNORE_RESULT"; - -/// Interceptor that ignores result matching for a query. -/// -/// Usage: `-- SQLNESS IGNORE_RESULT` -/// -/// When this interceptor is used, the test runner will only check that the query -/// executes successfully, without comparing the actual output to the expected result. -/// This is useful for operations where the exact output may vary (e.g., timestamps, -/// auto-generated IDs) or when testing that a feature doesn't crash. -pub struct IgnoreResultInterceptor; - -impl Interceptor for IgnoreResultInterceptor { - fn after_execute(&self, result: &mut String) { - *result = "-- IGNORE_RESULT: Query executed successfully".to_string(); - } -} - -pub struct IgnoreResultInterceptorFactory; - -impl InterceptorFactory for IgnoreResultInterceptorFactory { - fn try_new(&self, _ctx: &str) -> Result { - Ok(Box::new(IgnoreResultInterceptor)) - } -} diff --git a/tests/runner/src/interceptors/mod.rs b/tests/runner/src/interceptors/mod.rs index 0ec6a98048..1bd11b1bf8 100644 --- a/tests/runner/src/interceptors/mod.rs +++ b/tests/runner/src/interceptors/mod.rs @@ -12,6 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod ignore_result; pub mod since; pub mod till; diff --git a/tests/runner/src/interceptors/since.rs b/tests/runner/src/interceptors/since.rs index a9f05d7a2f..18b03ef175 100644 --- a/tests/runner/src/interceptors/since.rs +++ b/tests/runner/src/interceptors/since.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use sqlness::SqlnessError; use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; +use sqlness::{SKIP_MARKER_PREFIX, SqlnessError}; use crate::version::Version; @@ -36,27 +36,38 @@ impl SinceInterceptor { target_version, } } + + fn maybe_rewrite_to_skip_sql(&self, sql: &mut Vec) { + if self.target_version < self.since_version { + let skip_marker = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + sql.clear(); + sql.push(format!("SELECT '{}';", skip_marker)); + } + } + + fn skip_reason(&self) -> String { + format!( + "target version {} < {}", + self.target_version, self.since_version + ) + } + + fn normalize_skip_result(&self, result: &mut String) { + if result.contains(SKIP_MARKER_PREFIX) { + *result = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + } + } } impl Interceptor for SinceInterceptor { fn before_execute(&self, sql: &mut Vec, _ctx: &mut sqlness::QueryContext) { - // Skip execution if target version is less than since version - if self.target_version < self.since_version { - sql.clear(); - } + self.maybe_rewrite_to_skip_sql(sql); } fn after_execute(&self, result: &mut String) { - result.clear(); - *result = format!( - "{} target version {} < {}", - sqlness::SKIP_MARKER_PREFIX, - self.target_version, - self.since_version - ); + self.normalize_skip_result(result); } } - pub struct SinceInterceptorFactory { target_version: Version, } @@ -80,3 +91,68 @@ impl InterceptorFactory for SinceInterceptorFactory { ))) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_before_execute_keeps_sql_when_not_skipped() { + let interceptor = SinceInterceptor::new( + Version::parse("0.15.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut sql = vec!["SELECT 1;".to_string()]; + + interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert_eq!(sql, vec!["SELECT 1;"]); + } + + #[test] + fn test_before_execute_rewrites_sql_when_skipped() { + let interceptor = SinceInterceptor::new( + Version::parse("0.16.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut sql = vec!["SELECT 1;".to_string()]; + + interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert_eq!(sql.len(), 1); + assert!(sql[0].contains(SKIP_MARKER_PREFIX)); + assert!(sql[0].contains("target version 0.15.0 < 0.16.0")); + } + + #[test] + fn test_after_execute_normalizes_skip_result() { + let interceptor = SinceInterceptor::new( + Version::parse("0.16.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut result = format!( + "+----------------+\n| {} target version 0.15.0 < 0.16.0 |\n+----------------+", + SKIP_MARKER_PREFIX + ); + + interceptor.normalize_skip_result(&mut result); + + assert_eq!( + result, + format!("{} target version 0.15.0 < 0.16.0", SKIP_MARKER_PREFIX) + ); + } + + #[test] + fn test_after_execute_keeps_non_skip_result() { + let interceptor = SinceInterceptor::new( + Version::parse("0.16.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut result = "Affected Rows: 1".to_string(); + + interceptor.normalize_skip_result(&mut result); + + assert_eq!(result, "Affected Rows: 1"); + } +} diff --git a/tests/runner/src/interceptors/till.rs b/tests/runner/src/interceptors/till.rs index 16beb7c84a..c89b45b148 100644 --- a/tests/runner/src/interceptors/till.rs +++ b/tests/runner/src/interceptors/till.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use sqlness::SqlnessError; use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; +use sqlness::{SKIP_MARKER_PREFIX, SqlnessError}; use crate::version::Version; @@ -36,26 +36,38 @@ impl TillInterceptor { target_version, } } + + fn maybe_rewrite_to_skip_sql(&self, sql: &mut Vec) { + if self.target_version > self.till_version { + let skip_marker = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + sql.clear(); + sql.push(format!("SELECT '{}';", skip_marker)); + } + } + + fn skip_reason(&self) -> String { + format!( + "target version {} > {}", + self.target_version, self.till_version + ) + } + + fn normalize_skip_result(&self, result: &mut String) { + if result.contains(SKIP_MARKER_PREFIX) { + *result = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + } + } } impl Interceptor for TillInterceptor { fn before_execute(&self, sql: &mut Vec, _ctx: &mut sqlness::QueryContext) { - if self.target_version > self.till_version { - sql.clear(); - } + self.maybe_rewrite_to_skip_sql(sql); } fn after_execute(&self, result: &mut String) { - result.clear(); - *result = format!( - "{} target version {} > {}", - sqlness::SKIP_MARKER_PREFIX, - self.target_version, - self.till_version - ); + self.normalize_skip_result(result); } } - pub struct TillInterceptorFactory { target_version: Version, } @@ -79,3 +91,68 @@ impl InterceptorFactory for TillInterceptorFactory { ))) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_before_execute_keeps_sql_when_not_skipped() { + let interceptor = TillInterceptor::new( + Version::parse("0.15.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut sql = vec!["SELECT 1;".to_string()]; + + interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert_eq!(sql, vec!["SELECT 1;"]); + } + + #[test] + fn test_before_execute_rewrites_sql_when_skipped() { + let interceptor = TillInterceptor::new( + Version::parse("0.14.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut sql = vec!["SELECT 1;".to_string()]; + + interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert_eq!(sql.len(), 1); + assert!(sql[0].contains(SKIP_MARKER_PREFIX)); + assert!(sql[0].contains("target version 0.15.0 > 0.14.0")); + } + + #[test] + fn test_after_execute_normalizes_skip_result() { + let interceptor = TillInterceptor::new( + Version::parse("0.14.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut result = format!( + "+----------------+\n| {} target version 0.15.0 > 0.14.0 |\n+----------------+", + SKIP_MARKER_PREFIX + ); + + interceptor.normalize_skip_result(&mut result); + + assert_eq!( + result, + format!("{} target version 0.15.0 > 0.14.0", SKIP_MARKER_PREFIX) + ); + } + + #[test] + fn test_after_execute_keeps_non_skip_result() { + let interceptor = TillInterceptor::new( + Version::parse("0.14.0").unwrap(), + Version::parse("0.15.0").unwrap(), + ); + let mut result = "Affected Rows: 1".to_string(); + + interceptor.normalize_skip_result(&mut result); + + assert_eq!(result, "Affected Rows: 1"); + } +} diff --git a/tests/runner/src/version.rs b/tests/runner/src/version.rs index 12c6dfc2ad..469d80641a 100644 --- a/tests/runner/src/version.rs +++ b/tests/runner/src/version.rs @@ -16,7 +16,7 @@ use std::cmp::Ordering; use std::fmt; use std::str::FromStr; -use semver::{Prerelease, Version as SemverVersion}; +use semver::Version as SemverVersion; pub type Result = std::result::Result; @@ -81,88 +81,10 @@ impl Version { let v2 = other.to_semantic(); match (v1, v2) { - (Some(v1), Some(v2)) => Self::compare_semantic(&v1, &v2), + (Some(v1), Some(v2)) => v1.cmp(&v2), (None, _) | (_, None) => Ordering::Equal, } } - - fn compare_semantic(v1: &SemverVersion, v2: &SemverVersion) -> Ordering { - match v1 - .major - .cmp(&v2.major) - .then_with(|| v1.minor.cmp(&v2.minor)) - .then_with(|| v1.patch.cmp(&v2.patch)) - { - Ordering::Equal => Self::compare_pre_release(v1, v2), - ordering => ordering, - } - } - - fn compare_pre_release(v1: &SemverVersion, v2: &SemverVersion) -> Ordering { - let pre1_empty = v1.pre.is_empty(); - let pre2_empty = v2.pre.is_empty(); - - match (pre1_empty, pre2_empty) { - (true, true) => Ordering::Equal, - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (false, false) => { - let rank1 = Self::pre_release_rank(&v1.pre); - let rank2 = Self::pre_release_rank(&v2.pre); - rank1 - .cmp(&rank2) - .then_with(|| Self::compare_pre_release_identifiers(&v1.pre, &v2.pre)) - } - } - } - - fn compare_pre_release_identifiers(pre1: &Prerelease, pre2: &Prerelease) -> Ordering { - let mut parts1 = pre1.as_str().split('.'); - let mut parts2 = pre2.as_str().split('.'); - - loop { - match (parts1.next(), parts2.next()) { - (None, None) => return Ordering::Equal, - (None, Some(_)) => return Ordering::Less, - (Some(_), None) => return Ordering::Greater, - (Some(a), Some(b)) => { - let ordering = Self::compare_pre_release_part(a, b); - if ordering != Ordering::Equal { - return ordering; - } - } - } - } - } - - fn compare_pre_release_part(a: &str, b: &str) -> Ordering { - let a_numeric = a.chars().all(|c| c.is_ascii_digit()); - let b_numeric = b.chars().all(|c| c.is_ascii_digit()); - - match (a_numeric, b_numeric) { - (true, true) => { - let a_num = a.parse::().unwrap_or(u64::MAX); - let b_num = b.parse::().unwrap_or(u64::MAX); - a_num.cmp(&b_num) - } - (true, false) => Ordering::Less, - (false, true) => Ordering::Greater, - (false, false) => a.cmp(b), - } - } - - fn pre_release_rank(pre: &Prerelease) -> u8 { - let s = pre.as_str(); - if s.starts_with("alpha") { - 0 - } else if s.starts_with("beta") { - 1 - } else if s.starts_with("rc") { - 2 - } else { - 3 - } - } } impl PartialOrd for Version {