superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python Test Framework Implementations.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- pytest
|
|
6
|
+
- unittest
|
|
7
|
+
- nose2
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from .base import (
|
|
17
|
+
TestFramework,
|
|
18
|
+
FrameworkConfig,
|
|
19
|
+
TestResult,
|
|
20
|
+
TestSuite,
|
|
21
|
+
ExecutionResult,
|
|
22
|
+
TestStatus,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PytestFramework(TestFramework):
|
|
27
|
+
"""Pytest test framework."""
|
|
28
|
+
|
|
29
|
+
NAME = "pytest"
|
|
30
|
+
DISPLAY_NAME = "Pytest"
|
|
31
|
+
LANGUAGE = "python"
|
|
32
|
+
FILE_PATTERNS = ["**/test_*.py", "**/*_test.py"]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def detect(cls, project_root: Path) -> bool:
|
|
36
|
+
"""Detect if pytest is used."""
|
|
37
|
+
# Check for pytest.ini, pyproject.toml with pytest, or conftest.py
|
|
38
|
+
if (project_root / "pytest.ini").exists():
|
|
39
|
+
return True
|
|
40
|
+
if (project_root / "conftest.py").exists():
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
pyproject = project_root / "pyproject.toml"
|
|
44
|
+
if pyproject.exists():
|
|
45
|
+
content = pyproject.read_text()
|
|
46
|
+
if "[tool.pytest" in content:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# Check for test files
|
|
50
|
+
for pattern in cls.FILE_PATTERNS:
|
|
51
|
+
if list(project_root.glob(pattern)):
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
async def discover(self) -> List[TestSuite]:
|
|
57
|
+
"""Discover pytest tests."""
|
|
58
|
+
command = ["pytest", "--collect-only", "-q", str(self.config.project_root)]
|
|
59
|
+
|
|
60
|
+
exit_code, stdout, stderr = await self.run_command(command, timeout=60)
|
|
61
|
+
|
|
62
|
+
suites = []
|
|
63
|
+
current_file = None
|
|
64
|
+
tests = []
|
|
65
|
+
|
|
66
|
+
for line in stdout.splitlines():
|
|
67
|
+
line = line.strip()
|
|
68
|
+
if not line or line.startswith("="):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Parse collected test items
|
|
72
|
+
if "::" in line:
|
|
73
|
+
parts = line.split("::")
|
|
74
|
+
file_path = parts[0]
|
|
75
|
+
|
|
76
|
+
if file_path != current_file:
|
|
77
|
+
if current_file and tests:
|
|
78
|
+
suites.append(
|
|
79
|
+
TestSuite(
|
|
80
|
+
name=Path(current_file).stem, file_path=current_file, tests=tests
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
current_file = file_path
|
|
84
|
+
tests = []
|
|
85
|
+
|
|
86
|
+
test_name = "::".join(parts[1:])
|
|
87
|
+
tests.append(test_name)
|
|
88
|
+
|
|
89
|
+
# Don't forget the last file
|
|
90
|
+
if current_file and tests:
|
|
91
|
+
suites.append(
|
|
92
|
+
TestSuite(name=Path(current_file).stem, file_path=current_file, tests=tests)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return suites
|
|
96
|
+
|
|
97
|
+
async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
|
|
98
|
+
"""Execute pytest tests."""
|
|
99
|
+
started_at = datetime.now()
|
|
100
|
+
|
|
101
|
+
command = ["pytest"]
|
|
102
|
+
|
|
103
|
+
# Add options
|
|
104
|
+
if self.config.verbose:
|
|
105
|
+
command.append("-v")
|
|
106
|
+
if self.config.fail_fast:
|
|
107
|
+
command.append("-x")
|
|
108
|
+
if self.config.coverage:
|
|
109
|
+
command.extend(["--cov", "--cov-report=json"])
|
|
110
|
+
if self.config.parallel and self.config.workers > 1:
|
|
111
|
+
command.extend(["-n", str(self.config.workers)])
|
|
112
|
+
|
|
113
|
+
# JSON output for parsing
|
|
114
|
+
command.append("--tb=short")
|
|
115
|
+
command.append("-q")
|
|
116
|
+
|
|
117
|
+
# Add extra args
|
|
118
|
+
command.extend(self.config.extra_args)
|
|
119
|
+
|
|
120
|
+
# Add specific tests
|
|
121
|
+
if tests:
|
|
122
|
+
command.extend(tests)
|
|
123
|
+
else:
|
|
124
|
+
command.append(str(self.config.project_root))
|
|
125
|
+
|
|
126
|
+
exit_code, stdout, stderr = await self.run_command(command)
|
|
127
|
+
|
|
128
|
+
ended_at = datetime.now()
|
|
129
|
+
duration = (ended_at - started_at).total_seconds()
|
|
130
|
+
|
|
131
|
+
# Parse results
|
|
132
|
+
test_results = self.parse_results(stdout)
|
|
133
|
+
|
|
134
|
+
# Count results
|
|
135
|
+
passed = sum(1 for r in test_results if r.status == TestStatus.PASSED)
|
|
136
|
+
failed = sum(1 for r in test_results if r.status == TestStatus.FAILED)
|
|
137
|
+
skipped = sum(1 for r in test_results if r.status == TestStatus.SKIPPED)
|
|
138
|
+
errors = sum(1 for r in test_results if r.status == TestStatus.ERROR)
|
|
139
|
+
|
|
140
|
+
# Get coverage if available
|
|
141
|
+
coverage = None
|
|
142
|
+
if self.config.coverage:
|
|
143
|
+
coverage_file = self.config.project_root / "coverage.json"
|
|
144
|
+
if coverage_file.exists():
|
|
145
|
+
try:
|
|
146
|
+
cov_data = json.loads(coverage_file.read_text())
|
|
147
|
+
coverage = cov_data.get("totals", {}).get("percent_covered", 0)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
return ExecutionResult(
|
|
152
|
+
framework=self.NAME,
|
|
153
|
+
started_at=started_at,
|
|
154
|
+
ended_at=ended_at,
|
|
155
|
+
duration_seconds=duration,
|
|
156
|
+
total=len(test_results),
|
|
157
|
+
passed=passed,
|
|
158
|
+
failed=failed,
|
|
159
|
+
skipped=skipped,
|
|
160
|
+
errors=errors,
|
|
161
|
+
test_results=test_results,
|
|
162
|
+
coverage_percentage=coverage,
|
|
163
|
+
output=stdout,
|
|
164
|
+
error_output=stderr,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def parse_results(self, output: str) -> List[TestResult]:
|
|
168
|
+
"""Parse pytest output."""
|
|
169
|
+
results = []
|
|
170
|
+
|
|
171
|
+
# Parse test results from output
|
|
172
|
+
# Format: test_file.py::test_name PASSED/FAILED
|
|
173
|
+
test_pattern = re.compile(
|
|
174
|
+
r"(\S+::\S+)\s+(PASSED|FAILED|SKIPPED|ERROR)(?:\s+\[.*?\])?\s*(?:\((.+?)\))?"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
for line in output.splitlines():
|
|
178
|
+
match = test_pattern.search(line)
|
|
179
|
+
if match:
|
|
180
|
+
name = match.group(1)
|
|
181
|
+
status_str = match.group(2)
|
|
182
|
+
duration_str = match.group(3)
|
|
183
|
+
|
|
184
|
+
status_map = {
|
|
185
|
+
"PASSED": TestStatus.PASSED,
|
|
186
|
+
"FAILED": TestStatus.FAILED,
|
|
187
|
+
"SKIPPED": TestStatus.SKIPPED,
|
|
188
|
+
"ERROR": TestStatus.ERROR,
|
|
189
|
+
}
|
|
190
|
+
status = status_map.get(status_str, TestStatus.ERROR)
|
|
191
|
+
|
|
192
|
+
# Parse duration if available
|
|
193
|
+
duration_ms = 0.0
|
|
194
|
+
if duration_str:
|
|
195
|
+
try:
|
|
196
|
+
if "s" in duration_str:
|
|
197
|
+
duration_ms = float(duration_str.replace("s", "")) * 1000
|
|
198
|
+
except ValueError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
results.append(
|
|
202
|
+
TestResult(
|
|
203
|
+
name=name,
|
|
204
|
+
status=status,
|
|
205
|
+
duration_ms=duration_ms,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return results
|
|
210
|
+
|
|
211
|
+
def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
|
|
212
|
+
"""Get pytest command."""
|
|
213
|
+
command = ["pytest"]
|
|
214
|
+
if tests:
|
|
215
|
+
command.extend(tests)
|
|
216
|
+
return command
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class UnittestFramework(TestFramework):
|
|
220
|
+
"""Python unittest framework."""
|
|
221
|
+
|
|
222
|
+
NAME = "unittest"
|
|
223
|
+
DISPLAY_NAME = "Unittest"
|
|
224
|
+
LANGUAGE = "python"
|
|
225
|
+
FILE_PATTERNS = ["**/test_*.py", "**/*_test.py"]
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def detect(cls, project_root: Path) -> bool:
|
|
229
|
+
"""Detect if unittest is used."""
|
|
230
|
+
# Check for test files with unittest imports
|
|
231
|
+
for pattern in cls.FILE_PATTERNS:
|
|
232
|
+
for test_file in project_root.glob(pattern):
|
|
233
|
+
try:
|
|
234
|
+
content = test_file.read_text()
|
|
235
|
+
if "import unittest" in content or "from unittest" in content:
|
|
236
|
+
return True
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
async def discover(self) -> List[TestSuite]:
|
|
242
|
+
"""Discover unittest tests."""
|
|
243
|
+
command = [
|
|
244
|
+
"python",
|
|
245
|
+
"-m",
|
|
246
|
+
"unittest",
|
|
247
|
+
"discover",
|
|
248
|
+
"-s",
|
|
249
|
+
str(self.config.project_root),
|
|
250
|
+
"-p",
|
|
251
|
+
"test_*.py",
|
|
252
|
+
"-v",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Just collect, don't run
|
|
256
|
+
exit_code, stdout, stderr = await self.run_command(
|
|
257
|
+
[
|
|
258
|
+
"python",
|
|
259
|
+
"-c",
|
|
260
|
+
f"""
|
|
261
|
+
import unittest
|
|
262
|
+
import sys
|
|
263
|
+
loader = unittest.TestLoader()
|
|
264
|
+
suite = loader.discover('{self.config.project_root}', pattern='test_*.py')
|
|
265
|
+
for group in suite:
|
|
266
|
+
for test_group in group:
|
|
267
|
+
for test in test_group:
|
|
268
|
+
print(str(test))
|
|
269
|
+
""",
|
|
270
|
+
],
|
|
271
|
+
timeout=60,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
suites_dict = {}
|
|
275
|
+
for line in stdout.splitlines():
|
|
276
|
+
line = line.strip()
|
|
277
|
+
if not line:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# Parse test name: test_method (module.TestClass)
|
|
281
|
+
match = re.match(r"(\w+)\s+\((.+)\)", line)
|
|
282
|
+
if match:
|
|
283
|
+
method = match.group(1)
|
|
284
|
+
module_class = match.group(2)
|
|
285
|
+
|
|
286
|
+
if module_class not in suites_dict:
|
|
287
|
+
suites_dict[module_class] = []
|
|
288
|
+
suites_dict[module_class].append(method)
|
|
289
|
+
|
|
290
|
+
return [
|
|
291
|
+
TestSuite(name=name, file_path=name.replace(".", "/") + ".py", tests=tests)
|
|
292
|
+
for name, tests in suites_dict.items()
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
|
|
296
|
+
"""Execute unittest tests."""
|
|
297
|
+
started_at = datetime.now()
|
|
298
|
+
|
|
299
|
+
command = ["python", "-m", "unittest"]
|
|
300
|
+
|
|
301
|
+
if self.config.verbose:
|
|
302
|
+
command.append("-v")
|
|
303
|
+
if self.config.fail_fast:
|
|
304
|
+
command.append("-f")
|
|
305
|
+
|
|
306
|
+
if tests:
|
|
307
|
+
command.extend(tests)
|
|
308
|
+
else:
|
|
309
|
+
command.extend(["discover", "-s", str(self.config.project_root), "-p", "test_*.py"])
|
|
310
|
+
|
|
311
|
+
exit_code, stdout, stderr = await self.run_command(command)
|
|
312
|
+
|
|
313
|
+
ended_at = datetime.now()
|
|
314
|
+
duration = (ended_at - started_at).total_seconds()
|
|
315
|
+
|
|
316
|
+
test_results = self.parse_results(stdout + stderr)
|
|
317
|
+
|
|
318
|
+
passed = sum(1 for r in test_results if r.status == TestStatus.PASSED)
|
|
319
|
+
failed = sum(1 for r in test_results if r.status == TestStatus.FAILED)
|
|
320
|
+
skipped = sum(1 for r in test_results if r.status == TestStatus.SKIPPED)
|
|
321
|
+
errors = sum(1 for r in test_results if r.status == TestStatus.ERROR)
|
|
322
|
+
|
|
323
|
+
return ExecutionResult(
|
|
324
|
+
framework=self.NAME,
|
|
325
|
+
started_at=started_at,
|
|
326
|
+
ended_at=ended_at,
|
|
327
|
+
duration_seconds=duration,
|
|
328
|
+
total=len(test_results),
|
|
329
|
+
passed=passed,
|
|
330
|
+
failed=failed,
|
|
331
|
+
skipped=skipped,
|
|
332
|
+
errors=errors,
|
|
333
|
+
test_results=test_results,
|
|
334
|
+
output=stdout,
|
|
335
|
+
error_output=stderr,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def parse_results(self, output: str) -> List[TestResult]:
|
|
339
|
+
"""Parse unittest output."""
|
|
340
|
+
results = []
|
|
341
|
+
|
|
342
|
+
# Parse verbose output: test_name (module.Class) ... ok/FAIL/ERROR
|
|
343
|
+
pattern = re.compile(r"(\w+)\s+\((.+?)\)\s+\.\.\.\s+(ok|FAIL|ERROR|skipped)")
|
|
344
|
+
|
|
345
|
+
for match in pattern.finditer(output):
|
|
346
|
+
method = match.group(1)
|
|
347
|
+
module_class = match.group(2)
|
|
348
|
+
status_str = match.group(3)
|
|
349
|
+
|
|
350
|
+
status_map = {
|
|
351
|
+
"ok": TestStatus.PASSED,
|
|
352
|
+
"FAIL": TestStatus.FAILED,
|
|
353
|
+
"ERROR": TestStatus.ERROR,
|
|
354
|
+
"skipped": TestStatus.SKIPPED,
|
|
355
|
+
}
|
|
356
|
+
status = status_map.get(status_str, TestStatus.ERROR)
|
|
357
|
+
|
|
358
|
+
results.append(
|
|
359
|
+
TestResult(
|
|
360
|
+
name=f"{module_class}.{method}",
|
|
361
|
+
status=status,
|
|
362
|
+
duration_ms=0,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return results
|
|
367
|
+
|
|
368
|
+
def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
|
|
369
|
+
"""Get unittest command."""
|
|
370
|
+
command = ["python", "-m", "unittest"]
|
|
371
|
+
if tests:
|
|
372
|
+
command.extend(tests)
|
|
373
|
+
return command
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework Registry - Central registry for test frameworks.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Framework registration
|
|
6
|
+
- Auto-detection
|
|
7
|
+
- Framework lookup
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Type
|
|
12
|
+
|
|
13
|
+
from .base import TestFramework, FrameworkConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FrameworkRegistry:
|
|
17
|
+
"""Registry for test frameworks."""
|
|
18
|
+
|
|
19
|
+
_frameworks: Dict[str, Type[TestFramework]] = {}
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def register(cls, framework_class: Type[TestFramework]) -> None:
|
|
23
|
+
"""Register a framework."""
|
|
24
|
+
cls._frameworks[framework_class.NAME] = framework_class
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get(cls, name: str, config: Optional[FrameworkConfig] = None) -> Optional[TestFramework]:
|
|
28
|
+
"""Get a framework by name."""
|
|
29
|
+
framework_class = cls._frameworks.get(name)
|
|
30
|
+
if framework_class:
|
|
31
|
+
return framework_class(config)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def detect(
|
|
36
|
+
cls, project_root: Path, config: Optional[FrameworkConfig] = None
|
|
37
|
+
) -> List[TestFramework]:
|
|
38
|
+
"""Detect frameworks in a project."""
|
|
39
|
+
detected = []
|
|
40
|
+
for framework_class in cls._frameworks.values():
|
|
41
|
+
if framework_class.detect(project_root):
|
|
42
|
+
cfg = config or FrameworkConfig(project_root=project_root)
|
|
43
|
+
detected.append(framework_class(cfg))
|
|
44
|
+
return detected
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def list_all(cls) -> List[Dict[str, str]]:
|
|
48
|
+
"""List all registered frameworks."""
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
"name": fw.NAME,
|
|
52
|
+
"display_name": fw.DISPLAY_NAME,
|
|
53
|
+
"language": fw.LANGUAGE,
|
|
54
|
+
}
|
|
55
|
+
for fw in cls._frameworks.values()
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_framework(name: str, config: Optional[FrameworkConfig] = None) -> Optional[TestFramework]:
|
|
60
|
+
"""Get a framework by name."""
|
|
61
|
+
return FrameworkRegistry.get(name, config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def detect_framework(
|
|
65
|
+
project_root: Path, config: Optional[FrameworkConfig] = None
|
|
66
|
+
) -> List[TestFramework]:
|
|
67
|
+
"""Detect frameworks in a project."""
|
|
68
|
+
return FrameworkRegistry.detect(project_root, config)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_frameworks() -> List[Dict[str, str]]:
|
|
72
|
+
"""List all available frameworks."""
|
|
73
|
+
return FrameworkRegistry.list_all()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Auto-register frameworks when module is imported
|
|
77
|
+
def _register_all_frameworks():
|
|
78
|
+
"""Register all built-in frameworks."""
|
|
79
|
+
from .python import PytestFramework, UnittestFramework
|
|
80
|
+
from .javascript import JestFramework, MochaFramework, VitestFramework
|
|
81
|
+
from .e2e import CypressFramework, PlaywrightFramework
|
|
82
|
+
|
|
83
|
+
FrameworkRegistry.register(PytestFramework)
|
|
84
|
+
FrameworkRegistry.register(UnittestFramework)
|
|
85
|
+
FrameworkRegistry.register(JestFramework)
|
|
86
|
+
FrameworkRegistry.register(MochaFramework)
|
|
87
|
+
FrameworkRegistry.register(VitestFramework)
|
|
88
|
+
FrameworkRegistry.register(CypressFramework)
|
|
89
|
+
FrameworkRegistry.register(PlaywrightFramework)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_register_all_frameworks()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Tools Module - Model Context Protocol tools with lazy loading.
|
|
3
|
+
|
|
4
|
+
Provides 100+ MCP tools organized by domain with 87% context reduction
|
|
5
|
+
through lazy loading (only load domain tools when needed).
|
|
6
|
+
|
|
7
|
+
Tool Categories:
|
|
8
|
+
- core: Always loaded (14 tools)
|
|
9
|
+
- testing: Test generation, execution, reporting
|
|
10
|
+
- security: Vulnerability scanning, OWASP analysis
|
|
11
|
+
- performance: Benchmarking, bottleneck analysis
|
|
12
|
+
- coverage: Gap detection, coverage analysis
|
|
13
|
+
- quality: Quality gates, deployment readiness
|
|
14
|
+
- flaky: Detection and stabilization
|
|
15
|
+
- accessibility: WCAG compliance, visual regression
|
|
16
|
+
- learning: Pattern management, training
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .registry import (
|
|
20
|
+
MCPToolRegistry,
|
|
21
|
+
MCPTool,
|
|
22
|
+
ToolDomain,
|
|
23
|
+
get_registry,
|
|
24
|
+
register_tool,
|
|
25
|
+
get_tool,
|
|
26
|
+
list_tools,
|
|
27
|
+
list_domains,
|
|
28
|
+
load_domain,
|
|
29
|
+
get_loaded_domains,
|
|
30
|
+
)
|
|
31
|
+
from .core_tools import register_core_tools
|
|
32
|
+
from .testing_tools import register_testing_tools
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"MCPToolRegistry",
|
|
36
|
+
"MCPTool",
|
|
37
|
+
"ToolDomain",
|
|
38
|
+
"get_registry",
|
|
39
|
+
"register_tool",
|
|
40
|
+
"get_tool",
|
|
41
|
+
"list_tools",
|
|
42
|
+
"list_domains",
|
|
43
|
+
"load_domain",
|
|
44
|
+
"get_loaded_domains",
|
|
45
|
+
"register_core_tools",
|
|
46
|
+
"register_testing_tools",
|
|
47
|
+
]
|