From 7fdf37fd8e415cde384c0f6726bba39e8307cfab Mon Sep 17 00:00:00 2001 From: Vedat Can Keklik Date: Sat, 7 Dec 2024 19:04:02 +0300 Subject: [PATCH 1/3] fix: Error reporting tests --- lykiadb-server/src/engine/error.rs | 70 +++++++++++++++++++++++++++--- lykiadb-shell/src/main.rs | 4 +- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/lykiadb-server/src/engine/error.rs b/lykiadb-server/src/engine/error.rs index 52247503..e4b67fe1 100644 --- a/lykiadb-server/src/engine/error.rs +++ b/lykiadb-server/src/engine/error.rs @@ -38,24 +38,23 @@ impl From for ExecutionError { } } -pub fn report_error(filename: &str, source: &str, error: ExecutionError) { +pub fn report_error(source_name: &str, source: &str, error: ExecutionError, mut writer: impl std::io::Write) { use ariadne::{Color, Label, Report, ReportKind, Source}; // Generate & choose some colours for each of our elements let out = Color::Fixed(81); - let print = |message: &str, hint: &str, span: Span| { - Report::build(ReportKind::Error, filename, 12) + let mut print = |message: &str, hint: &str, span: Span| { + Report::build(ReportKind::Error, source_name, 12) .with_code(3) .with_message(format!("{} at line {}", message, span.line + 1)) .with_label( - Label::new((filename, span.start..span.end)) + Label::new((source_name, span.start..span.end)) .with_message(hint) .with_color(out), ) .finish() - .print((filename, Source::from(&source))) - .unwrap(); + .write((source_name, Source::from(&source)), &mut writer).unwrap(); }; match error { @@ -153,7 +152,6 @@ pub fn report_error(filename: &str, source: &str, error: ExecutionError) { ); } ExecutionError::Plan(PlannerError::SubqueryNotAllowed(span)) => { - println!("{:?}", span); print( "Subquery not allowed", "Subqueries are not allowed in this context.", @@ -165,5 +163,63 @@ pub fn report_error(filename: &str, source: &str, error: ExecutionError) { print(&message, "", Span::default()); } _ => {} + }; +} +#[cfg(test)] +mod tests { + use super::*; + use lykiadb_lang::tokenizer::token::{Token, TokenType}; + + fn capture_error_output(filename: &str, source: &str, error: ExecutionError) -> String { + let mut output = Vec::new(); + report_error(filename, source, error, &mut output); + String::from_utf8(output).unwrap() + } + + #[test] + fn test_scan_error_reporting() { + let source = "let x = @"; + let error = ExecutionError::Scan(ScanError::UnexpectedCharacter { + span: Span { + start: 8, + end: 9, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Unexpected character")); + assert!(output.contains("Remove this character")); + assert!(output.contains("at line 1")); + } + + #[test] + fn test_interpret_error_reporting() { + let source = "obj.nonexistent"; + let error = ExecutionError::Interpret(InterpretError::PropertyNotFound { + property: "nonexistent".to_string(), + span: Span { + start: 4, + end: 14, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Property nonexistent not found")); + assert!(output.contains("Check if that field is present")); + } + + #[test] + fn test_environment_error_reporting() { + let source = ""; + let error = ExecutionError::Environment(EnvironmentError::Other { + message: "Variable not found".to_string(), + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Variable not found")); } } diff --git a/lykiadb-shell/src/main.rs b/lykiadb-shell/src/main.rs index 43309f3e..1decb2f0 100644 --- a/lykiadb-shell/src/main.rs +++ b/lykiadb-shell/src/main.rs @@ -71,7 +71,9 @@ impl Shell { Message::Response(Response::Program(value)) => { println!("{}", serde_json::to_string_pretty(&value).unwrap()) } - Message::Response(Response::Error(err)) => report_error(filename, content, err.clone()), + Message::Response(Response::Error(err)) => { + report_error(filename, content, err.clone(), &mut stdout()) + } _ => panic!(""), } } From 3d5a3d192258c36fa057790a586ba413363ff75d Mon Sep 17 00:00:00 2001 From: Vedat Can Keklik Date: Sat, 7 Dec 2024 20:43:59 +0300 Subject: [PATCH 2/3] fix: More error handler tests --- lykiadb-server/src/engine/error.rs | 162 ++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/lykiadb-server/src/engine/error.rs b/lykiadb-server/src/engine/error.rs index e4b67fe1..1a3cbaa3 100644 --- a/lykiadb-server/src/engine/error.rs +++ b/lykiadb-server/src/engine/error.rs @@ -168,7 +168,7 @@ pub fn report_error(source_name: &str, source: &str, error: ExecutionError, mut #[cfg(test)] mod tests { use super::*; - use lykiadb_lang::tokenizer::token::{Token, TokenType}; + use lykiadb_lang::{kw, tokenizer::token::{Keyword, Token, TokenType}, Identifier, Literal}; fn capture_error_output(filename: &str, source: &str, error: ExecutionError) -> String { let mut output = Vec::new(); @@ -176,6 +176,166 @@ mod tests { String::from_utf8(output).unwrap() } + + // Scanner Error Tests + #[test] + fn test_scanner_unterminated_string() { + let source = r#"let x = "unterminated"#; + let error = ExecutionError::Scan(ScanError::UnterminatedString { + span: Span { + start: 8, + end: 21, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Unterminated string")); + assert!(output.contains("Terminate the string with a double quote")); + } + + #[test] + fn test_scanner_malformed_number() { + let source = "let x = 123.456.789"; + let error = ExecutionError::Scan(ScanError::MalformedNumberLiteral { + span: Span { + start: 8, + end: 19, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Malformed number literal")); + assert!(output.contains("Make sure that number literal is up to spec")); + } + + // Parser Error Tests + #[test] + fn test_parser_missing_token() { + let source = "var x = "; + let error = ExecutionError::Parse(ParseError::MissingToken { + token: Token { + tok_type: kw!(Keyword::Var), + lexeme: Some("var".to_string()), + span: Span { + start: 0, + end: 3, + line: 0, + line_end: 0, + }, + literal: None, + }, + expected: TokenType::Identifier { dollar: true } + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Missing token")); + assert!(output.contains("Add a Identifier { dollar: true } token after \"var\".")); + } + + #[test] + fn test_parser_invalid_assignment() { + let source = "5 = 10"; + let error = ExecutionError::Parse(ParseError::InvalidAssignmentTarget { + left: Token { + tok_type: TokenType::Num, + lexeme: Some("5".to_string()), + span: Span { + start: 0, + end: 1, + line: 0, + line_end: 0, + }, + literal: Some(Literal::Num(5.0)), + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Invalid assignment target")); + assert!(output.contains("No values can be assigned to 5")); + } + + // Planner Error Tests + #[test] + fn test_planner_duplicate_object() { + let source = "CREATE TABLE users; CREATE TABLE users;"; + let error = ExecutionError::Plan(PlannerError::DuplicateObjectInScope { + previous: Identifier::new("users", false), + ident: Identifier::new("users", false), + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Duplicate object in scope")); + assert!(output.contains("Object users is already defined in the scope")); + } + + #[test] + fn test_planner_subquery_not_allowed() { + let source = "SELECT * FROM (SELECT * FROM users) WHERE id IN (SELECT id FROM users)"; + let error = ExecutionError::Plan(PlannerError::SubqueryNotAllowed(Span { + start: 14, + end: 33, + line: 0, + line_end: 0, + })); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Subquery not allowed")); + assert!(output.contains("Subqueries are not allowed in this context")); + } + + // Interpreter Error Tests + #[test] + fn test_interpreter_arity_mismatch() { + let source = "function test(a, b) {}; test(1);"; + let error = ExecutionError::Interpret(InterpretError::ArityMismatch { + span: Span { + start: 24, + end: 29, + line: 0, + line_end: 0, + }, + expected: 2, + found: 1, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Function arity mismatch")); + assert!(output.contains("Function expects 2 arguments, while provided 1")); + } + + #[test] + fn test_interpreter_not_callable() { + let source = "let x = 5; x();"; + let error = ExecutionError::Interpret(InterpretError::NotCallable { + span: Span { + start: 12, + end: 15, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Not callable")); + assert!(output.contains("Expression does not yield a callable")); + } + + // Environment Error Tests + #[test] + fn test_environment_variable_not_found() { + let source = "io::print(undefined_var);"; + let error = ExecutionError::Environment(EnvironmentError::Other { + message: "Variable 'undefined_var' not found in current scope".to_string(), + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Variable 'undefined_var' not found in current scope")); + } + #[test] fn test_scan_error_reporting() { let source = "let x = @"; From 740c073f6f172e66359b30c6e77ca34795895386 Mon Sep 17 00:00:00 2001 From: Vedat Can Keklik Date: Sat, 7 Dec 2024 21:07:22 +0300 Subject: [PATCH 3/3] fix: Test fixes --- lykiadb-server/src/engine/error.rs | 65 ++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/lykiadb-server/src/engine/error.rs b/lykiadb-server/src/engine/error.rs index 1a3cbaa3..28f4a967 100644 --- a/lykiadb-server/src/engine/error.rs +++ b/lykiadb-server/src/engine/error.rs @@ -88,8 +88,8 @@ pub fn report_error(source_name: &str, source: &str, error: ExecutionError, mut } ExecutionError::Parse(ParseError::NoTokens) => { print( - "There is nothing to parse.", - "What about adding some tokens?", + "There is nothing to parse", + "", Span::default(), ); } @@ -167,8 +167,10 @@ pub fn report_error(source_name: &str, source: &str, error: ExecutionError, mut } #[cfg(test)] mod tests { + use core::panic; + use super::*; - use lykiadb_lang::{kw, tokenizer::token::{Keyword, Token, TokenType}, Identifier, Literal}; + use lykiadb_lang::{kw, sym, tokenizer::token::{Keyword, Symbol, Token, TokenType}, Identifier, Literal}; fn capture_error_output(filename: &str, source: &str, error: ExecutionError) -> String { let mut output = Vec::new(); @@ -236,6 +238,55 @@ mod tests { assert!(output.contains("Add a Identifier { dollar: true } token after \"var\".")); } + #[test] + fn test_parser_no_tokens() { + let source = ""; + let error = ExecutionError::Parse(ParseError::NoTokens); + + let output = capture_error_output("test.txt", source, error); + + assert!(output.contains("There is nothing to parse")); + } + + #[test] + fn test_parser_unexpected_token() { + let source = "let x = ;"; + let error = ExecutionError::Parse(ParseError::UnexpectedToken { + token: Token { + tok_type: sym!(Symbol::Semicolon), + lexeme: Some(";".to_string()), + span: Span { + start: 8, + end: 9, + line: 0, + line_end: 0, + }, + literal: None, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Unexpected token")); + assert!(output.contains("Unexpected token ;")); + } + + #[test] + fn test_interpreter_unexpected_statement() { + let source = "break;"; + let error = ExecutionError::Interpret(InterpretError::UnexpectedStatement { + span: Span { + start: 0, + end: 5, + line: 0, + line_end: 0, + }, + }); + + let output = capture_error_output("test.txt", source, error); + assert!(output.contains("Unexpected statement")); + assert!(output.contains("Remove this")); + } + #[test] fn test_parser_invalid_assignment() { let source = "5 = 10"; @@ -261,7 +312,7 @@ mod tests { // Planner Error Tests #[test] fn test_planner_duplicate_object() { - let source = "CREATE TABLE users; CREATE TABLE users;"; + let source = "Select * from users, users;"; let error = ExecutionError::Plan(PlannerError::DuplicateObjectInScope { previous: Identifier::new("users", false), ident: Identifier::new("users", false), @@ -274,10 +325,10 @@ mod tests { #[test] fn test_planner_subquery_not_allowed() { - let source = "SELECT * FROM (SELECT * FROM users) WHERE id IN (SELECT id FROM users)"; + let source = "SELECT * FROM users inner join orders on users.id = (SELECT id FROM users);"; let error = ExecutionError::Plan(PlannerError::SubqueryNotAllowed(Span { - start: 14, - end: 33, + start: 47, + end: 70, line: 0, line_end: 0, }));