diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c870a6..cefbdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* runner: Add `` to skip the volatile parts of the output. + ## [0.28.3] - 2025-05-16 * bin: Add `--shutdown-timeout` to set a timeout for shutting down the database connections after a test file is finished. By default, this is unspecified, meaning to wait forever. diff --git a/README.md b/README.md index fe8f94f..61d3805 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,19 @@ SELECT * FROM foo; 4 5 ``` +### Extension: Ignore volatile parts of output + +You can use `` to skip the volatile parts of the output. This is helpful for e.g., testing the format +of `EXPLAIN`-like statements. + +```text +query T +EXPLAIN SELECT * FROM foo; +---- +Seq Scan on t (cost= rows= width=) + Filter: (x > 1) +``` + ### Extension: Run a query/statement that should fail with the expacted error message The syntax: diff --git a/sqllogictest/src/runner.rs b/sqllogictest/src/runner.rs index 60b8cc6..1be55dc 100644 --- a/sqllogictest/src/runner.rs +++ b/sqllogictest/src/runner.rs @@ -487,6 +487,44 @@ pub fn default_validator( actual: &[Vec], expected: &[String], ) -> bool { + // Support ignore marker to skip volatile parts of output. + const IGNORE_MARKER: &str = ""; + let contains_ignore_marker = expected.iter().any(|line| line.contains(IGNORE_MARKER)); + + // Normalize expected lines. + // If ignore marker present, perform fragment-based matching on the full snapshot. + if contains_ignore_marker { + // If ignore marker present, perform fragment-based matching on the full snapshot. + // The actual results might contain \n, and may not be a normal "row", which is not suitable to normalize. + let expected_results = expected; + let actual_rows = actual + .iter() + .map(|strs| strs.iter().join(" ")) + .collect_vec(); + + let expected_snapshot = expected_results.join("\n"); + let actual_snapshot = actual_rows.join("\n"); + let fragments: Vec<&str> = expected_snapshot.split(IGNORE_MARKER).collect(); + let mut pos = 0; + for frag in fragments { + if frag.is_empty() { + continue; + } + if let Some(idx) = actual_snapshot[pos..].find(frag) { + pos += idx + frag.len(); + } else { + tracing::error!( + "mismatch at: {}\nexpected: {}\nactual: {}", + pos, + frag, + &actual_snapshot[pos..] + ); + return false; + } + } + return true; + } + let expected_results = expected.iter().map(normalizer).collect_vec(); // Default, we compare normalized results. Whitespace characters are ignored. let normalized_rows = actual @@ -2294,4 +2332,36 @@ Caused by: write!(f, "TestError: {}", self.0) } } + + #[test] + fn test_default_validator_ignore_simple() { + let normalizer = default_normalizer; + let actual = vec![vec!["foo".to_string(), "bar".to_string()]]; + let expected = vec!["foobar".to_string()]; + assert!(default_validator(normalizer, &actual, &expected)); + } + + #[test] + fn test_default_validator_ignore_multiple_fragments() { + let normalizer = default_normalizer; + let actual = vec![vec![ + "one".to_string(), + "two".to_string(), + "three".to_string(), + ]]; + let expected = vec!["onethree".to_string()]; + assert!(default_validator(normalizer, &actual, &expected)); + } + + #[test] + fn test_default_validator_ignore_fail() { + let normalizer = default_normalizer; + let actual = vec![vec![ + "alpha".to_string(), + "beta".to_string(), + "gamma".to_string(), + ]]; + let expected = vec!["alphadelta".to_string()]; + assert!(!default_validator(normalizer, &actual, &expected)); + } }