test: basic compat framework

Signed-off-by: discord9 <discord9@163.com>
This commit is contained in:
discord9
2026-02-05 19:51:40 +08:00
parent 0e0a892f6b
commit 96b690bfa2
21 changed files with 872 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
-- SQLNESS ARG since=0.15.0
-- SQLNESS IGNORE_RESULT
CREATE TABLE granularity_and_false_positive_rate (
ts timestamp time index,
val double
) with (
"index.granularity" = "8192",
"index.false_positive_rate" = "0.01"
);
-- IGNORE_RESULT: Query executed successfully

View File

@@ -0,0 +1,9 @@
-- SQLNESS ARG since=0.15.0
-- SQLNESS IGNORE_RESULT
CREATE TABLE granularity_and_false_positive_rate (
ts timestamp time index,
val double
) with (
"index.granularity" = "8192",
"index.false_positive_rate" = "0.01"
);

View File

@@ -0,0 +1 @@
../distributed/common

View File

@@ -0,0 +1,4 @@
SHOW CREATE TABLE granularity_and_false_positive_rate;
Error: 4001(TableNotFound), Table not found: granularity_and_false_positive_rate

View File

@@ -0,0 +1 @@
SHOW CREATE TABLE granularity_and_false_positive_rate;

View File

@@ -0,0 +1 @@
../distributed/common

View File

@@ -0,0 +1,4 @@
DROP TABLE IF EXISTS granularity_and_false_positive_rate;
Affected Rows: 0

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS granularity_and_false_positive_rate;

View File

@@ -0,0 +1 @@
../distributed/common

View File

@@ -13,12 +13,14 @@
// limitations under the License.
pub(crate) mod bare;
pub(crate) mod compat;
pub(crate) mod kube;
use std::path::PathBuf;
use bare::BareCommand;
use clap::Parser;
use compat::CompatCommand;
use kube::KubeCommand;
#[derive(Parser)]
@@ -32,6 +34,7 @@ pub struct Command {
pub enum SubCommand {
Bare(BareCommand),
Kube(KubeCommand),
Compat(CompatCommand),
}
#[derive(Debug, Parser)]

View File

@@ -21,6 +21,7 @@ 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)]
@@ -121,6 +122,10 @@ 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
&& !d.is_dir()

View File

@@ -0,0 +1,100 @@
// 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 std::path::PathBuf;
use clap::Parser;
use crate::compatibility_runner::CompatibilityRunner;
use crate::version::Version;
#[derive(Debug, Parser)]
pub struct CompatCommand {
#[clap(long)]
from: String,
#[clap(long)]
to: String,
#[clap(short, long)]
case_dir: Option<PathBuf>,
/// Fail this run as soon as one case fails if true
#[arg(short, long, default_value = "false")]
fail_fast: bool,
/// Name of test cases to run. Accept as a regexp.
#[clap(short, long, default_value = ".*")]
test_filter: String,
#[clap(long)]
preserve_state: bool,
}
impl CompatCommand {
pub async fn run(self) {
let from_version = match Version::parse(&self.from) {
Ok(v) => v,
Err(e) => {
eprintln!("Error parsing 'from' version: {}", e);
std::process::exit(1);
}
};
let to_version = match Version::parse(&self.to) {
Ok(v) => v,
Err(e) => {
eprintln!("Error parsing 'to' version: {}", e);
std::process::exit(1);
}
};
let temp_dir = tempfile::Builder::new()
.prefix("compat-test")
.tempdir()
.unwrap();
let data_dir = temp_dir.keep();
let runner = match CompatibilityRunner::new(
from_version,
to_version,
self.case_dir,
data_dir.clone(),
self.test_filter,
self.fail_fast,
)
.await
{
Ok(r) => r,
Err(e) => {
eprintln!("Failed to create compatibility runner: {}", e);
std::process::exit(1);
}
};
match runner.run().await {
Ok(_) => {
println!("\x1b[32mCompatibility tests passed!\x1b[0m");
}
Err(e) => {
eprintln!("\x1b[31mCompatibility tests failed: {}\x1b[0m", e);
std::process::exit(1);
}
}
if !self.preserve_state {
tokio::fs::remove_dir_all(data_dir).await.unwrap();
}
}
}

View File

