From bd7de43251477a21c3df7e58312d1e42e9c1b14f Mon Sep 17 00:00:00 2001 From: harehare Date: Sat, 25 Oct 2025 00:09:32 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=85=20test(mq-lang):=20add=20comprehe?= =?UTF-8?q?nsive=20error=20and=20debugger=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add exhaustive test cases for diagnostic_code_and_help covering all InnerError variants - Add debugger_tests module for testing debugger functionality with breakpoints - Exclude debugger.rs from codecov as it contains test infrastructure --- .github/codecov.yml | 1 + crates/mq-lang/src/error.rs | 241 ++++++++++++++++++++++++++++++++++++ crates/mq-lang/src/eval.rs | 91 ++++++++++++++ 3 files changed, 333 insertions(+) diff --git a/.github/codecov.yml b/.github/codecov.yml index 4efa6354..6bda44dd 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -8,6 +8,7 @@ ignore: - "crates/mq-mcp" - "crates/mq-web-api" - "crates/mq-dap/src/server.rs" + - "crates/mq-lang/src/eval/debugger.rs" - "crates/mq-test" coverage: status: diff --git a/crates/mq-lang/src/error.rs b/crates/mq-lang/src/error.rs index 1d87fd8f..a7785733 100644 --- a/crates/mq-lang/src/error.rs +++ b/crates/mq-lang/src/error.rs @@ -651,4 +651,245 @@ mod test { assert_eq!(error.source_code, ModuleLoader::BUILTIN_FILE); } + + #[rstest] + #[case::lexer_unexpected_token( + InnerError::Lexer(LexerError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::lexer_unexpected_eof(InnerError::Lexer(LexerError::UnexpectedEOFDetected( + ArenaId::new(0) + )))] + #[case::parse_env_not_found( + InnerError::Parse(ParseError::EnvNotFound(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "ENV_VAR".into())) + )] + #[case::parse_unexpected_token( + InnerError::Parse(ParseError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::parse_unexpected_eof_detected(InnerError::Parse(ParseError::UnexpectedEOFDetected( + ArenaId::new(0) + )))] + #[case::parse_insufficient_tokens( + InnerError::Parse(ParseError::InsufficientTokens(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::parse_expected_closing_paren( + InnerError::Parse(ParseError::ExpectedClosingParen(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::parse_expected_closing_brace( + InnerError::Parse(ParseError::ExpectedClosingBrace(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::parse_expected_closing_bracket( + InnerError::Parse(ParseError::ExpectedClosingBracket(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::eval_recursion_error(InnerError::Eval(EvalError::RecursionError(0)))] + #[case::eval_module_load_error( + InnerError::Eval(EvalError::ModuleLoadError(ModuleError::NotFound("mod".into()))) + )] + #[case::eval_user_defined( + InnerError::Eval(EvalError::UserDefined { + token: Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, + message: "msg".to_string(), + }) + )] + #[case::eval_invalid_base64_string( + InnerError::Eval(EvalError::InvalidBase64String(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "bad".to_string())) + )] + #[case::eval_not_defined( + InnerError::Eval(EvalError::NotDefined(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "name".to_string())) + )] + #[case::eval_datetime_format_error( + InnerError::Eval(EvalError::DateTimeFormatError(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "fmt".to_string())) + )] + #[case::eval_index_out_of_bounds( + InnerError::Eval(EvalError::IndexOutOfBounds(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, 1.into())) + )] + #[case::eval_invalid_definition( + InnerError::Eval(EvalError::InvalidDefinition(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "bad".into())) + )] + #[case::eval_invalid_types( + InnerError::Eval(EvalError::InvalidTypes { + token: Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, + name: "int".into(), + args: vec!["str".into()], + }) + )] + #[case::eval_invalid_number_of_arguments( + InnerError::Eval(EvalError::InvalidNumberOfArguments(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "func".to_string(), 2, 1)) + )] + #[case::eval_invalid_regular_expression( + InnerError::Eval(EvalError::InvalidRegularExpression(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "bad".to_string())) + )] + #[case::eval_internal_error( + InnerError::Eval(EvalError::InternalError(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::eval_runtime_error( + InnerError::Eval(EvalError::RuntimeError(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "err".to_string())) + )] + #[case::eval_zero_division( + InnerError::Eval(EvalError::ZeroDivision(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::eval_break( + InnerError::Eval(EvalError::Break(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::eval_continue( + InnerError::Eval(EvalError::Continue(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + })) + )] + #[case::eval_env_not_found( + InnerError::Eval(EvalError::EnvNotFound(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "ENV".into())) + )] + #[case::module_not_found( + InnerError::Module(ModuleError::NotFound("mod".to_string())) + )] + #[case::module_io_error( + InnerError::Module(ModuleError::IOError("io".to_string())) + )] + #[case::module_lexer_error_unexpected_token( + InnerError::Module(ModuleError::LexerError(LexerError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + #[case::module_lexer_error_unexpected_eof(InnerError::Module(ModuleError::LexerError( + LexerError::UnexpectedEOFDetected(ArenaId::new(0)) + )))] + #[case::module_parse_error_env_not_found( + InnerError::Module(ModuleError::ParseError(ParseError::EnvNotFound(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }, "ENV".into()))) + )] + #[case::module_parse_error_unexpected_token( + InnerError::Module(ModuleError::ParseError(ParseError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + #[case::module_parse_error_unexpected_eof(InnerError::Module(ModuleError::ParseError( + ParseError::UnexpectedEOFDetected(ArenaId::new(0)) + )))] + #[case::module_parse_error_insufficient_tokens( + InnerError::Module(ModuleError::ParseError(ParseError::InsufficientTokens(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + #[case::module_invalid_module(InnerError::Module(ModuleError::InvalidModule))] + #[case::module_parse_error_expected_closing_paren( + InnerError::Module(ModuleError::ParseError(ParseError::ExpectedClosingParen(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + #[case::module_parse_error_expected_closing_brace( + InnerError::Module(ModuleError::ParseError(ParseError::ExpectedClosingBrace(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + #[case::module_parse_error_expected_closing_bracket( + InnerError::Module(ModuleError::ParseError(ParseError::ExpectedClosingBracket(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: ArenaId::new(0), + }))) + )] + fn test_diagnostic_code_and_help(module_loader: ModuleLoader, #[case] cause: InnerError) { + let error = Error::from_error("source code", cause, module_loader); + // code() and help() must not panic + let _ = error.code(); + let _ = error.help(); + } } diff --git a/crates/mq-lang/src/eval.rs b/crates/mq-lang/src/eval.rs index 5849f160..792b2f05 100644 --- a/crates/mq-lang/src/eval.rs +++ b/crates/mq-lang/src/eval.rs @@ -5467,3 +5467,94 @@ mod tests { ); } } + +#[cfg(test)] +#[cfg(all(feature = "debugger", feature = "sync"))] +mod debugger_tests { + use rstest::{fixture, rstest}; + use smallvec::SmallVec; + + use super::*; + use crate::ast::node::Args; + use crate::eval::debugger::{DebugContext, DebuggerHandler}; + use crate::{AstNode, DebuggerAction, IdentWithToken, ModuleLoader, Range, token_alloc}; + + #[fixture] + fn token_arena() -> Shared>>> { + let token_arena = Shared::new(SharedCell::new(Arena::new(10))); + + token_alloc( + &token_arena, + &Shared::new(Token { + kind: TokenKind::Eof, + range: Range::default(), + module_id: 1.into(), + }), + ); + + token_arena + } + + fn ast_call(name: &str, args: Args) -> Shared { + Shared::new(AstNode { + token_id: 0.into(), + expr: Shared::new(ast::Expr::Call(IdentWithToken::new(name), args)), + }) + } + + #[derive(Debug)] + struct TestDebuggerHandler { + breakpoints_hit: Shared>>, + steps_taken: Shared>>, + next_action: DebuggerAction, + } + + impl TestDebuggerHandler { + fn new(action: DebuggerAction) -> Self { + Self { + breakpoints_hit: Shared::new(SharedCell::new(Vec::new())), + steps_taken: Shared::new(SharedCell::new(Vec::new())), + next_action: action, + } + } + } + + impl DebuggerHandler for TestDebuggerHandler { + fn on_breakpoint_hit( + &self, + _breakpoint: &crate::eval::debugger::Breakpoint, + context: &DebugContext, + ) -> DebuggerAction { + self.breakpoints_hit + .write() + .unwrap() + .push(format!("breakpoint:{}", context.current_value.to_string())); + self.next_action.clone() + } + + fn on_step(&self, context: &DebugContext) -> DebuggerAction { + self.steps_taken + .write() + .unwrap() + .push(format!("step:{}", context.current_value.to_string())); + self.next_action.clone() + } + } + + #[rstest] + fn test_eval_debugger_breakpoint_call(token_arena: Shared>>>) { + let handler = Shared::new(SharedCell::new(Box::new(TestDebuggerHandler::new( + DebuggerAction::Continue, + )) as Box)); + + let mut evaluator = Evaluator::new(ModuleLoader::new(None), token_arena); + evaluator.debugger_handler = Shared::clone(&handler); + + let program = vec![ast_call("breakpoint", SmallVec::new())]; + let runtime_values = vec![RuntimeValue::String("test".to_string())]; + + let result = evaluator.eval(&program, runtime_values.into_iter()); + + assert_eq!(result, Ok(vec![RuntimeValue::String("test".to_string())])); + } +} From c40228264a1a3163f4f425333a9d2fe4639e5f80 Mon Sep 17 00:00:00 2001 From: harehare Date: Sat, 25 Oct 2025 10:33:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20Fix(debugger=5Ftests):=20for?= =?UTF-8?q?mat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/mq-lang/src/eval.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mq-lang/src/eval.rs b/crates/mq-lang/src/eval.rs index 792b2f05..83ac5ece 100644 --- a/crates/mq-lang/src/eval.rs +++ b/crates/mq-lang/src/eval.rs @@ -5528,7 +5528,7 @@ mod debugger_tests { self.breakpoints_hit .write() .unwrap() - .push(format!("breakpoint:{}", context.current_value.to_string())); + .push(format!("breakpoint:{}", context.current_value)); self.next_action.clone() } @@ -5536,7 +5536,7 @@ mod debugger_tests { self.steps_taken .write() .unwrap() - .push(format!("step:{}", context.current_value.to_string())); + .push(format!("step:{}", context.current_value)); self.next_action.clone() } }