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 && ( )}
@@ -97,20 +98,20 @@ const RegexModal = ({ )}
diff --git a/frontend/src/components/regex/RegexTestingTab.jsx b/frontend/src/components/regex/RegexTestingTab.jsx index 8b64dc4..cf26c5a 100644 --- a/frontend/src/components/regex/RegexTestingTab.jsx +++ b/frontend/src/components/regex/RegexTestingTab.jsx @@ -13,52 +13,47 @@ const RegexTestingTab = ({ }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [editingTest, setEditingTest] = useState(null); + const [testResults, setTestResults] = useState({}); + + // Wrapped run tests function that stores results + const handleRunTests = useCallback(async (testPattern, testData) => { + const results = await onRunTests(testPattern, testData); + if (results && Array.isArray(results)) { + // Store results by test ID + const resultsMap = {}; + results.forEach(result => { + resultsMap[result.id] = result; + }); + setTestResults(resultsMap); + } + return results; + }, [onRunTests]); useEffect(() => { - const needsAutoRun = - tests?.length > 0 && - pattern && - tests.some(test => test.passes !== undefined && !test.matchSpan); - - if (needsAutoRun && !isRunningTests) { - onRunTests(pattern, tests); + // Run tests when pattern or tests change + if (tests?.length > 0 && pattern && !isRunningTests) { + handleRunTests(pattern, tests); } - }, []); + }, [pattern]); // Only re-run when pattern changes const handleAddOrUpdateTest = useCallback( testData => { let updatedTests; if (editingTest) { updatedTests = tests.map(test => - test.id === testData.id - ? { - ...testData, - passes: false, - lastRun: null, - matchedContent: null, - matchSpan: null, - matchedGroups: [] - } - : test + test.id === testData.id ? testData : test ); } else { - updatedTests = [ - ...tests, - { - ...testData, - passes: false, - lastRun: null, - matchedContent: null, - matchSpan: null, - matchedGroups: [] - } - ]; + updatedTests = [...tests, testData]; } onTestsChange(updatedTests); - onRunTests(pattern, updatedTests); + // Run tests automatically after adding/updating + if (pattern) { + handleRunTests(pattern, updatedTests); + } setEditingTest(null); }, - [tests, onTestsChange, onRunTests, pattern, editingTest] + [tests, onTestsChange, handleRunTests, pattern, editingTest] ); const handleEditTest = useCallback(test => { @@ -80,72 +75,81 @@ const RegexTestingTab = ({ }, []); const totalTests = tests?.length || 0; - const passedTests = tests?.filter(test => test.passes)?.length || 0; + const passedTests = tests?.filter(test => { + const result = testResults[test.id]; + return result?.passes; + })?.length || 0; return (
- {/* Header with Progress Bar */} -
+ {/* Header */} +
-

+

Unit Tests

-
-
-
-
- - {totalTests > 0 - ? `${passedTests}/${totalTests} tests passing` - : 'No tests added yet'} - -
+ {totalTests > 0 && ( +

+ {passedTests} of {totalTests} tests passing + {totalTests > 0 && ` (${Math.round((passedTests / totalTests) * 100)}%)`} +

+ )}
{tests?.length > 0 && ( )}
+ + {/* Progress Bar */} + {totalTests > 0 && ( +
+
+
+
+
+ )} {/* Test List */}
{tests?.length > 0 ? (
- {tests.map(test => ( - handleDeleteTest(test.id)} - onEdit={() => handleEditTest(test)} - /> - ))} + {tests.map(test => { + // Merge saved test with runtime results + const testWithResults = { + ...test, + ...testResults[test.id] + }; + return ( + handleDeleteTest(test.id)} + onEdit={() => handleEditTest(test)} + /> + ); + })}
) : (
@@ -173,15 +177,7 @@ RegexTestingTab.propTypes = { PropTypes.shape({ id: PropTypes.number.isRequired, input: PropTypes.string.isRequired, - expected: PropTypes.bool.isRequired, - passes: PropTypes.bool.isRequired, - lastRun: PropTypes.string, - matchedContent: PropTypes.string, - matchedGroups: PropTypes.arrayOf(PropTypes.string), - matchSpan: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number - }) + expected: PropTypes.bool.isRequired }) ), onTestsChange: PropTypes.func.isRequired, diff --git a/frontend/src/components/regex/UnitTest.jsx b/frontend/src/components/regex/UnitTest.jsx index 77bc381..ee3eb10 100644 --- a/frontend/src/components/regex/UnitTest.jsx +++ b/frontend/src/components/regex/UnitTest.jsx @@ -68,11 +68,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => { : 'Should Not Match'}
-
- - Last run: {test.lastRun} - -
+
-
@@ -112,7 +107,6 @@ UnitTest.propTypes = { input: PropTypes.string.isRequired, expected: PropTypes.bool.isRequired, passes: PropTypes.bool.isRequired, - lastRun: PropTypes.string, matchedContent: PropTypes.string, matchedGroups: PropTypes.arrayOf(PropTypes.string), matchSpan: PropTypes.shape({ diff --git a/frontend/src/hooks/useRegexModal.js b/frontend/src/hooks/useRegexModal.js index 4ddb9c7..2a63778 100644 --- a/frontend/src/hooks/useRegexModal.js +++ b/frontend/src/hooks/useRegexModal.js @@ -79,12 +79,19 @@ export const useRegexModal = (initialPattern, onSave) => { } try { + // Clean tests to only include saved data + const cleanTests = tests.map((test, index) => ({ + id: test.id || index + 1, + input: test.input, + expected: test.expected + })); + const data = { name, pattern: patternValue, description, tags, - tests + tests: cleanTests }; if (initialPattern && !isCloning) { @@ -111,15 +118,16 @@ export const useRegexModal = (initialPattern, onSave) => { const handleRunTests = useCallback( async (pattern, tests) => { try { - const updatedTests = await runTests(pattern, tests); - if (updatedTests) { - setTests(updatedTests); - } + const testResults = await runTests(pattern, tests); + // We don't update the tests state with results + // Results are only used for display, not saved + return testResults; } catch (error) { console.error('Error running tests:', error); Alert.error( error.message || 'Failed to run tests. Please try again.' ); + return null; } }, [runTests] diff --git a/frontend/src/hooks/useRegexTesting.js b/frontend/src/hooks/useRegexTesting.js index 2011a2a..96fedd5 100644 --- a/frontend/src/hooks/useRegexTesting.js +++ b/frontend/src/hooks/useRegexTesting.js @@ -34,14 +34,12 @@ export const useRegexTesting = onUpdateTests => { } ); - // Update tests through the callback - if (onUpdateTests) { - onUpdateTests(result.tests); - } + // Return the test results (with match information) + // Don't save these results, just return them for display return result.tests; } else { Alert.error(result.message || 'Failed to run tests'); - return tests; + return null; } } catch (error) { console.error('Error running tests:', error);