@@ -0,0 +1,251 @@
// 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 std::path::PathBuf;
use std::sync::Arc;
use sqlness::interceptor::Registry;
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::version::Version;
use crate::{protocol_interceptor, util};
pub struct CompatibilityRunner {
from_version: Version,
to_version: Version,
case_dir: PathBuf,
data_dir: PathBuf,
test_filter: String,
fail_fast: bool,
server_addr: ServerAddr,
wal_config: WalConfig,
store_config: StoreConfig,
pull_version_on_need: bool,
extra_args: Vec<String>,
}
impl CompatibilityRunner {
pub async fn new(
from_version: Version,
to_version: Version,
case_dir: Option<PathBuf>,
data_dir: PathBuf,
test_filter: String,
fail_fast: bool,
) -> anyhow::Result<Self> {
let case_dir = case_dir.unwrap_or_else(|| {
let mut path = PathBuf::from(util::get_workspace_root());
path.push("tests");
path.push("compatibility");
path
});
Ok(Self {
from_version,
to_version,
case_dir,
data_dir,
test_filter,
fail_fast,
server_addr: ServerAddr {
server_addr: None,
pg_server_addr: None,
mysql_server_addr: None,
},
wal_config: WalConfig::RaftEngine,
store_config: StoreConfig {
store_addrs: vec![],
setup_etcd: false,
setup_pg: None,
setup_mysql: None,
enable_flat_format: false,
},
pull_version_on_need: true,
extra_args: vec![],
})
}
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?;
Ok(())
}
async fn run_phase(&self, version: &Version, phase: &str) -> 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);
if !case_root.exists() {
return Ok(());
}
let bins_dir = Self::resolve_bins_dir(version).await?;
let env = Env::new_bare(
self.data_dir.clone(),
self.server_addr.clone(),
self.wal_config.clone(),
self.pull_version_on_need,
bins_dir.clone(),
self.store_config.clone(),
self.extra_args.clone(),
);
let mut interceptor_registry: Registry = Default::default();
interceptor_registry.register(
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())),
);
interceptor_registry.register(
till::PREFIX,
Arc::new(till::TillInterceptorFactory::new(version.clone())),
);
let env_filter = Self::env_filter(mode_filter);
let test_filter = Self::build_test_filter(remainder.as_str());
let bins_dir_display = bins_dir
.as_ref()
.map(|dir| dir.display().to_string())
.unwrap_or_else(|| "<build>".to_string());
println!(
"compat phase={} version={} case_dir={} test_filter={} env_filter={} phase_root={} bins_dir={}",
phase,
version,
self.case_dir.display(),
self.test_filter,
env_filter,
case_root.display(),
bins_dir_display
);
let config = ConfigBuilder::default()
.case_dir(case_root.to_string_lossy().to_string())
.fail_fast(self.fail_fast)
.test_filter(test_filter)
.env_filter(env_filter)
.follow_links(true)
.interceptor_registry(interceptor_registry)
.parallelism(1)
.build()?;
let runner = Runner::new(config, env);
runner.run().await?;
Ok(())
}
fn split_filter(filter: &str) -> (ModeFilter, String) {
let trimmed = filter.trim();
if trimmed.is_empty() || trimmed == ".*" {
return (ModeFilter::Any, String::new());
}
let mut normalized = trimmed;
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))
}
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;
}
}
rest.to_string()
}
fn env_filter(mode_filter: ModeFilter) -> String {
match mode_filter {
ModeFilter::Standalone => "^standalone$".to_string(),
ModeFilter::Distributed => "^distributed$".to_string(),
ModeFilter::Any => ".*".to_string(),
}
}
fn build_test_filter(remainder: &str) -> String {
if remainder.is_empty() {
return ".*".to_string();
}
if remainder.contains(':') {
return remainder.to_string();
}
format!(".*:{}", remainder)
}
async fn resolve_bins_dir(version: &Version) -> anyhow::Result<Option<PathBuf>> {
match version {
Version::Current => Ok(None),
Version::Semantic(v) => {
let version_str = format!("v{}", v);
let dir = PathBuf::from(&version_str);
let binary_path = dir.join(util::PROGRAM);
if !binary_path.exists() {
util::pull_binary(&version_str).await;
}
Ok(Some(dir))
}
}
}
}
#[derive(Copy, Clone)]
enum ModeFilter {
Any,
Standalone,
Distributed,
}

