kailash 0.6.6__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +35 -5
- kailash/access_control.py +64 -46
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/api/workflow_api.py +34 -3
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +57 -18
- kailash/middleware/communication/api_gateway.py +23 -3
- kailash/middleware/communication/realtime.py +83 -0
- kailash/middleware/core/agent_ui.py +1 -1
- kailash/middleware/gateway/storage_backends.py +393 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/cli/__init__.py +5 -0
- kailash/nexus/cli/__main__.py +6 -0
- kailash/nexus/cli/main.py +176 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +8 -5
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base.py +29 -5
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/code/python.py +50 -6
- kailash/nodes/data/async_sql.py +90 -0
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/security/behavior_analysis.py +414 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/access_controlled.py +9 -7
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/runtime/runner.py +6 -4
- kailash/runtime/testing.py +1 -1
- kailash/security.py +22 -3
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +522 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +293 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +382 -15
- kailash/workflow/cyclic_runner.py +102 -10
- kailash/workflow/validation.py +144 -8
- kailash/workflow/visualization.py +99 -27
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
- kailash/workflow/builder_improvements.py +0 -207
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
@@ -483,7 +483,23 @@ class DataTransformer(Node):
|
|
483
483
|
f"Error executing transformation '{transform_str}': {str(e)}\n{tb}"
|
484
484
|
)
|
485
485
|
|
486
|
-
|
486
|
+
# Validate result before returning to prevent data type issues
|
487
|
+
from kailash.utils.data_validation import DataTypeValidator
|
488
|
+
|
489
|
+
# Log result type and structure for debugging
|
490
|
+
self.logger.debug(f"DataTransformer result type: {type(result)}")
|
491
|
+
if isinstance(result, dict):
|
492
|
+
self.logger.debug(f"DataTransformer result keys: {list(result.keys())}")
|
493
|
+
elif isinstance(result, list) and len(result) > 0:
|
494
|
+
self.logger.debug(
|
495
|
+
f"DataTransformer result list length: {len(result)}, first item type: {type(result[0])}"
|
496
|
+
)
|
497
|
+
|
498
|
+
output = {"result": result}
|
499
|
+
node_id = getattr(self, "node_id", getattr(self, "id", "DataTransformer"))
|
500
|
+
validated_output = DataTypeValidator.validate_node_output(node_id, output)
|
501
|
+
|
502
|
+
return validated_output
|
487
503
|
|
488
504
|
|
489
505
|
@register_node()
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Validation and test execution framework for Kailash nodes.
|
2
|
+
|
3
|
+
This module provides comprehensive validation capabilities for test-driven
|
4
|
+
development, including code validation, test execution, and schema validation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .test_executor import ValidationLevel, ValidationResult, ValidationTestExecutor
|
8
|
+
from .validation_nodes import (
|
9
|
+
CodeValidationNode,
|
10
|
+
ValidationTestSuiteExecutorNode,
|
11
|
+
WorkflowValidationNode,
|
12
|
+
)
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"ValidationTestExecutor",
|
16
|
+
"ValidationLevel",
|
17
|
+
"ValidationResult",
|
18
|
+
"CodeValidationNode",
|
19
|
+
"WorkflowValidationNode",
|
20
|
+
"ValidationTestSuiteExecutorNode",
|
21
|
+
]
|
@@ -0,0 +1,532 @@
|
|
1
|
+
"""Test execution framework for validation-based convergence.
|
2
|
+
|
3
|
+
This module provides a robust test execution framework that supports multiple
|
4
|
+
validation levels, sandboxed execution, and detailed error analysis.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import ast
|
8
|
+
import importlib
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
import subprocess
|
12
|
+
import sys
|
13
|
+
import tempfile
|
14
|
+
import time
|
15
|
+
import traceback
|
16
|
+
from dataclasses import dataclass, field
|
17
|
+
from enum import Enum
|
18
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
19
|
+
|
20
|
+
|
21
|
+
class ValidationLevel(Enum):
|
22
|
+
"""Levels of validation from basic to comprehensive."""
|
23
|
+
|
24
|
+
SYNTAX = "syntax" # Code compiles/parses
|
25
|
+
IMPORTS = "imports" # Imports resolve
|
26
|
+
SEMANTIC = "semantic" # Code runs without errors
|
27
|
+
FUNCTIONAL = "functional" # Code produces expected outputs
|
28
|
+
INTEGRATION = "integration" # Code works with other components
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class ValidationResult:
|
33
|
+
"""Result of a validation test."""
|
34
|
+
|
35
|
+
level: ValidationLevel
|
36
|
+
passed: bool
|
37
|
+
test_name: str
|
38
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
39
|
+
error: Optional[str] = None
|
40
|
+
suggestions: List[str] = field(default_factory=list)
|
41
|
+
execution_time: float = 0.0
|
42
|
+
|
43
|
+
|
44
|
+
class ValidationTestExecutor:
|
45
|
+
"""Execute validation tests for IterativeLLMAgent deliverables."""
|
46
|
+
|
47
|
+
def __init__(self, sandbox_enabled: bool = True, timeout: int = 30):
|
48
|
+
"""Initialize test executor.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
sandbox_enabled: Whether to use sandboxed execution
|
52
|
+
timeout: Maximum execution time in seconds
|
53
|
+
"""
|
54
|
+
self.sandbox_enabled = sandbox_enabled
|
55
|
+
self.timeout = timeout
|
56
|
+
|
57
|
+
def validate_python_syntax(self, code: str) -> ValidationResult:
|
58
|
+
"""Validate Python code syntax.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
code: Python code to validate
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
ValidationResult with syntax validation details
|
65
|
+
"""
|
66
|
+
start = time.time()
|
67
|
+
|
68
|
+
try:
|
69
|
+
tree = ast.parse(code)
|
70
|
+
|
71
|
+
# Additional checks
|
72
|
+
has_imports = any(
|
73
|
+
isinstance(node, (ast.Import, ast.ImportFrom))
|
74
|
+
for node in ast.walk(tree)
|
75
|
+
)
|
76
|
+
has_functions = any(
|
77
|
+
isinstance(node, ast.FunctionDef) for node in ast.walk(tree)
|
78
|
+
)
|
79
|
+
has_classes = any(isinstance(node, ast.ClassDef) for node in ast.walk(tree))
|
80
|
+
|
81
|
+
return ValidationResult(
|
82
|
+
level=ValidationLevel.SYNTAX,
|
83
|
+
passed=True,
|
84
|
+
test_name="python_syntax",
|
85
|
+
details={
|
86
|
+
"code_length": len(code),
|
87
|
+
"line_count": len(code.splitlines()),
|
88
|
+
"has_imports": has_imports,
|
89
|
+
"has_functions": has_functions,
|
90
|
+
"has_classes": has_classes,
|
91
|
+
},
|
92
|
+
execution_time=time.time() - start,
|
93
|
+
)
|
94
|
+
|
95
|
+
except SyntaxError as e:
|
96
|
+
return ValidationResult(
|
97
|
+
level=ValidationLevel.SYNTAX,
|
98
|
+
passed=False,
|
99
|
+
test_name="python_syntax",
|
100
|
+
details={
|
101
|
+
"error_line": e.lineno,
|
102
|
+
"error_offset": e.offset,
|
103
|
+
"error_text": e.text,
|
104
|
+
},
|
105
|
+
error=str(e),
|
106
|
+
suggestions=[
|
107
|
+
"Check for missing colons after if/for/def/class statements",
|
108
|
+
"Verify proper indentation (use 4 spaces)",
|
109
|
+
"Ensure all parentheses/brackets/braces are balanced",
|
110
|
+
f"Error at line {e.lineno}: {e.msg}",
|
111
|
+
],
|
112
|
+
execution_time=time.time() - start,
|
113
|
+
)
|
114
|
+
|
115
|
+
def validate_imports(self, code: str) -> ValidationResult:
|
116
|
+
"""Verify all imports in the code can be resolved.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
code: Python code containing imports
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
ValidationResult with import validation details
|
123
|
+
"""
|
124
|
+
start = time.time()
|
125
|
+
|
126
|
+
# Extract import statements
|
127
|
+
try:
|
128
|
+
tree = ast.parse(code)
|
129
|
+
except SyntaxError:
|
130
|
+
return ValidationResult(
|
131
|
+
level=ValidationLevel.IMPORTS,
|
132
|
+
passed=False,
|
133
|
+
test_name="import_validation",
|
134
|
+
error="Cannot validate imports - syntax error in code",
|
135
|
+
execution_time=time.time() - start,
|
136
|
+
)
|
137
|
+
|
138
|
+
imports = []
|
139
|
+
for node in ast.walk(tree):
|
140
|
+
if isinstance(node, ast.Import):
|
141
|
+
for alias in node.names:
|
142
|
+
imports.append(alias.name)
|
143
|
+
elif isinstance(node, ast.ImportFrom):
|
144
|
+
module = node.module or ""
|
145
|
+
for alias in node.names:
|
146
|
+
if alias.name == "*":
|
147
|
+
imports.append(f"{module}")
|
148
|
+
else:
|
149
|
+
imports.append(
|
150
|
+
f"{module}.{alias.name}" if module else alias.name
|
151
|
+
)
|
152
|
+
|
153
|
+
# Check each import
|
154
|
+
unresolved = []
|
155
|
+
resolved = []
|
156
|
+
|
157
|
+
for imp in imports:
|
158
|
+
module_name = imp.split(".")[0]
|
159
|
+
try:
|
160
|
+
if module_name in sys.modules:
|
161
|
+
resolved.append(imp)
|
162
|
+
else:
|
163
|
+
# Try to import
|
164
|
+
importlib.import_module(module_name)
|
165
|
+
resolved.append(imp)
|
166
|
+
except ImportError as e:
|
167
|
+
unresolved.append({"import": imp, "error": str(e)})
|
168
|
+
|
169
|
+
passed = len(unresolved) == 0
|
170
|
+
|
171
|
+
return ValidationResult(
|
172
|
+
level=ValidationLevel.IMPORTS,
|
173
|
+
passed=passed,
|
174
|
+
test_name="import_validation",
|
175
|
+
details={
|
176
|
+
"total_imports": len(imports),
|
177
|
+
"resolved": len(resolved),
|
178
|
+
"unresolved": len(unresolved),
|
179
|
+
"unresolved_list": unresolved,
|
180
|
+
},
|
181
|
+
error=(
|
182
|
+
f"{len(unresolved)} imports could not be resolved"
|
183
|
+
if unresolved
|
184
|
+
else None
|
185
|
+
),
|
186
|
+
suggestions=(
|
187
|
+
[
|
188
|
+
f"Install missing package: {u['import'].split('.')[0]}"
|
189
|
+
for u in unresolved
|
190
|
+
]
|
191
|
+
if unresolved
|
192
|
+
else []
|
193
|
+
),
|
194
|
+
execution_time=time.time() - start,
|
195
|
+
)
|
196
|
+
|
197
|
+
def execute_code_safely(
|
198
|
+
self, code: str, inputs: Dict[str, Any] = None
|
199
|
+
) -> ValidationResult:
|
200
|
+
"""Execute code in a safe environment and capture results.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
code: Python code to execute
|
204
|
+
inputs: Input variables for the code
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
ValidationResult with execution details
|
208
|
+
"""
|
209
|
+
start = time.time()
|
210
|
+
|
211
|
+
if inputs is None:
|
212
|
+
inputs = {}
|
213
|
+
|
214
|
+
if self.sandbox_enabled:
|
215
|
+
# Use subprocess for isolation
|
216
|
+
return self._execute_in_subprocess(code, inputs, start)
|
217
|
+
else:
|
218
|
+
# Direct execution (less safe)
|
219
|
+
return self._execute_directly(code, inputs, start)
|
220
|
+
|
221
|
+
def _execute_directly(
|
222
|
+
self, code: str, inputs: Dict[str, Any], start_time: float
|
223
|
+
) -> ValidationResult:
|
224
|
+
"""Execute code directly in current process."""
|
225
|
+
namespace = {"__builtins__": __builtins__, **inputs}
|
226
|
+
|
227
|
+
try:
|
228
|
+
exec(code, namespace)
|
229
|
+
|
230
|
+
# Extract results
|
231
|
+
results = {
|
232
|
+
k: v
|
233
|
+
for k, v in namespace.items()
|
234
|
+
if k not in inputs and not k.startswith("_")
|
235
|
+
}
|
236
|
+
|
237
|
+
return ValidationResult(
|
238
|
+
level=ValidationLevel.SEMANTIC,
|
239
|
+
passed=True,
|
240
|
+
test_name="code_execution",
|
241
|
+
details={
|
242
|
+
"output_keys": list(results.keys()),
|
243
|
+
"output_types": {k: type(v).__name__ for k, v in results.items()},
|
244
|
+
"execution_mode": "direct",
|
245
|
+
},
|
246
|
+
execution_time=time.time() - start_time,
|
247
|
+
)
|
248
|
+
|
249
|
+
except Exception as e:
|
250
|
+
tb = traceback.format_exc()
|
251
|
+
return ValidationResult(
|
252
|
+
level=ValidationLevel.SEMANTIC,
|
253
|
+
passed=False,
|
254
|
+
test_name="code_execution",
|
255
|
+
details={
|
256
|
+
"error_type": type(e).__name__,
|
257
|
+
"error_line": self._extract_error_line(tb),
|
258
|
+
"execution_mode": "direct",
|
259
|
+
},
|
260
|
+
error=str(e),
|
261
|
+
suggestions=self._get_error_suggestions(e, tb),
|
262
|
+
execution_time=time.time() - start_time,
|
263
|
+
)
|
264
|
+
|
265
|
+
def _execute_in_subprocess(
|
266
|
+
self, code: str, inputs: Dict[str, Any], start_time: float
|
267
|
+
) -> ValidationResult:
|
268
|
+
"""Execute code in isolated subprocess."""
|
269
|
+
# Create execution script
|
270
|
+
# Use repr to properly escape the code
|
271
|
+
exec_script = f"""
|
272
|
+
import json
|
273
|
+
import sys
|
274
|
+
|
275
|
+
# Load inputs
|
276
|
+
inputs = json.loads('{json.dumps(inputs)}')
|
277
|
+
namespace = {{'__builtins__': __builtins__, **inputs}}
|
278
|
+
|
279
|
+
# Execute code
|
280
|
+
code = {repr(code)}
|
281
|
+
try:
|
282
|
+
exec(code, namespace)
|
283
|
+
|
284
|
+
# Extract results
|
285
|
+
results = {{
|
286
|
+
k: str(type(v).__name__) if not isinstance(v, (int, float, str, bool, list, dict)) else v
|
287
|
+
for k, v in namespace.items()
|
288
|
+
if k not in inputs and not k.startswith('_')
|
289
|
+
}}
|
290
|
+
|
291
|
+
print(json.dumps({{"success": True, "results": results}}))
|
292
|
+
except Exception as e:
|
293
|
+
import traceback
|
294
|
+
print(json.dumps({{
|
295
|
+
"success": False,
|
296
|
+
"error": str(e),
|
297
|
+
"error_type": type(e).__name__,
|
298
|
+
"traceback": traceback.format_exc()
|
299
|
+
}}))
|
300
|
+
"""
|
301
|
+
|
302
|
+
# Write to temp file and execute
|
303
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
304
|
+
f.write(exec_script)
|
305
|
+
temp_path = f.name
|
306
|
+
|
307
|
+
try:
|
308
|
+
result = subprocess.run(
|
309
|
+
[sys.executable, temp_path],
|
310
|
+
capture_output=True,
|
311
|
+
text=True,
|
312
|
+
timeout=self.timeout,
|
313
|
+
)
|
314
|
+
|
315
|
+
if result.returncode == 0:
|
316
|
+
output = json.loads(result.stdout)
|
317
|
+
if output["success"]:
|
318
|
+
return ValidationResult(
|
319
|
+
level=ValidationLevel.SEMANTIC,
|
320
|
+
passed=True,
|
321
|
+
test_name="code_execution",
|
322
|
+
details={
|
323
|
+
"output_keys": list(output["results"].keys()),
|
324
|
+
"execution_mode": "subprocess",
|
325
|
+
},
|
326
|
+
execution_time=time.time() - start_time,
|
327
|
+
)
|
328
|
+
else:
|
329
|
+
return ValidationResult(
|
330
|
+
level=ValidationLevel.SEMANTIC,
|
331
|
+
passed=False,
|
332
|
+
test_name="code_execution",
|
333
|
+
details={
|
334
|
+
"error_type": output["error_type"],
|
335
|
+
"execution_mode": "subprocess",
|
336
|
+
},
|
337
|
+
error=output["error"],
|
338
|
+
suggestions=self._get_error_suggestions(
|
339
|
+
Exception(output["error"]), output.get("traceback", "")
|
340
|
+
),
|
341
|
+
execution_time=time.time() - start_time,
|
342
|
+
)
|
343
|
+
else:
|
344
|
+
return ValidationResult(
|
345
|
+
level=ValidationLevel.SEMANTIC,
|
346
|
+
passed=False,
|
347
|
+
test_name="code_execution",
|
348
|
+
error=f"Subprocess failed: {result.stderr}",
|
349
|
+
execution_time=time.time() - start_time,
|
350
|
+
)
|
351
|
+
|
352
|
+
except subprocess.TimeoutExpired:
|
353
|
+
return ValidationResult(
|
354
|
+
level=ValidationLevel.SEMANTIC,
|
355
|
+
passed=False,
|
356
|
+
test_name="code_execution",
|
357
|
+
error=f"Code execution timed out after {self.timeout} seconds",
|
358
|
+
suggestions=[
|
359
|
+
"Check for infinite loops",
|
360
|
+
"Optimize algorithm complexity",
|
361
|
+
],
|
362
|
+
execution_time=self.timeout,
|
363
|
+
)
|
364
|
+
finally:
|
365
|
+
os.unlink(temp_path)
|
366
|
+
|
367
|
+
def validate_output_schema(
|
368
|
+
self, output: Any, expected_schema: Dict
|
369
|
+
) -> ValidationResult:
|
370
|
+
"""Validate that output matches expected schema.
|
371
|
+
|
372
|
+
Args:
|
373
|
+
output: Actual output to validate
|
374
|
+
expected_schema: Expected schema definition
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
ValidationResult with schema validation details
|
378
|
+
"""
|
379
|
+
start = time.time()
|
380
|
+
|
381
|
+
def check_schema(data: Any, schema: Any, path: str = "") -> List[str]:
|
382
|
+
"""Recursively check schema compliance."""
|
383
|
+
errors = []
|
384
|
+
|
385
|
+
if isinstance(schema, type):
|
386
|
+
if not isinstance(data, schema):
|
387
|
+
errors.append(
|
388
|
+
f"{path}: expected {schema.__name__}, got {type(data).__name__}"
|
389
|
+
)
|
390
|
+
|
391
|
+
elif isinstance(schema, dict):
|
392
|
+
if not isinstance(data, dict):
|
393
|
+
errors.append(f"{path}: expected dict, got {type(data).__name__}")
|
394
|
+
else:
|
395
|
+
for key, expected_type in schema.items():
|
396
|
+
if key not in data:
|
397
|
+
errors.append(f"{path}.{key}: missing required key")
|
398
|
+
else:
|
399
|
+
errors.extend(
|
400
|
+
check_schema(data[key], expected_type, f"{path}.{key}")
|
401
|
+
)
|
402
|
+
|
403
|
+
elif isinstance(schema, list):
|
404
|
+
if not isinstance(data, list):
|
405
|
+
errors.append(f"{path}: expected list, got {type(data).__name__}")
|
406
|
+
elif len(schema) > 0:
|
407
|
+
# Check each item against first schema element
|
408
|
+
for i, item in enumerate(data):
|
409
|
+
errors.extend(check_schema(item, schema[0], f"{path}[{i}]"))
|
410
|
+
|
411
|
+
return errors
|
412
|
+
|
413
|
+
errors = check_schema(output, expected_schema)
|
414
|
+
|
415
|
+
return ValidationResult(
|
416
|
+
level=ValidationLevel.FUNCTIONAL,
|
417
|
+
passed=len(errors) == 0,
|
418
|
+
test_name="output_schema_validation",
|
419
|
+
details={"errors": errors, "error_count": len(errors)},
|
420
|
+
error="; ".join(errors) if errors else None,
|
421
|
+
suggestions=(
|
422
|
+
[
|
423
|
+
"Check data types match expected schema",
|
424
|
+
"Ensure all required keys are present",
|
425
|
+
"Verify list elements have correct structure",
|
426
|
+
]
|
427
|
+
if errors
|
428
|
+
else []
|
429
|
+
),
|
430
|
+
execution_time=time.time() - start,
|
431
|
+
)
|
432
|
+
|
433
|
+
def run_test_suite(
|
434
|
+
self, code: str, test_suite: List[Dict[str, Any]]
|
435
|
+
) -> ValidationResult:
|
436
|
+
"""Run a suite of tests against the code.
|
437
|
+
|
438
|
+
Args:
|
439
|
+
code: Code to test
|
440
|
+
test_suite: List of test cases
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
ValidationResult with test suite results
|
444
|
+
"""
|
445
|
+
start = time.time()
|
446
|
+
|
447
|
+
results = []
|
448
|
+
all_passed = True
|
449
|
+
|
450
|
+
for test in test_suite:
|
451
|
+
test_name = test.get("name", "unnamed_test")
|
452
|
+
test_type = test.get("type", "execution")
|
453
|
+
|
454
|
+
if test_type == "execution":
|
455
|
+
inputs = test.get("inputs", {})
|
456
|
+
expected_output = test.get("expected_output")
|
457
|
+
|
458
|
+
# Execute code
|
459
|
+
exec_result = self.execute_code_safely(code, inputs)
|
460
|
+
|
461
|
+
if exec_result.passed and expected_output:
|
462
|
+
# Validate output
|
463
|
+
namespace = {**inputs}
|
464
|
+
exec(code, namespace)
|
465
|
+
actual_output = {
|
466
|
+
k: v
|
467
|
+
for k, v in namespace.items()
|
468
|
+
if k not in inputs and not k.startswith("_")
|
469
|
+
}
|
470
|
+
|
471
|
+
# Simple comparison
|
472
|
+
test_passed = actual_output == expected_output
|
473
|
+
else:
|
474
|
+
test_passed = exec_result.passed
|
475
|
+
|
476
|
+
results.append(
|
477
|
+
{
|
478
|
+
"name": test_name,
|
479
|
+
"passed": test_passed,
|
480
|
+
"details": exec_result.details if not test_passed else {},
|
481
|
+
}
|
482
|
+
)
|
483
|
+
|
484
|
+
if not test_passed:
|
485
|
+
all_passed = False
|
486
|
+
|
487
|
+
return ValidationResult(
|
488
|
+
level=ValidationLevel.FUNCTIONAL,
|
489
|
+
passed=all_passed,
|
490
|
+
test_name="test_suite_execution",
|
491
|
+
details={
|
492
|
+
"total_tests": len(test_suite),
|
493
|
+
"passed": sum(1 for r in results if r["passed"]),
|
494
|
+
"failed": sum(1 for r in results if not r["passed"]),
|
495
|
+
"results": results,
|
496
|
+
},
|
497
|
+
error=(
|
498
|
+
f"{sum(1 for r in results if not r['passed'])} tests failed"
|
499
|
+
if not all_passed
|
500
|
+
else None
|
501
|
+
),
|
502
|
+
execution_time=time.time() - start,
|
503
|
+
)
|
504
|
+
|
505
|
+
def _extract_error_line(self, traceback_str: str) -> Optional[int]:
|
506
|
+
"""Extract line number from traceback."""
|
507
|
+
import re
|
508
|
+
|
509
|
+
match = re.search(r"line (\d+)", traceback_str)
|
510
|
+
return int(match.group(1)) if match else None
|
511
|
+
|
512
|
+
def _get_error_suggestions(self, error: Exception, traceback_str: str) -> List[str]:
|
513
|
+
"""Generate helpful suggestions based on error type."""
|
514
|
+
suggestions = []
|
515
|
+
|
516
|
+
if isinstance(error, NameError):
|
517
|
+
suggestions.append("Check variable names for typos")
|
518
|
+
suggestions.append("Ensure all variables are defined before use")
|
519
|
+
elif isinstance(error, TypeError):
|
520
|
+
suggestions.append("Check function arguments match expected parameters")
|
521
|
+
suggestions.append("Verify data types are compatible")
|
522
|
+
elif isinstance(error, AttributeError):
|
523
|
+
suggestions.append("Check object has the attribute/method you're calling")
|
524
|
+
suggestions.append("Verify correct import statements")
|
525
|
+
elif isinstance(error, KeyError):
|
526
|
+
suggestions.append("Check dictionary keys exist before accessing")
|
527
|
+
suggestions.append("Use .get() method with default values")
|
528
|
+
elif isinstance(error, IndexError):
|
529
|
+
suggestions.append("Check list/array bounds before accessing")
|
530
|
+
suggestions.append("Verify loop ranges are correct")
|
531
|
+
|
532
|
+
return suggestions
|