-
Notifications
You must be signed in to change notification settings - Fork 0
feat: skip marker #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <reason> | ||
| /// ``` | ||
| /// | ||
| /// 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<String>, _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<InterceptorRef> { | ||
| 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"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<E: EnvController> Runner<E> { | |
| 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())?; | ||
|
Comment on lines
+225
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current logic for writing the result file when a test is skipped can lead to misleading results. If a test case produced a different result previously (e.g., a failing result, or a correct result from a passing test), and is now skipped, the old result will be written back to the The result file should always reflect the actual outcome of the test run. In the case of a skipped test, it should contain the skip marker. The I suggest always writing the let new_result = String::from_utf8(new_result.into_inner()).expect("not utf8 string");
// Truncate and write result back
result_file.set_len(0)?;
result_file.rewind()?;
result_file.write_all(new_result.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<E: EnvController> Runner<E> { | |
| /// Compare result, return None if them are the same, else return diff changes | ||
| fn compare(&self, expected: &str, actual: &str) -> Option<String> { | ||
| 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<dyn Display> { | ||
| 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<MockEnvController> { | ||
| 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::<MockEnvController>::is_skip_marker( | ||
| "-- SQLNESS_SKIP: version 0.14.0 < required 0.15.0" | ||
| )); | ||
| assert!(Runner::<MockEnvController>::is_skip_marker( | ||
| " -- SQLNESS_SKIP: some reason" | ||
| )); | ||
| assert!(!Runner::<MockEnvController>::is_skip_marker( | ||
| "-- SQLNESS TEMPLATE {}" | ||
| )); | ||
| assert!(!Runner::<MockEnvController>::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())); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of
SkipInterceptoris unfortunately flawed due to the execution flow inTestCase::execute(insqlness/src/case.rs).By calling
execute_query.clear()inbefore_execute, you prevent any SQL from being run. However, this also means the loop over queries inTestCase::executeis skipped, and consequently,after_execute_intercept(which callsafter_execute) is never called. As a result, the skip marker is never written to the result file.The unit test for this interceptor doesn't catch this because it calls
before_executeandafter_executein isolation, not through theTestCase::executeflow.To fix this, the logic in
TestCase::executeneeds to be adjusted to handle cases where the query becomes empty afterbefore_execute_intercept. For example, it could callafter_execute_intercepteven if there's no SQL to run.Here's a possible modification for
sqlness/src/case.rs:Since
case.rsis not in this PR's diff, I cannot provide a direct code suggestion for that file. However, theSkipInterceptoras it is will not work.