diff --git a/backend/app/data/__init__.py b/backend/app/data/__init__.py index ab6bbdb..c997d2a 100644 --- a/backend/app/data/__init__.py +++ b/backend/app/data/__init__.py @@ -259,25 +259,29 @@ def run_tests(category): try: data = request.get_json() if not data: - logger.warning("Rejected test request - no JSON data provided") + logger.warning("Test request rejected: no JSON data") return jsonify({"error": "No JSON data provided"}), 400 tests = data.get('tests', []) if not tests: - logger.warning("Rejected test request - no test cases provided") + logger.warning("Test request rejected: no tests provided") return jsonify({"error": "At least one test case is required"}), 400 if category == 'regex_pattern': pattern = data.get('pattern') - logger.info(f"Processing regex test request - Pattern: {pattern}") if not pattern: - logger.warning("Rejected test request - missing pattern") + logger.warning("Test request rejected: missing pattern") return jsonify({"error": "Pattern is required"}), 400 success, message, updated_tests = test_regex_pattern( pattern, tests) + + if success and updated_tests: + passed = sum(1 for t in updated_tests if t.get('passes')) + total = len(updated_tests) + logger.info(f"Tests completed: {passed}/{total} passed") elif category == 'custom_format': conditions = data.get('conditions', []) @@ -300,10 +304,8 @@ def run_tests(category): return jsonify( {"error": "Testing not supported for this category"}), 400 - logger.info(f"Test execution completed - Success: {success}") - if not success: - logger.warning(f"Test execution failed - {message}") + logger.error(f"Test execution failed: {message}") return jsonify({"success": False, "message": message}), 400 return jsonify({"success": True, "tests": updated_tests}), 200 diff --git a/backend/app/data/utils.py b/backend/app/data/utils.py index fefc40c..ccd2769 100644 --- a/backend/app/data/utils.py +++ b/backend/app/data/utils.py @@ -542,76 +542,67 @@ def test_regex_pattern( pattern: str, tests: List[Dict[str, Any]]) -> Tuple[bool, str, List[Dict[str, Any]]]: """ - Test a regex pattern against a list of test cases using PCRE2 compatible engine. + Test a regex pattern against a list of test cases using .NET regex engine via PowerShell. Returns match information along with test results. """ - logger.info(f"Starting regex pattern test - Pattern: {pattern}") try: - try: - compiled_pattern = regex.compile(pattern, - regex.V1 | regex.IGNORECASE) - logger.info( - "Pattern compiled successfully with PCRE2 compatibility") - except regex.error as e: - logger.warning(f"Invalid regex pattern: {str(e)}") - return False, f"Invalid regex pattern: {str(e)}", tests - - current_time = datetime.now().isoformat() - logger.info(f"Processing {len(tests)} test cases") - - for test in tests: - test_id = test.get('id', 'unknown') - test_input = test.get('input', '') - expected = test.get('expected', False) - - try: - match = compiled_pattern.search(test_input) - matches = bool(match) - - # Update test result with basic fields - test['passes'] = matches == expected - test['lastRun'] = current_time - - # Add match information - if match: - test['matchedContent'] = match.group(0) - test['matchSpan'] = { - 'start': match.start(), - 'end': match.end() - } - # Get all capture groups if they exist - test['matchedGroups'] = [g for g in match.groups() - ] if match.groups() else [] - else: - test['matchedContent'] = None - test['matchSpan'] = None - test['matchedGroups'] = [] - - logger.info( - f"Test {test_id} {'passed' if test['passes'] else 'failed'} - Match: {matches}, Expected: {expected}" - ) - - except Exception as e: - logger.error(f"Error running test {test_id}: {str(e)}") - test['passes'] = False - test['lastRun'] = current_time - test['matchedContent'] = None - test['matchSpan'] = None - test['matchedGroups'] = [] - - # Log overall results - passed_tests = sum(1 for test in tests if test.get('passes', False)) - logger.info( - f"Test execution complete - {passed_tests}/{len(tests)} tests passed" + # Get the path to the test.ps1 script + script_path = os.path.join('/app', 'scripts', 'test.ps1') + if not os.path.exists(script_path): + # Fallback for local development + script_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'scripts', 'test.ps1') + + # Prepare the input data + input_data = { + 'pattern': pattern, + 'tests': tests + } + + # Run PowerShell script + result = subprocess.run( + ['pwsh', '-File', script_path], + input=json.dumps(input_data), + capture_output=True, + text=True, + timeout=10 ) - - return True, "", tests - + + if result.returncode != 0 and not result.stdout: + logger.error(f"PowerShell script failed: {result.stderr}") + return False, "Failed to run tests", tests + + # Parse JSON output + try: + output = json.loads(result.stdout.strip()) + except json.JSONDecodeError: + # Try to find JSON in the output + lines = result.stdout.strip().split('\n') + for line in reversed(lines): + if line.strip(): + try: + output = json.loads(line) + break + except json.JSONDecodeError: + continue + else: + logger.error(f"No valid JSON found in output: {result.stdout}") + return False, "Failed to parse test results", tests + + if output.get('success'): + return True, "Tests completed successfully", output.get('tests', tests) + else: + return False, output.get('message', 'Tests failed'), tests + + except subprocess.TimeoutExpired: + logger.error("Test execution timed out") + return False, "Test execution timed out", tests + except FileNotFoundError: + logger.error("PowerShell (pwsh) not found") + return False, "PowerShell is not available", tests except Exception as e: - logger.error(f"Unexpected error in test_regex_pattern: {str(e)}", - exc_info=True) - return False, str(e), tests + logger.error(f"Error running tests: {e}") + return False, f"Test error: {str(e)}", tests def test_format_conditions(conditions: List[Dict], diff --git a/backend/scripts/test.ps1 b/backend/scripts/test.ps1 new file mode 100755 index 0000000..eab6743 --- /dev/null +++ b/backend/scripts/test.ps1 @@ -0,0 +1,107 @@ +#!/usr/bin/env pwsh +# Run regex tests against a pattern + +# Set output encoding to UTF-8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +# Read from stdin +$inputText = $input +if (-not $inputText) { + $inputText = [System.Console]::In.ReadToEnd() +} + +if (-not $inputText) { + Write-Output (ConvertTo-Json @{ + success = $false + message = "No input provided" + } -Compress) + exit 0 +} + +try { + $data = $inputText | ConvertFrom-Json + $Pattern = $data.pattern + $tests = $data.tests +} +catch { + Write-Output (ConvertTo-Json @{ + success = $false + message = "Failed to parse input JSON: $_" + } -Compress) + exit 0 +} + +# Ensure we have required inputs +if ([string]::IsNullOrWhiteSpace($Pattern)) { + Write-Output (ConvertTo-Json @{ + success = $false + message = "No pattern provided" + } -Compress) + exit 0 +} + +if (-not $tests -or $tests.Count -eq 0) { + Write-Output (ConvertTo-Json @{ + success = $false + message = "No tests provided" + } -Compress) + exit 0 +} + +try { + + # Create the regex object with case-insensitive option + $regex = [System.Text.RegularExpressions.Regex]::new($Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + # Process each test + $results = @() + + foreach ($test in $tests) { + $match = $regex.Match($test.input) + $passes = ($match.Success -eq $test.expected) + + $result = @{ + id = $test.id + input = $test.input + expected = $test.expected + passes = $passes + } + + if ($match.Success) { + # Include match details for highlighting (using original format) + $result.matchedContent = $match.Value + $result.matchSpan = @{ + start = $match.Index + end = $match.Index + $match.Length + } + + # Include capture groups if any + $groups = @() + for ($i = 1; $i -lt $match.Groups.Count; $i++) { + if ($match.Groups[$i].Success) { + $groups += $match.Groups[$i].Value + } + } + $result.matchedGroups = $groups + } + else { + $result.matchedContent = $null + $result.matchSpan = $null + $result.matchedGroups = @() + } + + $results += $result + } + + Write-Output (ConvertTo-Json @{ + success = $true + tests = $results + } -Compress -Depth 10) +} +catch { + Write-Output (ConvertTo-Json @{ + success = $false + message = $_.Exception.Message + } -Compress) +} \ No newline at end of file diff --git a/frontend/src/components/regex/AddUnitTestModal.jsx b/frontend/src/components/regex/AddUnitTestModal.jsx index 4efe4cd..7456675 100644 --- a/frontend/src/components/regex/AddUnitTestModal.jsx +++ b/frontend/src/components/regex/AddUnitTestModal.jsx @@ -23,15 +23,13 @@ const AddUnitTestModal = ({isOpen, onClose, onAdd, tests, editTest = null}) => { const handleSubmit = () => { const getNextTestId = testArray => { if (!testArray || testArray.length === 0) return 1; - return Math.max(...testArray.map(test => test.id)) + 1; + return Math.max(...testArray.map(test => test.id || 0)) + 1; }; const testData = { id: editTest ? editTest.id : getNextTestId(tests), input, - expected: shouldMatch, - passes: false, - lastRun: null + expected: shouldMatch }; onAdd(testData); diff --git a/frontend/src/components/regex/RegexModal.jsx b/frontend/src/components/regex/RegexModal.jsx index d07359a..2275018 100644 --- a/frontend/src/components/regex/RegexModal.jsx +++ b/frontend/src/components/regex/RegexModal.jsx @@ -6,7 +6,7 @@ import RegexTestingTab from './RegexTestingTab'; import {useRegexModal} from '@hooks/useRegexModal'; import {RegexPatterns} from '@api/data'; import Alert from '@ui/Alert'; -import {Loader, Play} from 'lucide-react'; +import {Loader, Play, Save, Trash2, Check} from 'lucide-react'; const RegexModal = ({ pattern: initialPattern, @@ -84,12 +84,13 @@ const RegexModal = ({ {initialPattern && !isCloning && ( )}
+ {passedTests} of {totalTests} tests passing + {totalTests > 0 && ` (${Math.round((passedTests / totalTests) * 100)}%)`} +
+ )}