64
tests/runner/src/env/compat.rs vendored Normal file
View File

@@ -0,0 +1,64 @@
// 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 std::path::Path;
use async_trait::async_trait;
use sqlness::EnvController;
use crate::cmd::bare::ServerAddr;
use crate::env::bare;
use crate::env::bare::{StoreConfig, WalConfig};
#[derive(Clone)]
pub struct Env {
inner: bare::Env,
}
impl Env {
pub fn new_bare(
data_home: std::path::PathBuf,
server_addrs: ServerAddr,
wal: WalConfig,
pull_version_on_need: bool,
bins_dir: Option<std::path::PathBuf>,
store_config: StoreConfig,
extra_args: Vec<String>,
) -> Self {
Self {
inner: bare::Env::new(
data_home,
server_addrs,
wal,
pull_version_on_need,
bins_dir,
store_config,
extra_args,
),
}
}
}
#[async_trait]
impl EnvController for Env {
type DB = bare::GreptimeDB;
async fn start(&self, mode: &str, id: usize, config: Option<&Path>) -> Self::DB {
EnvController::start(&self.inner, mode, id, config).await
}
async fn stop(&self, mode: &str, database: Self::DB) {
EnvController::stop(&self.inner, mode, database).await
}
}

View File

@@ -13,4 +13,5 @@
// limitations under the License.
pub mod bare;
pub mod compat;
pub mod kube;

View File

@@ -0,0 +1,42 @@
// 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<InterceptorRef, SqlnessError> {
Ok(Box::new(IgnoreResultInterceptor))
}
}

View File

@@ -0,0 +1,17 @@
// 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.
pub mod ignore_result;
pub mod since;
pub mod till;

View File

@@ -0,0 +1,72 @@
// 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};
use crate::version::Version;
pub const PREFIX: &str = "SINCE";
/// Interceptor that skips tests if the target version is less than the specified version.
///
/// Usage: `-- SQLNESS SINCE 0.15.0`
///
/// The test will be skipped if `target_version < since_version`.
pub struct SinceInterceptor {
since_version: Version,
target_version: Version,
}
impl SinceInterceptor {
pub fn new(since_version: Version, target_version: Version) -> Self {
Self {
since_version,
target_version,
}
}
}
impl Interceptor for SinceInterceptor {
fn before_execute(&self, sql: &mut Vec<String>, _ctx: &mut sqlness::QueryContext) {
// Skip execution if target version is less than since version
if self.target_version < self.since_version {
sql.clear();
}
}
}
pub struct SinceInterceptorFactory {
target_version: Version,
}
impl SinceInterceptorFactory {
pub fn new(target_version: Version) -> Self {
Self { target_version }
}
}
impl InterceptorFactory for SinceInterceptorFactory {
fn try_new(&self, ctx: &str) -> Result<InterceptorRef, SqlnessError> {
let since_version = Version::parse(ctx).map_err(|e| SqlnessError::InvalidContext {
prefix: PREFIX.to_string(),
msg: format!("Failed to parse version '{}': {:?}", ctx, e),
})?;
Ok(Box::new(SinceInterceptor::new(
since_version,
self.target_version.clone(),
)))
}
}

View File

@@ -0,0 +1,71 @@
// 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};
use crate::version::Version;
pub const PREFIX: &str = "TILL";
/// Interceptor that skips tests if the target version is greater than the specified version.
///
/// Usage: `-- SQLNESS TILL 0.15.0`
///
/// The test will be skipped if `target_version > till_version`.
pub struct TillInterceptor {
till_version: Version,
target_version: Version,
}
impl TillInterceptor {
pub fn new(till_version: Version, target_version: Version) -> Self {
Self {
till_version,
target_version,
}
}
}
impl Interceptor for TillInterceptor {
fn before_execute(&self, sql: &mut Vec<String>, _ctx: &mut sqlness::QueryContext) {
if self.target_version > self.till_version {
sql.clear();
}
}
}
pub struct TillInterceptorFactory {
target_version: Version,
}
impl TillInterceptorFactory {
pub fn new(target_version: Version) -> Self {
Self { target_version }
}
}
impl InterceptorFactory for TillInterceptorFactory {
fn try_new(&self, ctx: &str) -> Result<InterceptorRef, SqlnessError> {
let till_version = Version::parse(ctx).map_err(|e| SqlnessError::InvalidContext {
prefix: PREFIX.to_string(),
msg: format!("Failed to parse version '{}': {:?}", ctx, e),
})?;
Ok(Box::new(TillInterceptor::new(
till_version,
self.target_version.clone(),
)))
}
}

