mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-15 12:30:38 +00:00
@@ -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 <version>`
|
||||
- `-- SQLNESS TILL <version>`
|
||||
- `-- SQLNESS VERSION <expression>`
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+----------+
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
2
tests/runner/src/env/bare.rs
vendored
2
tests/runner/src/env/bare.rs
vendored
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>) {
|
||||
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<String>, _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<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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
@@ -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<String>) {
|
||||
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<String>, _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<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(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
386
tests/runner/src/interceptors/version.rs
Normal file
386
tests/runner/src/interceptors/version.rs
Normal file
@@ -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<String>) -> 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<String>, _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<InterceptorRef, SqlnessError> {
|
||||
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<Expression>, Box<Expression>),
|
||||
Or(Box<Expression>, Box<Expression>),
|
||||
Not(Box<Expression>),
|
||||
}
|
||||
|
||||
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<Expression, String> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user