mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-15 12:30:38 +00:00
test: basic compat framework
Signed-off-by: discord9 <discord9@163.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
1
tests/compatibility/1.feature/standalone/common
Symbolic link
1
tests/compatibility/1.feature/standalone/common
Symbolic link
@@ -0,0 +1 @@
|
||||
../distributed/common
|
||||
@@ -0,0 +1,4 @@
|
||||
SHOW CREATE TABLE granularity_and_false_positive_rate;
|
||||
|
||||
Error: 4001(TableNotFound), Table not found: granularity_and_false_positive_rate
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SHOW CREATE TABLE granularity_and_false_positive_rate;
|
||||
1
tests/compatibility/2.verify/standalone/common
Symbolic link
1
tests/compatibility/2.verify/standalone/common
Symbolic link
@@ -0,0 +1 @@
|
||||
../distributed/common
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TABLE IF EXISTS granularity_and_false_positive_rate;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS granularity_and_false_positive_rate;
|
||||
1
tests/compatibility/3.cleanup/standalone/common
Symbolic link
1
tests/compatibility/3.cleanup/standalone/common
Symbolic link
@@ -0,0 +1 @@
|
||||
../distributed/common
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
|
||||
100
tests/runner/src/cmd/compat.rs
Normal file
100
tests/runner/src/cmd/compat.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
251
tests/runner/src/compatibility_runner.rs
Normal file
251
tests/runner/src/compatibility_runner.rs
Normal 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
64
tests/runner/src/env/compat.rs
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,5 @@
|
||||
// limitations under the License.
|
||||
|
||||
pub mod bare;
|
||||
pub mod compat;
|
||||
pub mod kube;
|
||||
42
tests/runner/src/interceptors/ignore_result.rs
Normal file
42
tests/runner/src/interceptors/ignore_result.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
17
tests/runner/src/interceptors/mod.rs
Normal file
17
tests/runner/src/interceptors/mod.rs
Normal 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;
|
||||
72
tests/runner/src/interceptors/since.rs
Normal file
72
tests/runner/src/interceptors/since.rs
Normal 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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
71
tests/runner/src/interceptors/till.rs
Normal file
71
tests/runner/src/interceptors/till.rs
Normal 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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -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
208
tests/runner/src/version.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user