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 new file mode 100644 index 0000000000..2974597747 --- /dev/null +++ b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.result @@ -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 + 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 new file mode 100644 index 0000000000..645882fca4 --- /dev/null +++ b/tests/compatibility/1.feature/distributed/common/granularity_and_false_positive_rate.sql @@ -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" +); diff --git a/tests/compatibility/1.feature/standalone/common b/tests/compatibility/1.feature/standalone/common new file mode 120000 index 0000000000..87837b1015 --- /dev/null +++ b/tests/compatibility/1.feature/standalone/common @@ -0,0 +1 @@ +../distributed/common \ No newline at end of file diff --git a/tests/compatibility/2.verify/distributed/common/show-create-table.result b/tests/compatibility/2.verify/distributed/common/show-create-table.result new file mode 100644 index 0000000000..27c01ed877 --- /dev/null +++ b/tests/compatibility/2.verify/distributed/common/show-create-table.result @@ -0,0 +1,4 @@ +SHOW CREATE TABLE granularity_and_false_positive_rate; + +Error: 4001(TableNotFound), Table not found: granularity_and_false_positive_rate + diff --git a/tests/compatibility/2.verify/distributed/common/show-create-table.sql b/tests/compatibility/2.verify/distributed/common/show-create-table.sql new file mode 100644 index 0000000000..37c764fded --- /dev/null +++ b/tests/compatibility/2.verify/distributed/common/show-create-table.sql @@ -0,0 +1 @@ +SHOW CREATE TABLE granularity_and_false_positive_rate; diff --git a/tests/compatibility/2.verify/standalone/common b/tests/compatibility/2.verify/standalone/common new file mode 120000 index 0000000000..87837b1015 --- /dev/null +++ b/tests/compatibility/2.verify/standalone/common @@ -0,0 +1 @@ +../distributed/common \ No newline at end of file diff --git a/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.result b/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.result new file mode 100644 index 0000000000..6ae5037f12 --- /dev/null +++ b/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.result @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS granularity_and_false_positive_rate; + +Affected Rows: 0 + diff --git a/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.sql b/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.sql new file mode 100644 index 0000000000..5cbd05d23f --- /dev/null +++ b/tests/compatibility/3.cleanup/distributed/common/granularity_and_false_positive_rate.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS granularity_and_false_positive_rate; diff --git a/tests/compatibility/3.cleanup/standalone/common b/tests/compatibility/3.cleanup/standalone/common new file mode 120000 index 0000000000..87837b1015 --- /dev/null +++ b/tests/compatibility/3.cleanup/standalone/common @@ -0,0 +1 @@ +../distributed/common \ No newline at end of file diff --git a/tests/runner/src/cmd.rs b/tests/runner/src/cmd.rs index f7aaacfc73..322ed92c1e 100644 --- a/tests/runner/src/cmd.rs +++ b/tests/runner/src/cmd.rs @@ -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)] diff --git a/tests/runner/src/cmd/bare.rs b/tests/runner/src/cmd/bare.rs index e9a4ff8b79..03fb88d742 100644 --- a/tests/runner/src/cmd/bare.rs +++ b/tests/runner/src/cmd/bare.rs @@ -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() diff --git a/tests/runner/src/cmd/compat.rs b/tests/runner/src/cmd/compat.rs new file mode 100644 index 0000000000..9a6a38f9aa --- /dev/null +++ b/tests/runner/src/cmd/compat.rs @@ -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, + + /// 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(); + } + } +} diff --git a/tests/runner/src/compatibility_runner.rs b/tests/runner/src/compatibility_runner.rs new file mode 100644 index 0000000000..1807b71772 --- /dev/null +++ b/tests/runner/src/compatibility_runner.rs @@ -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, +} + +impl CompatibilityRunner { + pub async fn new( + from_version: Version, + to_version: Version, + case_dir: Option, + data_dir: PathBuf, + test_filter: String, + fail_fast: bool, + ) -> anyhow::Result { + 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(|| "".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> { + 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, +} diff --git a/tests/runner/src/env/compat.rs b/tests/runner/src/env/compat.rs new file mode 100644 index 0000000000..bb1f58bce7 --- /dev/null +++ b/tests/runner/src/env/compat.rs @@ -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, + store_config: StoreConfig, + extra_args: Vec, + ) -> 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 + } +} diff --git a/tests/runner/src/env.rs b/tests/runner/src/env/mod.rs similarity index 97% rename from tests/runner/src/env.rs rename to tests/runner/src/env/mod.rs index e9a9d05fe8..d930313d8c 100644 --- a/tests/runner/src/env.rs +++ b/tests/runner/src/env/mod.rs @@ -13,4 +13,5 @@ // limitations under the License. pub mod bare; +pub mod compat; pub mod kube; diff --git a/tests/runner/src/interceptors/ignore_result.rs b/tests/runner/src/interceptors/ignore_result.rs new file mode 100644 index 0000000000..38733d639d --- /dev/null +++ b/tests/runner/src/interceptors/ignore_result.rs @@ -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 { + Ok(Box::new(IgnoreResultInterceptor)) + } +} diff --git a/tests/runner/src/interceptors/mod.rs b/tests/runner/src/interceptors/mod.rs new file mode 100644 index 0000000000..0ec6a98048 --- /dev/null +++ b/tests/runner/src/interceptors/mod.rs @@ -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; diff --git a/tests/runner/src/interceptors/since.rs b/tests/runner/src/interceptors/since.rs new file mode 100644 index 0000000000..f55151c177 --- /dev/null +++ b/tests/runner/src/interceptors/since.rs @@ -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, _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 { + 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(), + ))) + } +} diff --git a/tests/runner/src/interceptors/till.rs b/tests/runner/src/interceptors/till.rs new file mode 100644 index 0000000000..8f80b9d270 --- /dev/null +++ b/tests/runner/src/interceptors/till.rs @@ -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, _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 { + 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(), + ))) + } +} diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs index c6cf5dd4b5..4dcf4d3dd1 100644 --- a/tests/runner/src/main.rs +++ b/tests/runner/src/main.rs @@ -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, } } diff --git a/tests/runner/src/version.rs b/tests/runner/src/version.rs new file mode 100644 index 0000000000..6c809e992f --- /dev/null +++ b/tests/runner/src/version.rs @@ -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 = std::result::Result; + +#[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 { + 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 { + match self { + Version::Semantic(v) => Some(v.clone()), + Version::Current => Self::current_version_from_cargo(), + } + } + + fn current_version_from_cargo() -> Option { + 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 { + 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::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()); + } +}