From 662d7dd24d8c11f62d960d2b90a92aa43ec699b5 Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 4 Feb 2026 20:18:52 +0800 Subject: [PATCH] feat: skip marker Signed-off-by: discord9 --- sqlness/src/interceptor.rs | 8 +- sqlness/src/interceptor/skip.rs | 69 ++++++++++++ sqlness/src/lib.rs | 1 + sqlness/src/runner.rs | 183 ++++++++++++++++++++++++++++++-- 4 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 sqlness/src/interceptor/skip.rs diff --git a/sqlness/src/interceptor.rs b/sqlness/src/interceptor.rs index 9471507..aa350ca 100644 --- a/sqlness/src/interceptor.rs +++ b/sqlness/src/interceptor.rs @@ -10,13 +10,15 @@ use crate::{ error::SqlnessError, interceptor::{ arg::ArgInterceptorFactory, env::EnvInterceptorFactory, replace::ReplaceInterceptorFactory, - sort_result::SortResultInterceptorFactory, template::TemplateInterceptorFactory, + skip::SkipInterceptorFactory, sort_result::SortResultInterceptorFactory, + template::TemplateInterceptorFactory, }, }; pub mod arg; pub mod env; pub mod replace; +pub mod skip; pub mod sleep; pub mod sort_result; pub mod template; @@ -113,6 +115,10 @@ fn builtin_interceptors() -> HashMap { sleep::PREFIX.to_string(), Arc::new(sleep::SleepInterceptorFactory {}) as _, ), + ( + skip::PREFIX.to_string(), + Arc::new(SkipInterceptorFactory {}) as _, + ), ] .into_iter() .map(|(prefix, factory)| (prefix.to_string(), factory)) diff --git a/sqlness/src/interceptor/skip.rs b/sqlness/src/interceptor/skip.rs new file mode 100644 index 00000000..fe4d6e4 --- /dev/null +++ b/sqlness/src/interceptor/skip.rs @@ -0,0 +1,69 @@ +// Copyright 2024 CeresDB Project Authors. Licensed under Apache-2.0. + +use crate::error::Result; +use crate::interceptor::{Interceptor, InterceptorFactory, InterceptorRef}; + +pub struct SkipInterceptorFactory; + +pub const PREFIX: &str = "SKIP"; +pub const SKIP_MARKER_PREFIX: &str = "-- SQLNESS_SKIP:"; + +/// Skip interceptor that generates SKIP markers in result output. +/// +/// Grammar: +/// ``` text +/// -- SQLNESS SKIP +/// ``` +/// +/// Example: +/// ``` sql +/// -- SQLNESS SKIP version 0.14.0 < required 0.15.0 +/// SELECT * FROM new_feature_table; +/// ``` +/// +/// The query will not be executed, and the SKIP marker will be written to the result file. +#[derive(Debug)] +pub struct SkipInterceptor { + reason: String, +} + +impl Interceptor for SkipInterceptor { + fn before_execute(&self, execute_query: &mut Vec, _context: &mut crate::QueryContext) { + execute_query.clear(); + } + + fn after_execute(&self, result: &mut String) { + *result = format!("{} {}", SKIP_MARKER_PREFIX, self.reason); + } +} + +impl InterceptorFactory for SkipInterceptorFactory { + fn try_new(&self, ctx: &str) -> Result { + Ok(Box::new(SkipInterceptor { + reason: ctx.to_string(), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::QueryContext; + + #[test] + fn test_skip_interceptor() { + let interceptor = SkipInterceptorFactory + .try_new("version 0.14.0 < required 0.15.0") + .unwrap(); + + let mut query = vec!["SELECT * FROM new_feature_table;".to_string()]; + let mut context = QueryContext::default(); + + interceptor.before_execute(&mut query, &mut context); + assert!(query.is_empty()); + + let mut result = "some result".to_string(); + interceptor.after_execute(&mut result); + assert_eq!(result, "-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0"); + } +} diff --git a/sqlness/src/lib.rs b/sqlness/src/lib.rs index b14f5fc..c3400d3 100644 --- a/sqlness/src/lib.rs +++ b/sqlness/src/lib.rs @@ -69,3 +69,4 @@ pub use database::Database; pub use environment::EnvController; pub use error::SqlnessError; pub use runner::Runner; +pub use runner::SKIP_MARKER_PREFIX; diff --git a/sqlness/src/runner.rs b/sqlness/src/runner.rs index f9fc833..50bf44d 100644 --- a/sqlness/src/runner.rs +++ b/sqlness/src/runner.rs @@ -16,6 +16,11 @@ use crate::case::TestCase; use crate::error::{Result, SqlnessError}; use crate::{config::Config, environment::EnvController}; +/// Prefix for SKIP markers in result files. +/// When comparing expected vs actual results, differences that consist solely +/// of SKIP markers are treated as "no difference" (test passes). +pub const SKIP_MARKER_PREFIX: &str = "-- SQLNESS_SKIP:"; + /// The entrypoint of this crate. /// /// To run your integration test cases, simply [`new`] a `Runner` and [`run`] it. @@ -217,13 +222,22 @@ impl Runner { case.execute(db, &mut new_result).await?; let elapsed = timer.elapsed(); - // Truncate and write new result back + // Compare old and new result + let new_result = String::from_utf8(new_result.into_inner()).expect("not utf8 string"); + let diff_only_skip_markers = self.are_all_changes_skip_markers_from_strings(&old_result, &new_result); + + // If diff is only SKIP markers, write back old_result to keep result file clean + let result_to_write = if diff_only_skip_markers { + &old_result + } else { + &new_result + }; + + // Truncate and write result back result_file.set_len(0)?; result_file.rewind()?; - result_file.write_all(new_result.get_ref())?; + result_file.write_all(result_to_write.as_bytes())?; - // Compare old and new result - let new_result = String::from_utf8(new_result.into_inner()).expect("not utf8 string"); if let Some(diff) = self.compare(&old_result, &new_result) { println!("Result unexpected, path:{case_path:?}"); println!("{diff}"); @@ -282,12 +296,167 @@ impl Runner { /// Compare result, return None if them are the same, else return diff changes fn compare(&self, expected: &str, actual: &str) -> Option { let diff = diff_lines(expected, actual); - let diff = diff.diff(); - let is_different = diff.iter().any(|d| !matches!(d, DiffOp::Equal(_))); + let diff_ops = diff.diff(); + + // Check if all differences are only SKIP markers + if self.are_all_changes_skip_markers(&diff_ops) { + return None; // Treat as no difference + } + + let is_different = diff_ops.iter().any(|d| !matches!(d, DiffOp::Equal(_))); if is_different { - return Some(format!("{}", SliceChangeset { diff })); + return Some(format!("{}", SliceChangeset { diff: diff_ops })); } None } + + /// Checks if a line is a SKIP marker + fn is_skip_marker(line: &str) -> bool { + line.trim().starts_with(SKIP_MARKER_PREFIX) + } + + /// Checks if all differences are only SKIP markers + /// + /// Returns true if: + /// - No differences between expected and actual, OR + /// - All inserted/deleted/replaced lines are SKIP markers + fn are_all_changes_skip_markers<'a>( + &self, + diff_ops: &'a [DiffOp<'a, &'a str>], + ) -> bool { + for op in diff_ops { + match op { + DiffOp::Equal(_) => continue, + DiffOp::Insert(lines) | DiffOp::Remove(lines) => { + if !lines.iter().all(|line| Self::is_skip_marker(line)) { + return false; + } + } + DiffOp::Replace(_old_lines, new_lines) => { + if !new_lines.iter().all(|line| Self::is_skip_marker(line)) { + return false; + } + } + } + } + + true + } + + /// Convenience method to check if two strings differ only by SKIP markers + fn are_all_changes_skip_markers_from_strings(&self, expected: &str, actual: &str) -> bool { + let diff = diff_lines(expected, actual); + self.are_all_changes_skip_markers(&diff.diff()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::Database; + use crate::environment::EnvController; + use async_trait::async_trait; + use std::fmt::Display; + use std::path::Path; + + struct MockDatabase; + + #[async_trait] + impl Database for MockDatabase { + async fn query( + &self, + _ctx: crate::case::QueryContext, + _query: String, + ) -> Box { + Box::new("mock result") + } + } + + struct MockEnvController; + + #[async_trait] + impl EnvController for MockEnvController { + type DB = MockDatabase; + + async fn start(&self, _env: &str, _config: Option<&Path>) -> Self::DB { + MockDatabase + } + + async fn stop(&self, _env: &str, _database: Self::DB) {} + } + + fn create_test_runner() -> Runner { + let config = Config { + case_dir: ".".to_string(), + test_case_extension: "sql".to_string(), + result_extension: "result".to_string(), + interceptor_prefix: "-- SQLNESS".to_string(), + env_config_file: "config.toml".to_string(), + fail_fast: true, + test_filter: ".*".to_string(), + env_filter: ".*".to_string(), + follow_links: true, + interceptor_registry: crate::interceptor::Registry::default(), + }; + Runner::new(config, MockEnvController) + } + + #[test] + fn test_is_skip_marker() { + assert!(Runner::::is_skip_marker( + "-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0" + )); + assert!(Runner::::is_skip_marker( + " -- SQLNESS_SKIP: some reason" + )); + assert!(!Runner::::is_skip_marker( + "-- SQLNESS TEMPLATE {}" + )); + assert!(!Runner::::is_skip_marker("SELECT 1;")); + } + + #[test] + fn test_are_all_changes_skip_markers_no_changes() { + let runner = create_test_runner(); + let diff = diff_lines("SELECT 1;\n1", "SELECT 1;\n1"); + assert!(runner.are_all_changes_skip_markers(&diff.diff())); + } + + #[test] + fn test_are_all_changes_skip_markers_only_skip_added() { + let runner = create_test_runner(); + let diff = diff_lines( + "SELECT 1;\n1", + "SELECT 1;\n1\n-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0", + ); + assert!(runner.are_all_changes_skip_markers(&diff.diff())); + } + + #[test] + fn test_are_all_changes_skip_markers_skip_replacing_content() { + let runner = create_test_runner(); + let diff = diff_lines( + "SELECT 1;\n1", + "SELECT 1;\n-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0", + ); + assert!(runner.are_all_changes_skip_markers(&diff.diff())); + } + + #[test] + fn test_are_all_changes_skip_markers_real_difference() { + let runner = create_test_runner(); + let diff = diff_lines("SELECT 1;\n1", "SELECT 1;\n2"); + assert!(!runner.are_all_changes_skip_markers(&diff.diff())); + } + + #[test] + fn test_are_all_changes_skip_markers_mixed_changes() { + let runner = create_test_runner(); + let diff = diff_lines( + "SELECT 1;\n1", + "SELECT 1;\n2\n-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0", + ); + assert!(!runner.are_all_changes_skip_markers(&diff.diff())); + } }