View File

@@ -20,11 +20,14 @@ use crate::cmd::{Command, SubCommand};
pub mod client;
mod cmd;
mod compatibility_runner;
mod env;
pub mod formatter;
mod interceptors;
pub mod protocol_interceptor;
mod server_mode;
mod util;
mod version;
#[tokio::main]
async fn main() {
@@ -33,5 +36,6 @@ async fn main() {
match cmd.subcmd {
SubCommand::Bare(cmd) => cmd.run().await,
SubCommand::Kube(cmd) => cmd.run().await,
SubCommand::Compat(cmd) => cmd.run().await,
}
}

208
tests/runner/src/version.rs Normal file
View File

@@ -0,0 +1,208 @@
// 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 std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use semver::{Prerelease, Version as SemverVersion};
pub type Result<T> = std::result::Result<T, VersionError>;
#[derive(Debug)]
pub enum VersionError {
ParseError(semver::Error),
}
impl fmt::Display for VersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionError::ParseError(e) => write!(f, "Failed to parse version: {}", e),
}
}
}
impl std::error::Error for VersionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
VersionError::ParseError(e) => Some(e),
}
}
}
/// Represents a version that can be either a semantic version or a special version like "Current"
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Version {
Semantic(SemverVersion),
Current,
}
impl Version {
pub fn parse(s: &str) -> Result<Self> {
let lower = s.to_lowercase();
if lower == "current" {
Ok(Version::Current)
} else {
let inner = SemverVersion::parse(s).map_err(|e| VersionError::ParseError(e))?;
Ok(Version::Semantic(inner))
}
}
pub fn is_current(&self) -> bool {
matches!(self, Version::Current)
}
pub fn to_semantic(&self) -> Option<SemverVersion> {
match self {
Version::Semantic(v) => Some(v.clone()),
Version::Current => Self::current_version_from_cargo(),
}
}
fn current_version_from_cargo() -> Option<SemverVersion> {
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
SemverVersion::parse(CARGO_PKG_VERSION).ok()
}
pub fn compare(&self, other: &Version) -> Ordering {
let v1 = self.to_semantic();
let v2 = other.to_semantic();
match (v1, v2) {
(Some(v1), Some(v2)) => Self::compare_semantic(&v1, &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(|| v1.pre.as_str().cmp(v2.pre.as_str()))
}
}
}
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 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.compare(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
self.compare(other)
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Version::Semantic(v) => write!(f, "{}", v),
Version::Current => write!(f, "current"),
}
}
}
impl FromStr for Version {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_semantic_version() {
let v = Version::parse("0.15.0").unwrap();
assert!(matches!(v, Version::Semantic(_)));
}
#[test]
fn test_parse_current() {
let v = Version::parse("current").unwrap();
assert_eq!(v, Version::Current);
let v = Version::parse("CURRENT").unwrap();
assert_eq!(v, Version::Current);
}
#[test]
fn test_semantic_comparison() {
assert!(Version::parse("0.15.0").unwrap() < Version::parse("0.16.0").unwrap());
assert!(Version::parse("1.0.0").unwrap() > Version::parse("0.15.0").unwrap());
assert_eq!(
Version::parse("0.15.0").unwrap(),
Version::parse("0.15.0").unwrap()
);
}
#[test]
fn test_pre_release_comparison() {
assert!(
Version::parse("1.0.0-alpha.1").unwrap() < Version::parse("1.0.0-alpha.2").unwrap()
);
assert!(Version::parse("1.0.0-alpha.2").unwrap() < Version::parse("1.0.0-beta.1").unwrap());
assert!(Version::parse("1.0.0-beta.1").unwrap() < Version::parse("1.0.0-rc.1").unwrap());
assert!(Version::parse("1.0.0-rc.1").unwrap() < Version::parse("1.0.0").unwrap());
}
#[test]
fn test_current_comparison() {
let current = Version::Current;
assert!(current == Version::Current);
let current_sem = current.to_semantic();
assert!(current_sem.is_some());
}
}