foundry-mcp 0.3.3__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.
- foundry_mcp/__init__.py +7 -0
- foundry_mcp/cli/__init__.py +80 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +633 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +652 -0
- foundry_mcp/cli/commands/session.py +479 -0
- foundry_mcp/cli/commands/specs.py +856 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +259 -0
- foundry_mcp/cli/flags.py +266 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +850 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1636 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/feature_flags.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/journal.py +694 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1350 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +123 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +317 -0
- foundry_mcp/core/prometheus.py +577 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +546 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
- foundry_mcp/core/prompts/plan_review.py +623 -0
- foundry_mcp/core/providers/__init__.py +225 -0
- foundry_mcp/core/providers/base.py +476 -0
- foundry_mcp/core/providers/claude.py +460 -0
- foundry_mcp/core/providers/codex.py +619 -0
- foundry_mcp/core/providers/cursor_agent.py +642 -0
- foundry_mcp/core/providers/detectors.py +488 -0
- foundry_mcp/core/providers/gemini.py +405 -0
- foundry_mcp/core/providers/opencode.py +616 -0
- foundry_mcp/core/providers/opencode_wrapper.js +302 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +729 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +934 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +1650 -0
- foundry_mcp/core/task.py +1289 -0
- foundry_mcp/core/testing.py +450 -0
- foundry_mcp/core/validation.py +2081 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +234 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +289 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +174 -0
- foundry_mcp/dashboard/views/overview.py +160 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/sdd-spec-schema.json +386 -0
- foundry_mcp/server.py +164 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +71 -0
- foundry_mcp/tools/unified/authoring.py +1487 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +198 -0
- foundry_mcp/tools/unified/environment.py +939 -0
- foundry_mcp/tools/unified/error.py +462 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +632 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +745 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +629 -0
- foundry_mcp/tools/unified/review.py +685 -0
- foundry_mcp/tools/unified/review_helpers.py +299 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +580 -0
- foundry_mcp/tools/unified/spec.py +808 -0
- foundry_mcp/tools/unified/task.py +2202 -0
- foundry_mcp/tools/unified/test.py +370 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.3.3.dist-info/METADATA +337 -0
- foundry_mcp-0.3.3.dist-info/RECORD +135 -0
- foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
- foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Testing operations for foundry-mcp.
|
|
3
|
+
Provides functions for running tests and test discovery.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Schema version for compatibility tracking
|
|
15
|
+
SCHEMA_VERSION = "1.0.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Presets for common test configurations
|
|
19
|
+
TEST_PRESETS = {
|
|
20
|
+
"quick": {
|
|
21
|
+
"timeout": 60,
|
|
22
|
+
"verbose": False,
|
|
23
|
+
"fail_fast": True,
|
|
24
|
+
"markers": "not slow",
|
|
25
|
+
},
|
|
26
|
+
"full": {
|
|
27
|
+
"timeout": 300,
|
|
28
|
+
"verbose": True,
|
|
29
|
+
"fail_fast": False,
|
|
30
|
+
"markers": None,
|
|
31
|
+
},
|
|
32
|
+
"unit": {
|
|
33
|
+
"timeout": 120,
|
|
34
|
+
"verbose": True,
|
|
35
|
+
"fail_fast": False,
|
|
36
|
+
"markers": "unit",
|
|
37
|
+
"pattern": "test_*.py",
|
|
38
|
+
},
|
|
39
|
+
"integration": {
|
|
40
|
+
"timeout": 300,
|
|
41
|
+
"verbose": True,
|
|
42
|
+
"fail_fast": False,
|
|
43
|
+
"markers": "integration",
|
|
44
|
+
},
|
|
45
|
+
"smoke": {
|
|
46
|
+
"timeout": 30,
|
|
47
|
+
"verbose": False,
|
|
48
|
+
"fail_fast": True,
|
|
49
|
+
"markers": "smoke",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Data structures
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class TestResult:
|
|
58
|
+
"""
|
|
59
|
+
Result of a single test.
|
|
60
|
+
"""
|
|
61
|
+
name: str
|
|
62
|
+
outcome: str # passed, failed, skipped, error
|
|
63
|
+
duration: float = 0.0
|
|
64
|
+
message: Optional[str] = None
|
|
65
|
+
file_path: Optional[str] = None
|
|
66
|
+
line_number: Optional[int] = None
|
|
67
|
+
stdout: Optional[str] = None
|
|
68
|
+
stderr: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class TestRunResult:
|
|
73
|
+
"""
|
|
74
|
+
Result of a test run.
|
|
75
|
+
"""
|
|
76
|
+
success: bool
|
|
77
|
+
execution_id: str = ""
|
|
78
|
+
schema_version: str = SCHEMA_VERSION
|
|
79
|
+
timestamp: str = ""
|
|
80
|
+
duration: float = 0.0
|
|
81
|
+
total: int = 0
|
|
82
|
+
passed: int = 0
|
|
83
|
+
failed: int = 0
|
|
84
|
+
skipped: int = 0
|
|
85
|
+
errors: int = 0
|
|
86
|
+
tests: List[TestResult] = field(default_factory=list)
|
|
87
|
+
command: str = ""
|
|
88
|
+
cwd: str = ""
|
|
89
|
+
stdout: str = ""
|
|
90
|
+
stderr: str = ""
|
|
91
|
+
error: Optional[str] = None
|
|
92
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
def __post_init__(self):
|
|
95
|
+
if not self.execution_id:
|
|
96
|
+
self.execution_id = str(uuid.uuid4())[:8]
|
|
97
|
+
if not self.timestamp:
|
|
98
|
+
self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class DiscoveredTest:
|
|
103
|
+
"""
|
|
104
|
+
A discovered test.
|
|
105
|
+
"""
|
|
106
|
+
name: str
|
|
107
|
+
file_path: str
|
|
108
|
+
line_number: Optional[int] = None
|
|
109
|
+
markers: List[str] = field(default_factory=list)
|
|
110
|
+
docstring: Optional[str] = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class TestDiscoveryResult:
|
|
115
|
+
"""
|
|
116
|
+
Result of test discovery.
|
|
117
|
+
"""
|
|
118
|
+
success: bool
|
|
119
|
+
schema_version: str = SCHEMA_VERSION
|
|
120
|
+
timestamp: str = ""
|
|
121
|
+
total: int = 0
|
|
122
|
+
tests: List[DiscoveredTest] = field(default_factory=list)
|
|
123
|
+
test_files: List[str] = field(default_factory=list)
|
|
124
|
+
error: Optional[str] = None
|
|
125
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
def __post_init__(self):
|
|
128
|
+
if not self.timestamp:
|
|
129
|
+
self.timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
130
|
+
self.total = len(self.tests)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Main test runner
|
|
134
|
+
|
|
135
|
+
class TestRunner:
|
|
136
|
+
"""
|
|
137
|
+
Test runner for pytest-based projects.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, workspace: Optional[Path] = None):
|
|
141
|
+
"""
|
|
142
|
+
Initialize test runner.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
workspace: Repository root (defaults to current directory)
|
|
146
|
+
"""
|
|
147
|
+
self.workspace = workspace or Path.cwd()
|
|
148
|
+
|
|
149
|
+
def run_tests(
|
|
150
|
+
self,
|
|
151
|
+
target: Optional[str] = None,
|
|
152
|
+
preset: Optional[str] = None,
|
|
153
|
+
timeout: int = 300,
|
|
154
|
+
verbose: bool = True,
|
|
155
|
+
fail_fast: bool = False,
|
|
156
|
+
markers: Optional[str] = None,
|
|
157
|
+
extra_args: Optional[List[str]] = None,
|
|
158
|
+
) -> TestRunResult:
|
|
159
|
+
"""
|
|
160
|
+
Run tests using pytest.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
target: Test target (file, directory, or test name)
|
|
164
|
+
preset: Use a preset configuration (quick, full, unit, integration, smoke)
|
|
165
|
+
timeout: Timeout in seconds
|
|
166
|
+
verbose: Enable verbose output
|
|
167
|
+
fail_fast: Stop on first failure
|
|
168
|
+
markers: Pytest markers expression
|
|
169
|
+
extra_args: Additional pytest arguments
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
TestRunResult with test outcomes
|
|
173
|
+
"""
|
|
174
|
+
# Apply preset if specified
|
|
175
|
+
if preset and preset in TEST_PRESETS:
|
|
176
|
+
preset_config = TEST_PRESETS[preset]
|
|
177
|
+
timeout = preset_config.get("timeout", timeout)
|
|
178
|
+
verbose = preset_config.get("verbose", verbose)
|
|
179
|
+
fail_fast = preset_config.get("fail_fast", fail_fast)
|
|
180
|
+
markers = preset_config.get("markers", markers)
|
|
181
|
+
|
|
182
|
+
# Build command
|
|
183
|
+
cmd = ["python", "-m", "pytest"]
|
|
184
|
+
|
|
185
|
+
if target:
|
|
186
|
+
cmd.append(target)
|
|
187
|
+
|
|
188
|
+
if verbose:
|
|
189
|
+
cmd.append("-v")
|
|
190
|
+
|
|
191
|
+
if fail_fast:
|
|
192
|
+
cmd.append("-x")
|
|
193
|
+
|
|
194
|
+
if markers:
|
|
195
|
+
cmd.extend(["-m", markers])
|
|
196
|
+
|
|
197
|
+
# Add JSON output for parsing
|
|
198
|
+
cmd.append("--tb=short")
|
|
199
|
+
|
|
200
|
+
if extra_args:
|
|
201
|
+
cmd.extend(extra_args)
|
|
202
|
+
|
|
203
|
+
command_str = " ".join(cmd)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
cmd,
|
|
208
|
+
cwd=str(self.workspace),
|
|
209
|
+
capture_output=True,
|
|
210
|
+
text=True,
|
|
211
|
+
timeout=timeout,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Parse output
|
|
215
|
+
tests, passed, failed, skipped, errors = self._parse_pytest_output(result.stdout)
|
|
216
|
+
|
|
217
|
+
return TestRunResult(
|
|
218
|
+
success=result.returncode == 0,
|
|
219
|
+
duration=0.0, # Would need timing wrapper
|
|
220
|
+
total=len(tests),
|
|
221
|
+
passed=passed,
|
|
222
|
+
failed=failed,
|
|
223
|
+
skipped=skipped,
|
|
224
|
+
errors=errors,
|
|
225
|
+
tests=tests,
|
|
226
|
+
command=command_str,
|
|
227
|
+
cwd=str(self.workspace),
|
|
228
|
+
stdout=result.stdout,
|
|
229
|
+
stderr=result.stderr,
|
|
230
|
+
metadata={
|
|
231
|
+
"return_code": result.returncode,
|
|
232
|
+
"preset": preset,
|
|
233
|
+
"target": target,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
return TestRunResult(
|
|
239
|
+
success=False,
|
|
240
|
+
command=command_str,
|
|
241
|
+
cwd=str(self.workspace),
|
|
242
|
+
error=f"Test run timed out after {timeout} seconds",
|
|
243
|
+
metadata={"timeout": timeout},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
except FileNotFoundError:
|
|
247
|
+
return TestRunResult(
|
|
248
|
+
success=False,
|
|
249
|
+
command=command_str,
|
|
250
|
+
cwd=str(self.workspace),
|
|
251
|
+
error="pytest not found. Install with: pip install pytest",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return TestRunResult(
|
|
256
|
+
success=False,
|
|
257
|
+
command=command_str,
|
|
258
|
+
cwd=str(self.workspace),
|
|
259
|
+
error=str(e),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def _parse_pytest_output(self, output: str) -> tuple:
|
|
263
|
+
"""
|
|
264
|
+
Parse pytest output to extract test results.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Tuple of (tests, passed, failed, skipped, errors)
|
|
268
|
+
"""
|
|
269
|
+
tests = []
|
|
270
|
+
passed = 0
|
|
271
|
+
failed = 0
|
|
272
|
+
skipped = 0
|
|
273
|
+
errors = 0
|
|
274
|
+
|
|
275
|
+
lines = output.split("\n")
|
|
276
|
+
|
|
277
|
+
for line in lines:
|
|
278
|
+
line = line.strip()
|
|
279
|
+
|
|
280
|
+
# Parse individual test results
|
|
281
|
+
if "::" in line:
|
|
282
|
+
if " PASSED" in line:
|
|
283
|
+
name = line.split(" PASSED")[0].strip()
|
|
284
|
+
tests.append(TestResult(name=name, outcome="passed"))
|
|
285
|
+
passed += 1
|
|
286
|
+
elif " FAILED" in line:
|
|
287
|
+
name = line.split(" FAILED")[0].strip()
|
|
288
|
+
tests.append(TestResult(name=name, outcome="failed"))
|
|
289
|
+
failed += 1
|
|
290
|
+
elif " SKIPPED" in line:
|
|
291
|
+
name = line.split(" SKIPPED")[0].strip()
|
|
292
|
+
tests.append(TestResult(name=name, outcome="skipped"))
|
|
293
|
+
skipped += 1
|
|
294
|
+
elif " ERROR" in line:
|
|
295
|
+
name = line.split(" ERROR")[0].strip()
|
|
296
|
+
tests.append(TestResult(name=name, outcome="error"))
|
|
297
|
+
errors += 1
|
|
298
|
+
|
|
299
|
+
# Parse summary line
|
|
300
|
+
if "passed" in line.lower() and ("failed" in line.lower() or "error" in line.lower() or "skipped" in line.lower()):
|
|
301
|
+
# Try to extract counts from summary like "5 passed, 2 failed"
|
|
302
|
+
import re
|
|
303
|
+
passed_match = re.search(r"(\d+) passed", line)
|
|
304
|
+
failed_match = re.search(r"(\d+) failed", line)
|
|
305
|
+
skipped_match = re.search(r"(\d+) skipped", line)
|
|
306
|
+
error_match = re.search(r"(\d+) error", line)
|
|
307
|
+
|
|
308
|
+
if passed_match:
|
|
309
|
+
passed = int(passed_match.group(1))
|
|
310
|
+
if failed_match:
|
|
311
|
+
failed = int(failed_match.group(1))
|
|
312
|
+
if skipped_match:
|
|
313
|
+
skipped = int(skipped_match.group(1))
|
|
314
|
+
if error_match:
|
|
315
|
+
errors = int(error_match.group(1))
|
|
316
|
+
|
|
317
|
+
return tests, passed, failed, skipped, errors
|
|
318
|
+
|
|
319
|
+
def discover_tests(
|
|
320
|
+
self,
|
|
321
|
+
target: Optional[str] = None,
|
|
322
|
+
pattern: str = "test_*.py",
|
|
323
|
+
) -> TestDiscoveryResult:
|
|
324
|
+
"""
|
|
325
|
+
Discover tests without running them.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
target: Directory or file to search
|
|
329
|
+
pattern: File pattern for test files
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
TestDiscoveryResult with discovered tests
|
|
333
|
+
"""
|
|
334
|
+
cmd = ["python", "-m", "pytest", "--collect-only", "-q"]
|
|
335
|
+
|
|
336
|
+
if target:
|
|
337
|
+
cmd.append(target)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
result = subprocess.run(
|
|
341
|
+
cmd,
|
|
342
|
+
cwd=str(self.workspace),
|
|
343
|
+
capture_output=True,
|
|
344
|
+
text=True,
|
|
345
|
+
timeout=60,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
tests, test_files = self._parse_collect_output(result.stdout)
|
|
349
|
+
|
|
350
|
+
return TestDiscoveryResult(
|
|
351
|
+
success=result.returncode == 0,
|
|
352
|
+
tests=tests,
|
|
353
|
+
test_files=test_files,
|
|
354
|
+
metadata={
|
|
355
|
+
"target": target,
|
|
356
|
+
"pattern": pattern,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
except subprocess.TimeoutExpired:
|
|
361
|
+
return TestDiscoveryResult(
|
|
362
|
+
success=False,
|
|
363
|
+
error="Test discovery timed out",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return TestDiscoveryResult(
|
|
368
|
+
success=False,
|
|
369
|
+
error=str(e),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _parse_collect_output(self, output: str) -> tuple:
|
|
373
|
+
"""
|
|
374
|
+
Parse pytest --collect-only output.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Tuple of (tests, test_files)
|
|
378
|
+
"""
|
|
379
|
+
tests = []
|
|
380
|
+
test_files = set()
|
|
381
|
+
|
|
382
|
+
for line in output.split("\n"):
|
|
383
|
+
line = line.strip()
|
|
384
|
+
if "::" in line and not line.startswith("="):
|
|
385
|
+
# Parse test path like "tests/test_foo.py::TestClass::test_method"
|
|
386
|
+
parts = line.split("::")
|
|
387
|
+
if parts:
|
|
388
|
+
file_path = parts[0]
|
|
389
|
+
test_files.add(file_path)
|
|
390
|
+
|
|
391
|
+
tests.append(DiscoveredTest(
|
|
392
|
+
name=line,
|
|
393
|
+
file_path=file_path,
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
return tests, list(test_files)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# Convenience functions
|
|
400
|
+
|
|
401
|
+
def run_tests(
|
|
402
|
+
target: Optional[str] = None,
|
|
403
|
+
preset: Optional[str] = None,
|
|
404
|
+
workspace: Optional[Path] = None,
|
|
405
|
+
**kwargs,
|
|
406
|
+
) -> TestRunResult:
|
|
407
|
+
"""
|
|
408
|
+
Run tests using pytest.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
target: Test target
|
|
412
|
+
preset: Preset configuration
|
|
413
|
+
workspace: Repository root
|
|
414
|
+
**kwargs: Additional arguments for TestRunner.run_tests
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
TestRunResult with test outcomes
|
|
418
|
+
"""
|
|
419
|
+
runner = TestRunner(workspace)
|
|
420
|
+
return runner.run_tests(target, preset, **kwargs)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def discover_tests(
|
|
424
|
+
target: Optional[str] = None,
|
|
425
|
+
workspace: Optional[Path] = None,
|
|
426
|
+
pattern: str = "test_*.py",
|
|
427
|
+
) -> TestDiscoveryResult:
|
|
428
|
+
"""
|
|
429
|
+
Discover tests without running them.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
target: Directory or file to search
|
|
433
|
+
workspace: Repository root
|
|
434
|
+
pattern: File pattern
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
TestDiscoveryResult with discovered tests
|
|
438
|
+
"""
|
|
439
|
+
runner = TestRunner(workspace)
|
|
440
|
+
return runner.discover_tests(target, pattern)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def get_presets() -> Dict[str, Dict[str, Any]]:
|
|
444
|
+
"""
|
|
445
|
+
Get available test presets.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Dict of preset names to configurations
|
|
449
|
+
"""
|
|
450
|
+
return TEST_PRESETS.copy()
|