diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f0692826..af61f0fa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -31,7 +31,8 @@ "Bash(rustc:*)", "Bash(gh release view:*)", "Bash(git rev-parse:*)", - "Bash(cargo run:*)" + "Bash(cargo run:*)", + "Bash(./target/release/wfl.exe:*)" ], "deny": [], "ask": [] diff --git a/Nexus/nexus.wfl b/Nexus/nexus.wfl index dd7a4db5..4427ce07 100644 --- a/Nexus/nexus.wfl +++ b/Nexus/nexus.wfl @@ -182,8 +182,8 @@ store count2 as 0 store total_odds as 0 repeat while count2 is less than 5: change count2 to count2 plus 1 - // Skip even numbers - check if ((count2 divided by 2) times 2) is equal to count2: + // Skip even numbers (using modulo operator) + check if (count2 % 2) is equal to 0: skip // (continue to next iteration) end check change total_odds to total_odds plus count2 @@ -252,16 +252,16 @@ repeat while outer_j is less than or equal to 3: store inner_j as 1 repeat while inner_j is less than or equal to 3: check if inner_j is equal to 2: - exit loop // exit the outer loop entirely + exit loop // exit ALL enclosing loops (both inner and outer) end check change inner_j to inner_j plus 1 end repeat - // Only increment outer counter if loop wasn't exited + // This code should NOT be reached because 'exit loop' exits all loops change exit_outer_counter to exit_outer_counter plus 1 change outer_j to outer_j plus 1 end repeat -// 'exit loop' should break out of the outer loop on the first iteration when inner_j == 2 -check if exit_outer_counter is equal to 1: +// 'exit loop' breaks out of ALL loops, so exit_outer_counter should remain 0 +check if exit_outer_counter is equal to 0: log_message with "Nested loop 'exit' test: PASS" otherwise: log_message with "Nested loop 'exit' test: FAIL (outer iterations = " with exit_outer_counter with ")" @@ -305,10 +305,8 @@ end action // 5.5 Action that triggers an error (for error handling test) define action called faulty: // This action will cause a division by zero error - store u as 1 - store v as 0 - store w as u divided by v // runtime error (division by zero) - give back w + // Directly perform division by zero to trigger error + give back 1 divided by 0 end action // Call actions and verify results @@ -355,44 +353,74 @@ end try log_message with "Action/Function Tests completed." /////////////////////////////////////////////////////////////////////////// -// 6. Pattern Matching Tests (COMMENTED OUT - NOT YET IMPLEMENTED) -/////////////////////////////////////////////////////////////////////////// -// TODO: Pattern matching with natural language syntax is not yet implemented. -// The syntax `pattern "3 digits"` needs to be implemented, or this section -// should be rewritten using the working `create pattern` syntax from -// patterns_working_comprehensive.wfl -// -// Working syntax example: -// create pattern pat: -// exactly 3 digit -// end pattern -// check if text matches pat: -// ... -// end check +// 6. Pattern Matching Tests /////////////////////////////////////////////////////////////////////////// +log_message with "Starting Pattern Matching Tests..." + +// Create a pattern to match exactly three digits +create pattern three_digits: + exactly 3 digit +end pattern + +// Test a string that contains three digits in a row +store text1 as "abc123xyz" +check if text1 matches three_digits: + log_message with "Pattern test (\"abc123xyz\" matches 3 digits): PASS" +otherwise: + log_message with "Pattern test (\"abc123xyz\" should match 3 digits): FAIL" +end check + +// Test a string that does not contain three consecutive digits +store text2 as "abc45xyz" +check if text2 matches three_digits: + log_message with "Pattern test (\"abc45xyz\" should NOT match 3 digits): FAIL" +otherwise: + log_message with "Pattern test (\"abc45xyz\" no 3-digit match): PASS" +end check + +// Create a pattern for simple word matching +create pattern word_pattern: + one or more letter +end pattern + +// Test valid word +store word1 as "hello" +check if word1 matches word_pattern: + log_message with "Pattern test (word matching): PASS" +otherwise: + log_message with "Pattern test (word matching): FAIL" +end check + +// Test non-word (should fail) +store word2 as "123" +check if word2 matches word_pattern: + log_message with "Pattern test (non-word should fail): FAIL" +otherwise: + log_message with "Pattern test (non-word correctly rejected): PASS" +end check + +// Create a pattern with quantifiers +create pattern between_pattern: + 2 to 5 digit +end pattern + +// Test number with 3 digits (should pass) +store num1 as "123" +check if num1 matches between_pattern: + log_message with "Pattern test (2-5 digits, testing 3): PASS" +otherwise: + log_message with "Pattern test (2-5 digits, testing 3): FAIL" +end check + +// Test number with 1 digit (should fail) +store num2 as "1" +check if num2 matches between_pattern: + log_message with "Pattern test (1 digit should fail 2-5 range): FAIL" +otherwise: + log_message with "Pattern test (1 digit correctly rejected): PASS" +end check -// log_message with "Starting Pattern Matching Tests..." -// -// // Create a pattern to match three digits -// store pat as pattern "3 digits" -// -// // Test a string that contains three digits in a row -// store text1 as "abc123xyz" -// check if text1 contains pat: -// log_message with "Pattern test (\"abc123xyz\" contains 3 digits): PASS" -// otherwise: -// log_message with "Pattern test (\"abc123xyz\" should contain 3 digits): FAIL" -// end check -// -// // Test a string that does not contain three consecutive digits -// store text2 as "abc45xyz" -// check if text2 contains pat: -// log_message with "Pattern test (\"abc45xyz\" should NOT contain 3 digits): FAIL" -// otherwise: -// log_message with "Pattern test (\"abc45xyz\" no 3-digit sequence): PASS" -// end check -// -// log_message with "Pattern Matching Tests completed." +log_message with "Pattern Matching Tests completed." /////////////////////////////////////////////////////////////////////////// // 6. Asynchronous I/O and Concurrency Tests (formerly section 7) @@ -425,7 +453,7 @@ count from 1 to 100: end count // Verify that both file contents were read correctly -check if content1 is equal to "FileOneContent" and content2 is equal to "FileTwoContent": +check if (content1 is equal to "FileOneContent") and (content2 is equal to "FileTwoContent"): log_message with "Concurrent file read test: PASS (content1 & content2 OK)" otherwise: log_message with "Concurrent file read test: FAIL (content1=" with content1 with ", content2=" with content2 with ")" @@ -437,12 +465,13 @@ log_message with "Async I/O and Concurrency Tests completed." // End of tests: Finalize /////////////////////////////////////////////////////////////////////////// -// TODO: Clean up temporary files (file deletion not yet implemented) -// delete file at "temp1.txt" -// delete file at "temp2.txt" +// Clean up temporary files created during async I/O tests +delete file at "temp1.txt" +delete file at "temp2.txt" // Final log message before closing log_message with "All tests completed." +log_message with "Temporary files cleaned up." // Close the log file close file logHandle diff --git a/scripts/run_integration_tests.ps1 b/scripts/run_integration_tests.ps1 index 7980efdf..64eec9c4 100644 --- a/scripts/run_integration_tests.ps1 +++ b/scripts/run_integration_tests.ps1 @@ -103,6 +103,17 @@ Write-Host "[SUCCESS] All integration tests passed" -ForegroundColor Green # Run WFL test programs Write-Host "[INFO] Running WFL test programs..." -ForegroundColor Blue +# Tests that require special handling (web servers, interactive tests) +# These are tested separately with dedicated scripts +$SkipTests = @( + "simple_web_test.wfl", # Web server - needs HTTP client + "web_server_test.wfl", # Web server - needs HTTP client + "websocket_test.wfl" # WebSocket - needs WS client +) + +# Timeout for each test (seconds) +$TestTimeout = 30 + if (-not (Test-Path "TestPrograms")) { Write-Host "[WARNING] TestPrograms directory not found, skipping WFL program tests" -ForegroundColor Yellow } else { @@ -111,19 +122,39 @@ if (-not (Test-Path "TestPrograms")) { Write-Host "[WARNING] No WFL test programs found in TestPrograms/" -ForegroundColor Yellow } else { Write-Host "[INFO] Found $($wflFiles.Count) WFL test programs" -ForegroundColor Blue - + $failedPrograms = 0 + $skippedPrograms = 0 foreach ($wflFile in $wflFiles) { + # Check if this test should be skipped + if ($SkipTests -contains $wflFile.Name) { + Write-Host "[SKIP] $($wflFile.Name) (requires special handling)" -ForegroundColor Yellow + $skippedPrograms++ + continue + } + Write-Host "[INFO] Testing: $($wflFile.Name)" -ForegroundColor Blue - $null = & ".\$BinaryPath" $wflFile.FullName 2>&1 - if ($LASTEXITCODE -eq 0) { + + # Run with timeout to prevent hangs + $process = Start-Process -FilePath ".\$BinaryPath" -ArgumentList $wflFile.FullName -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL" + $completed = $process.WaitForExit($TestTimeout * 1000) + + if (-not $completed) { + # Test timed out + $process.Kill() + Write-Host "[ERROR] TIMEOUT $($wflFile.Name) (exceeded ${TestTimeout}s)" -ForegroundColor Red + $failedPrograms++ + } elseif ($process.ExitCode -eq 0) { Write-Host "[SUCCESS] PASS $($wflFile.Name)" -ForegroundColor Green } else { - Write-Host "[ERROR] FAIL $($wflFile.Name)" -ForegroundColor Red + Write-Host "[ERROR] FAIL $($wflFile.Name) (exit code: $($process.ExitCode))" -ForegroundColor Red $failedPrograms++ } } - + + Write-Host "" + Write-Host "[INFO] Results: $($wflFiles.Count - $skippedPrograms - $failedPrograms) passed, $failedPrograms failed, $skippedPrograms skipped" -ForegroundColor Blue + if ($failedPrograms -eq 0) { Write-Host "[SUCCESS] All WFL test programs passed" -ForegroundColor Green } else { diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh index 75c8bd12..4b199cbe 100644 --- a/scripts/run_integration_tests.sh +++ b/scripts/run_integration_tests.sh @@ -77,46 +77,91 @@ run_integration_tests() { return 0 } +# Tests that require special handling (web servers, interactive tests) +# These are tested separately with dedicated scripts +SKIP_TESTS=( + "simple_web_test.wfl" # Web server - needs HTTP client + "web_server_test.wfl" # Web server - needs HTTP client + "websocket_test.wfl" # WebSocket - needs WS client +) + +# Timeout for each test (seconds) +TEST_TIMEOUT=30 + +# Function to check if a test should be skipped +should_skip() { + local test_name="$1" + for skip in "${SKIP_TESTS[@]}"; do + if [ "$test_name" == "$skip" ]; then + return 0 + fi + done + return 1 +} + # Function to run TestPrograms run_test_programs() { print_status "Running WFL test programs..." - + # Determine binary path based on OS if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then WFL_BINARY="target/release/wfl.exe" else WFL_BINARY="target/release/wfl" fi - + # Check if TestPrograms directory exists if [ ! -d "TestPrograms" ]; then print_warning "TestPrograms directory not found, skipping WFL program tests" return 0 fi - + # Count WFL files - wfl_files=$(find TestPrograms -name "*.wfl" 2>/dev/null | wc -l) + wfl_files=$(find TestPrograms -maxdepth 1 -name "*.wfl" 2>/dev/null | wc -l) if [ "$wfl_files" -eq 0 ]; then print_warning "No WFL test programs found in TestPrograms/" return 0 fi - + print_status "Found $wfl_files WFL test programs" - + # Run each WFL program failed_programs=0 + skipped_programs=0 + passed_programs=0 + for wfl_file in TestPrograms/*.wfl; do if [ -f "$wfl_file" ]; then - print_status "Testing: $wfl_file" - if "./$WFL_BINARY" "$wfl_file" > /dev/null 2>&1; then - print_success "✓ $wfl_file" + test_name=$(basename "$wfl_file") + + # Check if this test should be skipped + if should_skip "$test_name"; then + print_warning "[SKIP] $test_name (requires special handling)" + ((skipped_programs++)) + continue + fi + + print_status "Testing: $test_name" + + # Run with timeout to prevent hangs + if timeout "${TEST_TIMEOUT}s" "./$WFL_BINARY" "$wfl_file" > /dev/null 2>&1; then + print_success "PASS $test_name" + ((passed_programs++)) else - print_error "✗ $wfl_file" + exit_code=$? + if [ $exit_code -eq 124 ]; then + print_error "TIMEOUT $test_name (exceeded ${TEST_TIMEOUT}s)" + else + print_error "FAIL $test_name (exit code: $exit_code)" + fi ((failed_programs++)) fi fi done - + + echo "" + print_status "Results: $passed_programs passed, $failed_programs failed, $skipped_programs skipped" + if [ "$failed_programs" -eq 0 ]; then print_success "All WFL test programs passed" return 0 diff --git a/scripts/run_web_tests.ps1 b/scripts/run_web_tests.ps1 new file mode 100644 index 00000000..48b75e54 --- /dev/null +++ b/scripts/run_web_tests.ps1 @@ -0,0 +1,139 @@ +# WFL Web Server Integration Test Runner (PowerShell) +# Tests WFL web server functionality by starting servers and sending HTTP requests + +param( + [switch]$Help, + [int]$Timeout = 10 +) + +if ($Help) { + Write-Host "WFL Web Server Test Runner" -ForegroundColor Cyan + Write-Host "" + Write-Host "Usage: .\run_web_tests.ps1 [options]" -ForegroundColor White + Write-Host "" + Write-Host "Options:" -ForegroundColor White + Write-Host " -Help Show this help message" -ForegroundColor Gray + Write-Host " -Timeout Timeout for each test (default: 10)" -ForegroundColor Gray + Write-Host "" + Write-Host "This script tests WFL web server functionality by:" -ForegroundColor Gray + Write-Host " 1. Starting the WFL web server in background" -ForegroundColor Gray + Write-Host " 2. Sending HTTP requests to verify it works" -ForegroundColor Gray + Write-Host " 3. Checking responses and cleaning up" -ForegroundColor Gray + exit 0 +} + +Write-Host "[INFO] WFL Web Server Test Runner" -ForegroundColor Blue +Write-Host "[INFO] ============================" -ForegroundColor Blue + +# Check if we're in the right directory +if (-not (Test-Path "Cargo.toml")) { + Write-Host "[ERROR] Cargo.toml not found. Please run this script from the WFL project root." -ForegroundColor Red + exit 1 +} + +$BinaryPath = "target\release\wfl.exe" + +# Check if release binary exists +if (-not (Test-Path $BinaryPath)) { + Write-Host "[ERROR] Release binary not found. Run 'cargo build --release' first." -ForegroundColor Red + exit 1 +} +Write-Host "[SUCCESS] Binary found: $BinaryPath" -ForegroundColor Green + +# Function to test a web server +function Test-WflWebServer { + param( + [string]$TestFile, + [int]$Port, + [string]$ExpectedResponse, + [int]$TimeoutSeconds + ) + + $testName = Split-Path $TestFile -Leaf + Write-Host "" + Write-Host "[INFO] Testing: $testName on port $Port" -ForegroundColor Blue + + # Start the WFL server in background + $serverProcess = Start-Process -FilePath ".\$BinaryPath" -ArgumentList $TestFile -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL" + + try { + # Wait for server to start (with retries) + $serverReady = $false + $retries = 0 + $maxRetries = $TimeoutSeconds * 2 # Check every 500ms + + while (-not $serverReady -and $retries -lt $maxRetries) { + Start-Sleep -Milliseconds 500 + $retries++ + + # Try to connect + try { + $response = Invoke-WebRequest -Uri "http://localhost:$Port/" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop + $serverReady = $true + } catch { + # Server not ready yet, continue waiting + } + } + + if (-not $serverReady) { + Write-Host "[ERROR] TIMEOUT: Server did not start within ${TimeoutSeconds}s" -ForegroundColor Red + return $false + } + + # Server is ready, check response + if ($response.Content -like "*$ExpectedResponse*") { + Write-Host "[SUCCESS] PASS: Got expected response" -ForegroundColor Green + return $true + } else { + Write-Host "[ERROR] FAIL: Unexpected response" -ForegroundColor Red + Write-Host " Expected: $ExpectedResponse" -ForegroundColor Gray + Write-Host " Got: $($response.Content)" -ForegroundColor Gray + return $false + } + } finally { + # Clean up - kill the server + if (-not $serverProcess.HasExited) { + $serverProcess.Kill() + Write-Host "[INFO] Server process terminated" -ForegroundColor Gray + } + } +} + +# Run web server tests +$totalTests = 0 +$passedTests = 0 + +# Test 1: simple_web_test.wfl +if (Test-Path "TestPrograms\simple_web_test.wfl") { + $totalTests++ + $result = Test-WflWebServer -TestFile "TestPrograms\simple_web_test.wfl" -Port 8095 -ExpectedResponse "Hello from WFL" -TimeoutSeconds $Timeout + if ($result) { $passedTests++ } +} + +# Test 2: web_server_test.wfl (if exists) +if (Test-Path "TestPrograms\web_server_test.wfl") { + $totalTests++ + # Read the file to find the port + $content = Get-Content "TestPrograms\web_server_test.wfl" -Raw + if ($content -match "port\s+(\d+)") { + $port = [int]$Matches[1] + $result = Test-WflWebServer -TestFile "TestPrograms\web_server_test.wfl" -Port $port -ExpectedResponse "" -TimeoutSeconds $Timeout + if ($result) { $passedTests++ } + } else { + Write-Host "[SKIP] web_server_test.wfl - could not determine port" -ForegroundColor Yellow + $totalTests-- + } +} + +# Summary +Write-Host "" +Write-Host "[INFO] ============================" -ForegroundColor Blue +Write-Host "[INFO] Results: $passedTests/$totalTests tests passed" -ForegroundColor Blue + +if ($passedTests -eq $totalTests) { + Write-Host "[SUCCESS] All web server tests passed!" -ForegroundColor Green + exit 0 +} else { + Write-Host "[ERROR] Some web server tests failed" -ForegroundColor Red + exit 1 +} diff --git a/scripts/run_web_tests.sh b/scripts/run_web_tests.sh new file mode 100644 index 00000000..588edbf9 --- /dev/null +++ b/scripts/run_web_tests.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# WFL Web Server Integration Test Runner +# Tests WFL web server functionality by starting servers and sending HTTP requests + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +GRAY='\033[0;90m' +NC='\033[0m' # No Color + +# Default timeout +TIMEOUT=10 + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --help|-h) + echo "WFL Web Server Test Runner" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --help, -h Show this help message" + echo " --timeout Timeout for each test (default: 10)" + echo "" + echo "This script tests WFL web server functionality by:" + echo " 1. Starting the WFL web server in background" + echo " 2. Sending HTTP requests to verify it works" + echo " 3. Checking responses and cleaning up" + exit 0 + ;; + --timeout) + TIMEOUT="$2" + shift 2 + ;; + *) + echo -e "${RED}[ERROR]${NC} Unknown option: $1" + exit 1 + ;; + esac +done + +echo -e "${BLUE}[INFO]${NC} WFL Web Server Test Runner" +echo -e "${BLUE}[INFO]${NC} ============================" + +# Check if we're in the right directory +if [ ! -f "Cargo.toml" ]; then + echo -e "${RED}[ERROR]${NC} Cargo.toml not found. Please run this script from the WFL project root." + exit 1 +fi + +# Determine binary path +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + BINARY_PATH="target/release/wfl.exe" +else + BINARY_PATH="target/release/wfl" +fi + +# Check if release binary exists +if [ ! -f "$BINARY_PATH" ]; then + echo -e "${RED}[ERROR]${NC} Release binary not found. Run 'cargo build --release' first." + exit 1 +fi +echo -e "${GREEN}[SUCCESS]${NC} Binary found: $BINARY_PATH" + +# Function to test a web server +test_wfl_webserver() { + local test_file="$1" + local port="$2" + local expected_response="$3" + local timeout_seconds="$4" + + local test_name=$(basename "$test_file") + echo "" + echo -e "${BLUE}[INFO]${NC} Testing: $test_name on port $port" + + # Start the WFL server in background + "./$BINARY_PATH" "$test_file" > /dev/null 2>&1 & + local server_pid=$! + + # Cleanup function + cleanup() { + if kill -0 $server_pid 2>/dev/null; then + kill $server_pid 2>/dev/null || true + echo -e "${GRAY}[INFO] Server process terminated${NC}" + fi + } + trap cleanup EXIT + + # Wait for server to start (with retries) + local server_ready=false + local retries=0 + local max_retries=$((timeout_seconds * 2)) # Check every 500ms + + while [ "$server_ready" = false ] && [ $retries -lt $max_retries ]; do + sleep 0.5 + ((retries++)) + + # Try to connect using curl + if response=$(curl -s --max-time 2 "http://localhost:$port/" 2>/dev/null); then + server_ready=true + fi + done + + if [ "$server_ready" = false ]; then + echo -e "${RED}[ERROR]${NC} TIMEOUT: Server did not start within ${timeout_seconds}s" + cleanup + return 1 + fi + + # Server is ready, check response + if [[ "$response" == *"$expected_response"* ]]; then + echo -e "${GREEN}[SUCCESS]${NC} PASS: Got expected response" + cleanup + return 0 + else + echo -e "${RED}[ERROR]${NC} FAIL: Unexpected response" + echo -e "${GRAY} Expected: $expected_response${NC}" + echo -e "${GRAY} Got: $response${NC}" + cleanup + return 1 + fi +} + +# Run web server tests +total_tests=0 +passed_tests=0 + +# Test 1: simple_web_test.wfl +if [ -f "TestPrograms/simple_web_test.wfl" ]; then + ((total_tests++)) + if test_wfl_webserver "TestPrograms/simple_web_test.wfl" 8095 "Hello from WFL" "$TIMEOUT"; then + ((passed_tests++)) + fi +fi + +# Test 2: web_server_test.wfl (if exists) +if [ -f "TestPrograms/web_server_test.wfl" ]; then + ((total_tests++)) + # Read the file to find the port + port=$(grep -oP 'port\s+\K\d+' "TestPrograms/web_server_test.wfl" 2>/dev/null || echo "") + if [ -n "$port" ]; then + if test_wfl_webserver "TestPrograms/web_server_test.wfl" "$port" "" "$TIMEOUT"; then + ((passed_tests++)) + fi + else + echo -e "${YELLOW}[SKIP]${NC} web_server_test.wfl - could not determine port" + ((total_tests--)) + fi +fi + +# Summary +echo "" +echo -e "${BLUE}[INFO]${NC} ============================" +echo -e "${BLUE}[INFO]${NC} Results: $passed_tests/$total_tests tests passed" + +if [ "$passed_tests" -eq "$total_tests" ]; then + echo -e "${GREEN}[SUCCESS]${NC} All web server tests passed!" + exit 0 +else + echo -e "${RED}[ERROR]${NC} Some web server tests failed" + exit 1 +fi diff --git a/src/config.rs b/src/config.rs index b7134e0c..a84b9052 100644 --- a/src/config.rs +++ b/src/config.rs @@ -652,8 +652,13 @@ mod tests { use super::*; use std::fs; use std::io::Write; + use std::sync::Mutex; use tempfile::tempdir; + // Mutex to serialize config tests that modify environment variables + // This prevents test interference when tests run in parallel + static TEST_ENV_LOCK: Mutex<()> = Mutex::new(()); + #[cfg(test)] fn set_test_env_var(val: Option<&str>) { match val { @@ -666,6 +671,9 @@ mod tests { where F: FnOnce() -> R, { + // Acquire lock to serialize tests that modify environment variables + let _guard = TEST_ENV_LOCK.lock().unwrap(); + let original = std::env::var("WFL_GLOBAL_CONFIG_PATH").ok(); let result = f(); diff --git a/src/fixer/mod.rs b/src/fixer/mod.rs index 606b99f6..d187fe6d 100644 --- a/src/fixer/mod.rs +++ b/src/fixer/mod.rs @@ -845,6 +845,7 @@ impl CodeFixer { Operator::Minus => output.push_str(" - "), Operator::Multiply => output.push_str(" * "), Operator::Divide => output.push_str(" / "), + Operator::Modulo => output.push_str(" % "), Operator::Equals => output.push_str(" == "), Operator::NotEquals => output.push_str(" != "), Operator::LessThan => output.push_str(" < "), diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index fe85b008..6f942704 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -462,6 +462,52 @@ impl IoClient { } } + /// Syncs file to disk with Windows-specific error handling. + /// + /// # Windows Behavior + /// On Windows, `sync_all()` can return spurious `PermissionDenied` errors when: + /// - Multiple processes/threads access the same file + /// - File locking or antivirus software interferes + /// - The filesystem has concurrent access patterns + /// + /// This is a known Windows limitation (not a real permission error). Since `flush()` + /// has already ensured data reaches OS buffers, it's safe to ignore PermissionDenied. + /// + /// # Error Handling + /// - Windows: Suppress ONLY PermissionDenied; propagate all other errors + /// - Unix: Propagate all errors + /// + /// # Why Other Errors Must Propagate + /// Errors like `StorageFull`, `IoUnavailable`, `ReadOnlyFilesystem` indicate real + /// I/O failures that the user must be notified about. Silently ignoring these would + /// cause data loss or corruption. + /// + /// # Parameters + /// - `file`: The file to sync + /// - `operation`: Description of the operation (for error messages) + async fn sync_file_with_windows_handling( + file: &mut tokio::fs::File, + operation: &str, + ) -> Result<(), String> { + match file.sync_all().await { + Ok(_) => Ok(()), + Err(e) => { + // On Windows, selectively suppress only PermissionDenied errors + #[cfg(windows)] + if e.kind() == std::io::ErrorKind::PermissionDenied { + eprintln!( + "Warning: Windows file sync encountered spurious PermissionDenied during {} (data already flushed)", + operation + ); + return Ok(()); + } + + // All other errors must be propagated on all platforms + Err(format!("Failed to sync file during {}: {e}", operation)) + } + } + } + #[allow(dead_code)] async fn write_file(&self, handle_id: &str, content: &str) -> Result<(), String> { let mut file_handles = self.file_handles.lock().await; @@ -496,27 +542,10 @@ impl IoClient { // Flush the data to ensure it's written to disk match file_clone.flush().await { Ok(_) => { - // Sync to disk for durability - match file_clone.sync_all().await { - Ok(_) => Ok(()), - Err(e) => { - // On Windows, sync_all can fail with "Access denied" in concurrent scenarios - // This is often a limitation of Windows filesystem, not a real error - if cfg!(windows) - && e.kind() == std::io::ErrorKind::PermissionDenied - { - // Log warning but don't fail - flush() already ensured data reaches OS buffers - eprintln!( - "Warning: Windows file sync limitation encountered: {}", - e - ); - Ok(()) - } else { - // On other platforms or different error types, this is a real failure - Err(format!("Failed to sync file to disk: {e}")) - } - } - } + // Platform-specific sync behavior + // Sync file to disk with Windows-aware error handling + Self::sync_file_with_windows_handling(&mut file_clone, "write") + .await } Err(e) => Err(format!("Failed to flush file: {e}")), } @@ -545,25 +574,8 @@ impl IoClient { // Flush the file before closing to ensure all data is written to disk match file.flush().await { Ok(_) => { - // Sync to disk for durability - match file.sync_all().await { - Ok(_) => Ok(()), - Err(e) => { - // On Windows, sync_all can fail with "Access denied" in concurrent scenarios - // This is often a limitation of Windows filesystem, not a real error - if cfg!(windows) && e.kind() == std::io::ErrorKind::PermissionDenied { - // Log warning but don't fail - flush() already ensured data reaches OS buffers - eprintln!( - "Warning: Windows file sync limitation encountered: {}", - e - ); - Ok(()) - } else { - // On other platforms or different error types, this is a real failure - Err(format!("Failed to sync file during close: {e}")) - } - } - } + // Sync file to disk with Windows-aware error handling + Self::sync_file_with_windows_handling(&mut file, "close").await } Err(e) => Err(format!("Failed to flush file during close: {e}")), } @@ -587,27 +599,8 @@ impl IoClient { // Flush the data to ensure it's written to disk match file.flush().await { Ok(_) => { - // Sync to disk for durability - match file.sync_all().await { - Ok(_) => Ok(()), - Err(e) => { - // On Windows, sync_all can fail with "Access denied" in concurrent scenarios - // This is often a limitation of Windows filesystem, not a real error - if cfg!(windows) - && e.kind() == std::io::ErrorKind::PermissionDenied - { - // Log warning but don't fail - flush() already ensured data reaches OS buffers - eprintln!( - "Warning: Windows file sync limitation encountered: {}", - e - ); - Ok(()) - } else { - // On other platforms or different error types, this is a real failure - Err(format!("Failed to sync appended data to disk: {e}")) - } - } - } + // Sync file to disk with Windows-aware error handling + Self::sync_file_with_windows_handling(file, "append").await } Err(e) => Err(format!("Failed to flush appended data: {e}")), } @@ -4804,6 +4797,15 @@ impl Interpreter { Ok(value) } } + Value::Function(func) => { + if func.params.is_empty() { + // Auto-call zero-argument user-defined functions + self.call_function(func, vec![], *line, *column).await + } else { + // Return function object for functions with arguments + Ok(value) + } + } _ => Ok(value), } } else if name == "count" { @@ -4840,6 +4842,7 @@ impl Interpreter { Operator::Minus => self.subtract(left_val, right_val, *line, *column), Operator::Multiply => self.multiply(left_val, right_val, *line, *column), Operator::Divide => self.divide(left_val, right_val, *line, *column), + Operator::Modulo => self.modulo(left_val, right_val, *line, *column), Operator::Equals => Ok(Value::Bool(self.is_equal(&left_val, &right_val))), Operator::NotEquals => Ok(Value::Bool(!self.is_equal(&left_val, &right_val))), Operator::GreaterThan => self.greater_than(left_val, right_val, *line, *column), @@ -5785,6 +5788,53 @@ impl Interpreter { } } + fn modulo( + &self, + left: Value, + right: Value, + line: usize, + column: usize, + ) -> Result { + #[cfg(feature = "dhat-ad-hoc")] + dhat::ad_hoc_event(1); // Track modulo operations for memory profiling + + match (left, right) { + (Value::Number(a), Value::Number(b)) => { + if b == 0.0 { + Err(RuntimeError::new( + "Modulo by zero".to_string(), + line, + column, + )) + } else { + // Calculate the result of the modulo operation + let result = a % b; + + // Check if the result is valid (not NaN or infinite) + if !result.is_finite() { + return Err(RuntimeError::new( + format!("Modulo resulted in invalid number: {result}"), + line, + column, + )); + } + + // Return the valid result as a Value::Number + Ok(Value::Number(result)) + } + } + (a, b) => Err(RuntimeError::new( + format!( + "Cannot compute modulo of {} by {}", + a.type_name(), + b.type_name() + ), + line, + column, + )), + } + } + fn is_equal(&self, left: &Value, right: &Value) -> bool { match (left, right) { (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON, diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 8fa3e074..88eb5f23 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -358,6 +358,9 @@ pub enum Token { #[token("-")] Minus, + #[token("%")] + Percent, + #[token(".")] Dot, diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 87439be0..5c0b6df2 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -705,6 +705,7 @@ pub enum Operator { Minus, Multiply, Divide, + Modulo, Equals, NotEquals, GreaterThan, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 150090da..714b94e4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1767,6 +1767,7 @@ impl<'a> Parser<'a> { Token::KeywordMinus => Some((Operator::Minus, 1)), Token::KeywordTimes => Some((Operator::Multiply, 2)), Token::KeywordDividedBy => Some((Operator::Divide, 2)), + Token::Percent => Some((Operator::Modulo, 2)), Token::KeywordDivided => { // Check if next token is "by" more efficiently if self.peek_divided_by() { @@ -1970,7 +1971,8 @@ impl<'a> Parser<'a> { continue; // Skip the rest of the loop since we've already updated left } Token::KeywordAnd => { - self.tokens.next(); // Consume "and" + // DON'T consume here - let precedence check happen first + // Token will be consumed in the block after precedence check Some((Operator::And, 0)) } Token::KeywordOr => { @@ -2247,9 +2249,15 @@ impl<'a> Parser<'a> { self.expect_token(Token::KeywordBy, "Expected 'by' after 'divided'")?; self.tokens.next(); // Consume "by" } + Token::Percent => { + self.tokens.next(); // Consume "%" + } Token::Equals => { self.tokens.next(); // Consume "=" } + Token::KeywordAnd => { + self.tokens.next(); // Consume "and" + } _ => { // For operators like "is" that have already consumed tokens in their detection // No additional consumption needed @@ -5473,7 +5481,9 @@ impl<'a> Parser<'a> { None }; - let arg_value = self.parse_primary_expression()?; + // FIX: Parse expressions with precedence >= 1 (arithmetic operators) + // This stops at 'and' (precedence 0), which is then used as argument separator + let arg_value = self.parse_binary_expression(1)?; arguments.push(Argument { name: arg_name, diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 3e23c2bd..91ed20bb 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -1589,7 +1589,7 @@ impl TypeChecker { Type::Error } } - Operator::Minus | Operator::Multiply | Operator::Divide => { + Operator::Minus | Operator::Multiply | Operator::Divide | Operator::Modulo => { // These operations require both operands to be numbers if left_type == Type::Number && right_type == Type::Number { Type::Number diff --git a/tests/file_io_windows_sync_errors_test.rs b/tests/file_io_windows_sync_errors_test.rs new file mode 100644 index 00000000..e93bead8 --- /dev/null +++ b/tests/file_io_windows_sync_errors_test.rs @@ -0,0 +1,316 @@ +/// Tests for Windows-specific file sync error handling +/// +/// This test suite verifies that: +/// 1. PermissionDenied errors from sync_all() are selectively suppressed on Windows +/// 2. All OTHER errors (disk full, I/O failures, etc.) are still propagated +/// 3. Data integrity is maintained despite sync errors +/// 4. Cross-platform behavior is consistent where appropriate +use std::env; +use std::fs; +use std::process::Command; + +#[cfg(windows)] +#[test] +fn test_windows_permission_denied_suppressed() { + // On Windows, this test verifies that PermissionDenied errors from sync_all() + // don't cause write/close/append operations to fail + + let wfl_binary = "target/release/wfl.exe"; + let binary_path = env::current_dir().unwrap().join(wfl_binary); + + if !binary_path.exists() { + panic!("WFL binary not found. Run 'cargo build --release' first."); + } + + // Create a test that writes, appends, and closes files + // This may trigger PermissionDenied on Windows with concurrent access + let pid = std::process::id(); + let test_program = format!( + r#" +// Test file operations that might trigger sync_all() PermissionDenied +open file at "test_sync_write_{}.txt" for writing as f1 +wait for write content "test data" into f1 +close file f1 + +// Verify the file was written successfully +open file at "test_sync_write_{}.txt" for reading as f2 +wait for store result as read content from f2 +close file f2 + +check if result is equal to "test data": + display "PASS: File write succeeded despite potential sync issues" +otherwise: + display "FAIL: Data mismatch - got: " with result +end check + +// Clean up +delete file at "test_sync_write_{}.txt" +"#, + pid, pid, pid + ); + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_windows_sync_{}.wfl", pid)); + fs::write(&test_file, &test_program).expect("Failed to write test file"); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + fs::remove_file(format!("test_sync_write_{}.txt", pid)).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL execution failed.\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + assert!( + stdout.contains("PASS"), + "Test failed.\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + // Verify that if PermissionDenied occurred, a warning was printed + // (but the operation still succeeded) + if stderr.contains("PermissionDenied") { + assert!( + stderr.contains("Warning"), + "PermissionDenied should trigger a warning, not a failure" + ); + } +} + +#[test] +fn test_data_integrity_after_write() { + // Cross-platform test: Verify data is correctly written and readable + + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!(binary_path.exists(), "WFL binary not found."); + + let pid = std::process::id(); + let test_program = format!( + r#" +// Test write and read cycle +store test_content as "Line 1\nLine 2\nLine 3\n" + +open file at "test_integrity_{}.txt" for writing as handle +wait for write content test_content into handle +close file handle + +// Read it back +open file at "test_integrity_{}.txt" for reading as handle2 +wait for store read_back as read content from handle2 +close file handle2 + +// Verify +check if read_back is equal to test_content: + display "PASS" +otherwise: + display "FAIL: Content mismatch" +end check + +delete file at "test_integrity_{}.txt" +"#, + pid, pid, pid + ); + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_integrity_check_{}.wfl", pid)); + fs::write(&test_file, &test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + fs::remove_file(format!("test_integrity_{}.txt", pid)).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!( + stdout.contains("PASS"), + "Data integrity check failed: {}", + stdout + ); +} + +#[test] +fn test_append_with_sync() { + // Test that append operations work correctly with sync error handling + + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!(binary_path.exists(), "WFL binary not found."); + + let pid = std::process::id(); + let test_program = format!( + r#" +// Create file with initial content +open file at "test_append_sync_{}.txt" for writing as f1 +wait for write content "Line 1\n" into f1 +close file f1 + +// Append additional content +open file at "test_append_sync_{}.txt" for appending as f2 +wait for append content "Line 2\n" into f2 +wait for append content "Line 3\n" into f2 +close file f2 + +// Read and verify +open file at "test_append_sync_{}.txt" for reading as f3 +wait for store result as read content from f3 +close file f3 + +store expected as "Line 1\nLine 2\nLine 3\n" +check if result is equal to expected: + display "PASS" +otherwise: + display "FAIL: Expected '" with expected with "' but got '" with result with "'" +end check + +delete file at "test_append_sync_{}.txt" +"#, + pid, pid, pid, pid + ); + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_append_with_sync_{}.wfl", pid)); + fs::write(&test_file, &test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + fs::remove_file(format!("test_append_sync_{}.txt", pid)).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!(stdout.contains("PASS"), "Append test failed: {}", stdout); +} + +#[test] +fn test_multiple_write_cycles_with_sync() { + // Test rapid write/close cycles to stress-test sync error handling + + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!(binary_path.exists(), "WFL binary not found."); + + let pid = std::process::id(); + let test_program = format!( + r#" +// Perform multiple write/close cycles to stress-test sync + +// Cycle 1 +open file at "test_multi_sync_{}.txt" for writing as h1 +wait for write content "Iteration 1" into h1 +close file h1 + +// Cycle 2 +open file at "test_multi_sync_{}.txt" for writing as h2 +wait for write content "Iteration 2" into h2 +close file h2 + +// Cycle 3 +open file at "test_multi_sync_{}.txt" for writing as h3 +wait for write content "Iteration 3" into h3 +close file h3 + +// Cycle 4 +open file at "test_multi_sync_{}.txt" for writing as h4 +wait for write content "Iteration 4" into h4 +close file h4 + +// Cycle 5 +open file at "test_multi_sync_{}.txt" for writing as h5 +wait for write content "Iteration 5" into h5 +close file h5 + +// Read final result +open file at "test_multi_sync_{}.txt" for reading as h_read +wait for store final_content as read content from h_read +close file h_read + +check if final_content is equal to "Iteration 5": + display "PASS" +otherwise: + display "FAIL: Got '" with final_content with "'" +end check + +delete file at "test_multi_sync_{}.txt" +"#, + pid, pid, pid, pid, pid, pid, pid + ); + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_multi_sync_cycles_{}.wfl", pid)); + fs::write(&test_file, &test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + fs::remove_file(format!("test_multi_sync_{}.txt", pid)).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!( + stdout.contains("PASS"), + "Multi-cycle test failed: {}", + stdout + ); +} diff --git a/tests/modulo_operator_test.rs b/tests/modulo_operator_test.rs new file mode 100644 index 00000000..40b493ac --- /dev/null +++ b/tests/modulo_operator_test.rs @@ -0,0 +1,186 @@ +use std::env; +/// Test that the modulo operator (%) works correctly +/// +/// This test verifies the implementation of the % operator for computing remainders. +use std::fs; +use std::process::Command; + +#[test] +fn test_modulo_operator_basic() { + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!( + binary_path.exists(), + "WFL binary not found. Run 'cargo build --release' first." + ); + + let test_program = r#" +// Test basic modulo operations +store r1 as 5 % 2 +store r2 as 10 % 3 +store r3 as 7 % 7 +store r4 as 6 % 4 + +store result as "PASS" + +check if r1 is equal to 1: + // OK +otherwise: + change result to "FAIL: 5 % 2 should be 1" +end check + +check if r2 is equal to 1: + // OK +otherwise: + change result to "FAIL: 10 % 3 should be 1" +end check + +check if r3 is equal to 0: + // OK +otherwise: + change result to "FAIL: 7 % 7 should be 0" +end check + +check if r4 is equal to 2: + // OK +otherwise: + change result to "FAIL: 6 % 4 should be 2" +end check + +display result +"#; + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_modulo_basic_{}.wfl", std::process::id())); + fs::write(&test_file, test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!(stdout.contains("PASS"), "Expected PASS, got: {}", stdout); + assert!(!stdout.contains("FAIL"), "Found FAIL: {}", stdout); +} + +#[test] +fn test_modulo_with_even_odd_check() { + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!(binary_path.exists(), "WFL binary not found."); + + let test_program = r#" +// Test modulo for even/odd checking (like the nexus test) +store total as 0 +store i as 0 + +repeat while i is less than 5: + change i to i plus 1 + check if (i % 2) is equal to 0: + skip // Skip even numbers + end check + change total to total plus i +end repeat + +// Should sum only odd numbers: 1 + 3 + 5 = 9 +check if total is equal to 9: + display "PASS" +otherwise: + display "FAIL: expected 9, got " with total +end check +"#; + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_modulo_even_odd_{}.wfl", std::process::id())); + fs::write(&test_file, test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!(stdout.contains("PASS"), "Expected PASS, got: {}", stdout); +} + +#[test] +fn test_modulo_by_zero_error() { + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + let binary_path = env::current_dir().unwrap().join(wfl_binary); + assert!(binary_path.exists(), "WFL binary not found."); + + let test_program = r#" +// Test that modulo by zero raises an error +store result as "FAIL" + +try: + store r as 5 % 0 + change result to "FAIL: No error raised" +catch: + change result to "PASS" +end try + +display result +"#; + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_modulo_zero_{}.wfl", std::process::id())); + fs::write(&test_file, test_program).unwrap(); + + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL"); + + fs::remove_file(&test_file).ok(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL failed: {}\n{}", + stdout, + stderr + ); + assert!(stdout.contains("PASS"), "Expected PASS, got: {}", stdout); +} diff --git a/tests/zero_arg_action_error_propagation_test.rs b/tests/zero_arg_action_error_propagation_test.rs new file mode 100644 index 00000000..27ad1650 --- /dev/null +++ b/tests/zero_arg_action_error_propagation_test.rs @@ -0,0 +1,165 @@ +use std::env; +/// Test that errors from zero-argument user-defined actions are properly propagated +/// when the action is called without arguments (auto-call behavior). +/// +/// This test verifies the fix for a bug where `store res as faulty` would store +/// the function value instead of calling it and catching errors. +use std::fs; +use std::process::Command; + +#[test] +fn test_zero_arg_action_error_propagation() { + // Get the path to the WFL binary + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + // Verify the binary exists + let binary_path = env::current_dir().unwrap().join(wfl_binary); + + if !binary_path.exists() { + panic!( + "WFL binary not found at {:?}. Run 'cargo build --release' first.", + binary_path + ); + } + + // Create a test WFL program + let test_program = r#" +// Define a zero-argument action that raises an error +define action called faulty_action: + give back 1 divided by 0 +end action + +store test_passed as "FAIL" + +// Test that the action is called and error is caught +try: + store res as faulty_action + // If we reach here, the error was not raised + change test_passed to "FAIL: No error raised" +catch: + // Error was properly caught + change test_passed to "PASS" +end try + +display test_passed +"#; + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_zero_arg_error_{}.wfl", std::process::id())); + fs::write(&test_file, test_program).expect("Failed to write test file"); + + // Run the WFL program + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL binary"); + + // Clean up + fs::remove_file(&test_file).ok(); + + // Check the output + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL program failed to execute.\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + assert!( + stdout.contains("PASS"), + "Expected 'PASS' in output, but got:\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + assert!( + !stdout.contains("FAIL"), + "Found 'FAIL' in output:\nStdout: {}\nStderr: {}", + stdout, + stderr + ); +} + +#[test] +fn test_zero_arg_action_auto_call() { + // Get the path to the WFL binary + let wfl_binary = if cfg!(target_os = "windows") { + "target/release/wfl.exe" + } else { + "target/release/wfl" + }; + + // Verify the binary exists + let binary_path = env::current_dir().unwrap().join(wfl_binary); + + if !binary_path.exists() { + panic!( + "WFL binary not found at {:?}. Run 'cargo build --release' first.", + binary_path + ); + } + + // Create a test WFL program + let test_program = r#" +// Define a zero-argument action that returns a value +define action called get_value: + give back 42 +end action + +// Test that the action is auto-called when referenced +store result as get_value + +check if result is equal to 42: + display "PASS: Action was auto-called" +otherwise: + display "FAIL: Got " with result with " instead of 42" +end check +"#; + + // Use unique temp file to avoid race conditions when tests run in parallel + let temp_dir = std::env::temp_dir(); + let test_file = temp_dir.join(format!("test_zero_arg_autocall_{}.wfl", std::process::id())); + fs::write(&test_file, test_program).expect("Failed to write test file"); + + // Run the WFL program + let output = Command::new(&binary_path) + .arg(&test_file) + .output() + .expect("Failed to execute WFL binary"); + + // Clean up + fs::remove_file(&test_file).ok(); + + // Check the output + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "WFL program failed to execute.\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + assert!( + stdout.contains("PASS"), + "Expected 'PASS' in output, but got:\nStdout: {}\nStderr: {}", + stdout, + stderr + ); + + assert!( + !stdout.contains("FAIL"), + "Found 'FAIL' in output:\nStdout: {}\nStderr: {}", + stdout, + stderr + ); +}