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,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for test framework support.
|
|
3
|
+
|
|
4
|
+
Provides abstract interfaces for:
|
|
5
|
+
- Test discovery
|
|
6
|
+
- Test execution
|
|
7
|
+
- Result parsing
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestStatus(str, Enum):
|
|
20
|
+
"""Status of a test execution."""
|
|
21
|
+
|
|
22
|
+
PASSED = "passed"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
SKIPPED = "skipped"
|
|
25
|
+
ERROR = "error"
|
|
26
|
+
PENDING = "pending"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FrameworkConfig:
|
|
31
|
+
"""Configuration for a test framework."""
|
|
32
|
+
|
|
33
|
+
project_root: Path = field(default_factory=Path.cwd)
|
|
34
|
+
test_pattern: str = "**/test_*.py"
|
|
35
|
+
timeout_seconds: int = 300
|
|
36
|
+
parallel: bool = True
|
|
37
|
+
workers: int = 4
|
|
38
|
+
verbose: bool = False
|
|
39
|
+
coverage: bool = False
|
|
40
|
+
fail_fast: bool = False
|
|
41
|
+
retry_count: int = 0
|
|
42
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
43
|
+
extra_args: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TestResult:
|
|
48
|
+
"""Result of a single test execution."""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
status: TestStatus
|
|
52
|
+
duration_ms: float
|
|
53
|
+
file_path: Optional[str] = None
|
|
54
|
+
line_number: Optional[int] = None
|
|
55
|
+
error_message: Optional[str] = None
|
|
56
|
+
stack_trace: Optional[str] = None
|
|
57
|
+
stdout: Optional[str] = None
|
|
58
|
+
stderr: Optional[str] = None
|
|
59
|
+
retry_count: int = 0
|
|
60
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TestSuite:
|
|
65
|
+
"""A collection of tests."""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
file_path: str
|
|
69
|
+
tests: List[str] = field(default_factory=list)
|
|
70
|
+
setup_file: Optional[str] = None
|
|
71
|
+
teardown_file: Optional[str] = None
|
|
72
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class ExecutionResult:
|
|
77
|
+
"""Result of executing tests."""
|
|
78
|
+
|
|
79
|
+
framework: str
|
|
80
|
+
started_at: datetime
|
|
81
|
+
ended_at: Optional[datetime] = None
|
|
82
|
+
duration_seconds: float = 0.0
|
|
83
|
+
total: int = 0
|
|
84
|
+
passed: int = 0
|
|
85
|
+
failed: int = 0
|
|
86
|
+
skipped: int = 0
|
|
87
|
+
errors: int = 0
|
|
88
|
+
test_results: List[TestResult] = field(default_factory=list)
|
|
89
|
+
coverage_percentage: Optional[float] = None
|
|
90
|
+
output: str = ""
|
|
91
|
+
error_output: str = ""
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def success(self) -> bool:
|
|
95
|
+
"""Did all tests pass?"""
|
|
96
|
+
return self.failed == 0 and self.errors == 0
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def pass_rate(self) -> float:
|
|
100
|
+
"""Calculate pass rate."""
|
|
101
|
+
if self.total == 0:
|
|
102
|
+
return 0.0
|
|
103
|
+
return self.passed / self.total
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
106
|
+
"""Convert to dictionary."""
|
|
107
|
+
return {
|
|
108
|
+
"framework": self.framework,
|
|
109
|
+
"started_at": self.started_at.isoformat(),
|
|
110
|
+
"ended_at": self.ended_at.isoformat() if self.ended_at else None,
|
|
111
|
+
"duration_seconds": self.duration_seconds,
|
|
112
|
+
"total": self.total,
|
|
113
|
+
"passed": self.passed,
|
|
114
|
+
"failed": self.failed,
|
|
115
|
+
"skipped": self.skipped,
|
|
116
|
+
"errors": self.errors,
|
|
117
|
+
"success": self.success,
|
|
118
|
+
"pass_rate": self.pass_rate,
|
|
119
|
+
"coverage": self.coverage_percentage,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestFramework(ABC):
|
|
124
|
+
"""
|
|
125
|
+
Abstract base class for test frameworks.
|
|
126
|
+
|
|
127
|
+
Implement this class to add support for a new test framework.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Framework metadata - override in subclasses
|
|
131
|
+
NAME = "base"
|
|
132
|
+
DISPLAY_NAME = "Base Framework"
|
|
133
|
+
LANGUAGE = "unknown"
|
|
134
|
+
FILE_PATTERNS = ["**/test_*.py"]
|
|
135
|
+
|
|
136
|
+
def __init__(self, config: Optional[FrameworkConfig] = None):
|
|
137
|
+
"""Initialize the framework."""
|
|
138
|
+
self.config = config or FrameworkConfig()
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
async def discover(self) -> List[TestSuite]:
|
|
142
|
+
"""
|
|
143
|
+
Discover tests in the project.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of TestSuite objects found
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
|
|
152
|
+
"""
|
|
153
|
+
Execute tests.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
tests: Specific tests to run (None = all)
|
|
157
|
+
**kwargs: Framework-specific options
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
ExecutionResult with all test results
|
|
161
|
+
"""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def parse_results(self, output: str) -> List[TestResult]:
|
|
166
|
+
"""
|
|
167
|
+
Parse test output into structured results.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
output: Raw output from test runner
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of TestResult objects
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
|
|
178
|
+
"""
|
|
179
|
+
Get the command to run tests.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tests: Specific tests to run
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Command as list of strings
|
|
186
|
+
"""
|
|
187
|
+
raise NotImplementedError
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def detect(cls, project_root: Path) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Detect if this framework is used in the project.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
project_root: Root directory of the project
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if framework is detected
|
|
199
|
+
"""
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
async def run_command(
|
|
203
|
+
self, command: List[str], timeout: Optional[int] = None
|
|
204
|
+
) -> tuple[int, str, str]:
|
|
205
|
+
"""
|
|
206
|
+
Run a shell command.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
command: Command to run
|
|
210
|
+
timeout: Timeout in seconds
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Tuple of (exit_code, stdout, stderr)
|
|
214
|
+
"""
|
|
215
|
+
timeout = timeout or self.config.timeout_seconds
|
|
216
|
+
|
|
217
|
+
process = await asyncio.create_subprocess_exec(
|
|
218
|
+
*command,
|
|
219
|
+
stdout=asyncio.subprocess.PIPE,
|
|
220
|
+
stderr=asyncio.subprocess.PIPE,
|
|
221
|
+
cwd=str(self.config.project_root),
|
|
222
|
+
env={**dict(__import__("os").environ), **self.config.env},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
227
|
+
return (
|
|
228
|
+
process.returncode or 0,
|
|
229
|
+
stdout.decode("utf-8", errors="replace"),
|
|
230
|
+
stderr.decode("utf-8", errors="replace"),
|
|
231
|
+
)
|
|
232
|
+
except asyncio.TimeoutError:
|
|
233
|
+
process.kill()
|
|
234
|
+
return (-1, "", f"Command timed out after {timeout}s")
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
E2E Test Framework Implementations.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Cypress
|
|
6
|
+
- Playwright
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from .base import (
|
|
16
|
+
TestFramework,
|
|
17
|
+
FrameworkConfig,
|
|
18
|
+
TestResult,
|
|
19
|
+
TestSuite,
|
|
20
|
+
ExecutionResult,
|
|
21
|
+
TestStatus,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CypressFramework(TestFramework):
|
|
26
|
+
"""Cypress E2E test framework."""
|
|
27
|
+
|
|
28
|
+
NAME = "cypress"
|
|
29
|
+
DISPLAY_NAME = "Cypress"
|
|
30
|
+
LANGUAGE = "javascript"
|
|
31
|
+
FILE_PATTERNS = ["cypress/e2e/**/*.cy.js", "cypress/e2e/**/*.cy.ts"]
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def detect(cls, project_root: Path) -> bool:
|
|
35
|
+
"""Detect if Cypress is used."""
|
|
36
|
+
if (project_root / "cypress.config.js").exists():
|
|
37
|
+
return True
|
|
38
|
+
if (project_root / "cypress.config.ts").exists():
|
|
39
|
+
return True
|
|
40
|
+
if (project_root / "cypress.json").exists():
|
|
41
|
+
return True
|
|
42
|
+
if (project_root / "cypress").is_dir():
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
package_json = project_root / "package.json"
|
|
46
|
+
if package_json.exists():
|
|
47
|
+
try:
|
|
48
|
+
data = json.loads(package_json.read_text())
|
|
49
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
50
|
+
if "cypress" in deps:
|
|
51
|
+
return True
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
async def discover(self) -> List[TestSuite]:
|
|
58
|
+
"""Discover Cypress tests."""
|
|
59
|
+
suites = []
|
|
60
|
+
for pattern in self.FILE_PATTERNS:
|
|
61
|
+
for file_path in self.config.project_root.glob(pattern):
|
|
62
|
+
suites.append(TestSuite(name=file_path.stem, file_path=str(file_path), tests=[]))
|
|
63
|
+
return suites
|
|
64
|
+
|
|
65
|
+
async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
|
|
66
|
+
"""Execute Cypress tests."""
|
|
67
|
+
started_at = datetime.now()
|
|
68
|
+
|
|
69
|
+
command = ["npx", "cypress", "run", "--reporter", "json"]
|
|
70
|
+
|
|
71
|
+
if self.config.parallel and self.config.workers > 1:
|
|
72
|
+
command.extend(["--parallel", "--record"])
|
|
73
|
+
|
|
74
|
+
if tests:
|
|
75
|
+
command.extend(["--spec", ",".join(tests)])
|
|
76
|
+
|
|
77
|
+
exit_code, stdout, stderr = await self.run_command(command)
|
|
78
|
+
|
|
79
|
+
ended_at = datetime.now()
|
|
80
|
+
duration = (ended_at - started_at).total_seconds()
|
|
81
|
+
|
|
82
|
+
# Parse results from output
|
|
83
|
+
test_results = []
|
|
84
|
+
total = passed = failed = skipped = 0
|
|
85
|
+
|
|
86
|
+
# Try to parse JSON output
|
|
87
|
+
try:
|
|
88
|
+
json_match = re.search(r'\{[\s\S]*"stats"[\s\S]*\}', stdout)
|
|
89
|
+
if json_match:
|
|
90
|
+
data = json.loads(json_match.group())
|
|
91
|
+
stats = data.get("stats", {})
|
|
92
|
+
total = stats.get("tests", 0)
|
|
93
|
+
passed = stats.get("passes", 0)
|
|
94
|
+
failed = stats.get("failures", 0)
|
|
95
|
+
skipped = stats.get("pending", 0)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
# Fallback to regex parsing
|
|
98
|
+
pass_match = re.search(r"(\d+) passing", stdout)
|
|
99
|
+
fail_match = re.search(r"(\d+) failing", stdout)
|
|
100
|
+
skip_match = re.search(r"(\d+) pending", stdout)
|
|
101
|
+
|
|
102
|
+
if pass_match:
|
|
103
|
+
passed = int(pass_match.group(1))
|
|
104
|
+
if fail_match:
|
|
105
|
+
failed = int(fail_match.group(1))
|
|
106
|
+
if skip_match:
|
|
107
|
+
skipped = int(skip_match.group(1))
|
|
108
|
+
total = passed + failed + skipped
|
|
109
|
+
|
|
110
|
+
return ExecutionResult(
|
|
111
|
+
framework=self.NAME,
|
|
112
|
+
started_at=started_at,
|
|
113
|
+
ended_at=ended_at,
|
|
114
|
+
duration_seconds=duration,
|
|
115
|
+
total=total,
|
|
116
|
+
passed=passed,
|
|
117
|
+
failed=failed,
|
|
118
|
+
skipped=skipped,
|
|
119
|
+
errors=0,
|
|
120
|
+
test_results=test_results,
|
|
121
|
+
output=stdout,
|
|
122
|
+
error_output=stderr,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def parse_results(self, output: str) -> List[TestResult]:
|
|
126
|
+
"""Parse Cypress output."""
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
|
|
130
|
+
"""Get Cypress command."""
|
|
131
|
+
command = ["npx", "cypress", "run"]
|
|
132
|
+
if tests:
|
|
133
|
+
command.extend(["--spec", ",".join(tests)])
|
|
134
|
+
return command
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PlaywrightFramework(TestFramework):
|
|
138
|
+
"""Playwright E2E test framework."""
|
|
139
|
+
|
|
140
|
+
NAME = "playwright"
|
|
141
|
+
DISPLAY_NAME = "Playwright"
|
|
142
|
+
LANGUAGE = "javascript"
|
|
143
|
+
FILE_PATTERNS = ["**/*.spec.ts", "**/*.spec.js", "tests/**/*.ts"]
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def detect(cls, project_root: Path) -> bool:
|
|
147
|
+
"""Detect if Playwright is used."""
|
|
148
|
+
if (project_root / "playwright.config.ts").exists():
|
|
149
|
+
return True
|
|
150
|
+
if (project_root / "playwright.config.js").exists():
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
package_json = project_root / "package.json"
|
|
154
|
+
if package_json.exists():
|
|
155
|
+
try:
|
|
156
|
+
data = json.loads(package_json.read_text())
|
|
157
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
158
|
+
if "@playwright/test" in deps:
|
|
159
|
+
return True
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
async def discover(self) -> List[TestSuite]:
|
|
166
|
+
"""Discover Playwright tests."""
|
|
167
|
+
command = ["npx", "playwright", "test", "--list"]
|
|
168
|
+
|
|
169
|
+
exit_code, stdout, stderr = await self.run_command(command, timeout=60)
|
|
170
|
+
|
|
171
|
+
suites = {}
|
|
172
|
+
for line in stdout.splitlines():
|
|
173
|
+
line = line.strip()
|
|
174
|
+
if " › " in line:
|
|
175
|
+
parts = line.split(" › ")
|
|
176
|
+
file_path = parts[0] if parts else ""
|
|
177
|
+
test_name = " › ".join(parts[1:]) if len(parts) > 1 else ""
|
|
178
|
+
|
|
179
|
+
if file_path not in suites:
|
|
180
|
+
suites[file_path] = []
|
|
181
|
+
if test_name:
|
|
182
|
+
suites[file_path].append(test_name)
|
|
183
|
+
|
|
184
|
+
return [
|
|
185
|
+
TestSuite(name=Path(fp).stem, file_path=fp, tests=tests) for fp, tests in suites.items()
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
async def execute(self, tests: Optional[List[str]] = None, **kwargs) -> ExecutionResult:
|
|
189
|
+
"""Execute Playwright tests."""
|
|
190
|
+
started_at = datetime.now()
|
|
191
|
+
|
|
192
|
+
command = ["npx", "playwright", "test", "--reporter=json"]
|
|
193
|
+
|
|
194
|
+
if self.config.parallel and self.config.workers > 1:
|
|
195
|
+
command.extend(["--workers", str(self.config.workers)])
|
|
196
|
+
|
|
197
|
+
if tests:
|
|
198
|
+
command.extend(tests)
|
|
199
|
+
|
|
200
|
+
exit_code, stdout, stderr = await self.run_command(command)
|
|
201
|
+
|
|
202
|
+
ended_at = datetime.now()
|
|
203
|
+
duration = (ended_at - started_at).total_seconds()
|
|
204
|
+
|
|
205
|
+
test_results = []
|
|
206
|
+
total = passed = failed = skipped = 0
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(stdout)
|
|
210
|
+
stats = data.get("stats", {})
|
|
211
|
+
total = stats.get("expected", 0) + stats.get("unexpected", 0) + stats.get("skipped", 0)
|
|
212
|
+
passed = stats.get("expected", 0)
|
|
213
|
+
failed = stats.get("unexpected", 0)
|
|
214
|
+
skipped = stats.get("skipped", 0)
|
|
215
|
+
|
|
216
|
+
for suite in data.get("suites", []):
|
|
217
|
+
for spec in suite.get("specs", []):
|
|
218
|
+
for test in spec.get("tests", []):
|
|
219
|
+
for result in test.get("results", []):
|
|
220
|
+
status = TestStatus.PASSED
|
|
221
|
+
if result.get("status") == "failed":
|
|
222
|
+
status = TestStatus.FAILED
|
|
223
|
+
elif result.get("status") == "skipped":
|
|
224
|
+
status = TestStatus.SKIPPED
|
|
225
|
+
|
|
226
|
+
test_results.append(
|
|
227
|
+
TestResult(
|
|
228
|
+
name=spec.get("title", ""),
|
|
229
|
+
status=status,
|
|
230
|
+
duration_ms=result.get("duration", 0),
|
|
231
|
+
file_path=spec.get("file"),
|
|
232
|
+
error_message=result.get("error", {}).get("message"),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
except json.JSONDecodeError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
return ExecutionResult(
|
|
240
|
+
framework=self.NAME,
|
|
241
|
+
started_at=started_at,
|
|
242
|
+
ended_at=ended_at,
|
|
243
|
+
duration_seconds=duration,
|
|
244
|
+
total=total,
|
|
245
|
+
passed=passed,
|
|
246
|
+
failed=failed,
|
|
247
|
+
skipped=skipped,
|
|
248
|
+
errors=0,
|
|
249
|
+
test_results=test_results,
|
|
250
|
+
output=stdout,
|
|
251
|
+
error_output=stderr,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def parse_results(self, output: str) -> List[TestResult]:
|
|
255
|
+
"""Parse Playwright output."""
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
def get_command(self, tests: Optional[List[str]] = None) -> List[str]:
|
|
259
|
+
"""Get Playwright command."""
|
|
260
|
+
command = ["npx", "playwright", "test"]
|
|
261
|
+
if tests:
|
|
262
|
+
command.extend(tests)
|
|
263
|
+
return command
|