From 0a7766081d3ccd55644fff78e28db3c4ff0be33c Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 06:43:58 -0600 Subject: [PATCH 01/10] feat: Add complete testing framework with natural language assertions Implements a production-ready testing framework for WFL with natural language syntax following the language's core philosophy of readability. Features: - describe/test block structure for organizing tests - 11 natural language assertion types (equal, be, greater than, less than, be yes/no, exist, contain, be empty, have length, be of type) - Setup and teardown hooks per describe block - Test isolation with independent environments - --test CLI flag with formatted output and exit codes - Comprehensive testing guide documentation Implementation: - AST extensions for DescribeBlock, TestBlock, ExpectStatement, and Assertion enum - Parser support in src/parser/stmt/testing.rs - Interpreter execution with test state tracking and assertion helpers - CLI integration with clear pass/fail reporting Testing: - 22 tests across 3 validation suites (all passing) - Backward compatibility verified with existing TestPrograms - Self-validating test files demonstrating all features Breaking changes: - Reserved keywords: describe, test, expect, setup, teardown, be - Contextual keywords: have, exist, contain - Fixed: Renamed 'test' variable to 'test_var' in 2 existing test files Documentation: - Added Docs/guides/testing-guide.md with comprehensive examples - Updated CLAUDE.md with testing framework info and CLI usage Co-Authored-By: Claude Sonnet 4.5 (1M context) --- CLAUDE.md | 10 +- Docs/guides/testing-guide.md | 366 +++++++++++++++++ TestPrograms/basic_syntax_comprehensive.wfl | 8 +- TestPrograms/math_operations.test.wfl | 71 ++++ TestPrograms/simple_test_validation.wfl | 20 + TestPrograms/test.wfl | 4 +- TestPrograms/text_operations.test.wfl | 66 +++ src/interpreter/assertion_helpers.rs | 147 +++++++ src/interpreter/mod.rs | 187 ++++++++- src/lexer/token.rs | 30 ++ src/lib.rs | 2 +- src/main.rs | 50 +++ src/parser/ast.rs | 37 ++ src/parser/mod.rs | 7 +- src/parser/stmt/mod.rs | 3 + src/parser/stmt/testing.rs | 428 ++++++++++++++++++++ src/typechecker/mod.rs | 49 +++ 17 files changed, 1473 insertions(+), 12 deletions(-) create mode 100644 Docs/guides/testing-guide.md create mode 100644 TestPrograms/math_operations.test.wfl create mode 100644 TestPrograms/simple_test_validation.wfl create mode 100644 TestPrograms/text_operations.test.wfl create mode 100644 src/interpreter/assertion_helpers.rs create mode 100644 src/parser/stmt/testing.rs diff --git a/CLAUDE.md b/CLAUDE.md index cb40f73d..148d1e48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,6 +78,7 @@ Source Code → Lexer → Parser → Analyzer → Type Checker → Interpreter - `wfl --configCheck` / `wfl --configFix`: Check/fix configuration. - `wfl --dump-env`: Dump environment for troubleshooting. - `wfl --analyze `: Run static analysis. +- `wfl --test `: Run file in test mode (executes describe/test blocks). ## Key Language Features - **Natural Language Syntax**: `store name as "value"`, `check if x is greater than 5`. @@ -85,6 +86,7 @@ Source Code → Lexer → Parser → Analyzer → Type Checker → Interpreter - **Async Support**: Built-in async/await using Tokio runtime. - **Pattern Matching**: Regex-like engine with Unicode support. - **Container System**: OOP with containers. +- **Testing Framework**: Built-in testing with `describe`, `test`, and natural language assertions. - **Security**: WFLHASH custom crypto, secure subprocess spawning. ## Coding Style & Naming @@ -98,9 +100,11 @@ Source Code → Lexer → Parser → Analyzer → Type Checker → Interpreter ## Testing Guidelines - **TDD is mandatory**: Write failing tests FIRST for any feature or bug fix. - **Locations**: - - Unit/Integration: `tests/` - - End-to-End: `TestPrograms/` (must pass with release build) -- **Conventions**: feature‑oriented names (`*_test.rs`), keep perf benches under `benches/`. + - Rust Unit/Integration: `tests/` + - WFL End-to-End: `TestPrograms/` (must pass with release build) + - WFL Test Framework: Use `describe`/`test` blocks, run with `wfl --test ` +- **Conventions**: feature‑oriented names (`*_test.rs`, `*.test.wfl`), keep perf benches under `benches/`. +- **Testing Guide**: See `Docs/guides/testing-guide.md` for WFL testing framework documentation. ## Commit & Pull Request Guidelines - **Conventional Commits**: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`. diff --git a/Docs/guides/testing-guide.md b/Docs/guides/testing-guide.md new file mode 100644 index 00000000..bf46f5cf --- /dev/null +++ b/Docs/guides/testing-guide.md @@ -0,0 +1,366 @@ +# WFL Testing Guide + +WFL includes a built-in testing framework with natural language syntax that makes it easy to write and run tests for your WFL programs. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Writing Your First Test](#writing-your-first-test) +- [Test Structure](#test-structure) +- [Assertions](#assertions) +- [Setup and Teardown](#setup-and-teardown) +- [Running Tests](#running-tests) +- [Best Practices](#best-practices) + +## Getting Started + +The WFL testing framework allows you to write tests using familiar natural language syntax. Tests are organized in `describe` blocks and individual `test` blocks, similar to popular testing frameworks like Jest, RSpec, or PHPUnit. + +## Writing Your First Test + +Create a file with a `.wfl` extension (commonly `.test.wfl` for test files): + +```wfl +describe "Basic arithmetic": + + test "addition works correctly": + store result as 5 plus 3 + expect result to equal 8 + end test + + test "subtraction works correctly": + store result as 10 minus 4 + expect result to equal 6 + end test + +end describe +``` + +Run your tests with: + +```bash +wfl --test my_tests.wfl +``` + +## Test Structure + +### Describe Blocks + +Describe blocks organize related tests together. They provide context and can be nested: + +```wfl +describe "Calculator": + + describe "Addition": + test "handles positive numbers": + store result as 2 plus 3 + expect result to equal 5 + end test + end describe + + describe "Subtraction": + test "handles negative results": + store result as 3 minus 8 + expect result to be less than 0 + end test + end describe + +end describe +``` + +### Test Blocks + +Each `test` block represents a single test case: + +```wfl +test "description of what is being tested": + // Test code goes here + store actual as some calculation + expect actual to equal expected_value +end test +``` + +**Important**: Each test runs in an isolated environment. Variables created in one test do not affect other tests. + +## Assertions + +WFL provides natural language assertions that read like English: + +### Equality + +```wfl +expect value to equal 42 +expect message to be "hello" // 'be' is a synonym for 'equal' +``` + +### Comparisons + +```wfl +expect score to be greater than 50 +expect age to be less than 100 +``` + +### Truthiness + +```wfl +expect condition to be yes // Truthy check +expect flag to be no // Falsy check +``` + +### Existence + +```wfl +expect value to exist +``` + +### Collections + +```wfl +expect numbers to contain 5 +expect list to be empty +expect items to have length 3 +``` + +### Text + +```wfl +expect message to contain "error" +expect text to be empty +expect word to have length 5 +``` + +### Type Checking + +```wfl +expect value to be of type "Number" +expect message to be of type "Text" +expect items to be of type "List" +``` + +## Setup and Teardown + +Use `setup` and `teardown` blocks to run code before and after tests in a describe block: + +```wfl +describe "Database operations": + + setup: + display "Connecting to test database..." + store test_data as [1, 2, 3, 4, 5] + end setup + + test "can read data": + expect test_data to have length 5 + end test + + test "can process data": + store sum as 0 + for each item in test_data: + change sum to sum plus item + end for + expect sum to equal 15 + end test + + teardown: + display "Cleaning up test database..." + end teardown + +end describe +``` + +**Note**: Setup and teardown run once per describe block, not before/after each individual test. + +## Running Tests + +### Basic Test Execution + +```bash +wfl --test my_tests.wfl +``` + +### Test Output + +The test runner provides clear, formatted output: + +``` +============================================================ +Test Results +============================================================ +Total: 10 +Passed: 9 ✓ +Failed: 1 ✗ + +──────────────────────────────────────────────────────────── +Failures: +──────────────────────────────────────────────────────────── + +1. addition handles large numbers + Context: Calculator > Addition + Expected value to equal 1000, but got 999 + at line 45 + +============================================================ +``` + +### Exit Codes + +- **0**: All tests passed +- **1**: One or more tests failed + +This allows integration with CI/CD pipelines. + +## Best Practices + +### 1. Organize Tests by Feature + +```wfl +describe "User authentication": + test "validates email format": ... end test + test "requires password": ... end test + test "creates session on success": ... end test +end describe +``` + +### 2. Use Descriptive Test Names + +Good: +```wfl +test "rejects invalid email addresses": +``` + +Bad: +```wfl +test "test 1": +``` + +### 3. Test One Thing Per Test + +Each test should verify one specific behavior: + +```wfl +// Good - tests one thing +test "addition returns correct sum": + store result as 2 plus 3 + expect result to equal 5 +end test + +// Avoid - tests multiple things +test "math operations": + expect 2 plus 3 to equal 5 + expect 10 minus 4 to equal 6 + expect 3 times 4 to equal 12 +end test +``` + +### 4. Avoid Reserved Keywords as Variable Names + +Some words are reserved for the testing framework: +- `empty` - Use `empty_string`, `empty_list`, etc. +- `type` - Use `data_type`, `value_type`, etc. +- `length` - Use `list_length`, `text_length`, etc. + +### 5. Keep Tests Independent + +Don't rely on test execution order. Each test should work in isolation: + +```wfl +// Good - each test is independent +describe "Counter": + test "increments from zero": + store counter as 0 + change counter to counter plus 1 + expect counter to equal 1 + end test + + test "decrements from initial value": + store counter as 5 + change counter to counter minus 1 + expect counter to equal 4 + end test +end describe +``` + +### 6. Use Setup for Common Initialization + +```wfl +describe "List operations": + setup: + store test_list as [1, 2, 3, 4, 5] + end setup + + test "list has correct length": + expect test_list to have length 5 + end test + + test "list contains all values": + expect test_list to contain 3 + end test +end describe +``` + +## Complete Example + +Here's a comprehensive example showing all features: + +```wfl +describe "Shopping Cart": + + setup: + display "Setting up test cart..." + store cart_items as [] + end setup + + test "starts empty": + expect cart_items to be empty + expect cart_items to have length 0 + end test + + test "can add items": + add "apple" to cart_items + add "banana" to cart_items + expect cart_items to have length 2 + expect cart_items to contain "apple" + end test + + test "total price calculation": + store price as 10 plus 20 plus 30 + expect price to equal 60 + expect price to be greater than 50 + end test + + teardown: + display "Cleaning up test cart..." + end teardown + +end describe +``` + +## Troubleshooting + +### Tests Not Running + +- Ensure you're using the `--test` flag: `wfl --test myfile.wfl` +- Check that your file has test blocks inside describe blocks +- Verify all `describe` and `test` blocks are properly closed with `end describe` and `end test` + +### Assertion Failures + +- Check the error message for the expected vs. actual values +- Verify variable names are spelled correctly +- Ensure you're using the correct assertion type for your data + +### Parse Errors + +- Make sure you're not using reserved keywords as variable names +- Check that all blocks are properly closed +- Verify the syntax of your assertions matches the examples above + +## Next Steps + +- Explore the [WFL Language Basics](../03-language-basics/README.md) for more on WFL syntax +- Check out example test files in the `TestPrograms/` directory +- Learn about [Best Practices](../06-best-practices/README.md) for writing maintainable code + +--- + +For questions or issues, please refer to the [WFL documentation](../README.md) or file an issue on GitHub. diff --git a/TestPrograms/basic_syntax_comprehensive.wfl b/TestPrograms/basic_syntax_comprehensive.wfl index 10806ac6..4b497123 100644 --- a/TestPrograms/basic_syntax_comprehensive.wfl +++ b/TestPrograms/basic_syntax_comprehensive.wfl @@ -37,10 +37,10 @@ display "" // === Variable Redefinition === display "4. Variable Redefinition Test" -store test var as "original" -display "Before: " with test var -change test var to "modified" -display "After: " with test var +store test_var as "original" +display "Before: " with test_var +change test_var to "modified" +display "After: " with test_var display "" // === Conditional Statements === diff --git a/TestPrograms/math_operations.test.wfl b/TestPrograms/math_operations.test.wfl new file mode 100644 index 00000000..c171c219 --- /dev/null +++ b/TestPrograms/math_operations.test.wfl @@ -0,0 +1,71 @@ +// Math operations test suite + +describe "Basic arithmetic": + + test "addition of positive numbers": + store result as 5 plus 3 + expect result to equal 8 + end test + + test "subtraction": + store result as 10 minus 7 + expect result to equal 3 + end test + + test "multiplication": + store result as 4 times 5 + expect result to equal 20 + end test + + test "division": + store result as 15 divided by 3 + expect result to equal 5 + end test + +end describe + +describe "Negative numbers": + + test "subtracting to get negative": + store result as 3 minus 8 + store expected as 0 minus 5 + expect result to equal expected + end test + + test "multiplying with negative": + store a as 0 minus 4 + store b as 3 + store result as a times b + store expected as 0 minus 12 + expect result to equal expected + end test + + test "comparison with negative": + store neg as 0 minus 10 + store pos as 10 + // Test that negative is less than positive + expect neg to be less than pos + end test + +end describe + +describe "Comparison operations": + + test "greater than": + store a as 10 + store b as 5 + expect a to be greater than b + end test + + test "less than": + store a as 3 + store b as 7 + expect a to be less than b + end test + + test "equal comparison": + store result as 5 plus 5 + expect result to equal 10 + end test + +end describe diff --git a/TestPrograms/simple_test_validation.wfl b/TestPrograms/simple_test_validation.wfl new file mode 100644 index 00000000..83346ea3 --- /dev/null +++ b/TestPrograms/simple_test_validation.wfl @@ -0,0 +1,20 @@ +// Simple test framework validation + +describe "Basic assertions": + + test "numbers are equal": + store result as 5 + expect result to equal 5 + end test + + test "text values are equal": + store message as "hello" + expect message to equal "hello" + end test + + test "boolean values work": + store flag as yes + expect flag to be yes + end test + +end describe diff --git a/TestPrograms/test.wfl b/TestPrograms/test.wfl index cfc2854b..81ea7572 100644 --- a/TestPrograms/test.wfl +++ b/TestPrograms/test.wfl @@ -1,5 +1,5 @@ -store test as "test" -display test +store test_value as "test" +display test_value create list protocol: add "http" diff --git a/TestPrograms/text_operations.test.wfl b/TestPrograms/text_operations.test.wfl new file mode 100644 index 00000000..d6eaaa29 --- /dev/null +++ b/TestPrograms/text_operations.test.wfl @@ -0,0 +1,66 @@ +// Text operations test suite + +describe "String operations": + + test "text concatenation": + store greeting as "Hello, " + store name as "World" + store full as greeting with name + expect full to equal "Hello, World" + end test + + test "text contains substring": + store message as "The quick brown fox" + expect message to contain "quick" + end test + + test "empty text is empty": + store empty_string as "" + expect empty_string to be empty + end test + + test "non-empty text is not empty": + store text as "hello" + // Verify it has content by checking length + expect text to have length 5 + end test + +end describe + +describe "Text properties": + + test "text has correct type": + store word as "hello" + expect word to be of type "Text" + end test + + test "text length is correct": + store word as "world" + expect word to have length 5 + end test + + test "empty string has zero length": + store empty_text as "" + expect empty_text to have length 0 + end test + +end describe + +describe "Text comparisons": + + test "identical strings are equal": + store a as "test" + store b as "test" + expect a to equal b + end test + + test "case-sensitive comparison": + store lower as "hello" + store upper as "Hello" + // These should NOT be equal (case sensitive) + // We can verify by checking they exist and have different properties + expect lower to have length 5 + expect upper to have length 5 + end test + +end describe diff --git a/src/interpreter/assertion_helpers.rs b/src/interpreter/assertion_helpers.rs new file mode 100644 index 00000000..215ac816 --- /dev/null +++ b/src/interpreter/assertion_helpers.rs @@ -0,0 +1,147 @@ +//! Assertion helper methods for the test framework + +use super::*; + +impl Interpreter { + /// Check if an assertion passes + pub(super) async fn check_assertion( + &self, + subject: &Value, + assertion: &Assertion, + env: Rc>, + ) -> Result { + match assertion { + Assertion::Equal(expected_expr) | Assertion::Be(expected_expr) => { + let expected = self.evaluate_expression(expected_expr, env).await?; + Ok(values_equal(subject, &expected)) + } + Assertion::GreaterThan(expected_expr) => { + let expected = self.evaluate_expression(expected_expr, env).await?; + match (subject, &expected) { + (Value::Number(a), Value::Number(b)) => Ok(a > b), + _ => Ok(false), + } + } + Assertion::LessThan(expected_expr) => { + let expected = self.evaluate_expression(expected_expr, env).await?; + match (subject, &expected) { + (Value::Number(a), Value::Number(b)) => Ok(a < b), + _ => Ok(false), + } + } + Assertion::BeYes => Ok(is_truthy(subject)), + Assertion::BeNo => Ok(!is_truthy(subject)), + Assertion::Exist => Ok(!matches!(subject, Value::Null)), + Assertion::Contain(item_expr) => { + let item = self.evaluate_expression(item_expr, env).await?; + match subject { + Value::List(list) => { + let list_ref = list.borrow(); + Ok(list_ref.iter().any(|v| values_equal(v, &item))) + } + Value::Text(text) => { + if let Value::Text(search) = &item { + Ok(text.contains(search.as_ref())) + } else { + Ok(false) + } + } + _ => Ok(false), + } + } + Assertion::BeEmpty => match subject { + Value::List(list) => Ok(list.borrow().is_empty()), + Value::Text(text) => Ok(text.is_empty()), + _ => Ok(false), + }, + Assertion::HaveLength(expected_expr) => { + let expected = self.evaluate_expression(expected_expr, env).await?; + if let Value::Number(expected_len) = expected { + let actual_len = match subject { + Value::List(list) => list.borrow().len() as f64, + Value::Text(text) => text.len() as f64, + _ => return Ok(false), + }; + Ok((actual_len - expected_len).abs() < f64::EPSILON) + } else { + Ok(false) + } + } + Assertion::BeOfType(type_name) => { + let actual_type = subject.type_name(); + Ok(actual_type.eq_ignore_ascii_case(type_name)) + } + } + } + + /// Create a helpful assertion failure message + pub(super) fn create_assertion_message(&self, assertion: &Assertion, subject: &Value) -> String { + match assertion { + Assertion::Equal(expr) | Assertion::Be(expr) => { + format!("Expected value to equal {:?}, but got {:?}", expr, subject) + } + Assertion::GreaterThan(expr) => { + format!("Expected {:?} to be greater than {:?}", subject, expr) + } + Assertion::LessThan(expr) => { + format!("Expected {:?} to be less than {:?}", subject, expr) + } + Assertion::BeYes => { + format!("Expected {:?} to be truthy", subject) + } + Assertion::BeNo => { + format!("Expected {:?} to be falsy", subject) + } + Assertion::Exist => { + format!("Expected value to exist, but got {:?}", subject) + } + Assertion::Contain(expr) => { + format!("Expected {:?} to contain {:?}", subject, expr) + } + Assertion::BeEmpty => { + format!("Expected {:?} to be empty", subject) + } + Assertion::HaveLength(expr) => { + format!("Expected {:?} to have length {:?}", subject, expr) + } + Assertion::BeOfType(type_name) => { + format!( + "Expected type {}, but got {}", + type_name, + subject.type_name() + ) + } + } + } +} + +/// Helper function to check if two values are equal +fn values_equal(a: &Value, b: &Value) -> bool { + match (a, b) { + (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON, + (Value::Text(a), Value::Text(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Null, Value::Null) => true, + (Value::List(a), Value::List(b)) => { + let a_ref = a.borrow(); + let b_ref = b.borrow(); + if a_ref.len() != b_ref.len() { + return false; + } + a_ref.iter().zip(b_ref.iter()).all(|(x, y)| values_equal(x, y)) + } + _ => false, + } +} + +/// Helper function to check if a value is truthy +fn is_truthy(value: &Value) -> bool { + match value { + Value::Bool(b) => *b, + Value::Null => false, + Value::Number(n) => *n != 0.0, + Value::Text(s) => !s.is_empty(), + Value::List(l) => !l.borrow().is_empty(), + _ => true, + } +} diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index b96e9350..d4019c89 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,4 +1,5 @@ #![allow(clippy::await_holding_refcell_ref)] +mod assertion_helpers; pub mod bounded_buffer; pub mod command_sanitizer; pub mod control_flow; @@ -39,7 +40,7 @@ use crate::exec_var_declare; #[cfg(debug_assertions)] use crate::logging::IndentGuard; use crate::parser::ast::{ - Expression, FileOpenMode, Literal, Operator, Program, Statement, UnaryOperator, + Assertion, Expression, FileOpenMode, Literal, Operator, Program, Statement, UnaryOperator, }; use crate::pattern::CompiledPattern; use crate::stdlib; @@ -260,6 +261,14 @@ fn stmt_type(stmt: &Statement) -> String { "StopAcceptingConnectionsStatement".to_string() } Statement::CloseServerStatement { .. } => "CloseServerStatement".to_string(), + // Test framework statements + Statement::DescribeBlock { description, .. } => { + format!("DescribeBlock '{description}'") + } + Statement::TestBlock { description, .. } => { + format!("TestBlock '{description}'") + } + Statement::ExpectStatement { .. } => "ExpectStatement".to_string(), } } @@ -337,6 +346,28 @@ pub struct Interpreter { config: Arc, // Configuration for security and other settings current_source_file: RefCell>, // Currently executing source file (for path resolution) loading_stack: RefCell>, // Stack of currently loading files (for circular dependency detection) + // Test execution state + test_mode: RefCell, + test_results: RefCell, + current_describe_stack: RefCell>, +} + +// Test framework data structures +#[derive(Debug, Default, Clone)] +pub struct TestResults { + pub total_tests: usize, + pub passed_tests: usize, + pub failed_tests: usize, + pub failures: Vec, +} + +#[derive(Debug, Clone)] +pub struct TestFailure { + pub describe_context: Vec, + pub test_name: String, + pub assertion_message: String, + pub line: usize, + pub column: usize, } // Process handle for managing subprocess state @@ -1147,6 +1178,10 @@ impl Interpreter { config, current_source_file: RefCell::new(None), // No source file initially loading_stack: RefCell::new(Vec::new()), // Empty loading stack + // Test execution state + test_mode: RefCell::new(false), + test_results: RefCell::new(TestResults::default()), + current_describe_stack: RefCell::new(Vec::new()), } } @@ -1170,6 +1205,16 @@ impl Interpreter { *self.current_source_file.borrow_mut() = Some(path); } + /// Enable or disable test mode + pub fn set_test_mode(&self, enabled: bool) { + *self.test_mode.borrow_mut() = enabled; + } + + /// Get test results after running in test mode + pub fn get_test_results(&self) -> TestResults { + self.test_results.borrow().clone() + } + /// Extract variables from the environment for module analyzer /// Returns a HashMap of variable names to inferred types fn extract_parent_variables( @@ -1706,6 +1751,10 @@ impl Interpreter { Statement::ReadProcessOutputStatement { line, column, .. } => (*line, *column), Statement::KillProcessStatement { line, column, .. } => (*line, *column), Statement::WaitForProcessStatement { line, column, .. } => (*line, *column), + // Test framework statements + Statement::DescribeBlock { line, column, .. } => (*line, *column), + Statement::TestBlock { line, column, .. } => (*line, *column), + Statement::ExpectStatement { line, column, .. } => (*line, *column), }; let result = match stmt { @@ -4854,6 +4903,142 @@ impl Interpreter { .map_err(|e| RuntimeError::new(e, *line, *column))?; } + Ok((Value::Null, ControlFlow::None)) + } + // Test framework statements + Statement::DescribeBlock { + description, + setup, + teardown, + tests, + line, + column, + } => { + if !*self.test_mode.borrow() { + return Err(RuntimeError::new( + "describe blocks can only be used in test mode (run with --test flag)" + .to_string(), + *line, + *column, + )); + } + + // Push describe context + self.current_describe_stack + .borrow_mut() + .push(description.clone()); + + // Run setup if present + if let Some(setup_stmts) = setup { + for stmt in setup_stmts { + Box::pin(self._execute_statement(stmt, env.clone())).await?; + } + } + + // Execute all tests + for test in tests { + Box::pin(self._execute_statement(test, env.clone())).await?; + } + + // Run teardown if present + if let Some(teardown_stmts) = teardown { + for stmt in teardown_stmts { + Box::pin(self._execute_statement(stmt, env.clone())).await?; + } + } + + // Pop describe context + self.current_describe_stack.borrow_mut().pop(); + + Ok((Value::Null, ControlFlow::None)) + } + Statement::TestBlock { + description, + body, + line, + column, + } => { + if !*self.test_mode.borrow() { + return Err(RuntimeError::new( + "test blocks can only be used in test mode (run with --test flag)" + .to_string(), + *line, + *column, + )); + } + + // Increment test count + self.test_results.borrow_mut().total_tests += 1; + + // Create isolated environment for test (child of current env) + let test_env = Environment::new_child_env(&env); + + // Execute test body and catch assertion failures + let mut test_passed = true; + for stmt in body { + match Box::pin(self._execute_statement(stmt, test_env.clone())).await { + Ok(_) => {} + Err(e) => { + // Check if this is an assertion failure (we'll mark it specially) + test_passed = false; + // The failure is already recorded in test_results + // Don't propagate the error - continue running other tests + eprintln!("Test failed: {}", e); + break; + } + } + } + + if test_passed { + self.test_results.borrow_mut().passed_tests += 1; + } + + Ok((Value::Null, ControlFlow::None)) + } + Statement::ExpectStatement { + subject, + assertion, + line, + column, + } => { + if !*self.test_mode.borrow() { + return Err(RuntimeError::new( + "expect statements can only be used in test mode (run with --test flag)" + .to_string(), + *line, + *column, + )); + } + + // Evaluate subject expression + let subject_value = self.evaluate_expression(subject, env.clone()).await?; + + // Check assertion + let passed = self.check_assertion(&subject_value, assertion, env.clone()).await?; + + if !passed { + // Record failure + let message = self.create_assertion_message(assertion, &subject_value); + let context = self.current_describe_stack.borrow().clone(); + + let failure = TestFailure { + describe_context: context, + test_name: "current test".to_string(), // TODO: track current test name + assertion_message: message.clone(), + line: *line, + column: *column, + }; + + self.test_results.borrow_mut().failures.push(failure); + self.test_results.borrow_mut().failed_tests += 1; + + return Err(RuntimeError::new( + format!("Assertion failed: {message}"), + *line, + *column, + )); + } + Ok((Value::Null, ControlFlow::None)) } }; diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 60e7ed45..ca13786e 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -359,6 +359,25 @@ pub enum Token { KeywordMilliseconds, #[token("formatted")] KeywordFormatted, + // Test framework keywords + #[token("describe")] + KeywordDescribe, + #[token("test")] + KeywordTest, + #[token("expect")] + KeywordExpect, + #[token("setup")] + KeywordSetup, + #[token("teardown")] + KeywordTeardown, + #[token("be")] + KeywordBe, + #[token("have")] + KeywordHave, + #[token("exist")] + KeywordExist, + #[token("contain")] + KeywordContain, #[token(":")] Colon, @@ -567,6 +586,13 @@ impl Token { | Token::KeywordPublic | Token::KeywordPrivate | Token::KeywordConstant + // Test framework keywords + | Token::KeywordDescribe + | Token::KeywordTest + | Token::KeywordExpect + | Token::KeywordSetup + | Token::KeywordTeardown + | Token::KeywordBe ) } @@ -576,6 +602,10 @@ impl Token { matches!( self, Token::KeywordCount // Only reserved in 'count from X to Y' context + // Test framework keywords that are contextual + | Token::KeywordHave // Only reserved in test assertions + | Token::KeywordExist // Only reserved in test assertions + | Token::KeywordContain // Only reserved in test assertions | Token::KeywordPattern // Only reserved in pattern matching context | Token::KeywordFiles // Only reserved in file operations context | Token::KeywordExtension diff --git a/src/lib.rs b/src/lib.rs index 9f181014..cc394964 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,7 @@ pub fn init_loggers(log_path: &Path, script_dir: &Path) { } } -pub use interpreter::Interpreter; +pub use interpreter::{Interpreter, TestFailure, TestResults}; pub fn add(left: u64, right: u64) -> u64 { left + right diff --git a/src/main.rs b/src/main.rs index 9c3c3984..7c2591f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ fn print_help() { println!(" --dump-env Dump the current environment details for troubleshooting"); println!(" --output Specify an output file for the environment dump"); println!(" --time Measure and display execution time"); + println!(" --test Run file in test mode"); println!(); println!("Configuration Maintenance:"); println!(" --configCheck Check configuration files for issues"); @@ -97,6 +98,7 @@ async fn main() -> io::Result<()> { let mut dump_env_mode = false; let mut output_path = None; let mut time_mode = false; + let mut test_mode = false; let mut file_path = String::new(); let mut i = 1; @@ -274,6 +276,16 @@ async fn main() -> io::Result<()> { time_mode = true; i += 1; } + "--test" => { + if lint_mode || analyze_mode || fix_mode || config_check_mode || config_fix_mode { + eprintln!( + "Error: --test cannot be combined with --lint, --analyze, --fix, --configCheck, or --configFix" + ); + process::exit(2); + } + test_mode = true; + i += 1; + } "--version" | "-v" => { println!("WebFirst Language (WFL) version {}", wfl::version::VERSION); return Ok(()); @@ -821,6 +833,7 @@ async fn main() -> io::Result<()> { let mut interpreter = Interpreter::with_timeout(config.timeout_seconds); interpreter.set_step_mode(step_mode); // Set step mode from CLI flag + interpreter.set_test_mode(test_mode); // Set test mode from CLI flag interpreter.set_script_args(script_args); // Pass script arguments interpreter.set_source_file(std::path::PathBuf::from(&file_path)); // Set source file for module resolution @@ -870,6 +883,43 @@ async fn main() -> io::Result<()> { info!("Program executed successfully"); } exec_trace!("Execution completed successfully. Result: {:?}", _result); + + // Handle test mode results + if test_mode { + let results = interpreter.get_test_results(); + + println!("\n{}", "=".repeat(60)); + println!("Test Results"); + println!("{}", "=".repeat(60)); + println!("Total: {}", results.total_tests); + println!("Passed: {} ✓", results.passed_tests); + println!("Failed: {} ✗", results.failed_tests); + + if !results.failures.is_empty() { + println!("\n{}", "─".repeat(60)); + println!("Failures:"); + println!("{}", "─".repeat(60)); + + for (i, failure) in results.failures.iter().enumerate() { + println!("\n{}. {}", i + 1, failure.test_name); + if !failure.describe_context.is_empty() { + println!( + " Context: {}", + failure.describe_context.join(" > ") + ); + } + println!(" {}", failure.assertion_message); + println!(" at line {}", failure.line); + } + } + + println!("\n{}", "=".repeat(60)); + + // Exit with error code if tests failed + if results.failed_tests > 0 { + process::exit(1); + } + } } Err(errors) => { if config.logging_enabled { diff --git a/src/parser/ast.rs b/src/parser/ast.rs index a43efa0d..1161b56b 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -465,6 +465,43 @@ pub enum Statement { line: usize, column: usize, }, + // Test framework statements + DescribeBlock { + description: String, + setup: Option>, + teardown: Option>, + tests: Vec, // Contains TestBlock and nested DescribeBlock + line: usize, + column: usize, + }, + TestBlock { + description: String, + body: Vec, + line: usize, + column: usize, + }, + ExpectStatement { + subject: Expression, + assertion: Assertion, + line: usize, + column: usize, + }, +} + +/// Represents different types of assertions in test expectations +#[derive(Debug, Clone, PartialEq)] +pub enum Assertion { + Equal(Expression), + Be(Expression), // Synonym for Equal + GreaterThan(Expression), + LessThan(Expression), + BeYes, // Truthy check + BeNo, // Falsy check + Exist, + Contain(Expression), // List/text contains + BeEmpty, + HaveLength(Expression), + BeOfType(String), // Type check } #[derive(Debug, Clone, PartialEq)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cc515495..8070d630 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13,7 +13,8 @@ pub use cursor::Cursor; // Re-export Cursor publicly for doctests use expr::ExprParser; use stmt::{ ActionParser, CollectionParser, ContainerParser, ControlFlowParser, ErrorHandlingParser, - IoParser, ModuleParser, PatternParser, ProcessParser, StmtParser, VariableParser, WebParser, + IoParser, ModuleParser, PatternParser, ProcessParser, StmtParser, TestingParser, + VariableParser, WebParser, }; pub struct Parser<'a> { @@ -530,6 +531,10 @@ impl<'a> StmtParser<'a> for Parser<'a> { self.parse_expression_statement() } } + // Test framework keywords + Token::KeywordDescribe => self.parse_describe_block(), + Token::KeywordTest => self.parse_test_block(), + Token::KeywordExpect => self.parse_expect_statement(), _ => self.parse_expression_statement(), } } else { diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index f5717054..06687ae0 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -14,6 +14,7 @@ mod io; mod module; mod patterns; mod processes; +mod testing; mod variables; mod web; @@ -26,6 +27,7 @@ pub(crate) use io::IoParser; pub(crate) use module::ModuleParser; pub(crate) use patterns::PatternParser; pub(crate) use processes::ProcessParser; +pub(crate) use testing::TestingParser; pub(crate) use variables::VariableParser; pub(crate) use web::WebParser; @@ -51,6 +53,7 @@ pub(crate) trait StmtParser<'a>: + PatternParser<'a> + ContainerParser<'a> + ModuleParser<'a> + + TestingParser<'a> { /// Parses a statement by dispatching to the appropriate parser based on the current token. /// diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs new file mode 100644 index 00000000..c5c7703d --- /dev/null +++ b/src/parser/stmt/testing.rs @@ -0,0 +1,428 @@ +//! Test framework statement parsing + +use super::super::{ParseError, Parser, Statement}; +use super::StmtParser; +use crate::lexer::token::Token; +use crate::parser::ast::Assertion; +use crate::parser::expr::ExprParser; + +pub(crate) trait TestingParser<'a>: ExprParser<'a> { + fn parse_describe_block(&mut self) -> Result; + fn parse_test_block(&mut self) -> Result; + fn parse_expect_statement(&mut self) -> Result; +} + +impl<'a> TestingParser<'a> for Parser<'a> { + fn parse_describe_block(&mut self) -> Result { + // Parse: describe "Description": + // [setup: ... end setup] + // test "...": ... end test + // [teardown: ... end teardown] + // end describe + + // Capture position + let describe_token = self.cursor.peek().ok_or_else(|| { + ParseError::from_span( + "Unexpected end of input while parsing describe block".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + ) + })?; + let (line, column) = (describe_token.line, describe_token.column); + + // Consume 'describe' token + self.expect_token(Token::KeywordDescribe, "Expected 'describe' keyword")?; + + // Parse description string + let description = if let Some(token) = self.cursor.peek() { + if let Token::StringLiteral(s) = &token.token { + let desc = s.clone(); + self.cursor.bump(); // Consume the string + desc + } else { + return Err(ParseError::from_token( + "Expected string literal after 'describe'".to_string(), + token, + )); + } + } else { + return Err(ParseError::from_span( + "Expected description string after 'describe'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )); + }; + + // Expect colon + self.expect_token(Token::Colon, "Expected ':' after describe description")?; + + // Consume newline after colon + self.skip_eol(); + + // Parse body (setup, tests, teardown) + let mut setup: Option> = None; + let mut teardown: Option> = None; + let mut tests: Vec = Vec::new(); + + loop { + self.skip_eol(); + + let token = self.cursor.peek(); + match token { + Some(t) => match &t.token { + Token::KeywordSetup => { + // Parse setup block + self.cursor.bump(); // Consume 'setup' + self.expect_token(Token::Colon, "Expected ':' after 'setup'")?; + self.skip_eol(); + + let mut setup_stmts = Vec::new(); + loop { + self.skip_eol(); + if let Some(token) = self.cursor.peek() { + if matches!(token.token, Token::KeywordEnd) { + break; + } + } + setup_stmts.push(self.parse_statement()?); + } + + self.expect_token(Token::KeywordEnd, "Expected 'end' to close setup block")?; + self.expect_token( + Token::KeywordSetup, + "Expected 'setup' after 'end' in setup block", + )?; + setup = Some(setup_stmts); + } + Token::KeywordTeardown => { + // Parse teardown block + self.cursor.bump(); // Consume 'teardown' + self.expect_token(Token::Colon, "Expected ':' after 'teardown'")?; + self.skip_eol(); + + let mut teardown_stmts = Vec::new(); + loop { + self.skip_eol(); + if let Some(token) = self.cursor.peek() { + if matches!(token.token, Token::KeywordEnd) { + break; + } + } + teardown_stmts.push(self.parse_statement()?); + } + + self.expect_token( + Token::KeywordEnd, + "Expected 'end' to close teardown block", + )?; + self.expect_token( + Token::KeywordTeardown, + "Expected 'teardown' after 'end' in teardown block", + )?; + teardown = Some(teardown_stmts); + } + Token::KeywordTest => { + // Parse test block + tests.push(self.parse_test_block()?); + } + Token::KeywordDescribe => { + // Nested describe block + tests.push(self.parse_describe_block()?); + } + Token::KeywordEnd => { + // End of describe block + break; + } + _ => { + return Err(ParseError::from_token( + "Expected 'setup', 'test', 'teardown', or 'end' in describe block" + .to_string(), + t, + )); + } + }, + None => { + return Err(ParseError::from_span( + "Unexpected end of input in describe block".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )); + } + } + } + + // Expect 'end describe' + self.expect_token(Token::KeywordEnd, "Expected 'end' to close describe block")?; + self.expect_token( + Token::KeywordDescribe, + "Expected 'describe' after 'end' in describe block", + )?; + + Ok(Statement::DescribeBlock { + description, + setup, + teardown, + tests, + line, + column, + }) + } + + fn parse_test_block(&mut self) -> Result { + // Parse: test "description": + // [statements] + // end test + + // Capture position + let test_token = self.cursor.peek().ok_or_else(|| { + ParseError::from_span( + "Unexpected end of input while parsing test block".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + ) + })?; + let (line, column) = (test_token.line, test_token.column); + + // Consume 'test' token + self.expect_token(Token::KeywordTest, "Expected 'test' keyword")?; + + // Parse description string + let description = if let Some(token) = self.cursor.peek() { + if let Token::StringLiteral(s) = &token.token { + let desc = s.clone(); + self.cursor.bump(); // Consume the string + desc + } else { + return Err(ParseError::from_token( + "Expected string literal after 'test'".to_string(), + token, + )); + } + } else { + return Err(ParseError::from_span( + "Expected description string after 'test'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )); + }; + + // Expect colon + self.expect_token(Token::Colon, "Expected ':' after test description")?; + + // Consume newline after colon + self.skip_eol(); + + // Parse body statements + let mut body = Vec::new(); + loop { + self.skip_eol(); + + if let Some(token) = self.cursor.peek() { + if matches!(token.token, Token::KeywordEnd) { + break; + } + } + + body.push(self.parse_statement()?); + } + + // Expect 'end test' + self.expect_token(Token::KeywordEnd, "Expected 'end' to close test block")?; + self.expect_token(Token::KeywordTest, "Expected 'test' after 'end' in test block")?; + + Ok(Statement::TestBlock { + description, + body, + line, + column, + }) + } + + fn parse_expect_statement(&mut self) -> Result { + // Parse: expect to + // + // Examples: + // expect result to equal 5 + // expect list to contain "item" + // expect value to be greater than 10 + + // Capture position + let expect_token = self.cursor.peek().ok_or_else(|| { + ParseError::from_span( + "Unexpected end of input while parsing expect statement".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + ) + })?; + let (line, column) = (expect_token.line, expect_token.column); + + // Consume 'expect' token + self.expect_token(Token::KeywordExpect, "Expected 'expect' keyword")?; + + // Parse subject expression + let subject = self.parse_expression()?; + + // Expect 'to' keyword + self.expect_token(Token::KeywordTo, "Expected 'to' after expect subject")?; + + // Parse assertion type based on next token + let assertion = self.parse_assertion()?; + + Ok(Statement::ExpectStatement { + subject, + assertion, + line, + column, + }) + } +} + +impl<'a> Parser<'a> { + /// Helper method to parse assertion types for expect statements + fn parse_assertion(&mut self) -> Result { + let token = self.cursor.peek().ok_or_else(|| { + ParseError::from_span( + "Expected assertion type after 'to'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + ) + })?; + + match &token.token { + Token::KeywordEqual => { + self.cursor.bump(); // Consume 'equal' + let value = self.parse_expression()?; + Ok(Assertion::Equal(value)) + } + Token::KeywordBe => { + self.cursor.bump(); // Consume 'be' + + // Check what comes after 'be' + if let Some(next_token) = self.cursor.peek() { + match &next_token.token { + Token::BooleanLiteral(true) => { + self.cursor.bump(); + Ok(Assertion::BeYes) + } + Token::BooleanLiteral(false) => { + self.cursor.bump(); + Ok(Assertion::BeNo) + } + Token::KeywordGreater => { + self.cursor.bump(); // Consume 'greater' + self.expect_token(Token::KeywordThan, "Expected 'than' after 'greater'")?; + let value = self.parse_expression()?; + Ok(Assertion::GreaterThan(value)) + } + Token::KeywordLess => { + self.cursor.bump(); // Consume 'less' + self.expect_token(Token::KeywordThan, "Expected 'than' after 'less'")?; + let value = self.parse_expression()?; + Ok(Assertion::LessThan(value)) + } + Token::Identifier(id) if id == "empty" => { + self.cursor.bump(); // Consume 'empty' + Ok(Assertion::BeEmpty) + } + Token::KeywordOf => { + self.cursor.bump(); // Consume 'of' + + // Check if next token is "type" identifier + if let Some(token) = self.cursor.peek() { + if let Token::Identifier(id) = &token.token { + if id == "type" { + self.cursor.bump(); // Consume 'type' + + // Expect a string literal for the type name + if let Some(token) = self.cursor.peek() { + if let Token::StringLiteral(type_name) = &token.token { + let tn = type_name.clone(); + self.cursor.bump(); + return Ok(Assertion::BeOfType(tn)); + } + } + } + } + } + + Err(ParseError::from_span( + "Expected 'type' after 'of'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )) + } + _ => { + // Default: treat as 'be' synonym for 'equal' + let value = self.parse_expression()?; + Ok(Assertion::Be(value)) + } + } + } else { + Err(ParseError::from_span( + "Expected value or condition after 'be'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )) + } + } + Token::KeywordGreater => { + self.cursor.bump(); // Consume 'greater' + self.expect_token(Token::KeywordThan, "Expected 'than' after 'greater'")?; + let value = self.parse_expression()?; + Ok(Assertion::GreaterThan(value)) + } + Token::KeywordLess => { + self.cursor.bump(); // Consume 'less' + self.expect_token(Token::KeywordThan, "Expected 'than' after 'less'")?; + let value = self.parse_expression()?; + Ok(Assertion::LessThan(value)) + } + Token::KeywordContain => { + self.cursor.bump(); // Consume 'contain' + let value = self.parse_expression()?; + Ok(Assertion::Contain(value)) + } + Token::KeywordExist => { + self.cursor.bump(); // Consume 'exist' + Ok(Assertion::Exist) + } + Token::KeywordHave => { + self.cursor.bump(); // Consume 'have' + + // Check if next token is "length" identifier + if let Some(token) = self.cursor.peek() { + if let Token::Identifier(id) = &token.token { + if id == "length" { + self.cursor.bump(); // Consume 'length' + let value = self.parse_expression()?; + return Ok(Assertion::HaveLength(value)); + } + } + } + + Err(ParseError::from_span( + "Expected 'length' after 'have'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )) + } + _ => Err(ParseError::from_token( + format!( + "Unknown assertion type: expected 'equal', 'be', 'contain', etc. Got: {:?}", + token.token + ), + token, + )), + } + } +} diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 4f61785d..06565e24 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -1516,6 +1516,55 @@ impl TypeChecker { // TODO: Add type checking for server expression // For now, just accept any type } + // Test framework statements + Statement::DescribeBlock { + description: _description, + setup, + teardown, + tests, + line: _line, + column: _column, + } => { + // Type check setup block if present + if let Some(setup_stmts) = setup { + for stmt in setup_stmts { + self.check_statement_types(stmt); + } + } + + // Type check all test blocks + for test in tests { + self.check_statement_types(test); + } + + // Type check teardown block if present + if let Some(teardown_stmts) = teardown { + for stmt in teardown_stmts { + self.check_statement_types(stmt); + } + } + } + Statement::TestBlock { + description: _description, + body, + line: _line, + column: _column, + } => { + // Type check test body + for stmt in body { + self.check_statement_types(stmt); + } + } + Statement::ExpectStatement { + subject, + assertion: _assertion, + line: _line, + column: _column, + } => { + // Type check the subject expression + self.infer_expression_type(subject); + // Note: assertion type checking will be done in the interpreter + } } } From fa00c4876ae7de9fae2ff3a1375b6d2d2a6c4e64 Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 07:17:07 -0600 Subject: [PATCH 02/10] fix: Improve testing framework isolation, error handling, and type checking Addresses three critical improvements to the testing framework: 1. Test Isolation (Fixed) - Tests now use isolated environments (new_isolated_child_env) - Setup variables are read-only in tests, preventing mutations - Each test gets a fresh isolated copy, ensuring true test independence - Setup/teardown run in describe-level environment shared across tests 2. Error Handling (Fixed) - Added current_test_name tracking to interpreter state - Test failures now properly display test names instead of "current test" - Improved error messages distinguish between assertion failures and runtime errors - Non-assertion errors in test code are now properly recorded with context 3. Type Checking (Added) - Assertions now validated at compile-time where possible - Numeric comparisons verify subject and value are numbers - Collection assertions verify subject is List or Text - Length assertions verify length value is numeric - Catches type errors early before test execution Testing: - All existing tests pass (30 total across 4 test files) - New test_improvements_validation.wfl validates all fixes (8 tests passing) - Backward compatibility maintained with existing WFL programs Technical Details: - Modified src/interpreter/mod.rs: * Added current_test_name: RefCell> to track test context * Changed test environments to use new_isolated_child_env for proper isolation * Improved failure tracking with actual test names * Enhanced error handling to distinguish assertion vs runtime errors - Modified src/typechecker/mod.rs: * Added comprehensive type checking for all assertion types * Validates subject/value type compatibility at compile-time * Provides early feedback on type mismatches Co-Authored-By: Claude Sonnet 4.5 (1M context) --- TestPrograms/test_framework_validation.wfl | 234 ++++++++++++++++++ TestPrograms/test_improvements_validation.wfl | 63 +++++ src/interpreter/mod.rs | 62 ++++- src/typechecker/mod.rs | 113 ++++++++- 4 files changed, 456 insertions(+), 16 deletions(-) create mode 100644 TestPrograms/test_framework_validation.wfl create mode 100644 TestPrograms/test_improvements_validation.wfl diff --git a/TestPrograms/test_framework_validation.wfl b/TestPrograms/test_framework_validation.wfl new file mode 100644 index 00000000..3de88c3b --- /dev/null +++ b/TestPrograms/test_framework_validation.wfl @@ -0,0 +1,234 @@ +// Comprehensive test framework validation +// Tests all assertion types and framework features + +describe "Basic equality assertions": + + test "numbers are equal": + store result as 5 + expect result to equal 5 + end test + + test "text values are equal": + store message as "hello" + expect message to equal "hello" + end test + + test "boolean values are equal": + store flag as yes + expect flag to equal yes + end test + + test "synonym 'be' works like 'equal'": + store value as 42 + expect value to be 42 + end test + +end describe + +describe "Comparison assertions": + + test "greater than works": + store num as 10 + expect num to be greater than 5 + end test + + test "less than works": + store num as 3 + expect num to be less than 10 + end test + + test "comparison with expressions": + store a as 7 + store b as 3 + expect a to be greater than b + end test + +end describe + +describe "Truthiness assertions": + + test "yes is truthy": + store flag as yes + expect flag to be yes + end test + + test "no is falsy": + store flag as no + expect flag to be no + end test + + test "non-zero numbers are truthy": + store num as 5 + expect num to be yes + end test + + test "zero is falsy": + store num as 0 + expect num to be no + end test + +end describe + +describe "Existence assertions": + + test "values exist": + store value as "something" + expect value to exist + end test + + test "numbers exist": + store num as 0 + expect num to exist + end test + +end describe + +describe "Collection assertions": + + test "list contains value": + store numbers as [1, 2, 3, 4, 5] + expect numbers to contain 3 + end test + + test "list has correct length": + store items as ["a", "b", "c"] + expect items to have length 3 + end test + + test "empty list is empty": + store empty as [] + expect empty to be empty + end test + + test "non-empty list is not empty": + store numbers as [1, 2, 3] + // This should pass because the list has items + store count as 0 + for each item in numbers: + change count to count plus 1 + end for + expect count to be greater than 0 + end test + +end describe + +describe "Text assertions": + + test "text contains substring": + store message as "hello world" + expect message to contain "world" + end test + + test "empty text is empty": + store empty as "" + expect empty to be empty + end test + + test "text has length": + store word as "hello" + expect word to have length 5 + end test + +end describe + +describe "Type assertions": + + test "number has correct type": + store num as 42 + expect num to be of type "Number" + end test + + test "text has correct type": + store text as "hello" + expect text to be of type "Text" + end test + + test "list has correct type": + store list as [1, 2, 3] + expect list to be of type "List" + end test + +end describe + +describe "Setup and teardown": + + setup: + display "Running setup for this describe block" + store shared_value as 100 + end setup + + test "can access setup variables": + expect shared_value to equal 100 + end test + + test "setup runs before each test": + expect shared_value to exist + end test + + teardown: + display "Running teardown for this describe block" + end teardown + +end describe + +describe "Variable isolation": + + test "first test sets variable": + store test_var as "value1" + expect test_var to equal "value1" + end test + + test "second test has fresh environment": + // test_var from previous test should not exist + // We can verify isolation by setting it independently + store test_var as "value2" + expect test_var to equal "value2" + end test + +end describe + +describe "Nested describe blocks": + + describe "Level 2": + + test "nested test executes": + store nested as yes + expect nested to be yes + end test + + describe "Level 3": + + test "deeply nested test executes": + store deep as 123 + expect deep to equal 123 + end test + + end describe + + end describe + +end describe + +describe "Arithmetic operations": + + test "addition": + store sum as 2 plus 3 + expect sum to equal 5 + end test + + test "subtraction": + store diff as 10 minus 4 + expect diff to equal 6 + end test + + test "multiplication": + store product as 6 times 7 + expect product to equal 42 + end test + + test "division": + store quotient as 20 divided by 4 + expect quotient to equal 5 + end test + +end describe diff --git a/TestPrograms/test_improvements_validation.wfl b/TestPrograms/test_improvements_validation.wfl new file mode 100644 index 00000000..034a59c2 --- /dev/null +++ b/TestPrograms/test_improvements_validation.wfl @@ -0,0 +1,63 @@ +// Test to validate improvements to testing framework + +describe "Test name tracking": + + test "first test with proper name": + store value as 42 + expect value to equal 42 + end test + + test "second test with different name": + store value as "hello" + expect value to equal "hello" + end test + +end describe + +describe "Setup/teardown isolation": + + setup: + store shared_value as 100 + end setup + + test "can access setup variable": + expect shared_value to equal 100 + end test + + test "can read setup variable": + // Setup variables are accessible (read-only) + expect shared_value to equal 100 + end test + + test "can create test-local variables": + // Tests can create their own local variables + store test_local as 200 + expect test_local to equal 200 + end test + + test "local variables don't leak": + // Variables from previous test don't leak to this test + // We can create our own variable with the same name + store test_local as 300 + expect test_local to equal 300 + end test + + teardown: + display "Teardown completes successfully" + end teardown + +end describe + +describe "Type checking catches errors early": + + test "numeric comparisons work": + store num as 10 + expect num to be greater than 5 + end test + + test "length checks on collections": + store items as [1, 2, 3] + expect items to have length 3 + end test + +end describe diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index d4019c89..d9cb75f9 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -350,6 +350,7 @@ pub struct Interpreter { test_mode: RefCell, test_results: RefCell, current_describe_stack: RefCell>, + current_test_name: RefCell>, } // Test framework data structures @@ -1182,6 +1183,7 @@ impl Interpreter { test_mode: RefCell::new(false), test_results: RefCell::new(TestResults::default()), current_describe_stack: RefCell::new(Vec::new()), + current_test_name: RefCell::new(None), } } @@ -4928,22 +4930,26 @@ impl Interpreter { .borrow_mut() .push(description.clone()); - // Run setup if present + // Create describe-level environment for setup/teardown sharing + // This allows tests to access setup variables while remaining isolated from each other + let describe_env = Environment::new_child_env(&env); + + // Run setup if present (runs in describe environment) if let Some(setup_stmts) = setup { for stmt in setup_stmts { - Box::pin(self._execute_statement(stmt, env.clone())).await?; + Box::pin(self._execute_statement(stmt, describe_env.clone())).await?; } } - // Execute all tests + // Execute all tests (each gets a child of describe_env for isolation) for test in tests { - Box::pin(self._execute_statement(test, env.clone())).await?; + Box::pin(self._execute_statement(test, describe_env.clone())).await?; } - // Run teardown if present + // Run teardown if present (runs in describe environment) if let Some(teardown_stmts) = teardown { for stmt in teardown_stmts { - Box::pin(self._execute_statement(stmt, env.clone())).await?; + Box::pin(self._execute_statement(stmt, describe_env.clone())).await?; } } @@ -4967,23 +4973,45 @@ impl Interpreter { )); } + // Set current test name for failure tracking + *self.current_test_name.borrow_mut() = Some(description.clone()); + // Increment test count self.test_results.borrow_mut().total_tests += 1; - // Create isolated environment for test (child of current env) - let test_env = Environment::new_child_env(&env); + // Create isolated environment for test (child of describe env) + // Using isolated mode prevents tests from mutating setup variables, + // ensuring each test gets a fresh copy for true isolation + let test_env = Environment::new_isolated_child_env(&env); // Execute test body and catch assertion failures let mut test_passed = true; + let mut failure_recorded = false; + for stmt in body { match Box::pin(self._execute_statement(stmt, test_env.clone())).await { Ok(_) => {} Err(e) => { - // Check if this is an assertion failure (we'll mark it specially) test_passed = false; - // The failure is already recorded in test_results + + // Only record failure if not already recorded by expect statement + // Check if this is an assertion failure (which has already been recorded) + let error_msg = e.to_string(); + if !error_msg.starts_with("Assertion failed:") { + // This is a non-assertion error (e.g., runtime error in test code) + let context = self.current_describe_stack.borrow().clone(); + let failure = TestFailure { + describe_context: context, + test_name: description.clone(), + assertion_message: error_msg, + line: *line, + column: *column, + }; + self.test_results.borrow_mut().failures.push(failure); + failure_recorded = true; + } + // Don't propagate the error - continue running other tests - eprintln!("Test failed: {}", e); break; } } @@ -4993,6 +5021,9 @@ impl Interpreter { self.test_results.borrow_mut().passed_tests += 1; } + // Clear current test name + *self.current_test_name.borrow_mut() = None; + Ok((Value::Null, ControlFlow::None)) } Statement::ExpectStatement { @@ -5017,13 +5048,18 @@ impl Interpreter { let passed = self.check_assertion(&subject_value, assertion, env.clone()).await?; if !passed { - // Record failure + // Record failure with proper test name tracking let message = self.create_assertion_message(assertion, &subject_value); let context = self.current_describe_stack.borrow().clone(); + let test_name = self + .current_test_name + .borrow() + .clone() + .unwrap_or_else(|| "unknown test".to_string()); let failure = TestFailure { describe_context: context, - test_name: "current test".to_string(), // TODO: track current test name + test_name, assertion_message: message.clone(), line: *line, column: *column, diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 06565e24..a7f807d8 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -1557,13 +1557,120 @@ impl TypeChecker { } Statement::ExpectStatement { subject, - assertion: _assertion, + assertion, line: _line, column: _column, } => { // Type check the subject expression - self.infer_expression_type(subject); - // Note: assertion type checking will be done in the interpreter + let subject_type = self.infer_expression_type(subject); + + // Perform compile-time type checking for assertions where possible + use crate::parser::ast::Assertion; + match assertion { + Assertion::Equal(expr) | Assertion::Be(expr) => { + // Type check the expected value + self.infer_expression_type(expr); + } + Assertion::GreaterThan(expr) | Assertion::LessThan(expr) => { + // Check that subject is a number + if subject_type != Type::Number + && subject_type != Type::Unknown + && subject_type != Type::Error + { + self.type_error( + "Comparison assertions require numeric types".to_string(), + Some(Type::Number), + Some(subject_type.clone()), + *_line, + *_column, + ); + } + // Type check the comparison value + let expr_type = self.infer_expression_type(expr); + if expr_type != Type::Number + && expr_type != Type::Unknown + && expr_type != Type::Error + { + self.type_error( + "Comparison value must be numeric".to_string(), + Some(Type::Number), + Some(expr_type), + *_line, + *_column, + ); + } + } + Assertion::BeYes | Assertion::BeNo => { + // Truthiness checks work on any type, no validation needed + } + Assertion::Exist => { + // Existence checks work on any type + } + Assertion::Contain(expr) => { + // Check that subject is a list or text + if !matches!( + subject_type, + Type::List(_) | Type::Text | Type::Unknown | Type::Error + ) { + self.type_error( + "contain assertion requires List or Text type".to_string(), + None, + Some(subject_type.clone()), + *_line, + *_column, + ); + } + // Type check the item expression + self.infer_expression_type(expr); + } + Assertion::BeEmpty => { + // Check that subject is a list or text + if !matches!( + subject_type, + Type::List(_) | Type::Text | Type::Unknown | Type::Error + ) { + self.type_error( + "be empty assertion requires List or Text type".to_string(), + None, + Some(subject_type.clone()), + *_line, + *_column, + ); + } + } + Assertion::HaveLength(expr) => { + // Check that subject is a list or text + if !matches!( + subject_type, + Type::List(_) | Type::Text | Type::Unknown | Type::Error + ) { + self.type_error( + "have length assertion requires List or Text type".to_string(), + None, + Some(subject_type.clone()), + *_line, + *_column, + ); + } + // Type check the length value (should be number) + let length_type = self.infer_expression_type(expr); + if length_type != Type::Number + && length_type != Type::Unknown + && length_type != Type::Error + { + self.type_error( + "Length value must be numeric".to_string(), + Some(Type::Number), + Some(length_type), + *_line, + *_column, + ); + } + } + Assertion::BeOfType(_type_name) => { + // Type name is validated at runtime + } + } } } } From f6359bd7cabd5a752ed81f0041fd86ebe17174ad Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 07:30:49 -0600 Subject: [PATCH 03/10] chore: Configure auto-format hook for Linux and apply formatting Updated Claude Code hooks configuration to use bash instead of PowerShell for automatic Rust formatting on Linux systems. Changes: - Updated .claude/settings.json to use bash .claude/hooks/format-rust.sh - Made format-rust.sh executable (chmod +x) - Updated .claude/hooks/README.md to reflect Linux configuration - Applied automatic formatting to recently edited Rust files The hook automatically runs 'cargo fmt --all' after any Edit or Write operation on .rs files, ensuring consistent code formatting throughout development. Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .claude/hooks/README.md | 6 +++--- .claude/hooks/format-rust.sh | 0 .claude/settings.json | 2 +- src/interpreter/assertion_helpers.rs | 11 +++++++++-- src/interpreter/mod.rs | 4 +++- src/parser/ast.rs | 10 +++++----- src/parser/stmt/testing.rs | 15 ++++++++++++--- 7 files changed, 33 insertions(+), 15 deletions(-) mode change 100644 => 100755 .claude/hooks/format-rust.sh diff --git a/.claude/hooks/README.md b/.claude/hooks/README.md index 37d2f38f..2fff7d17 100644 --- a/.claude/hooks/README.md +++ b/.claude/hooks/README.md @@ -13,7 +13,7 @@ Automatically runs `cargo fmt --all` after any Edit or Write operation on Rust f ### Prerequisites -The hook configured in `../.claude/settings.json` currently uses PowerShell: +The hook configured in `../.claude/settings.json` currently uses Bash: - **Windows PowerShell**: Built into Windows (default configuration) - Verify: `powershell --version` @@ -43,12 +43,12 @@ For bash (Unix/macOS/Git Bash on Windows): } ``` -Current configuration (Windows PowerShell): +Current configuration (Bash for Linux): ```json { "type": "command", - "command": "powershell -File .claude/hooks/format-rust.ps1", + "command": "bash .claude/hooks/format-rust.sh", "timeout": 120 } ``` diff --git a/.claude/hooks/format-rust.sh b/.claude/hooks/format-rust.sh old mode 100644 new mode 100755 diff --git a/.claude/settings.json b/.claude/settings.json index 86da7456..d2fe0215 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "powershell -File .claude/hooks/format-rust.ps1", + "command": "bash .claude/hooks/format-rust.sh", "timeout": 120 } ] diff --git a/src/interpreter/assertion_helpers.rs b/src/interpreter/assertion_helpers.rs index 215ac816..6f0d6e7b 100644 --- a/src/interpreter/assertion_helpers.rs +++ b/src/interpreter/assertion_helpers.rs @@ -75,7 +75,11 @@ impl Interpreter { } /// Create a helpful assertion failure message - pub(super) fn create_assertion_message(&self, assertion: &Assertion, subject: &Value) -> String { + pub(super) fn create_assertion_message( + &self, + assertion: &Assertion, + subject: &Value, + ) -> String { match assertion { Assertion::Equal(expr) | Assertion::Be(expr) => { format!("Expected value to equal {:?}, but got {:?}", expr, subject) @@ -128,7 +132,10 @@ fn values_equal(a: &Value, b: &Value) -> bool { if a_ref.len() != b_ref.len() { return false; } - a_ref.iter().zip(b_ref.iter()).all(|(x, y)| values_equal(x, y)) + a_ref + .iter() + .zip(b_ref.iter()) + .all(|(x, y)| values_equal(x, y)) } _ => false, } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index d9cb75f9..deffd8b5 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -5045,7 +5045,9 @@ impl Interpreter { let subject_value = self.evaluate_expression(subject, env.clone()).await?; // Check assertion - let passed = self.check_assertion(&subject_value, assertion, env.clone()).await?; + let passed = self + .check_assertion(&subject_value, assertion, env.clone()) + .await?; if !passed { // Record failure with proper test name tracking diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 1161b56b..7d5a9628 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -492,16 +492,16 @@ pub enum Statement { #[derive(Debug, Clone, PartialEq)] pub enum Assertion { Equal(Expression), - Be(Expression), // Synonym for Equal + Be(Expression), // Synonym for Equal GreaterThan(Expression), LessThan(Expression), - BeYes, // Truthy check - BeNo, // Falsy check + BeYes, // Truthy check + BeNo, // Falsy check Exist, - Contain(Expression), // List/text contains + Contain(Expression), // List/text contains BeEmpty, HaveLength(Expression), - BeOfType(String), // Type check + BeOfType(String), // Type check } #[derive(Debug, Clone, PartialEq)] diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs index c5c7703d..26989b42 100644 --- a/src/parser/stmt/testing.rs +++ b/src/parser/stmt/testing.rs @@ -89,7 +89,10 @@ impl<'a> TestingParser<'a> for Parser<'a> { setup_stmts.push(self.parse_statement()?); } - self.expect_token(Token::KeywordEnd, "Expected 'end' to close setup block")?; + self.expect_token( + Token::KeywordEnd, + "Expected 'end' to close setup block", + )?; self.expect_token( Token::KeywordSetup, "Expected 'setup' after 'end' in setup block", @@ -233,7 +236,10 @@ impl<'a> TestingParser<'a> for Parser<'a> { // Expect 'end test' self.expect_token(Token::KeywordEnd, "Expected 'end' to close test block")?; - self.expect_token(Token::KeywordTest, "Expected 'test' after 'end' in test block")?; + self.expect_token( + Token::KeywordTest, + "Expected 'test' after 'end' in test block", + )?; Ok(Statement::TestBlock { description, @@ -317,7 +323,10 @@ impl<'a> Parser<'a> { } Token::KeywordGreater => { self.cursor.bump(); // Consume 'greater' - self.expect_token(Token::KeywordThan, "Expected 'than' after 'greater'")?; + self.expect_token( + Token::KeywordThan, + "Expected 'than' after 'greater'", + )?; let value = self.parse_expression()?; Ok(Assertion::GreaterThan(value)) } From 02330f9d06812683dd1af0026daf711a1c3b503d Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 07:44:41 -0600 Subject: [PATCH 04/10] fix: Address PR #273 review feedback - make 'empty' a proper keyword MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses final review feedback from copilot, codex, and claude reviewers on PR #273 to improve code consistency. Changes: - Made 'empty' a structural keyword (Token::KeywordEmpty) for consistency - Updated parser to use Token::KeywordEmpty instead of identifier matching - Fixed 2 existing test files that used 'empty' as variable name: * TestPrograms/substring_perf.wfl: empty -> empty_string * TestPrograms/text_split_edge_cases.wfl: empty -> empty_str Note: All three major review issues were previously addressed in fa00c48: 1. ✅ Test isolation - tests use isolated environments 2. ✅ Test name tracking - proper names in failure reports 3. ✅ Compile-time type checking - assertions validated at parse time This commit addresses the fourth and final suggestion for consistency. Validation: - All test suites passing (30 tests across 5 files) - Backward compatibility verified with existing TestPrograms - New pr273_fixes_validation.wfl demonstrates all fixes working Co-Authored-By: Claude Sonnet 4.5 (1M context) --- TestPrograms/pr273_fixes_validation.wfl | 47 +++++++++++++++++++++++++ TestPrograms/substring_perf.wfl | 4 +-- TestPrograms/text_split_edge_cases.wfl | 4 +-- src/lexer/token.rs | 3 ++ src/parser/stmt/testing.rs | 2 +- 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 TestPrograms/pr273_fixes_validation.wfl diff --git a/TestPrograms/pr273_fixes_validation.wfl b/TestPrograms/pr273_fixes_validation.wfl new file mode 100644 index 00000000..08126a67 --- /dev/null +++ b/TestPrograms/pr273_fixes_validation.wfl @@ -0,0 +1,47 @@ +// Validates that all PR #273 review comments have been addressed + +describe "Test name tracking works correctly": + + test "first test with unique name": + store value as 1 + expect value to equal 1 // Fixed: now passes + end test + +end describe + +describe "Type checking catches assertion errors": + + test "numeric comparison requires numbers": + store num as 10 + expect num to be greater than 5 + end test + + test "length assertion on lists": + store items as [1, 2, 3] + expect items to have length 3 + end test + +end describe + +describe "Test isolation prevents pollution": + + setup: + store shared as 100 + end setup + + test "reads setup variable": + expect shared to equal 100 + end test + + test "creates independent local variable": + store local as 200 + expect local to equal 200 + end test + + test "previous test's local variable not visible": + // This should work because each test is isolated + store local as 300 + expect local to equal 300 + end test + +end describe diff --git a/TestPrograms/substring_perf.wfl b/TestPrograms/substring_perf.wfl index 501c75c6..9b8c94a0 100644 --- a/TestPrograms/substring_perf.wfl +++ b/TestPrograms/substring_perf.wfl @@ -8,8 +8,8 @@ display "Sub(0, 5): " with substring of text and 0 and 5 display "Sub(7, 5): " with substring of text and 7 and 5 display "--- Edge Cases ---" -store empty as "" -display "Empty sub(0, 1): '" with substring of empty and 0 and 1 with "'" +store empty_string as "" +display "Empty sub(0, 1): '" with substring of empty_string and 0 and 1 with "'" store short as "Hi" display "Start > len (3, 1): '" with substring of short and 3 and 1 with "'" diff --git a/TestPrograms/text_split_edge_cases.wfl b/TestPrograms/text_split_edge_cases.wfl index 86217683..ebff5287 100644 --- a/TestPrograms/text_split_edge_cases.wfl +++ b/TestPrograms/text_split_edge_cases.wfl @@ -1,8 +1,8 @@ # Test edge cases for string split functionality # Test 1: Empty string -store empty as "" -store empty_result as split empty by "," +store empty_str as "" +store empty_result as split empty_str by "," display "Empty string split count: " with length of empty_result # Test 2: String with no delimiter diff --git a/src/lexer/token.rs b/src/lexer/token.rs index ca13786e..b4d028c7 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -378,6 +378,8 @@ pub enum Token { KeywordExist, #[token("contain")] KeywordContain, + #[token("empty")] + KeywordEmpty, #[token(":")] Colon, @@ -593,6 +595,7 @@ impl Token { | Token::KeywordSetup | Token::KeywordTeardown | Token::KeywordBe + | Token::KeywordEmpty ) } diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs index 26989b42..09f2587d 100644 --- a/src/parser/stmt/testing.rs +++ b/src/parser/stmt/testing.rs @@ -336,7 +336,7 @@ impl<'a> Parser<'a> { let value = self.parse_expression()?; Ok(Assertion::LessThan(value)) } - Token::Identifier(id) if id == "empty" => { + Token::KeywordEmpty => { self.cursor.bump(); // Consume 'empty' Ok(Assertion::BeEmpty) } From e1a963fc1866367f4b37c552bd876f8d5cbeb78d Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 08:04:41 -0600 Subject: [PATCH 05/10] test: Fix test suite failures due to 'test' becoming reserved keyword Fixes failing tests in fixer and analyzer that used 'test' as an action/function name. Since 'test' is now a reserved keyword for the testing framework, these identifiers have been renamed. Changes: - src/fixer/tests.rs: Renamed 'test' -> 'my_test' in test_fix_indentation - src/analyzer/tests.rs: Renamed 'test' -> 'my_action' in 4 tests: * test_unreachable_code_detection * test_shadowing_detection * test_inconsistent_returns * test_static_analyzer_integration All 334 library tests now passing. Related: PR #273 testing framework implementation Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/analyzer/tests.rs | 4 ++-- src/fixer/tests.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analyzer/tests.rs b/src/analyzer/tests.rs index 3e047e7d..4eaa56ae 100644 --- a/src/analyzer/tests.rs +++ b/src/analyzer/tests.rs @@ -20,7 +20,7 @@ fn test_unused_variable_detection() { #[test] fn test_unreachable_code_detection() { - let input = "define action called test:\n give back 10\n display \"This is unreachable\"\nend action"; + let input = "define action called my_action:\n give back 10\n display \"This is unreachable\"\nend action"; let tokens = lex_wfl_with_positions(input); let program = Parser::new(&tokens).parse().unwrap(); @@ -34,7 +34,7 @@ fn test_unreachable_code_detection() { #[test] fn test_shadowing_detection() { - let input = "store x as 10\ndefine action called test:\n store x as 20\n display x\nend action"; + let input = "store x as 10\ndefine action called my_action:\n store x as 20\n display x\nend action"; let tokens = lex_wfl_with_positions(input); let program = Parser::new(&tokens).parse().unwrap(); diff --git a/src/fixer/tests.rs b/src/fixer/tests.rs index 73afe73b..01b4c2a3 100644 --- a/src/fixer/tests.rs +++ b/src/fixer/tests.rs @@ -17,7 +17,7 @@ fn test_fix_variable_naming() { #[test] fn test_fix_indentation() { - let input = "define action called test:\ndisplay \"Hello\"\nend action"; + let input = "define action called my_test:\ndisplay \"Hello\"\nend action"; let tokens = lex_wfl_with_positions(input); let program = Parser::new(&tokens).parse().unwrap(); From ed3e0b31abece83c9119b11971d3c89fb415748d Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 08:42:30 -0600 Subject: [PATCH 06/10] refactor: Fix clippy warnings in testing framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses all clippy warnings when running with -D warnings flag: 1. Removed unused `failure_recorded` variable in test execution - Variable was assigned but never read - Simplified error handling logic without changing behavior 2. Collapsed nested if statements for better readability - Applied let-chain pattern matching (&&) in 5 locations - Improved code clarity per clippy::collapsible_if suggestions - Files: src/parser/stmt/testing.rs (setup, teardown, test body, type assertion, length assertion) All tests passing: - cargo clippy --all-targets --all-features -- -D warnings: ✓ - cargo test --lib: 334 passed - WFL test framework: 36 tests passing No functional changes, only code quality improvements. Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/interpreter/mod.rs | 2 -- src/parser/stmt/testing.rs | 66 ++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index deffd8b5..f1caabca 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -4986,7 +4986,6 @@ impl Interpreter { // Execute test body and catch assertion failures let mut test_passed = true; - let mut failure_recorded = false; for stmt in body { match Box::pin(self._execute_statement(stmt, test_env.clone())).await { @@ -5008,7 +5007,6 @@ impl Interpreter { column: *column, }; self.test_results.borrow_mut().failures.push(failure); - failure_recorded = true; } // Don't propagate the error - continue running other tests diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs index 09f2587d..fd38e63b 100644 --- a/src/parser/stmt/testing.rs +++ b/src/parser/stmt/testing.rs @@ -81,10 +81,10 @@ impl<'a> TestingParser<'a> for Parser<'a> { let mut setup_stmts = Vec::new(); loop { self.skip_eol(); - if let Some(token) = self.cursor.peek() { - if matches!(token.token, Token::KeywordEnd) { - break; - } + if let Some(token) = self.cursor.peek() + && matches!(token.token, Token::KeywordEnd) + { + break; } setup_stmts.push(self.parse_statement()?); } @@ -108,10 +108,10 @@ impl<'a> TestingParser<'a> for Parser<'a> { let mut teardown_stmts = Vec::new(); loop { self.skip_eol(); - if let Some(token) = self.cursor.peek() { - if matches!(token.token, Token::KeywordEnd) { - break; - } + if let Some(token) = self.cursor.peek() + && matches!(token.token, Token::KeywordEnd) + { + break; } teardown_stmts.push(self.parse_statement()?); } @@ -225,10 +225,10 @@ impl<'a> TestingParser<'a> for Parser<'a> { loop { self.skip_eol(); - if let Some(token) = self.cursor.peek() { - if matches!(token.token, Token::KeywordEnd) { - break; - } + if let Some(token) = self.cursor.peek() + && matches!(token.token, Token::KeywordEnd) + { + break; } body.push(self.parse_statement()?); @@ -344,20 +344,19 @@ impl<'a> Parser<'a> { self.cursor.bump(); // Consume 'of' // Check if next token is "type" identifier - if let Some(token) = self.cursor.peek() { - if let Token::Identifier(id) = &token.token { - if id == "type" { - self.cursor.bump(); // Consume 'type' - - // Expect a string literal for the type name - if let Some(token) = self.cursor.peek() { - if let Token::StringLiteral(type_name) = &token.token { - let tn = type_name.clone(); - self.cursor.bump(); - return Ok(Assertion::BeOfType(tn)); - } - } - } + if let Some(token) = self.cursor.peek() + && let Token::Identifier(id) = &token.token + && id == "type" + { + self.cursor.bump(); // Consume 'type' + + // Expect a string literal for the type name + if let Some(token) = self.cursor.peek() + && let Token::StringLiteral(type_name) = &token.token + { + let tn = type_name.clone(); + self.cursor.bump(); + return Ok(Assertion::BeOfType(tn)); } } @@ -408,14 +407,13 @@ impl<'a> Parser<'a> { self.cursor.bump(); // Consume 'have' // Check if next token is "length" identifier - if let Some(token) = self.cursor.peek() { - if let Token::Identifier(id) = &token.token { - if id == "length" { - self.cursor.bump(); // Consume 'length' - let value = self.parse_expression()?; - return Ok(Assertion::HaveLength(value)); - } - } + if let Some(token) = self.cursor.peek() + && let Token::Identifier(id) = &token.token + && id == "length" + { + self.cursor.bump(); // Consume 'length' + let value = self.parse_expression()?; + return Ok(Assertion::HaveLength(value)); } Err(ParseError::from_span( From 5d589dedee1b61d79f67f26f9ff9a89b4f7a12c0 Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 09:14:22 -0600 Subject: [PATCH 07/10] fix: Improve testing framework parser error messages Enhanced error handling in the testing parser to provide clearer, more actionable error messages for common mistakes. Changes: 1. Prevent duplicate setup/teardown blocks - Parser now detects and rejects duplicate setup blocks - Parser now detects and rejects duplicate teardown blocks - Error: "Duplicate 'setup' block found. Only one setup block is allowed per describe block" - Previously silently overwrote first block, now fails fast with clear error at parse time 2. Improved 'be of type' assertion error messages - Separated error cases for better diagnostics - When "type" is present but type name is missing: Error: "Expected type name as string literal after 'be of type'" - When "type" keyword itself is missing: Error: "Expected 'type' after 'of'" - Previously gave generic error regardless of which part was missing Benefits: - Catches configuration errors earlier (at parse time vs runtime) - Provides specific, actionable error messages - Helps developers fix issues faster with clearer feedback Testing: - All 334 Rust tests passing - All 36 WFL test framework tests passing - clippy --all-targets --all-features -- -D warnings: clean Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/parser/stmt/testing.rs | 44 ++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs index fd38e63b..6e009955 100644 --- a/src/parser/stmt/testing.rs +++ b/src/parser/stmt/testing.rs @@ -74,7 +74,17 @@ impl<'a> TestingParser<'a> for Parser<'a> { Some(t) => match &t.token { Token::KeywordSetup => { // Parse setup block + let setup_token = t.clone(); self.cursor.bump(); // Consume 'setup' + + // Check for duplicate setup block + if setup.is_some() { + return Err(ParseError::from_token( + "Duplicate 'setup' block found. Only one setup block is allowed per describe block".to_string(), + &setup_token, + )); + } + self.expect_token(Token::Colon, "Expected ':' after 'setup'")?; self.skip_eol(); @@ -101,7 +111,17 @@ impl<'a> TestingParser<'a> for Parser<'a> { } Token::KeywordTeardown => { // Parse teardown block + let teardown_token = t.clone(); self.cursor.bump(); // Consume 'teardown' + + // Check for duplicate teardown block + if teardown.is_some() { + return Err(ParseError::from_token( + "Duplicate 'teardown' block found. Only one teardown block is allowed per describe block".to_string(), + &teardown_token, + )); + } + self.expect_token(Token::Colon, "Expected ':' after 'teardown'")?; self.skip_eol(); @@ -351,12 +371,24 @@ impl<'a> Parser<'a> { self.cursor.bump(); // Consume 'type' // Expect a string literal for the type name - if let Some(token) = self.cursor.peek() - && let Token::StringLiteral(type_name) = &token.token - { - let tn = type_name.clone(); - self.cursor.bump(); - return Ok(Assertion::BeOfType(tn)); + if let Some(token) = self.cursor.peek() { + if let Token::StringLiteral(type_name) = &token.token { + let tn = type_name.clone(); + self.cursor.bump(); + return Ok(Assertion::BeOfType(tn)); + } else { + return Err(ParseError::from_token( + "Expected type name as string literal after 'be of type'".to_string(), + token, + )); + } + } else { + return Err(ParseError::from_span( + "Expected type name as string literal after 'be of type'".to_string(), + self.cursor.current_span(), + self.cursor.current_line(), + 1, + )); } } From 70c967e1fd0297d89dceb1d52e38e30e2672924a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:35:47 +0000 Subject: [PATCH 08/10] feat: Improve assertion error messages with actual values - Replace create_assertion_message with create_assertion_message_with_values - Show actual evaluated values instead of AST debug output - Improve message readability (5 instead of NumberLiteral(5)) - Use character count for text length instead of byte count - Fix Value::Nothing handling (falsy, fails existence checks, equality) - Better error messages with actual vs expected values Co-authored-by: logbie --- src/interpreter/assertion_helpers.rs | 227 +++++++++++++++++++++------ src/interpreter/mod.rs | 8 +- src/parser/stmt/testing.rs | 3 +- test_assertion_fix.wfl | 16 ++ 4 files changed, 205 insertions(+), 49 deletions(-) create mode 100644 test_assertion_fix.wfl diff --git a/src/interpreter/assertion_helpers.rs b/src/interpreter/assertion_helpers.rs index 6f0d6e7b..1223f34e 100644 --- a/src/interpreter/assertion_helpers.rs +++ b/src/interpreter/assertion_helpers.rs @@ -3,110 +3,213 @@ use super::*; impl Interpreter { - /// Check if an assertion passes + /// Check if an assertion passes and return the expected value for error messages pub(super) async fn check_assertion( &self, subject: &Value, assertion: &Assertion, env: Rc>, - ) -> Result { + ) -> Result<(bool, Option), RuntimeError> { match assertion { Assertion::Equal(expected_expr) | Assertion::Be(expected_expr) => { let expected = self.evaluate_expression(expected_expr, env).await?; - Ok(values_equal(subject, &expected)) + Ok((values_equal(subject, &expected), Some(expected))) } Assertion::GreaterThan(expected_expr) => { let expected = self.evaluate_expression(expected_expr, env).await?; - match (subject, &expected) { - (Value::Number(a), Value::Number(b)) => Ok(a > b), - _ => Ok(false), - } + let result = match (subject, &expected) { + (Value::Number(a), Value::Number(b)) => a > b, + _ => false, + }; + Ok((result, Some(expected))) } Assertion::LessThan(expected_expr) => { let expected = self.evaluate_expression(expected_expr, env).await?; - match (subject, &expected) { - (Value::Number(a), Value::Number(b)) => Ok(a < b), - _ => Ok(false), - } + let result = match (subject, &expected) { + (Value::Number(a), Value::Number(b)) => a < b, + _ => false, + }; + Ok((result, Some(expected))) } - Assertion::BeYes => Ok(is_truthy(subject)), - Assertion::BeNo => Ok(!is_truthy(subject)), - Assertion::Exist => Ok(!matches!(subject, Value::Null)), + Assertion::BeYes => Ok((is_truthy(subject), None)), + Assertion::BeNo => Ok((!is_truthy(subject), None)), + Assertion::Exist => Ok((!matches!(subject, Value::Null | Value::Nothing), None)), Assertion::Contain(item_expr) => { let item = self.evaluate_expression(item_expr, env).await?; - match subject { + let result = match subject { Value::List(list) => { let list_ref = list.borrow(); - Ok(list_ref.iter().any(|v| values_equal(v, &item))) + list_ref.iter().any(|v| values_equal(v, &item)) } Value::Text(text) => { if let Value::Text(search) = &item { - Ok(text.contains(search.as_ref())) + text.contains(search.as_ref()) } else { - Ok(false) + false } } - _ => Ok(false), - } + _ => false, + }; + Ok((result, Some(item))) + } + Assertion::BeEmpty => { + let result = match subject { + Value::List(list) => list.borrow().is_empty(), + Value::Text(text) => text.is_empty(), + _ => false, + }; + Ok((result, None)) } - Assertion::BeEmpty => match subject { - Value::List(list) => Ok(list.borrow().is_empty()), - Value::Text(text) => Ok(text.is_empty()), - _ => Ok(false), - }, Assertion::HaveLength(expected_expr) => { let expected = self.evaluate_expression(expected_expr, env).await?; if let Value::Number(expected_len) = expected { let actual_len = match subject { Value::List(list) => list.borrow().len() as f64, - Value::Text(text) => text.len() as f64, - _ => return Ok(false), + Value::Text(text) => text.chars().count() as f64, // Use character count for text length + _ => return Ok((false, Some(Value::Number(expected_len)))), }; - Ok((actual_len - expected_len).abs() < f64::EPSILON) + Ok(( + (actual_len - expected_len).abs() < f64::EPSILON, + Some(Value::Number(expected_len)), + )) } else { - Ok(false) + Ok((false, Some(expected))) } } Assertion::BeOfType(type_name) => { let actual_type = subject.type_name(); - Ok(actual_type.eq_ignore_ascii_case(type_name)) + Ok((actual_type.eq_ignore_ascii_case(type_name), None)) } } } - /// Create a helpful assertion failure message - pub(super) fn create_assertion_message( + /// Create a helpful assertion failure message with actual values + pub(super) fn create_assertion_message_with_values( &self, assertion: &Assertion, subject: &Value, + expected_value: Option<&Value>, ) -> String { match assertion { - Assertion::Equal(expr) | Assertion::Be(expr) => { - format!("Expected value to equal {:?}, but got {:?}", expr, subject) + Assertion::Equal(_) | Assertion::Be(_) => { + if let Some(expected) = expected_value { + format!( + "Expected {} to equal {}", + self.format_value_for_message(subject), + self.format_value_for_message(expected) + ) + } else { + format!( + "Expected value to equal expected value, but got {}", + self.format_value_for_message(subject) + ) + } } - Assertion::GreaterThan(expr) => { - format!("Expected {:?} to be greater than {:?}", subject, expr) + Assertion::GreaterThan(_) => { + if let Some(expected) = expected_value { + format!( + "Expected {} to be greater than {}, but it was not", + self.format_value_for_message(subject), + self.format_value_for_message(expected) + ) + } else { + format!( + "Expected {} to be greater than expected value", + self.format_value_for_message(subject) + ) + } } - Assertion::LessThan(expr) => { - format!("Expected {:?} to be less than {:?}", subject, expr) + Assertion::LessThan(_) => { + if let Some(expected) = expected_value { + format!( + "Expected {} to be less than {}, but it was not", + self.format_value_for_message(subject), + self.format_value_for_message(expected) + ) + } else { + format!( + "Expected {} to be less than expected value", + self.format_value_for_message(subject) + ) + } } Assertion::BeYes => { - format!("Expected {:?} to be truthy", subject) + format!( + "Expected {} to be truthy, but it was falsy", + self.format_value_for_message(subject) + ) } Assertion::BeNo => { - format!("Expected {:?} to be falsy", subject) + format!( + "Expected {} to be falsy, but it was truthy", + self.format_value_for_message(subject) + ) } Assertion::Exist => { - format!("Expected value to exist, but got {:?}", subject) + format!( + "Expected value to exist, but got {}", + self.format_value_for_message(subject) + ) } - Assertion::Contain(expr) => { - format!("Expected {:?} to contain {:?}", subject, expr) + Assertion::Contain(_) => { + if let Some(item) = expected_value { + format!( + "Expected {} to contain {}, but it did not", + self.format_value_for_message(subject), + self.format_value_for_message(item) + ) + } else { + format!( + "Expected {} to contain expected item", + self.format_value_for_message(subject) + ) + } } Assertion::BeEmpty => { - format!("Expected {:?} to be empty", subject) + let actual_length = match subject { + Value::Text(s) => Some(s.chars().count()), + Value::List(list) => Some(list.borrow().len()), + _ => None, + }; + match actual_length { + Some(len) => format!( + "Expected {} to be empty, but it has {} item{}", + self.format_value_for_message(subject), + len, + if len == 1 { "" } else { "s" } + ), + None => format!( + "Expected {} to be empty, but it is not applicable for this type", + self.format_value_for_message(subject) + ), + } } - Assertion::HaveLength(expr) => { - format!("Expected {:?} to have length {:?}", subject, expr) + Assertion::HaveLength(_) => { + if let Some(Value::Number(expected_len)) = expected_value { + let actual_length = match subject { + Value::Text(s) => Some(s.chars().count()), + Value::List(list) => Some(list.borrow().len()), + _ => None, + }; + match actual_length { + Some(len) => format!( + "Expected {} to have length {}, but its length is {}", + self.format_value_for_message(subject), + *expected_len as usize, + len + ), + None => format!( + "Expected {} to have length {}, but length is not applicable for this type", + self.format_value_for_message(subject), + *expected_len as usize + ), + } + } else { + format!( + "Expected {} to have expected length", + self.format_value_for_message(subject) + ) + } } Assertion::BeOfType(type_name) => { format!( @@ -117,6 +220,36 @@ impl Interpreter { } } } + + /// Format a value for display in error messages + fn format_value_for_message(&self, value: &Value) -> String { + match value { + Value::Number(n) => { + if n.fract() == 0.0 { + format!("{}", *n as i64) + } else { + format!("{}", n) + } + } + Value::Text(s) => format!("\"{}\"", s), + Value::Bool(b) => format!("{}", if *b { "yes" } else { "no" }), + Value::List(list) => { + let items = list.borrow(); + if items.is_empty() { + "empty list".to_string() + } else { + format!( + "list with {} item{}", + items.len(), + if items.len() == 1 { "" } else { "s" } + ) + } + } + Value::Null => "null".to_string(), + Value::Nothing => "nothing".to_string(), + _ => format!("{}", value.type_name()), + } + } } /// Helper function to check if two values are equal @@ -126,6 +259,7 @@ fn values_equal(a: &Value, b: &Value) -> bool { (Value::Text(a), Value::Text(b)) => a == b, (Value::Bool(a), Value::Bool(b)) => a == b, (Value::Null, Value::Null) => true, + (Value::Nothing, Value::Nothing) => true, (Value::List(a), Value::List(b)) => { let a_ref = a.borrow(); let b_ref = b.borrow(); @@ -146,6 +280,7 @@ fn is_truthy(value: &Value) -> bool { match value { Value::Bool(b) => *b, Value::Null => false, + Value::Nothing => false, Value::Number(n) => *n != 0.0, Value::Text(s) => !s.is_empty(), Value::List(l) => !l.borrow().is_empty(), diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index f1caabca..00d1499e 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -5043,13 +5043,17 @@ impl Interpreter { let subject_value = self.evaluate_expression(subject, env.clone()).await?; // Check assertion - let passed = self + let (passed, expected_value) = self .check_assertion(&subject_value, assertion, env.clone()) .await?; if !passed { // Record failure with proper test name tracking - let message = self.create_assertion_message(assertion, &subject_value); + let message = self.create_assertion_message_with_values( + assertion, + &subject_value, + expected_value.as_ref(), + ); let context = self.current_describe_stack.borrow().clone(); let test_name = self .current_test_name diff --git a/src/parser/stmt/testing.rs b/src/parser/stmt/testing.rs index 6e009955..f17d3ef6 100644 --- a/src/parser/stmt/testing.rs +++ b/src/parser/stmt/testing.rs @@ -384,7 +384,8 @@ impl<'a> Parser<'a> { } } else { return Err(ParseError::from_span( - "Expected type name as string literal after 'be of type'".to_string(), + "Expected type name as string literal after 'be of type'" + .to_string(), self.cursor.current_span(), self.cursor.current_line(), 1, diff --git a/test_assertion_fix.wfl b/test_assertion_fix.wfl new file mode 100644 index 00000000..f9165f28 --- /dev/null +++ b/test_assertion_fix.wfl @@ -0,0 +1,16 @@ +describe "test assertion message improvements": + test "simple equal assertion that should fail": + store x as 5 + expect x to equal 10 + + test "greater than assertion that should fail": + store y as 3 + expect y to be greater than 7 + + test "list contain assertion that should fail": + store mylist as [1, 2, 3] + expect mylist to contain 5 + + test "length assertion that should fail": + store text as "hello" + expect text to have length 10 \ No newline at end of file From 1cb1a23c8624907856d310ac64023b4b972dfb23 Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 10:12:48 -0600 Subject: [PATCH 09/10] refactor: Move test_assertion_fix.wfl to TestPrograms directory Moves test_assertion_fix.wfl from repository root to TestPrograms/ directory to comply with WFL end-to-end program organization standards. Changes: - Moved test_assertion_fix.wfl -> TestPrograms/test_assertion_fix.wfl - Added missing 'end test' statements to close each test block - Added missing 'end describe' statement to close describe block - Preserved all test contents and variable names (including 'text' variable which is valid outside keyword contexts) The file now follows the proper structure for WFL test programs and is located in the correct directory alongside other test programs. Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../test_assertion_fix.wfl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename test_assertion_fix.wfl => TestPrograms/test_assertion_fix.wfl (81%) diff --git a/test_assertion_fix.wfl b/TestPrograms/test_assertion_fix.wfl similarity index 81% rename from test_assertion_fix.wfl rename to TestPrograms/test_assertion_fix.wfl index f9165f28..7f91b99b 100644 --- a/test_assertion_fix.wfl +++ b/TestPrograms/test_assertion_fix.wfl @@ -2,15 +2,21 @@ describe "test assertion message improvements": test "simple equal assertion that should fail": store x as 5 expect x to equal 10 + end test test "greater than assertion that should fail": store y as 3 expect y to be greater than 7 + end test test "list contain assertion that should fail": store mylist as [1, 2, 3] expect mylist to contain 5 + end test test "length assertion that should fail": store text as "hello" - expect text to have length 10 \ No newline at end of file + expect text to have length 10 + end test + +end describe \ No newline at end of file From a6f713876f6587e44db02617d94e7f393a03ffcc Mon Sep 17 00:00:00 2001 From: brad Date: Sat, 17 Jan 2026 10:27:27 -0600 Subject: [PATCH 10/10] refactor: Fix clippy useless_format warnings in assertion helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses clippy warnings about unnecessary use of format! macro where .to_string() is more appropriate. Changes: - Line 235: format!("{}", if *b { "yes" } else { "no" }) -> (if *b { "yes" } else { "no" }).to_string() - Line 250: format!("{}", value.type_name()) -> value.type_name().to_string() Benefits: - More idiomatic Rust code - Slightly better performance (avoids format machinery) - Clearer intent (direct string conversion) All tests passing: - cargo clippy --all-targets --all-features -- -D warnings: ✓ - cargo test --lib: 334/334 passing Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/interpreter/assertion_helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interpreter/assertion_helpers.rs b/src/interpreter/assertion_helpers.rs index 1223f34e..4bfc22c4 100644 --- a/src/interpreter/assertion_helpers.rs +++ b/src/interpreter/assertion_helpers.rs @@ -232,7 +232,7 @@ impl Interpreter { } } Value::Text(s) => format!("\"{}\"", s), - Value::Bool(b) => format!("{}", if *b { "yes" } else { "no" }), + Value::Bool(b) => (if *b { "yes" } else { "no" }).to_string(), Value::List(list) => { let items = list.borrow(); if items.is_empty() { @@ -247,7 +247,7 @@ impl Interpreter { } Value::Null => "null".to_string(), Value::Nothing => "nothing".to_string(), - _ => format!("{}", value.type_name()), + _ => value.type_name().to_string(), } } }