socialseed-e2e 0.1.0__py3-none-any.whl → 0.1.2__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.
- socialseed_e2e/__init__.py +184 -20
- socialseed_e2e/__version__.py +2 -2
- socialseed_e2e/cli.py +353 -190
- socialseed_e2e/core/base_page.py +368 -49
- socialseed_e2e/core/config_loader.py +15 -3
- socialseed_e2e/core/headers.py +11 -4
- socialseed_e2e/core/loaders.py +6 -4
- socialseed_e2e/core/test_orchestrator.py +2 -0
- socialseed_e2e/core/test_runner.py +487 -0
- socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
- socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
- socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
- socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
- socialseed_e2e/templates/data_schema.py.template +111 -70
- socialseed_e2e/templates/e2e.conf.template +19 -0
- socialseed_e2e/templates/service_page.py.template +82 -27
- socialseed_e2e/templates/test_module.py.template +21 -7
- socialseed_e2e/templates/verify_installation.py +192 -0
- socialseed_e2e/utils/__init__.py +29 -0
- socialseed_e2e/utils/ai_generator.py +463 -0
- socialseed_e2e/utils/pydantic_helpers.py +392 -0
- socialseed_e2e/utils/state_management.py +312 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
- socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
- socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -265,6 +265,16 @@ class ApiConfigLoader:
|
|
|
265
265
|
if path.exists():
|
|
266
266
|
return str(path)
|
|
267
267
|
|
|
268
|
+
# Priority 4.5: Search in any immediate subdirectory (useful for monorepos)
|
|
269
|
+
try:
|
|
270
|
+
for sub_item in Path.cwd().iterdir():
|
|
271
|
+
if sub_item.is_dir() and not sub_item.name.startswith("."):
|
|
272
|
+
sub_config = sub_item / "e2e.conf"
|
|
273
|
+
if sub_config.exists():
|
|
274
|
+
return str(sub_config)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass # Avoid crashes during directory iteration
|
|
277
|
+
|
|
268
278
|
# Priority 5: Global config in home directory
|
|
269
279
|
home_config = Path.home() / ".config" / "socialseed-e2e" / "default.conf"
|
|
270
280
|
if home_config.exists():
|
|
@@ -291,9 +301,11 @@ class ApiConfigLoader:
|
|
|
291
301
|
" 2. ./e2e.conf\n"
|
|
292
302
|
" 3. ./config/e2e.conf\n"
|
|
293
303
|
" 4. ./tests/e2e.conf\n"
|
|
294
|
-
" 5.
|
|
295
|
-
" 6.
|
|
296
|
-
" 7.
|
|
304
|
+
" 5. Immediate subdirectories (e.g., ./otrotest/e2e.conf)\n"
|
|
305
|
+
" 6. ~/.config/socialseed-e2e/default.conf\n"
|
|
306
|
+
" 7. verify_services/api.conf (legacy)\n"
|
|
307
|
+
" 8. ./api.conf (legacy)\n\n"
|
|
308
|
+
f"Current directory: {os.getcwd()}\n\n"
|
|
297
309
|
"To create a default configuration, run:\n"
|
|
298
310
|
" e2e init\n\n"
|
|
299
311
|
"Or set the E2E_CONFIG_PATH environment variable:\n"
|
socialseed_e2e/core/headers.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"""Header definitions for API requests."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
2
4
|
|
|
3
5
|
DEFAULT_JSON_HEADERS: Dict[str, str] = {
|
|
4
6
|
"Content-Type": "application/json",
|
|
@@ -6,14 +8,19 @@ DEFAULT_JSON_HEADERS: Dict[str, str] = {
|
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
DEFAULT_BROWSER_HEADERS: Dict[str, str] = {
|
|
9
|
-
"User-Agent":
|
|
11
|
+
"User-Agent": (
|
|
12
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
13
|
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
14
|
+
),
|
|
10
15
|
"Accept-Language": "en-US,en;q=0.9",
|
|
11
16
|
"Connection": "keep-alive",
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
|
|
15
|
-
def get_combined_headers(
|
|
16
|
-
|
|
20
|
+
def get_combined_headers(
|
|
21
|
+
custom_headers: Optional[Dict[str, str]] = None,
|
|
22
|
+
) -> Dict[str, str]:
|
|
23
|
+
"""Combine default headers with custom ones."""
|
|
17
24
|
headers = {**DEFAULT_JSON_HEADERS, **DEFAULT_BROWSER_HEADERS}
|
|
18
25
|
if custom_headers:
|
|
19
26
|
headers.update(custom_headers)
|
socialseed_e2e/core/loaders.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
"""Module loaders for dynamic test discovery."""
|
|
2
|
+
|
|
1
3
|
import importlib.util
|
|
2
4
|
import sys
|
|
3
5
|
from pathlib import Path
|
|
4
|
-
from typing import Callable, List, Optional
|
|
6
|
+
from typing import Callable, List, Optional, cast
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class ModuleLoader:
|
|
@@ -9,7 +11,7 @@ class ModuleLoader:
|
|
|
9
11
|
|
|
10
12
|
@staticmethod
|
|
11
13
|
def load_runnable_from_file(file_path: Path, function_name: str = "run") -> Optional[Callable]:
|
|
12
|
-
"""
|
|
14
|
+
"""Load a specific function from a python file."""
|
|
13
15
|
if not file_path.exists() or file_path.suffix != ".py":
|
|
14
16
|
return None
|
|
15
17
|
|
|
@@ -25,7 +27,7 @@ class ModuleLoader:
|
|
|
25
27
|
|
|
26
28
|
func = getattr(module, function_name, None)
|
|
27
29
|
if callable(func):
|
|
28
|
-
return func
|
|
30
|
+
return cast(Callable, func)
|
|
29
31
|
except Exception as e:
|
|
30
32
|
print(f"Error loading {file_path}: {e}")
|
|
31
33
|
|
|
@@ -33,7 +35,7 @@ class ModuleLoader:
|
|
|
33
35
|
|
|
34
36
|
def discover_runnables(self, root_path: Path, pattern: str = "*.py") -> List[Callable]:
|
|
35
37
|
"""Discovers all runnable modules in a directory matching a pattern."""
|
|
36
|
-
runnables = []
|
|
38
|
+
runnables: List[Callable] = []
|
|
37
39
|
if not root_path.exists() or not root_path.is_dir():
|
|
38
40
|
return runnables
|
|
39
41
|
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""Test runner implementation for socialseed-e2e framework.
|
|
2
|
+
|
|
3
|
+
This module provides the actual test execution logic for the e2e run command.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
|
|
14
|
+
|
|
15
|
+
from playwright.sync_api import sync_playwright
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from socialseed_e2e import BasePage
|
|
22
|
+
from socialseed_e2e.core.config_loader import ApiConfigLoader, ServiceConfig, get_service_config
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TestResult:
|
|
29
|
+
"""Result of a single test execution."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
service: str
|
|
33
|
+
status: str # "passed", "failed", "skipped", "error"
|
|
34
|
+
duration_ms: float = 0.0
|
|
35
|
+
error_message: str = ""
|
|
36
|
+
error_traceback: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TestSuiteResult:
|
|
41
|
+
"""Result of a complete test suite execution."""
|
|
42
|
+
|
|
43
|
+
total: int = 0
|
|
44
|
+
passed: int = 0
|
|
45
|
+
failed: int = 0
|
|
46
|
+
skipped: int = 0
|
|
47
|
+
errors: int = 0
|
|
48
|
+
total_duration_ms: float = 0.0
|
|
49
|
+
results: List[TestResult] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def success_rate(self) -> float:
|
|
53
|
+
if self.total == 0:
|
|
54
|
+
return 0.0
|
|
55
|
+
return (self.passed / self.total) * 100
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestDiscoveryError(Exception):
|
|
59
|
+
"""Error during test discovery."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestExecutionError(Exception):
|
|
65
|
+
"""Error during test execution."""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def discover_services(services_path: Path = Path("services")) -> List[str]:
|
|
71
|
+
"""Discover all services with test modules.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
services_path: Path to services directory
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of service names
|
|
78
|
+
"""
|
|
79
|
+
if not services_path.exists():
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
services = []
|
|
83
|
+
for item in services_path.iterdir():
|
|
84
|
+
if item.is_dir() and not item.name.startswith("__"):
|
|
85
|
+
# Check if it has a modules directory
|
|
86
|
+
modules_path = item / "modules"
|
|
87
|
+
if modules_path.exists():
|
|
88
|
+
services.append(item.name)
|
|
89
|
+
|
|
90
|
+
return sorted(services)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def discover_test_modules(service_path: Path) -> List[Path]:
|
|
94
|
+
"""Discover all test modules in a service.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
service_path: Path to service directory
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of test module paths, sorted by filename
|
|
101
|
+
"""
|
|
102
|
+
modules_path = service_path / "modules"
|
|
103
|
+
if not modules_path.exists():
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
# Find modules with pattern _XX_*.py or XX_*.py
|
|
107
|
+
modules = []
|
|
108
|
+
for item in modules_path.iterdir():
|
|
109
|
+
if item.is_file() and item.suffix == ".py" and not item.name.startswith("__"):
|
|
110
|
+
# Check if it matches test module pattern
|
|
111
|
+
name = item.name
|
|
112
|
+
if name.startswith("_") and len(name) > 3 and name[1:3].isdigit():
|
|
113
|
+
modules.append(item)
|
|
114
|
+
elif len(name) > 2 and name[:2].isdigit():
|
|
115
|
+
modules.append(item)
|
|
116
|
+
|
|
117
|
+
# Sort by filename
|
|
118
|
+
return sorted(modules, key=lambda p: p.name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_test_module(module_path: Path) -> Optional[Callable]:
|
|
122
|
+
"""Load a test module and return its run function.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
module_path: Path to test module
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The run function from the module, or None if not found
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
# Create a module spec
|
|
132
|
+
module_name = f"test_module_{module_path.stem}"
|
|
133
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
134
|
+
|
|
135
|
+
if spec is None or spec.loader is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Load the module
|
|
139
|
+
module = importlib.util.module_from_spec(spec)
|
|
140
|
+
sys.modules[module_name] = module
|
|
141
|
+
spec.loader.exec_module(module)
|
|
142
|
+
|
|
143
|
+
# Get the run function
|
|
144
|
+
if hasattr(module, "run"):
|
|
145
|
+
return module.run
|
|
146
|
+
|
|
147
|
+
return None
|
|
148
|
+
except Exception as e:
|
|
149
|
+
console.print(f"[red]Error loading module {module_path}: {e}[/red]")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_page_class(service_name: str, service_path: Path) -> Optional[Type[BasePage]]:
|
|
154
|
+
"""Create or find the Page class for a service.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
service_name: Name of the service
|
|
158
|
+
service_path: Path to service directory
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Page class, or None if not found
|
|
162
|
+
"""
|
|
163
|
+
# Add project root to sys.path for imports
|
|
164
|
+
project_root = service_path.parent.parent.absolute()
|
|
165
|
+
if str(project_root) not in sys.path:
|
|
166
|
+
sys.path.insert(0, str(project_root))
|
|
167
|
+
|
|
168
|
+
# Try to find page file
|
|
169
|
+
page_files = list(service_path.glob("*_page.py"))
|
|
170
|
+
if not page_files:
|
|
171
|
+
# Try with service name
|
|
172
|
+
page_file = service_path / f"{service_name}_page.py"
|
|
173
|
+
if not page_file.exists():
|
|
174
|
+
return None
|
|
175
|
+
page_files = [page_file]
|
|
176
|
+
|
|
177
|
+
page_file = page_files[0]
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Load the page module
|
|
181
|
+
module_name = f"page_{service_name}"
|
|
182
|
+
spec = importlib.util.spec_from_file_location(module_name, page_file)
|
|
183
|
+
|
|
184
|
+
if spec is None or spec.loader is None:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
module = importlib.util.module_from_spec(spec)
|
|
188
|
+
sys.modules[module_name] = module
|
|
189
|
+
spec.loader.exec_module(module)
|
|
190
|
+
|
|
191
|
+
# Find the Page class (should be the one inheriting from BasePage)
|
|
192
|
+
for attr_name in dir(module):
|
|
193
|
+
attr = getattr(module, attr_name)
|
|
194
|
+
if isinstance(attr, type) and issubclass(attr, BasePage) and attr is not BasePage:
|
|
195
|
+
return attr
|
|
196
|
+
|
|
197
|
+
return None
|
|
198
|
+
except Exception as e:
|
|
199
|
+
console.print(f"[red]Error loading page class from {page_file}: {e}[/red]")
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def execute_single_test(module_path: Path, page: BasePage, service_name: str) -> TestResult:
|
|
204
|
+
"""Execute a single test module.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
module_path: Path to test module
|
|
208
|
+
page: Page instance to pass to test
|
|
209
|
+
service_name: Name of the service
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
TestResult with execution details
|
|
213
|
+
"""
|
|
214
|
+
start_time = time.time()
|
|
215
|
+
test_name = module_path.stem
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Load the test module
|
|
219
|
+
run_func = load_test_module(module_path)
|
|
220
|
+
|
|
221
|
+
if run_func is None:
|
|
222
|
+
return TestResult(
|
|
223
|
+
name=test_name,
|
|
224
|
+
service=service_name,
|
|
225
|
+
status="error",
|
|
226
|
+
error_message="No 'run' function found in module",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Execute the test
|
|
230
|
+
run_func(page)
|
|
231
|
+
|
|
232
|
+
duration = (time.time() - start_time) * 1000
|
|
233
|
+
return TestResult(
|
|
234
|
+
name=test_name, service=service_name, status="passed", duration_ms=duration
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except AssertionError as e:
|
|
238
|
+
duration = (time.time() - start_time) * 1000
|
|
239
|
+
return TestResult(
|
|
240
|
+
name=test_name,
|
|
241
|
+
service=service_name,
|
|
242
|
+
status="failed",
|
|
243
|
+
duration_ms=duration,
|
|
244
|
+
error_message=str(e),
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
duration = (time.time() - start_time) * 1000
|
|
248
|
+
return TestResult(
|
|
249
|
+
name=test_name,
|
|
250
|
+
service=service_name,
|
|
251
|
+
status="error",
|
|
252
|
+
duration_ms=duration,
|
|
253
|
+
error_message=str(e),
|
|
254
|
+
error_traceback=traceback.format_exc(),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_service_tests(
|
|
259
|
+
service_name: str,
|
|
260
|
+
service_config: Optional[Any],
|
|
261
|
+
services_path: Path = Path("services"),
|
|
262
|
+
specific_module: Optional[str] = None,
|
|
263
|
+
verbose: bool = False,
|
|
264
|
+
) -> TestSuiteResult:
|
|
265
|
+
"""Run all tests for a service.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
service_name: Name of the service
|
|
269
|
+
service_config: Service configuration
|
|
270
|
+
services_path: Path to services directory
|
|
271
|
+
specific_module: If specified, only run this module
|
|
272
|
+
verbose: Whether to show verbose output
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
TestSuiteResult with all test results
|
|
276
|
+
"""
|
|
277
|
+
service_path = services_path / service_name
|
|
278
|
+
suite_result = TestSuiteResult()
|
|
279
|
+
|
|
280
|
+
# Discover test modules
|
|
281
|
+
if specific_module:
|
|
282
|
+
# Find specific module
|
|
283
|
+
modules_path = service_path / "modules"
|
|
284
|
+
module_path = modules_path / specific_module
|
|
285
|
+
if not module_path.exists():
|
|
286
|
+
module_path = modules_path / f"_{specific_module}.py"
|
|
287
|
+
if not module_path.exists():
|
|
288
|
+
console.print(f"[red]Module '{specific_module}' not found[/red]")
|
|
289
|
+
return suite_result
|
|
290
|
+
test_modules = [module_path]
|
|
291
|
+
else:
|
|
292
|
+
test_modules = discover_test_modules(service_path)
|
|
293
|
+
|
|
294
|
+
if not test_modules:
|
|
295
|
+
console.print(f"[yellow]No test modules found for service '{service_name}'[/yellow]")
|
|
296
|
+
return suite_result
|
|
297
|
+
|
|
298
|
+
# Get base URL
|
|
299
|
+
base_url = service_config.base_url if service_config else f"http://localhost:8080"
|
|
300
|
+
|
|
301
|
+
# Create page class
|
|
302
|
+
PageClass = create_page_class(service_name, service_path)
|
|
303
|
+
if PageClass is None:
|
|
304
|
+
console.print(f"[yellow]No Page class found for '{service_name}', using BasePage[/yellow]")
|
|
305
|
+
PageClass = BasePage
|
|
306
|
+
|
|
307
|
+
console.print(f"\n[bold cyan]Running tests for service: {service_name}[/bold cyan]")
|
|
308
|
+
console.print(f"[dim]Base URL: {base_url}[/dim]")
|
|
309
|
+
console.print(f"[dim]Test modules: {len(test_modules)}[/dim]\n")
|
|
310
|
+
|
|
311
|
+
# Execute tests with Playwright
|
|
312
|
+
with sync_playwright() as p:
|
|
313
|
+
# Create page instance
|
|
314
|
+
page = PageClass(base_url=base_url, playwright=p)
|
|
315
|
+
page.setup()
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
for module_path in test_modules:
|
|
319
|
+
result = execute_single_test(module_path, page, service_name)
|
|
320
|
+
suite_result.results.append(result)
|
|
321
|
+
suite_result.total += 1
|
|
322
|
+
|
|
323
|
+
if result.status == "passed":
|
|
324
|
+
suite_result.passed += 1
|
|
325
|
+
console.print(f" [green]✓[/green] {result.name} ({result.duration_ms:.0f}ms)")
|
|
326
|
+
elif result.status == "failed":
|
|
327
|
+
suite_result.failed += 1
|
|
328
|
+
console.print(f" [red]✗[/red] {result.name} - {result.error_message[:50]}")
|
|
329
|
+
if verbose and result.error_message:
|
|
330
|
+
console.print(f" [dim]{result.error_message}[/dim]")
|
|
331
|
+
elif result.status == "error":
|
|
332
|
+
suite_result.errors += 1
|
|
333
|
+
console.print(
|
|
334
|
+
f" [red]⚠[/red] {result.name} - Error: {result.error_message[:50]}"
|
|
335
|
+
)
|
|
336
|
+
if verbose:
|
|
337
|
+
console.print(f" [dim]{result.error_traceback[:200]}[/dim]")
|
|
338
|
+
finally:
|
|
339
|
+
page.teardown()
|
|
340
|
+
|
|
341
|
+
return suite_result
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def run_all_tests(
|
|
345
|
+
services_path: Path = Path("services"),
|
|
346
|
+
specific_service: Optional[str] = None,
|
|
347
|
+
specific_module: Optional[str] = None,
|
|
348
|
+
verbose: bool = False,
|
|
349
|
+
) -> Dict[str, TestSuiteResult]:
|
|
350
|
+
"""Run all tests for all services or a specific service.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
services_path: Path to services directory
|
|
354
|
+
specific_service: If specified, only run tests for this service
|
|
355
|
+
specific_module: If specified, only run this module
|
|
356
|
+
verbose: Whether to show verbose output
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Dictionary mapping service names to their TestSuiteResults
|
|
360
|
+
"""
|
|
361
|
+
results: Dict[str, TestSuiteResult] = {}
|
|
362
|
+
|
|
363
|
+
# Add project root to sys.path for imports
|
|
364
|
+
project_root = services_path.parent.absolute()
|
|
365
|
+
if str(project_root) not in sys.path:
|
|
366
|
+
sys.path.insert(0, str(project_root))
|
|
367
|
+
|
|
368
|
+
# Discover services
|
|
369
|
+
if specific_service:
|
|
370
|
+
services = [specific_service]
|
|
371
|
+
else:
|
|
372
|
+
services = discover_services(services_path)
|
|
373
|
+
|
|
374
|
+
if not services:
|
|
375
|
+
console.print("[yellow]No services found with test modules[/yellow]")
|
|
376
|
+
return results
|
|
377
|
+
|
|
378
|
+
# Load configuration
|
|
379
|
+
try:
|
|
380
|
+
loader = ApiConfigLoader()
|
|
381
|
+
config = loader.load()
|
|
382
|
+
except Exception:
|
|
383
|
+
config = None
|
|
384
|
+
|
|
385
|
+
# Run tests for each service
|
|
386
|
+
for service_name in services:
|
|
387
|
+
# Get service configuration
|
|
388
|
+
service_config = None
|
|
389
|
+
if config and service_name in config.services:
|
|
390
|
+
service_config = config.services[service_name]
|
|
391
|
+
|
|
392
|
+
suite_result = run_service_tests(
|
|
393
|
+
service_name=service_name,
|
|
394
|
+
service_config=service_config,
|
|
395
|
+
services_path=services_path,
|
|
396
|
+
specific_module=specific_module,
|
|
397
|
+
verbose=verbose,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
results[service_name] = suite_result
|
|
401
|
+
|
|
402
|
+
return results
|
|
403
|
+
|
|
404
|
+
# Load configuration
|
|
405
|
+
try:
|
|
406
|
+
loader = ApiConfigLoader()
|
|
407
|
+
config = loader.load()
|
|
408
|
+
except Exception:
|
|
409
|
+
config = None
|
|
410
|
+
|
|
411
|
+
# Run tests for each service
|
|
412
|
+
for service_name in services:
|
|
413
|
+
# Get service configuration
|
|
414
|
+
service_config = None
|
|
415
|
+
if config and service_name in config.services:
|
|
416
|
+
service_config = config.services[service_name]
|
|
417
|
+
|
|
418
|
+
suite_result = run_service_tests(
|
|
419
|
+
service_name=service_name,
|
|
420
|
+
service_config=service_config,
|
|
421
|
+
services_path=services_path,
|
|
422
|
+
specific_module=specific_module,
|
|
423
|
+
verbose=verbose,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
results[service_name] = suite_result
|
|
427
|
+
|
|
428
|
+
return results
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def print_summary(results: Dict[str, TestSuiteResult]) -> bool:
|
|
432
|
+
"""Print summary of all test results.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
results: Dictionary of service results
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if all tests passed, False otherwise
|
|
439
|
+
"""
|
|
440
|
+
console.print("\n" + "═" * 60)
|
|
441
|
+
console.print("[bold]Test Execution Summary[/bold]")
|
|
442
|
+
console.print("═" * 60)
|
|
443
|
+
|
|
444
|
+
total_tests = 0
|
|
445
|
+
total_passed = 0
|
|
446
|
+
total_failed = 0
|
|
447
|
+
total_errors = 0
|
|
448
|
+
|
|
449
|
+
for service_name, suite_result in results.items():
|
|
450
|
+
total_tests += suite_result.total
|
|
451
|
+
total_passed += suite_result.passed
|
|
452
|
+
total_failed += suite_result.failed
|
|
453
|
+
total_errors += suite_result.errors
|
|
454
|
+
|
|
455
|
+
status_color = "green" if suite_result.failed == 0 and suite_result.errors == 0 else "red"
|
|
456
|
+
console.print(
|
|
457
|
+
f"\n[{status_color}]{service_name}[/{status_color}]: "
|
|
458
|
+
f"{suite_result.passed}/{suite_result.total} passed "
|
|
459
|
+
f"({suite_result.success_rate:.1f}%)"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Show failed tests
|
|
463
|
+
for result in suite_result.results:
|
|
464
|
+
if result.status in ("failed", "error"):
|
|
465
|
+
console.print(f" [red] - {result.name}[/red]")
|
|
466
|
+
|
|
467
|
+
console.print("\n" + "─" * 60)
|
|
468
|
+
|
|
469
|
+
if total_tests == 0:
|
|
470
|
+
console.print("[yellow]No tests were executed[/yellow]")
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
overall_success = total_failed == 0 and total_errors == 0
|
|
474
|
+
|
|
475
|
+
if overall_success:
|
|
476
|
+
console.print(f"[bold green]✓ All {total_tests} tests passed![/bold green]")
|
|
477
|
+
else:
|
|
478
|
+
console.print(
|
|
479
|
+
f"[bold red]✗ {total_failed + total_errors} of {total_tests} tests failed[/bold red]"
|
|
480
|
+
)
|
|
481
|
+
console.print(f" [green]Passed: {total_passed}[/green]")
|
|
482
|
+
console.print(f" [red]Failed: {total_failed}[/red]")
|
|
483
|
+
console.print(f" [red]Errors: {total_errors}[/red]")
|
|
484
|
+
|
|
485
|
+
console.print("═" * 60)
|
|
486
|
+
|
|
487
|
+
return overall_success
|