From f02682691ba803dd89bba5a14b60a4fc538c63e2 Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 11 Mar 2026 11:34:19 +0800 Subject: [PATCH] feat: version interceptor Signed-off-by: discord9 --- tests/compat/README.md | 12 +- ...granularity_and_false_positive_rate.result | 13 +- .../granularity_and_false_positive_rate.sql | 9 +- tests/runner/Cargo.toml | 1 + tests/runner/src/compatibility_runner.rs | 12 +- tests/runner/src/env/bare.rs | 2 +- tests/runner/src/interceptors/mod.rs | 3 +- tests/runner/src/interceptors/since.rs | 158 ------- tests/runner/src/interceptors/till.rs | 158 ------- tests/runner/src/interceptors/version.rs | 386 ++++++++++++++++++ tests/runner/src/util.rs | 23 +- 11 files changed, 434 insertions(+), 343 deletions(-) delete mode 100644 tests/runner/src/interceptors/since.rs delete mode 100644 tests/runner/src/interceptors/till.rs create mode 100644 tests/runner/src/interceptors/version.rs diff --git a/tests/compat/README.md b/tests/compat/README.md index 75bdfb02e0..66571bfa35 100644 --- a/tests/compat/README.md +++ b/tests/compat/README.md @@ -36,12 +36,16 @@ The compat command runs cases in three phases: ## Case markers -Compatibility cases can use sqlness markers: +Compatibility cases can use sqlness marker: -- `-- SQLNESS SINCE ` -- `-- SQLNESS TILL ` +- `-- SQLNESS VERSION ` -For `since`/`till` skips, runner rewrites the statement before execution to avoid running skipped SQL. +Examples: + +- `-- SQLNESS VERSION version >= 0.15.0` +- `-- SQLNESS VERSION version > 1.0.0 AND version < 1.1.0` + +For `VERSION` skips, runner rewrites the statement before execution to avoid running skipped SQL. ## Filter behavior 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 1433015795..06d1049e22 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 SINCE 0.15.0 +-- SQLNESS VERSION version >= 0.15.0 CREATE TABLE granularity_and_false_positive_rate ( ts timestamp time index, val double @@ -9,18 +9,17 @@ CREATE TABLE granularity_and_false_positive_rate ( Affected Rows: 0 --- SQLNESS SINCE 99.0.0 +-- SQLNESS VERSION version >= 99.0.0 SELECT * FROM __sqlness_since_till_should_not_exist__; --- SQLNESS_SKIP: target version 1.0.0-beta.1 < 99.0.0 +-- SQLNESS_SKIP: target version 1.0.0-beta.1 does not satisfy expression: version >= 99.0.0 --- SQLNESS TILL 0.1.0 +-- SQLNESS VERSION version <= 0.1.0 SELECT * FROM __sqlness_since_till_should_not_exist__; --- SQLNESS_SKIP: target version 1.0.0-beta.1 > 0.1.0 +-- SQLNESS_SKIP: target version 1.0.0-beta.1 does not satisfy expression: version <= 0.1.0 --- SQLNESS SINCE 0.1.0 --- SQLNESS TILL 99.0.0 +-- SQLNESS VERSION version >= 0.1.0 AND version <= 99.0.0 SELECT 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 69b9fe1304..ad3b864f27 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 SINCE 0.15.0 +-- SQLNESS VERSION version >= 0.15.0 CREATE TABLE granularity_and_false_positive_rate ( ts timestamp time index, val double @@ -7,12 +7,11 @@ CREATE TABLE granularity_and_false_positive_rate ( "index.false_positive_rate" = "0.01" ); --- SQLNESS SINCE 99.0.0 +-- SQLNESS VERSION version >= 99.0.0 SELECT * FROM __sqlness_since_till_should_not_exist__; --- SQLNESS TILL 0.1.0 +-- SQLNESS VERSION version <= 0.1.0 SELECT * FROM __sqlness_since_till_should_not_exist__; --- SQLNESS SINCE 0.1.0 --- SQLNESS TILL 99.0.0 +-- SQLNESS VERSION version >= 0.1.0 AND version <= 99.0.0 SELECT 1; diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml index a55a3ce38e..3a887b39fa 100644 --- a/tests/runner/Cargo.toml +++ b/tests/runner/Cargo.toml @@ -21,6 +21,7 @@ flate2 = "1.0" hex = "0.4" local-ip-address = "0.6" mysql = { version = "26", default-features = false, features = ["minimal", "rustls-tls-ring"] } +nom = "7.1.3" num_cpus = "1.16" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } semver = "1.0" diff --git a/tests/runner/src/compatibility_runner.rs b/tests/runner/src/compatibility_runner.rs index 7970bfa58e..661adcaa26 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::{since, till}; +use crate::interceptors::version as version_interceptor; use crate::version::Version; use crate::{protocol_interceptor, util}; @@ -118,12 +118,10 @@ impl CompatibilityRunner { Arc::new(protocol_interceptor::ProtocolInterceptorFactory), ); interceptor_registry.register( - since::PREFIX, - Arc::new(since::SinceInterceptorFactory::new(version.clone())), - ); - interceptor_registry.register( - till::PREFIX, - Arc::new(till::TillInterceptorFactory::new(version.clone())), + version_interceptor::PREFIX, + Arc::new(version_interceptor::VersionInterceptorFactory::new( + version.clone(), + )), ); let env_filter = Self::env_filter(mode_filter); diff --git a/tests/runner/src/env/bare.rs b/tests/runner/src/env/bare.rs index 1501d18512..4f3ce715d6 100644 --- a/tests/runner/src/env/bare.rs +++ b/tests/runner/src/env/bare.rs @@ -316,7 +316,7 @@ impl Env { let abs_bins_dir = bins_dir .canonicalize() - .expect("Failed to canonicalize bins_dir"); + .unwrap_or_else(|_| panic!("Failed to canonicalize bins_dir: {}", bins_dir.display())); let mut process = Command::new(abs_bins_dir.join(program)) .current_dir(bins_dir.clone()) diff --git a/tests/runner/src/interceptors/mod.rs b/tests/runner/src/interceptors/mod.rs index 1bd11b1bf8..99ca5250da 100644 --- a/tests/runner/src/interceptors/mod.rs +++ b/tests/runner/src/interceptors/mod.rs @@ -12,5 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod since; -pub mod till; +pub mod version; diff --git a/tests/runner/src/interceptors/since.rs b/tests/runner/src/interceptors/since.rs deleted file mode 100644 index 18b03ef175..0000000000 --- a/tests/runner/src/interceptors/since.rs +++ /dev/null @@ -1,158 +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::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; -use sqlness::{SKIP_MARKER_PREFIX, SqlnessError}; - -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, - } - } - - 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) { - self.maybe_rewrite_to_skip_sql(sql); - } - - fn after_execute(&self, result: &mut String) { - self.normalize_skip_result(result); - } -} -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(), - ))) - } -} - -#[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 deleted file mode 100644 index c89b45b148..0000000000 --- a/tests/runner/src/interceptors/till.rs +++ /dev/null @@ -1,158 +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::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; -use sqlness::{SKIP_MARKER_PREFIX, SqlnessError}; - -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, - } - } - - 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) { - self.maybe_rewrite_to_skip_sql(sql); - } - - fn after_execute(&self, result: &mut String) { - self.normalize_skip_result(result); - } -} -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(), - ))) - } -} - -#[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/interceptors/version.rs b/tests/runner/src/interceptors/version.rs new file mode 100644 index 0000000000..f2d4fc2c38 --- /dev/null +++ b/tests/runner/src/interceptors/version.rs @@ -0,0 +1,386 @@ +// 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 nom::branch::alt; +use nom::bytes::complete::{tag, tag_no_case, take_while1}; +use nom::character::complete::multispace0; +use nom::combinator::{all_consuming, map}; +use nom::error::Error; +use nom::multi::fold_many0; +use nom::sequence::{delimited, preceded}; +use nom::{IResult, Parser as NomParser}; +use sqlness::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; +use sqlness::{SKIP_MARKER_PREFIX, SqlnessError}; + +use crate::version::Version; + +pub const PREFIX: &str = "VERSION"; + +pub struct VersionInterceptor { + target_version: Version, + expression: Expression, + raw_expression: String, +} + +impl VersionInterceptor { + pub fn new(target_version: Version, expression: Expression, raw_expression: String) -> Self { + Self { + target_version, + expression, + raw_expression, + } + } + + fn skip_reason(&self) -> String { + format!( + "target version {} does not satisfy expression: {}", + self.target_version, self.raw_expression + ) + } + + fn maybe_rewrite_to_skip_sql(&self, sql: &mut Vec) -> bool { + if self.expression.eval(&self.target_version) { + return false; + } + + let skip_marker = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + sql.clear(); + sql.push(format!("SELECT '{}';", skip_marker)); + true + } + + fn normalize_skip_result(&self, result: &mut String) { + let reason = self.skip_reason(); + if result.contains(SKIP_MARKER_PREFIX) && result.contains(reason.as_str()) { + *result = format!("{} {}", SKIP_MARKER_PREFIX, self.skip_reason()); + } + } +} + +impl Interceptor for VersionInterceptor { + fn before_execute(&self, sql: &mut Vec, _ctx: &mut sqlness::QueryContext) { + self.maybe_rewrite_to_skip_sql(sql); + } + + fn after_execute(&self, result: &mut String) { + self.normalize_skip_result(result); + } +} + +pub struct VersionInterceptorFactory { + target_version: Version, +} + +impl VersionInterceptorFactory { + pub fn new(target_version: Version) -> Self { + Self { target_version } + } +} + +impl InterceptorFactory for VersionInterceptorFactory { + fn try_new(&self, ctx: &str) -> Result { + let raw_expression = ctx.trim(); + if raw_expression.is_empty() { + return Err(SqlnessError::InvalidContext { + prefix: PREFIX.to_string(), + msg: "Expression cannot be empty".to_string(), + }); + } + + let expression = + Parser::parse(raw_expression).map_err(|e| SqlnessError::InvalidContext { + prefix: PREFIX.to_string(), + msg: e, + })?; + + Ok(Box::new(VersionInterceptor::new( + self.target_version.clone(), + expression, + raw_expression.to_string(), + ))) + } +} + +#[derive(Debug, Clone)] +pub enum Expression { + Comparison(ComparisonOp, Version), + And(Box, Box), + Or(Box, Box), + Not(Box), +} + +impl Expression { + fn eval(&self, target: &Version) -> bool { + match self { + Expression::Comparison(op, rhs) => op.eval(target, rhs), + Expression::And(lhs, rhs) => lhs.eval(target) && rhs.eval(target), + Expression::Or(lhs, rhs) => lhs.eval(target) || rhs.eval(target), + Expression::Not(inner) => !inner.eval(target), + } + } +} + +#[derive(Debug, Clone)] +pub enum ComparisonOp { + Lt, + Le, + Gt, + Ge, + Eq, + Ne, +} + +impl ComparisonOp { + fn eval(&self, lhs: &Version, rhs: &Version) -> bool { + match self { + ComparisonOp::Lt => lhs < rhs, + ComparisonOp::Le => lhs <= rhs, + ComparisonOp::Gt => lhs > rhs, + ComparisonOp::Ge => lhs >= rhs, + ComparisonOp::Eq => lhs == rhs, + ComparisonOp::Ne => lhs != rhs, + } + } +} + +struct Parser { + _private: (), +} + +impl Parser { + fn parse(input: &str) -> Result { + match all_consuming(Self::ws(Self::parse_or_expr))(input) { + Ok((_, expression)) => Ok(expression), + Err(err) => Err(format!("Failed to parse VERSION expression: {err:?}")), + } + } + + fn ws<'a, O, F>(inner: F) -> impl NomParser<&'a str, O, Error<&'a str>> + where + F: NomParser<&'a str, O, Error<&'a str>>, + { + delimited(multispace0, inner, multispace0) + } + + fn parse_or_expr(input: &str) -> IResult<&str, Expression> { + let (input, left) = Self::parse_and_expr(input)?; + fold_many0( + preceded(Self::ws(tag_no_case("OR")), Self::parse_and_expr), + move || left.clone(), + |acc, rhs| Expression::Or(Box::new(acc), Box::new(rhs)), + ) + .parse(input) + } + + fn parse_and_expr(input: &str) -> IResult<&str, Expression> { + let (input, left) = Self::parse_unary_expr(input)?; + fold_many0( + preceded(Self::ws(tag_no_case("AND")), Self::parse_unary_expr), + move || left.clone(), + |acc, rhs| Expression::And(Box::new(acc), Box::new(rhs)), + ) + .parse(input) + } + + fn parse_unary_expr(input: &str) -> IResult<&str, Expression> { + let not_parser = map( + preceded(Self::ws(tag_no_case("NOT")), Self::parse_unary_expr), + |expr| Expression::Not(Box::new(expr)), + ); + alt((not_parser, Self::parse_primary_expr)).parse(input) + } + + fn parse_primary_expr(input: &str) -> IResult<&str, Expression> { + alt(( + delimited(Self::ws(tag("(")), Self::parse_or_expr, Self::ws(tag(")"))), + Self::parse_comparison, + )) + .parse(input) + } + + fn parse_comparison(input: &str) -> IResult<&str, Expression> { + let (input, identifier) = Self::ws(Self::parse_identifier).parse(input)?; + if !identifier.eq_ignore_ascii_case("version") { + return Err(nom::Err::Failure(Error::new( + input, + nom::error::ErrorKind::Tag, + ))); + } + + let (input, op) = Self::ws(Self::parse_comparison_op).parse(input)?; + let (input, version_token) = Self::ws(Self::parse_version_literal).parse(input)?; + let version = Version::parse(version_token) + .map_err(|_| nom::Err::Failure(Error::new(input, nom::error::ErrorKind::Fail)))?; + + Ok((input, Expression::Comparison(op, version))) + } + + fn parse_identifier(input: &str) -> IResult<&str, &str> { + take_while1(|c: char| c.is_ascii_alphabetic() || c == '_').parse(input) + } + + fn parse_version_literal(input: &str) -> IResult<&str, &str> { + take_while1(|c: char| !c.is_whitespace() && c != ')').parse(input) + } + + fn parse_comparison_op(input: &str) -> IResult<&str, ComparisonOp> { + map( + alt(( + tag(">="), + tag("<="), + tag("=="), + tag("!="), + tag(">"), + tag("<"), + )), + |op| match op { + ">=" => ComparisonOp::Ge, + "<=" => ComparisonOp::Le, + "==" => ComparisonOp::Eq, + "!=" => ComparisonOp::Ne, + ">" => ComparisonOp::Gt, + "<" => ComparisonOp::Lt, + _ => unreachable!(), + }, + ) + .parse(input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser_allows_range_expression() { + let expr = Parser::parse("version > 1.0.0 AND version < 1.1.0").unwrap(); + let target = Version::parse("1.0.5").unwrap(); + assert!(expr.eval(&target)); + + let target = Version::parse("1.1.0").unwrap(); + assert!(!expr.eval(&target)); + } + + #[test] + fn test_parser_honors_parentheses_and_not() { + let expr = Parser::parse("NOT (version < 1.0.0 OR version >= 2.0.0)").unwrap(); + + assert!(expr.eval(&Version::parse("1.5.0").unwrap())); + assert!(!expr.eval(&Version::parse("0.9.9").unwrap())); + assert!(!expr.eval(&Version::parse("2.0.0").unwrap())); + } + + #[test] + fn test_parser_supports_current_literal() { + let expr = Parser::parse("version == current").unwrap(); + assert!(expr.eval(&Version::Current)); + } + + #[test] + fn test_parser_rejects_invalid_identifier() { + assert!(Parser::parse("target > 1.0.0").is_err()); + } + + #[test] + fn test_parser_rejects_invalid_version_literal() { + assert!(Parser::parse("version > nope").is_err()); + } + + #[test] + fn test_before_execute_keeps_sql_when_expression_matches() { + let expr = Parser::parse("version >= 0.15.0").unwrap(); + let interceptor = VersionInterceptor::new( + Version::parse("0.15.0").unwrap(), + expr, + "version >= 0.15.0".to_string(), + ); + + let mut sql = vec!["SELECT 1;".to_string()]; + let skipped = interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert!(!skipped); + assert_eq!(sql, vec!["SELECT 1;"]); + } + + #[test] + fn test_before_execute_rewrites_sql_when_expression_not_match() { + let expr = Parser::parse("version >= 0.16.0").unwrap(); + let interceptor = VersionInterceptor::new( + Version::parse("0.15.0").unwrap(), + expr, + "version >= 0.16.0".to_string(), + ); + + let mut sql = vec!["SELECT 1;".to_string()]; + let skipped = interceptor.maybe_rewrite_to_skip_sql(&mut sql); + + assert!(skipped); + assert_eq!(sql.len(), 1); + assert!(sql[0].contains(SKIP_MARKER_PREFIX)); + assert!( + sql[0].contains("target version 0.15.0 does not satisfy expression: version >= 0.16.0") + ); + } + + #[test] + fn test_after_execute_normalizes_skip_result_when_skipped() { + let expr = Parser::parse("version >= 0.16.0").unwrap(); + let interceptor = VersionInterceptor::new( + Version::parse("0.15.0").unwrap(), + expr, + "version >= 0.16.0".to_string(), + ); + let mut result = format!( + "+----------------+\n| {} {} |\n+----------------+", + SKIP_MARKER_PREFIX, + interceptor.skip_reason() + ); + interceptor.after_execute(&mut result); + + assert_eq!( + result, + format!( + "{} target version 0.15.0 does not satisfy expression: version >= 0.16.0", + SKIP_MARKER_PREFIX + ) + ); + } + + #[test] + fn test_after_execute_does_not_rewrite_when_not_skipped() { + let expr = Parser::parse("version <= 0.15.0").unwrap(); + let interceptor = VersionInterceptor::new( + Version::parse("0.15.0").unwrap(), + expr, + "version <= 0.15.0".to_string(), + ); + + let expected = format!("{} some other reason", SKIP_MARKER_PREFIX); + let mut result = expected.clone(); + interceptor.after_execute(&mut result); + + assert_eq!(result, expected); + } + + #[test] + fn test_factory_rejects_empty_expression() { + let factory = VersionInterceptorFactory::new(Version::parse("0.15.0").unwrap()); + let err = match factory.try_new(" ") { + Ok(_) => panic!("expected empty expression to fail"), + Err(err) => err, + }; + let msg = err.to_string(); + assert!(msg.contains("Expression cannot be empty")); + } +} diff --git a/tests/runner/src/util.rs b/tests/runner/src/util.rs index 6c00c8a9a6..4b4dd4ef7c 100644 --- a/tests/runner/src/util.rs +++ b/tests/runner/src/util.rs @@ -448,7 +448,28 @@ pub fn get_binary_dir(mode: &str) -> PathBuf { workspace_root.push("target"); workspace_root.push(mode); - workspace_root + if workspace_root.is_dir() { + workspace_root + } else { + // get build.target-dir from cargo config get "build.target-dir" -Z unstable-options --format json-value + let output = Command::new("cargo") + .args([ + "config", + "get", + "build.target-dir", + "-Z", + "unstable-options", + "--format", + "json-value", + ]) + .output() + .expect("Failed to execute cargo metadata"); + let target_dir = + String::from_utf8(output.stdout).expect("Failed to parse cargo config output"); + let mut target_dir_path = PathBuf::from(target_dir.trim().trim_matches('\"')); + target_dir_path.push(mode); + target_dir_path + } } /// Spin-waiting a socket address is available, or timeout.