add raw keyword for expect tests to not trim anything from the block

This commit is contained in:
pedrocarlo 2025-12-14 01:37:22 -03:00
parent a99d02d59c
commit 028dbd4893
2 changed files with 77 additions and 12 deletions

View file

@ -3,6 +3,7 @@ use miette::{Diagnostic, SourceSpan};
use std::fmt;
/// Extract block content between braces, handling nested braces
/// Note: Does NOT trim content - trimming is handled by the parser based on context
fn extract_block_content(lexer: &mut Lexer<'_, Token>) -> Option<String> {
let remainder = lexer.remainder();
let mut depth = 1;
@ -13,7 +14,7 @@ fn extract_block_content(lexer: &mut Lexer<'_, Token>) -> Option<String> {
'}' => {
depth -= 1;
if depth == 0 {
let content = remainder[..idx].trim().to_string();
let content = remainder[..idx].to_string();
// Bump past the content and the closing brace
lexer.bump(idx + 1);
return Some(content);
@ -67,6 +68,10 @@ pub enum Token {
#[token("unordered")]
Unordered,
/// `raw` modifier (preserves whitespace in expect blocks)
#[token("raw")]
Raw,
/// `readonly` modifier
#[token("readonly")]
Readonly,
@ -121,6 +126,7 @@ impl fmt::Display for Token {
Token::Error => write!(f, "error"),
Token::Pattern => write!(f, "pattern"),
Token::Unordered => write!(f, "unordered"),
Token::Raw => write!(f, "raw"),
Token::Readonly => write!(f, "readonly"),
Token::Memory => write!(f, ":memory:"),
Token::TempFile => write!(f, ":temp:"),
@ -259,9 +265,10 @@ mod tests {
assert_eq!(tokens.len(), 3);
assert_eq!(tokens[0].token, Token::Setup);
assert_eq!(tokens[1].token, Token::Identifier("users".to_string()));
// Block content is not trimmed by the lexer (parser handles trimming)
assert_eq!(
tokens[2].token,
Token::BlockContent("CREATE TABLE users (id INTEGER);".to_string())
Token::BlockContent(" CREATE TABLE users (id INTEGER); ".to_string())
);
}
@ -287,9 +294,10 @@ mod tests {
non_newline[5].token,
Token::Identifier("select-1".to_string())
);
// Block content is not trimmed by the lexer (parser handles trimming)
assert_eq!(
non_newline[6].token,
Token::BlockContent("SELECT 1;".to_string())
Token::BlockContent(" SELECT 1; ".to_string())
);
}
@ -298,9 +306,10 @@ mod tests {
let tokens = tokenize("expect error { no such table }").unwrap();
assert_eq!(tokens[0].token, Token::Expect);
assert_eq!(tokens[1].token, Token::Error);
// Block content is not trimmed by the lexer (parser handles trimming)
assert_eq!(
tokens[2].token,
Token::BlockContent("no such table".to_string())
Token::BlockContent(" no such table ".to_string())
);
let tokens = tokenize("expect pattern { ^\\d+$ }").unwrap();
@ -318,9 +327,10 @@ mod tests {
assert_eq!(tokens[0].token, Token::Test);
assert_eq!(tokens[1].token, Token::Identifier("nested".to_string()));
// The json_object call has parens but no braces, should work fine
// Block content is not trimmed by the lexer (parser handles trimming)
assert_eq!(
tokens[2].token,
Token::BlockContent("SELECT json_object('a', 1);".to_string())
Token::BlockContent(" SELECT json_object('a', 1); ".to_string())
);
}

View file

@ -114,7 +114,7 @@ impl Parser {
self.expect_token(Token::Setup)?;
let name = self.expect_identifier()?;
let content = self.expect_block_content()?;
let content = self.expect_block_content()?.trim().to_string();
Ok((name, content))
}
@ -148,7 +148,7 @@ impl Parser {
// Parse test
self.expect_token(Token::Test)?;
let (name, name_span) = self.expect_identifier_with_span()?;
let sql = self.expect_block_content()?;
let sql = self.expect_block_content()?.trim().to_string();
self.skip_newlines_and_comments();
@ -171,7 +171,7 @@ impl Parser {
match self.peek() {
Some(Token::Error) => {
self.advance();
let content = self.expect_block_content()?;
let content = self.expect_block_content()?.trim().to_string();
let pattern = if content.is_empty() {
None
} else {
@ -181,7 +181,7 @@ impl Parser {
}
Some(Token::Pattern) => {
self.advance();
let content = self.expect_block_content()?;
let content = self.expect_block_content()?.trim().to_string();
Ok(Expectation::Pattern(content))
}
Some(Token::Unordered) => {
@ -189,16 +189,28 @@ impl Parser {
let content = self.expect_block_content()?;
// Trim each line to handle indentation in expect blocks
let rows = content
.trim()
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(Expectation::Unordered(rows))
}
Some(Token::Raw) => {
self.advance();
let content = self.expect_block_content()?;
// Raw mode: preserve whitespace exactly, only split on newlines
// We still strip the leading/trailing newlines from the block itself
let content = content.strip_prefix('\n').unwrap_or(&content);
let content = content.strip_suffix('\n').unwrap_or(content);
let rows = content.lines().map(|s| s.to_string()).collect();
Ok(Expectation::Exact(rows))
}
Some(Token::BlockContent(_)) => {
let content = self.expect_block_content()?;
// Trim each line to handle indentation in expect blocks
let rows = content
.trim()
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@ -300,9 +312,10 @@ impl Parser {
}
fn error(&self, message: String) -> ParseError {
let span = self.tokens.get(self.pos).map(|token| {
SourceSpan::new(token.span.start.into(), token.span.len())
});
let span = self
.tokens
.get(self.pos)
.map(|token| SourceSpan::new(token.span.start.into(), token.span.len()));
ParseError::SyntaxError {
message,
@ -551,6 +564,48 @@ expect {
assert_eq!(file.tests[0].skip, Some("known bug".to_string()));
}
#[test]
fn test_parse_expect_raw() {
// Using explicit string to control whitespace precisely
// The content " hello " has 2 leading and 2 trailing spaces
let input = "@database :memory:\n\ntest select-spaces {\n SELECT 1;\n}\nexpect raw {\n hello \n}\n";
let file = parse(input).unwrap();
// Raw mode preserves leading/trailing whitespace
assert!(matches!(
&file.tests[0].expectation,
Expectation::Exact(rows) if rows == &vec![" hello ".to_string()]
));
}
#[test]
fn test_parse_expect_raw_vs_normal() {
// Normal mode trims whitespace
let input_normal = r#"
@database :memory:
test select-1 {
SELECT 1;
}
expect {
hello world
}
"#;
let file_normal = parse(input_normal).unwrap();
assert!(matches!(
&file_normal.tests[0].expectation,
Expectation::Exact(rows) if rows == &vec!["hello world".to_string()]
));
// Raw mode preserves whitespace (4 leading spaces, 2 trailing)
let input_raw = "@database :memory:\n\ntest select-1 {\n SELECT 1;\n}\nexpect raw {\n hello world \n}\n";
let file_raw = parse(input_raw).unwrap();
assert!(matches!(
&file_raw.tests[0].expectation,
Expectation::Exact(rows) if rows == &vec![" hello world ".to_string()]
));
}
#[test]
fn test_validation_no_database() {
let input = r#"