erdo 0.1.7__py3-none-any.whl → 0.1.9__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.
Potentially problematic release.
This version of erdo might be problematic. Click here for more details.
- erdo/__init__.py +8 -10
- erdo/_generated/actions/codeexec.py +4 -2
- erdo/_generated/condition/__init__.py +145 -145
- erdo/_generated/types.py +72 -72
- erdo/actions/__init__.py +4 -4
- erdo/config/__init__.py +2 -2
- erdo/formatting.py +279 -0
- erdo/invoke/__init__.py +2 -1
- erdo/invoke/client.py +23 -8
- erdo/invoke/invoke.py +255 -30
- erdo/sync/extractor.py +6 -0
- erdo/test/__init__.py +41 -0
- erdo/test/evaluate.py +272 -0
- erdo/test/runner.py +263 -0
- erdo/types.py +0 -311
- {erdo-0.1.7.dist-info → erdo-0.1.9.dist-info}/METADATA +96 -9
- {erdo-0.1.7.dist-info → erdo-0.1.9.dist-info}/RECORD +20 -17
- erdo/py.typed +0 -1
- {erdo-0.1.7.dist-info → erdo-0.1.9.dist-info}/WHEEL +0 -0
- {erdo-0.1.7.dist-info → erdo-0.1.9.dist-info}/entry_points.txt +0 -0
- {erdo-0.1.7.dist-info → erdo-0.1.9.dist-info}/licenses/LICENSE +0 -0
erdo/test/evaluate.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Evaluation functions for agent test assertions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def text_contains(value: Any, expected: str, case_sensitive: bool = True) -> bool:
|
|
9
|
+
"""Check if text contains the expected substring.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
value: The value to check (will be converted to string)
|
|
13
|
+
expected: The substring to look for
|
|
14
|
+
case_sensitive: Whether the match should be case-sensitive
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
True if the text contains the expected substring
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> text_contains("Hello World", "World")
|
|
21
|
+
True
|
|
22
|
+
>>> text_contains("Hello World", "world", case_sensitive=False)
|
|
23
|
+
True
|
|
24
|
+
>>> text_contains("Hello World", "Goodbye")
|
|
25
|
+
False
|
|
26
|
+
"""
|
|
27
|
+
text = str(value)
|
|
28
|
+
if not case_sensitive:
|
|
29
|
+
text = text.lower()
|
|
30
|
+
expected = expected.lower()
|
|
31
|
+
return expected in text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def text_equals(value: Any, expected: str, case_sensitive: bool = True) -> bool:
|
|
35
|
+
"""Check if text exactly equals the expected value.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
value: The value to check (will be converted to string)
|
|
39
|
+
expected: The exact string to match
|
|
40
|
+
case_sensitive: Whether the match should be case-sensitive
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the text exactly matches
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> text_equals("Hello", "Hello")
|
|
47
|
+
True
|
|
48
|
+
>>> text_equals("Hello", "hello", case_sensitive=False)
|
|
49
|
+
True
|
|
50
|
+
>>> text_equals("Hello World", "Hello")
|
|
51
|
+
False
|
|
52
|
+
"""
|
|
53
|
+
text = str(value)
|
|
54
|
+
if not case_sensitive:
|
|
55
|
+
text = text.lower()
|
|
56
|
+
expected = expected.lower()
|
|
57
|
+
return text == expected
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def text_matches(value: Any, pattern: str, flags: int = 0) -> bool:
|
|
61
|
+
"""Check if text matches a regular expression pattern.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
value: The value to check (will be converted to string)
|
|
65
|
+
pattern: The regex pattern to match
|
|
66
|
+
flags: Optional regex flags (e.g., re.IGNORECASE)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the text matches the pattern
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> text_matches("Hello 123", r"Hello \\d+")
|
|
73
|
+
True
|
|
74
|
+
>>> text_matches("hello world", r"^hello", re.IGNORECASE)
|
|
75
|
+
True
|
|
76
|
+
>>> text_matches("abc", r"\\d+")
|
|
77
|
+
False
|
|
78
|
+
"""
|
|
79
|
+
text = str(value)
|
|
80
|
+
return re.search(pattern, text, flags) is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def json_path_equals(data: Union[Dict, str], path: str, expected: Any) -> bool:
|
|
84
|
+
"""Check if a JSON path in the data equals the expected value.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data: Dictionary or JSON string to search
|
|
88
|
+
path: Dot-notation path (e.g., "user.name" or "items[0].id")
|
|
89
|
+
expected: The expected value at that path
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if the value at the path equals expected
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> data = {"user": {"name": "Alice", "age": 30}}
|
|
96
|
+
>>> json_path_equals(data, "user.name", "Alice")
|
|
97
|
+
True
|
|
98
|
+
>>> json_path_equals(data, "user.age", 30)
|
|
99
|
+
True
|
|
100
|
+
>>> json_path_equals(data, "user.name", "Bob")
|
|
101
|
+
False
|
|
102
|
+
|
|
103
|
+
Note:
|
|
104
|
+
Supports simple paths like "a.b.c" and array access like "items[0]"
|
|
105
|
+
For complex JSONPath queries, consider using a dedicated JSONPath library
|
|
106
|
+
"""
|
|
107
|
+
# Convert string to dict if needed
|
|
108
|
+
if isinstance(data, str):
|
|
109
|
+
try:
|
|
110
|
+
data = json.loads(data)
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Navigate the path
|
|
115
|
+
value = _get_json_path_value(data, path)
|
|
116
|
+
return value == expected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def json_path_exists(data: Union[Dict, str], path: str) -> bool:
|
|
120
|
+
"""Check if a JSON path exists in the data.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: Dictionary or JSON string to search
|
|
124
|
+
path: Dot-notation path (e.g., "user.name" or "items[0].id")
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the path exists (even if value is None)
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> data = {"user": {"name": "Alice"}}
|
|
131
|
+
>>> json_path_exists(data, "user.name")
|
|
132
|
+
True
|
|
133
|
+
>>> json_path_exists(data, "user.email")
|
|
134
|
+
False
|
|
135
|
+
"""
|
|
136
|
+
# Convert string to dict if needed
|
|
137
|
+
if isinstance(data, str):
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(data)
|
|
140
|
+
except json.JSONDecodeError:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
_get_json_path_value(data, path)
|
|
145
|
+
return True
|
|
146
|
+
except (KeyError, IndexError, TypeError):
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def has_dataset(
|
|
151
|
+
response: Any, dataset_id: Optional[str] = None, dataset_key: Optional[str] = None
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Check if a response includes a specific dataset.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
response: The invocation response
|
|
157
|
+
dataset_id: Optional dataset ID to check for
|
|
158
|
+
dataset_key: Optional dataset key to check for
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if the dataset is present in the response
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> # Check if any dataset is present
|
|
165
|
+
>>> has_dataset(response)
|
|
166
|
+
True
|
|
167
|
+
>>> # Check for specific dataset by ID
|
|
168
|
+
>>> has_dataset(response, dataset_id="abc-123")
|
|
169
|
+
True
|
|
170
|
+
>>> # Check for dataset by key
|
|
171
|
+
>>> has_dataset(response, dataset_key="sales_data")
|
|
172
|
+
True
|
|
173
|
+
"""
|
|
174
|
+
# Try to extract datasets from common response formats
|
|
175
|
+
datasets = []
|
|
176
|
+
|
|
177
|
+
# Check if response has datasets attribute
|
|
178
|
+
if hasattr(response, "datasets"):
|
|
179
|
+
datasets = response.datasets
|
|
180
|
+
# Check if response has events with dataset info
|
|
181
|
+
elif hasattr(response, "events"):
|
|
182
|
+
for event in response.events:
|
|
183
|
+
if isinstance(event, dict):
|
|
184
|
+
payload = event.get("payload", {})
|
|
185
|
+
if "datasets" in payload:
|
|
186
|
+
datasets.extend(payload["datasets"])
|
|
187
|
+
if "dataset" in payload:
|
|
188
|
+
datasets.append(payload["dataset"])
|
|
189
|
+
|
|
190
|
+
# Check if specific dataset is present
|
|
191
|
+
if dataset_id or dataset_key:
|
|
192
|
+
for dataset in datasets:
|
|
193
|
+
if isinstance(dataset, dict):
|
|
194
|
+
if dataset_id and dataset.get("id") == dataset_id:
|
|
195
|
+
return True
|
|
196
|
+
if dataset_key and dataset.get("key") == dataset_key:
|
|
197
|
+
return True
|
|
198
|
+
elif isinstance(dataset, str):
|
|
199
|
+
# Dataset might just be an ID string
|
|
200
|
+
if dataset_id and dataset == dataset_id:
|
|
201
|
+
return True
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# Check if any dataset is present
|
|
205
|
+
return len(datasets) > 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _get_json_path_value(data: Any, path: str) -> Any:
|
|
209
|
+
"""Navigate a JSON path and return the value.
|
|
210
|
+
|
|
211
|
+
Supports:
|
|
212
|
+
- Dot notation: "user.name"
|
|
213
|
+
- Array access: "items[0]"
|
|
214
|
+
- Mixed: "users[0].name"
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
KeyError, IndexError, TypeError if path doesn't exist
|
|
218
|
+
"""
|
|
219
|
+
if not path:
|
|
220
|
+
return data
|
|
221
|
+
|
|
222
|
+
parts = _parse_path(path)
|
|
223
|
+
current = data
|
|
224
|
+
|
|
225
|
+
for part in parts:
|
|
226
|
+
if isinstance(part, int):
|
|
227
|
+
# Array index
|
|
228
|
+
current = current[part]
|
|
229
|
+
else:
|
|
230
|
+
# Object key
|
|
231
|
+
current = current[part]
|
|
232
|
+
|
|
233
|
+
return current
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_path(path: str) -> List[Union[str, int]]:
|
|
237
|
+
"""Parse a JSON path into parts.
|
|
238
|
+
|
|
239
|
+
Examples:
|
|
240
|
+
"user.name" → ["user", "name"]
|
|
241
|
+
"items[0]" → ["items", 0]
|
|
242
|
+
"users[0].name" → ["users", 0, "name"]
|
|
243
|
+
"""
|
|
244
|
+
parts: List[Union[str, int]] = []
|
|
245
|
+
current = ""
|
|
246
|
+
|
|
247
|
+
i = 0
|
|
248
|
+
while i < len(path):
|
|
249
|
+
char = path[i]
|
|
250
|
+
|
|
251
|
+
if char == ".":
|
|
252
|
+
if current:
|
|
253
|
+
parts.append(current)
|
|
254
|
+
current = ""
|
|
255
|
+
elif char == "[":
|
|
256
|
+
if current:
|
|
257
|
+
parts.append(current)
|
|
258
|
+
current = ""
|
|
259
|
+
# Find closing bracket
|
|
260
|
+
j = path.index("]", i)
|
|
261
|
+
index = int(path[i + 1 : j])
|
|
262
|
+
parts.append(index)
|
|
263
|
+
i = j
|
|
264
|
+
else:
|
|
265
|
+
current += char
|
|
266
|
+
|
|
267
|
+
i += 1
|
|
268
|
+
|
|
269
|
+
if current:
|
|
270
|
+
parts.append(current)
|
|
271
|
+
|
|
272
|
+
return parts
|
erdo/test/runner.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Agent test runner that discovers and runs agent_test_* functions in parallel.
|
|
2
|
+
|
|
3
|
+
This module provides a test runner that:
|
|
4
|
+
1. Discovers all agent_test_*() functions in a Python file
|
|
5
|
+
2. Invokes all agents in parallel (fast)
|
|
6
|
+
3. Runs assertions sequentially (already fast)
|
|
7
|
+
4. Reports results in a clean format
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python -m erdo.test.runner path/to/test_file.py
|
|
11
|
+
|
|
12
|
+
Or via CLI:
|
|
13
|
+
erdo agent-test path/to/test_file.py
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import concurrent.futures
|
|
17
|
+
import importlib.util
|
|
18
|
+
import inspect
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
import traceback
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Callable, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TestResult:
|
|
29
|
+
"""Result from running a single test."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
passed: bool
|
|
33
|
+
duration: float
|
|
34
|
+
error: Optional[str] = None
|
|
35
|
+
assertion_error: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TestSummary:
|
|
40
|
+
"""Summary of all test results."""
|
|
41
|
+
|
|
42
|
+
total: int
|
|
43
|
+
passed: int
|
|
44
|
+
failed: int
|
|
45
|
+
duration: float
|
|
46
|
+
results: List[TestResult]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def discover_tests(file_path: str) -> List[Tuple[str, Callable]]:
|
|
50
|
+
"""Discover all agent_test_* functions in a Python file.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
file_path: Path to the Python file
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of (test_name, test_function) tuples
|
|
57
|
+
"""
|
|
58
|
+
# Load the module
|
|
59
|
+
spec = importlib.util.spec_from_file_location("test_module", file_path)
|
|
60
|
+
if not spec or not spec.loader:
|
|
61
|
+
raise ImportError(f"Cannot load module from {file_path}")
|
|
62
|
+
|
|
63
|
+
module = importlib.util.module_from_spec(spec)
|
|
64
|
+
sys.modules["test_module"] = module
|
|
65
|
+
spec.loader.exec_module(module)
|
|
66
|
+
|
|
67
|
+
# Find all agent_test_* functions
|
|
68
|
+
tests = []
|
|
69
|
+
for name, obj in inspect.getmembers(module):
|
|
70
|
+
if name.startswith("agent_test_") and callable(obj):
|
|
71
|
+
tests.append((name, obj))
|
|
72
|
+
|
|
73
|
+
return tests
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_test(test_name: str, test_func: Callable) -> TestResult:
|
|
77
|
+
"""Run a single test function.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
test_name: Name of the test
|
|
81
|
+
test_func: Test function to run
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
TestResult with outcome
|
|
85
|
+
"""
|
|
86
|
+
start_time = time.time()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Run the test function
|
|
90
|
+
test_func()
|
|
91
|
+
|
|
92
|
+
# If we get here, test passed
|
|
93
|
+
duration = time.time() - start_time
|
|
94
|
+
return TestResult(name=test_name, passed=True, duration=duration)
|
|
95
|
+
|
|
96
|
+
except AssertionError as e:
|
|
97
|
+
# Assertion failed
|
|
98
|
+
duration = time.time() - start_time
|
|
99
|
+
return TestResult(
|
|
100
|
+
name=test_name,
|
|
101
|
+
passed=False,
|
|
102
|
+
duration=duration,
|
|
103
|
+
assertion_error=str(e),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
except Exception:
|
|
107
|
+
# Other error (invocation failed, etc.)
|
|
108
|
+
duration = time.time() - start_time
|
|
109
|
+
tb = traceback.format_exc()
|
|
110
|
+
return TestResult(name=test_name, passed=False, duration=duration, error=tb)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_tests_parallel(
|
|
114
|
+
tests: List[Tuple[str, Callable]], max_workers: Optional[int] = None
|
|
115
|
+
) -> TestSummary:
|
|
116
|
+
"""Run all tests in parallel.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
tests: List of (test_name, test_function) tuples
|
|
120
|
+
max_workers: Maximum number of parallel workers
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
TestSummary with all results
|
|
124
|
+
"""
|
|
125
|
+
start_time = time.time()
|
|
126
|
+
results = []
|
|
127
|
+
|
|
128
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
129
|
+
# Submit all tests
|
|
130
|
+
futures = {executor.submit(run_test, name, func): name for name, func in tests}
|
|
131
|
+
|
|
132
|
+
# Collect results as they complete
|
|
133
|
+
for future in concurrent.futures.as_completed(futures):
|
|
134
|
+
result = future.result()
|
|
135
|
+
results.append(result)
|
|
136
|
+
|
|
137
|
+
duration = time.time() - start_time
|
|
138
|
+
passed = sum(1 for r in results if r.passed)
|
|
139
|
+
failed = len(results) - passed
|
|
140
|
+
|
|
141
|
+
return TestSummary(
|
|
142
|
+
total=len(results),
|
|
143
|
+
passed=passed,
|
|
144
|
+
failed=failed,
|
|
145
|
+
duration=duration,
|
|
146
|
+
results=sorted(
|
|
147
|
+
results, key=lambda r: r.name
|
|
148
|
+
), # Sort by name for consistent output
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def print_summary(summary: TestSummary, verbose: bool = False):
|
|
153
|
+
"""Print test results summary.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
summary: TestSummary to print
|
|
157
|
+
verbose: Whether to show detailed error messages
|
|
158
|
+
"""
|
|
159
|
+
print()
|
|
160
|
+
print("=" * 70)
|
|
161
|
+
print("AGENT TEST RESULTS")
|
|
162
|
+
print("=" * 70)
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
# Print each test result
|
|
166
|
+
for result in summary.results:
|
|
167
|
+
if result.passed:
|
|
168
|
+
print(f"✅ {result.name} ({result.duration:.2f}s)")
|
|
169
|
+
else:
|
|
170
|
+
print(f"❌ {result.name} ({result.duration:.2f}s)")
|
|
171
|
+
if verbose:
|
|
172
|
+
if result.assertion_error:
|
|
173
|
+
print(f" Assertion: {result.assertion_error}")
|
|
174
|
+
if result.error:
|
|
175
|
+
print(f" Error:\n{result.error}")
|
|
176
|
+
|
|
177
|
+
# Print summary
|
|
178
|
+
print()
|
|
179
|
+
print("-" * 70)
|
|
180
|
+
print(
|
|
181
|
+
f"Total: {summary.total} | "
|
|
182
|
+
f"Passed: {summary.passed} | "
|
|
183
|
+
f"Failed: {summary.failed} | "
|
|
184
|
+
f"Duration: {summary.duration:.2f}s"
|
|
185
|
+
)
|
|
186
|
+
print("-" * 70)
|
|
187
|
+
print()
|
|
188
|
+
|
|
189
|
+
# Print failed test details if not verbose
|
|
190
|
+
if not verbose and summary.failed > 0:
|
|
191
|
+
print("Failed tests:")
|
|
192
|
+
for result in summary.results:
|
|
193
|
+
if not result.passed:
|
|
194
|
+
print(f"\n{result.name}:")
|
|
195
|
+
if result.assertion_error:
|
|
196
|
+
print(f" {result.assertion_error}")
|
|
197
|
+
if result.error:
|
|
198
|
+
# Print just the last line of the error
|
|
199
|
+
error_lines = result.error.strip().split("\n")
|
|
200
|
+
print(f" {error_lines[-1]}")
|
|
201
|
+
print("\n(Use --verbose for full error traces)")
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main(
|
|
206
|
+
file_path: str, verbose: bool = False, max_workers: Optional[int] = None
|
|
207
|
+
) -> int:
|
|
208
|
+
"""Main entry point for the test runner.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
file_path: Path to the test file
|
|
212
|
+
verbose: Whether to show detailed output
|
|
213
|
+
max_workers: Maximum number of parallel workers
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Exit code (0 for success, 1 for failure)
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
# Check file exists
|
|
220
|
+
if not Path(file_path).exists():
|
|
221
|
+
print(f"Error: File not found: {file_path}")
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
# Discover tests
|
|
225
|
+
print(f"Discovering tests in {file_path}...")
|
|
226
|
+
tests = discover_tests(file_path)
|
|
227
|
+
|
|
228
|
+
if not tests:
|
|
229
|
+
print("No tests found (looking for agent_test_* functions)")
|
|
230
|
+
return 1
|
|
231
|
+
|
|
232
|
+
print(f"Found {len(tests)} tests")
|
|
233
|
+
print()
|
|
234
|
+
|
|
235
|
+
# Run tests
|
|
236
|
+
print("Running tests in parallel...")
|
|
237
|
+
summary = run_tests_parallel(tests, max_workers=max_workers)
|
|
238
|
+
|
|
239
|
+
# Print results
|
|
240
|
+
print_summary(summary, verbose=verbose)
|
|
241
|
+
|
|
242
|
+
# Return exit code
|
|
243
|
+
return 0 if summary.failed == 0 else 1
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"Error running tests: {e}")
|
|
247
|
+
traceback.print_exc()
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
import argparse
|
|
253
|
+
|
|
254
|
+
parser = argparse.ArgumentParser(description="Run agent tests")
|
|
255
|
+
parser.add_argument("file", help="Path to test file")
|
|
256
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"-j", "--jobs", type=int, default=None, help="Number of parallel jobs"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
args = parser.parse_args()
|
|
262
|
+
|
|
263
|
+
sys.exit(main(args.file, verbose=args.verbose, max_workers=args.jobs))
|