rodski 5.7.1__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.
- rodski/LICENSE +21 -0
- rodski/README.md +56 -0
- rodski/__init__.py +2 -0
- rodski/api/__init__.py +0 -0
- rodski/api/rest_helper.py +116 -0
- rodski/cli_main.py +106 -0
- rodski/config/__init__.py +0 -0
- rodski/config/android_config.json +8 -0
- rodski/config/config.json +25 -0
- rodski/config/default_config.yaml +96 -0
- rodski/config/ios_config.json +7 -0
- rodski/config/llm_config.yaml +58 -0
- rodski/config/reviewer_config.yaml +31 -0
- rodski/config/vision_config.yaml +14 -0
- rodski/core/__init__.py +36 -0
- rodski/core/assertion/__init__.py +16 -0
- rodski/core/assertion/base_assertion.py +35 -0
- rodski/core/assertion/image_matcher.py +266 -0
- rodski/core/assertion/video_analyzer.py +271 -0
- rodski/core/browser_recycler.py +251 -0
- rodski/core/case_metadata.py +104 -0
- rodski/core/case_parser.py +159 -0
- rodski/core/condition_evaluator.py +92 -0
- rodski/core/config_manager.py +95 -0
- rodski/core/data_table_parser.py +93 -0
- rodski/core/diagnosis_engine.py +216 -0
- rodski/core/driver_factory.py +470 -0
- rodski/core/dynamic_executor.py +283 -0
- rodski/core/dynamic_steps.py +73 -0
- rodski/core/exceptions.py +408 -0
- rodski/core/execution_stats.py +81 -0
- rodski/core/global_value_parser.py +52 -0
- rodski/core/json_formatter.py +73 -0
- rodski/core/keyword_engine.py +2040 -0
- rodski/core/logger.py +81 -0
- rodski/core/loop_executor.py +202 -0
- rodski/core/metadata_writer.py +63 -0
- rodski/core/model_parser.py +306 -0
- rodski/core/parallel_executor.py +92 -0
- rodski/core/performance.py +48 -0
- rodski/core/profiler.py +85 -0
- rodski/core/recording/__init__.py +12 -0
- rodski/core/recording/recorder.py +142 -0
- rodski/core/recovery_engine.py +287 -0
- rodski/core/result_writer.py +227 -0
- rodski/core/runtime_context.py +20 -0
- rodski/core/runtime_control.py +115 -0
- rodski/core/ski_executor.py +652 -0
- rodski/core/statistics_collector.py +240 -0
- rodski/core/task_executor.py +122 -0
- rodski/core/test_case_explainer.py +1032 -0
- rodski/core/test_runner.py +269 -0
- rodski/core/xml_schema_validator.py +179 -0
- rodski/data/__init__.py +5 -0
- rodski/data/data_resolver.py +144 -0
- rodski/data/model_manager.py +90 -0
- rodski/drivers/__init__.py +39 -0
- rodski/drivers/android_driver.py +33 -0
- rodski/drivers/appium_driver.py +272 -0
- rodski/drivers/base_driver.py +356 -0
- rodski/drivers/desktop_driver.py +692 -0
- rodski/drivers/ios_driver.py +24 -0
- rodski/drivers/playwright_driver.py +629 -0
- rodski/drivers/pywinauto_driver.py +79 -0
- rodski/llm/__init__.py +10 -0
- rodski/llm/capabilities/__init__.py +23 -0
- rodski/llm/capabilities/base.py +14 -0
- rodski/llm/capabilities/screenshot_verifier.py +78 -0
- rodski/llm/capabilities/test_reviewer.py +131 -0
- rodski/llm/capabilities/vision_locator.py +62 -0
- rodski/llm/client.py +66 -0
- rodski/llm/config.py +134 -0
- rodski/llm/exceptions.py +21 -0
- rodski/llm/providers/__init__.py +17 -0
- rodski/llm/providers/base.py +38 -0
- rodski/llm/providers/claude.py +78 -0
- rodski/llm/providers/openai.py +73 -0
- rodski/reviewers/__init__.py +10 -0
- rodski/reviewers/cli.py +35 -0
- rodski/reviewers/llm_reviewer.py +149 -0
- rodski/rodski_cli/__init__.py +43 -0
- rodski/rodski_cli/config.py +50 -0
- rodski/rodski_cli/docs.py +126 -0
- rodski/rodski_cli/explain.py +129 -0
- rodski/rodski_cli/log.py +58 -0
- rodski/rodski_cli/model.py +57 -0
- rodski/rodski_cli/profile.py +57 -0
- rodski/rodski_cli/report.py +620 -0
- rodski/rodski_cli/run.py +229 -0
- rodski/rodski_cli/stats.py +305 -0
- rodski/schemas/case.xsd +128 -0
- rodski/schemas/data.xsd +56 -0
- rodski/schemas/globalvalue.xsd +48 -0
- rodski/schemas/model.xsd +177 -0
- rodski/schemas/result.xsd +104 -0
- rodski/ski_gui.py +6 -0
- rodski/ski_run.py +132 -0
- rodski/ui/__init__.py +0 -0
- rodski/utils/__init__.py +4 -0
- rodski/utils/helpers.py +29 -0
- rodski/vision/__init__.py +67 -0
- rodski/vision/ai_verifier.py +378 -0
- rodski/vision/bbox_locator.py +147 -0
- rodski/vision/cache.py +253 -0
- rodski/vision/coordinate_utils.py +315 -0
- rodski/vision/desktop_driver.py +258 -0
- rodski/vision/exceptions.py +267 -0
- rodski/vision/llm_analyzer.py +92 -0
- rodski/vision/locator.py +575 -0
- rodski/vision/matcher.py +136 -0
- rodski/vision/ocr_locator.py +333 -0
- rodski/vision/omni_client.py +220 -0
- rodski/vision/screen_recorder.py +210 -0
- rodski/vision/screenshot.py +148 -0
- rodski-5.7.1.dist-info/METADATA +101 -0
- rodski-5.7.1.dist-info/RECORD +119 -0
- rodski-5.7.1.dist-info/WHEEL +5 -0
- rodski-5.7.1.dist-info/entry_points.txt +2 -0
- rodski-5.7.1.dist-info/top_level.txt +1 -0
rodski/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lightning Strike Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rodski/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# RodSki - 自动化测试框架
|
|
2
|
+
|
|
3
|
+
基于关键字驱动的现代化自动化测试框架,Python 重写版本。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🎯 关键字驱动 - 简单易用的测试语法
|
|
8
|
+
- 🔧 模型驱动 - 模型与数据分离
|
|
9
|
+
- 🌐 多驱动支持 - Playwright (Web) + PyWinAuto (Desktop) + Appium (Mobile)
|
|
10
|
+
- 📊 XML 用例 - 结构化测试定义
|
|
11
|
+
- 🔌 RESTful API - 接口测试支持
|
|
12
|
+
|
|
13
|
+
## 快速开始
|
|
14
|
+
|
|
15
|
+
📖 **[5分钟快速入门指南](docs/user-guides/QUICKSTART.md)** - 新手必读!
|
|
16
|
+
|
|
17
|
+
### 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -r requirements.txt
|
|
21
|
+
playwright install chromium
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### CLI 运行
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 cli_main.py run examples/product/DEMO/demo_site/case/demo_case.xml
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 项目结构
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
rodski/
|
|
34
|
+
├── core/ # 核心引擎
|
|
35
|
+
├── drivers/ # 驱动层
|
|
36
|
+
├── api/ # API 测试
|
|
37
|
+
├── config/ # 配置
|
|
38
|
+
├── data/ # 测试数据
|
|
39
|
+
├── examples/ # 示例用例
|
|
40
|
+
└── docs/ # 文档
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 开发
|
|
44
|
+
|
|
45
|
+
### 环境要求
|
|
46
|
+
|
|
47
|
+
- Python 3.8+
|
|
48
|
+
- Playwright
|
|
49
|
+
|
|
50
|
+
### 贡献
|
|
51
|
+
|
|
52
|
+
欢迎提交 Issue 和 Pull Request!
|
|
53
|
+
|
|
54
|
+
## 许可证
|
|
55
|
+
|
|
56
|
+
MIT License
|
rodski/__init__.py
ADDED
rodski/api/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""RESTful API 测试辅助工具"""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from jsonschema import validate, ValidationError
|
|
6
|
+
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
from urllib3.util.retry import Retry
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RestHelper:
|
|
14
|
+
"""REST API 测试辅助类"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def _get_session(max_retries: int = 3, pool_connections: int = 10, pool_maxsize: int = 10) -> requests.Session:
|
|
18
|
+
"""获取配置了连接池和重试的 Session"""
|
|
19
|
+
session = requests.Session()
|
|
20
|
+
retry_strategy = Retry(
|
|
21
|
+
total=max_retries,
|
|
22
|
+
backoff_factor=0.3,
|
|
23
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
24
|
+
)
|
|
25
|
+
adapter = HTTPAdapter(max_retries=retry_strategy, pool_connections=pool_connections, pool_maxsize=pool_maxsize)
|
|
26
|
+
session.mount("http://", adapter)
|
|
27
|
+
session.mount("https://", adapter)
|
|
28
|
+
return session
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def validate_json_schema(data: Dict, schema: Dict) -> bool:
|
|
32
|
+
"""验证 JSON Schema"""
|
|
33
|
+
try:
|
|
34
|
+
validate(instance=data, schema=schema)
|
|
35
|
+
return True
|
|
36
|
+
except ValidationError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def jsonpath_extract(data: Dict, path: str) -> Any:
|
|
41
|
+
"""简单 JSONPath 提取(支持点号路径)"""
|
|
42
|
+
keys = path.strip('$.').split('.')
|
|
43
|
+
current = data
|
|
44
|
+
for key in keys:
|
|
45
|
+
if isinstance(current, dict):
|
|
46
|
+
current = current.get(key)
|
|
47
|
+
else:
|
|
48
|
+
return None
|
|
49
|
+
return current
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def send_request(
|
|
53
|
+
method: str,
|
|
54
|
+
url: str,
|
|
55
|
+
headers: Optional[Dict[str, str]] = None,
|
|
56
|
+
body: Any = None,
|
|
57
|
+
timeout: float = 30.0,
|
|
58
|
+
max_retries: int = 3,
|
|
59
|
+
use_session: bool = True,
|
|
60
|
+
cookies: Optional[Dict[str, str]] = None,
|
|
61
|
+
) -> requests.Response:
|
|
62
|
+
"""发送 HTTP 请求"""
|
|
63
|
+
method = method.upper()
|
|
64
|
+
kwargs: Dict[str, Any] = {"headers": headers, "timeout": timeout}
|
|
65
|
+
if body is not None:
|
|
66
|
+
kwargs["json"] = body
|
|
67
|
+
if cookies:
|
|
68
|
+
kwargs["cookies"] = cookies
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
logger.debug(f"Sending {method} request to {url}")
|
|
72
|
+
if use_session:
|
|
73
|
+
session = RestHelper._get_session(max_retries=max_retries)
|
|
74
|
+
response = session.request(method, url, **kwargs)
|
|
75
|
+
else:
|
|
76
|
+
response = requests.request(method, url, **kwargs)
|
|
77
|
+
logger.debug(f"Response: {response.status_code} in {response.elapsed.total_seconds():.3f}s")
|
|
78
|
+
return response
|
|
79
|
+
except requests.exceptions.Timeout as e:
|
|
80
|
+
logger.error(f"Request timeout: {url}")
|
|
81
|
+
raise
|
|
82
|
+
except requests.exceptions.ConnectionError as e:
|
|
83
|
+
logger.error(f"Connection error: {url}")
|
|
84
|
+
raise
|
|
85
|
+
except requests.exceptions.RequestException as e:
|
|
86
|
+
logger.error(f"Request failed: {url} - {str(e)}")
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def assert_response_time(response: requests.Response, max_ms: float) -> bool:
|
|
91
|
+
"""断言响应时间(毫秒)"""
|
|
92
|
+
elapsed_ms = response.elapsed.total_seconds() * 1000
|
|
93
|
+
return elapsed_ms <= max_ms
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def extract_headers(response: requests.Response, header_name: str) -> Optional[str]:
|
|
97
|
+
"""提取响应头"""
|
|
98
|
+
return response.headers.get(header_name)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def compare_responses(
|
|
102
|
+
resp1: Dict,
|
|
103
|
+
resp2: Dict,
|
|
104
|
+
ignore_fields: Optional[List[str]] = None,
|
|
105
|
+
) -> bool:
|
|
106
|
+
"""比较两个响应(忽略指定字段)"""
|
|
107
|
+
ignore_fields = ignore_fields or []
|
|
108
|
+
|
|
109
|
+
def _strip(d: Any) -> Any:
|
|
110
|
+
if isinstance(d, dict):
|
|
111
|
+
return {k: _strip(v) for k, v in d.items() if k not in ignore_fields}
|
|
112
|
+
if isinstance(d, list):
|
|
113
|
+
return [_strip(i) for i in d]
|
|
114
|
+
return d
|
|
115
|
+
|
|
116
|
+
return _strip(resp1) == _strip(resp2)
|
rodski/cli_main.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""RodSki CLI 入口"""
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import traceback
|
|
6
|
+
from .rodski_cli import run, model, config, log, report, profile, docs
|
|
7
|
+
|
|
8
|
+
from rodski import __version__ as VERSION
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_error(e, verbose=False):
|
|
12
|
+
"""格式化错误信息,提高可读性"""
|
|
13
|
+
error_type = type(e).__name__
|
|
14
|
+
error_msg = str(e)
|
|
15
|
+
|
|
16
|
+
# 常见错误的友好提示
|
|
17
|
+
hints = {
|
|
18
|
+
FileNotFoundError: "请检查文件路径是否正确",
|
|
19
|
+
PermissionError: "请检查文件权限或以管理员身份运行",
|
|
20
|
+
ConnectionError: "请检查网络连接或目标服务是否可用",
|
|
21
|
+
TimeoutError: "操作超时,请检查网络或增加超时时间",
|
|
22
|
+
KeyboardInterrupt: "用户中断执行",
|
|
23
|
+
ImportError: "缺少依赖包,请运行: pip install -r requirements.txt",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hint = hints.get(type(e), "")
|
|
27
|
+
lines = [f"错误 [{error_type}]: {error_msg}"]
|
|
28
|
+
if hint:
|
|
29
|
+
lines.append(f"提示: {hint}")
|
|
30
|
+
if verbose:
|
|
31
|
+
lines.append("")
|
|
32
|
+
lines.append("详细堆栈信息:")
|
|
33
|
+
lines.append(traceback.format_exc())
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser(
|
|
39
|
+
prog="rodski",
|
|
40
|
+
description="RodSki - 关键字驱动测试框架",
|
|
41
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
42
|
+
epilog="示例:\n"
|
|
43
|
+
" ski run case/ 执行测试用例\n"
|
|
44
|
+
" ski run case/ --dry-run 验证用例但不执行\n"
|
|
45
|
+
" ski run case/ --verbose 详细输出模式\n"
|
|
46
|
+
" ski run case/ --step-by-step 单步执行模式\n"
|
|
47
|
+
" ski run case/ --pause-on-error 错误时暂停\n"
|
|
48
|
+
" ski run case/ --interactive 交互模式\n"
|
|
49
|
+
" ski config list 查看配置\n"
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument("--version", action="version", version=f"RodSki {VERSION}")
|
|
52
|
+
parser.add_argument("--verbose", action="store_true", help="详细输出模式")
|
|
53
|
+
|
|
54
|
+
subparsers = parser.add_subparsers(dest="command", help="子命令")
|
|
55
|
+
|
|
56
|
+
# run 子命令
|
|
57
|
+
run.setup_parser(subparsers)
|
|
58
|
+
|
|
59
|
+
# model 子命令
|
|
60
|
+
model.setup_parser(subparsers)
|
|
61
|
+
|
|
62
|
+
# config 子命令
|
|
63
|
+
config.setup_parser(subparsers)
|
|
64
|
+
|
|
65
|
+
# log 子命令
|
|
66
|
+
log.setup_parser(subparsers)
|
|
67
|
+
|
|
68
|
+
# report 子命令
|
|
69
|
+
report.setup_parser(subparsers)
|
|
70
|
+
|
|
71
|
+
# profile 子命令
|
|
72
|
+
profile.setup_parser(subparsers)
|
|
73
|
+
|
|
74
|
+
# docs 子命令
|
|
75
|
+
docs.setup_parser(subparsers)
|
|
76
|
+
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
if not args.command:
|
|
80
|
+
parser.print_help()
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
# 执行对应子命令
|
|
84
|
+
handlers = {
|
|
85
|
+
"run": run.handle,
|
|
86
|
+
"model": model.handle,
|
|
87
|
+
"config": config.handle,
|
|
88
|
+
"log": log.handle,
|
|
89
|
+
"report": report.handle,
|
|
90
|
+
"profile": profile.handle,
|
|
91
|
+
"docs": docs.handle
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
verbose = getattr(args, "verbose", False)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return handlers[args.command](args)
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
print("\n操作已被用户中断", file=sys.stderr)
|
|
100
|
+
return 130
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(format_error(e, verbose=verbose), file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"driver": "web",
|
|
3
|
+
"browser": "chromium",
|
|
4
|
+
"headless": false,
|
|
5
|
+
"timeout": 60,
|
|
6
|
+
"retry": 0,
|
|
7
|
+
"retry_delay": 1.0,
|
|
8
|
+
"retry_on_errors": [
|
|
9
|
+
"ElementNotFound",
|
|
10
|
+
"Timeout",
|
|
11
|
+
"StaleElement"
|
|
12
|
+
],
|
|
13
|
+
"log_level": "INFO",
|
|
14
|
+
"log_dir": "logs",
|
|
15
|
+
"report_format": "html",
|
|
16
|
+
"auto_screenshot_on_failure": true,
|
|
17
|
+
"auto_screenshot_on_step": true,
|
|
18
|
+
"screenshot_dir": "screenshots",
|
|
19
|
+
"smart_wait_enabled": true,
|
|
20
|
+
"smart_wait_max_retries": 30,
|
|
21
|
+
"smart_wait_retry_interval": 0.3,
|
|
22
|
+
"smart_wait_log_retry": true,
|
|
23
|
+
"element_wait_timeout": 0.5,
|
|
24
|
+
"element_retry_interval": 0.1
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# RodSki 默认配置 - Iteration-04 异常处理与智能恢复
|
|
2
|
+
|
|
3
|
+
execution:
|
|
4
|
+
# 最大重试次数(全局)
|
|
5
|
+
max_retries: 3
|
|
6
|
+
# 是否启用智能恢复
|
|
7
|
+
recovery_enabled: true
|
|
8
|
+
# 单次恢复最大尝试次数
|
|
9
|
+
recovery_max_attempts: 2
|
|
10
|
+
# 失败时自动截图
|
|
11
|
+
screenshot_on_failure: true
|
|
12
|
+
# 浏览器实例最大步数(超过后自动回收)
|
|
13
|
+
browser_restart_interval: 50
|
|
14
|
+
# 恢复动作之间等待秒数
|
|
15
|
+
recovery_wait_seconds: 1
|
|
16
|
+
|
|
17
|
+
verification:
|
|
18
|
+
# AI 视觉验证模型
|
|
19
|
+
model: claude
|
|
20
|
+
# AI 请求超时(秒)
|
|
21
|
+
timeout: 30
|
|
22
|
+
# 模型名称(空表示提供者默认值)
|
|
23
|
+
model_name: ""
|
|
24
|
+
# API 密钥环境变量名
|
|
25
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
26
|
+
# 代理地址(空表示官方地址)
|
|
27
|
+
base_url: ""
|
|
28
|
+
|
|
29
|
+
diagnosis:
|
|
30
|
+
# 是否启用 AI 辅助诊断
|
|
31
|
+
ai_enabled: true
|
|
32
|
+
# 诊断超时(秒)
|
|
33
|
+
timeout: 30
|
|
34
|
+
# AI 视觉分析失败时是否回退到规则诊断
|
|
35
|
+
fallback_to_rules: true
|
|
36
|
+
|
|
37
|
+
recovery:
|
|
38
|
+
# 恢复动作注册表(可扩展自定义动作)
|
|
39
|
+
actions: {}
|
|
40
|
+
# custom_action: "my_module.custom_handler"
|
|
41
|
+
|
|
42
|
+
snapshot:
|
|
43
|
+
# 是否启用执行快照(断点续跑)
|
|
44
|
+
enabled: false
|
|
45
|
+
# 快照存放目录
|
|
46
|
+
snapshot_dir: "snapshots"
|
|
47
|
+
# 每次步骤执行后保存快照(可能导致性能下降)
|
|
48
|
+
save_on_every_step: false
|
|
49
|
+
|
|
50
|
+
result:
|
|
51
|
+
# 记录步骤执行时间
|
|
52
|
+
record_step_timing: true
|
|
53
|
+
# 捕获失败上下文(页面源码、控制台日志等)
|
|
54
|
+
capture_failure_context: true
|
|
55
|
+
# 在结果中嵌入诊断信息
|
|
56
|
+
embed_diagnosis: true
|
|
57
|
+
|
|
58
|
+
metadata:
|
|
59
|
+
# 从用例中提取元数据
|
|
60
|
+
extract_from_case: true
|
|
61
|
+
# 在 CLI 中显示元数据
|
|
62
|
+
show_in_cli: true
|
|
63
|
+
|
|
64
|
+
statistics:
|
|
65
|
+
# 结果目录
|
|
66
|
+
result_dir: "result"
|
|
67
|
+
# 不稳定用例阈值(失败率)
|
|
68
|
+
flaky_threshold: 0.3
|
|
69
|
+
# 显示最慢用例数量
|
|
70
|
+
top_slow_count: 10
|
|
71
|
+
|
|
72
|
+
execution:
|
|
73
|
+
# 最大重试次数(全局)
|
|
74
|
+
max_retries: 3
|
|
75
|
+
# 是否启用智能恢复
|
|
76
|
+
recovery_enabled: true
|
|
77
|
+
# 单次恢复最大尝试次数
|
|
78
|
+
recovery_max_attempts: 2
|
|
79
|
+
# 失败时自动截图
|
|
80
|
+
screenshot_on_failure: true
|
|
81
|
+
# 浏览器实例最大步数(超过后自动回收)
|
|
82
|
+
browser_restart_interval: 50
|
|
83
|
+
# 恢复动作之间等待秒数
|
|
84
|
+
recovery_wait_seconds: 1
|
|
85
|
+
# 允许条件执行
|
|
86
|
+
allow_conditions: true
|
|
87
|
+
# 允许循环执行
|
|
88
|
+
allow_loops: true
|
|
89
|
+
# 允许动态步骤注入
|
|
90
|
+
allow_dynamic_steps: true
|
|
91
|
+
# 循环最大迭代次数
|
|
92
|
+
loop_max_iterations: 1000
|
|
93
|
+
# 动态步骤配置
|
|
94
|
+
dynamic_step:
|
|
95
|
+
# 每个触发点最大动态步骤数
|
|
96
|
+
max_per_trigger: 10
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# RodSki LLM 统一配置
|
|
2
|
+
# 所有 LLM 相关配置集中在此文件
|
|
3
|
+
|
|
4
|
+
# 默认 provider
|
|
5
|
+
provider: claude
|
|
6
|
+
|
|
7
|
+
# Provider 配置
|
|
8
|
+
providers:
|
|
9
|
+
claude:
|
|
10
|
+
model: claude-opus-4-6
|
|
11
|
+
base_url: "http://code.casstime.ai" # 可选代理地址
|
|
12
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
13
|
+
timeout: 10
|
|
14
|
+
max_tokens: 1024
|
|
15
|
+
|
|
16
|
+
openai:
|
|
17
|
+
model: gpt-5.4
|
|
18
|
+
base_url: "http://code.casstime.ai"
|
|
19
|
+
api_key_env: OPENAI_API_KEY
|
|
20
|
+
timeout: 10
|
|
21
|
+
max_tokens: 2000
|
|
22
|
+
|
|
23
|
+
# 能力配置
|
|
24
|
+
capabilities:
|
|
25
|
+
vision_locator:
|
|
26
|
+
provider: claude # 可覆盖默认 provider
|
|
27
|
+
screenshot_verifier:
|
|
28
|
+
provider: claude
|
|
29
|
+
test_reviewer:
|
|
30
|
+
provider: openai
|
|
31
|
+
temperature: 0.1
|
|
32
|
+
max_tokens: 2000
|
|
33
|
+
enable_vision: true
|
|
34
|
+
max_screenshots: 10
|
|
35
|
+
system_prompt: |
|
|
36
|
+
你是一个专业的自动化测试结果审查员。
|
|
37
|
+
你的任务是根据测试用例定义、执行日志和截图,判断测试是否真正成功。
|
|
38
|
+
|
|
39
|
+
判断标准:
|
|
40
|
+
1. 所有步骤是否按预期执行
|
|
41
|
+
2. 截图中的界面状态是否符合预期
|
|
42
|
+
3. 是否有异常、错误或警告
|
|
43
|
+
4. 最终结果是否达到测试目标
|
|
44
|
+
|
|
45
|
+
输出格式:
|
|
46
|
+
{
|
|
47
|
+
"verdict": "PASS|FAIL|SUSPICIOUS",
|
|
48
|
+
"confidence": 0.0-1.0,
|
|
49
|
+
"reason": "判断理由",
|
|
50
|
+
"issues": ["发现的问题列表"]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# OmniParser 配置
|
|
54
|
+
omniparser:
|
|
55
|
+
url: "http://14.103.175.167:7862/parse/"
|
|
56
|
+
box_threshold: 0.18
|
|
57
|
+
iou_threshold: 0.7
|
|
58
|
+
timeout: 5
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# DEPRECATED: 配置已迁移到 llm_config.yaml。本文件仅作为向后兼容保留。
|
|
2
|
+
# LLM Reviewer 配置
|
|
3
|
+
|
|
4
|
+
# API 配置
|
|
5
|
+
api_base: "http://code.casstime.ai"
|
|
6
|
+
api_key: "sk-f3tf9sUywnTBNbkQesLsaEE5Abtz1vZX88Sqvqm5MG7cGg3w"
|
|
7
|
+
model: "gpt-5.4"
|
|
8
|
+
temperature: 0.1
|
|
9
|
+
max_tokens: 2000
|
|
10
|
+
|
|
11
|
+
# 审查配置
|
|
12
|
+
review:
|
|
13
|
+
enable_vision: true
|
|
14
|
+
max_screenshots: 10
|
|
15
|
+
system_prompt: |
|
|
16
|
+
你是一个专业的自动化测试结果审查员。
|
|
17
|
+
你的任务是根据测试用例定义、执行日志和截图,判断测试是否真正成功。
|
|
18
|
+
|
|
19
|
+
判断标准:
|
|
20
|
+
1. 所有步骤是否按预期执行
|
|
21
|
+
2. 截图中的界面状态是否符合预期
|
|
22
|
+
3. 是否有异常、错误或警告
|
|
23
|
+
4. 最终结果是否达到测试目标
|
|
24
|
+
|
|
25
|
+
输出格式:
|
|
26
|
+
{
|
|
27
|
+
"verdict": "PASS|FAIL|SUSPICIOUS",
|
|
28
|
+
"confidence": 0.0-1.0,
|
|
29
|
+
"reason": "判断理由",
|
|
30
|
+
"issues": ["发现的问题列表"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# DEPRECATED: 配置已迁移到 llm_config.yaml。本文件仅作为向后兼容保留。
|
|
2
|
+
omniparser:
|
|
3
|
+
url: http://14.103.175.167:7862/parse/
|
|
4
|
+
box_threshold: 0.18
|
|
5
|
+
iou_threshold: 0.7
|
|
6
|
+
timeout: 5
|
|
7
|
+
|
|
8
|
+
llm:
|
|
9
|
+
provider: claude # claude / openai / qwen
|
|
10
|
+
model: claude-opus-4-6
|
|
11
|
+
base_url: "" # 留空使用默认,或填代理地址
|
|
12
|
+
api_key_env: ANTHROPIC_API_KEY # 环境变量名
|
|
13
|
+
timeout: 10
|
|
14
|
+
max_tokens: 1024
|
rodski/core/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Core package — lazy imports to avoid heavy optional dependencies at import time"""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
'KeywordEngine',
|
|
5
|
+
'TaskExecutor',
|
|
6
|
+
'ConfigManager',
|
|
7
|
+
'Logger',
|
|
8
|
+
'RodskiXmlValidator',
|
|
9
|
+
'XmlSchemaValidationError',
|
|
10
|
+
'DriverFactory',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name):
|
|
15
|
+
if name == 'KeywordEngine':
|
|
16
|
+
from .keyword_engine import KeywordEngine
|
|
17
|
+
return KeywordEngine
|
|
18
|
+
if name == 'TaskExecutor':
|
|
19
|
+
from .task_executor import TaskExecutor
|
|
20
|
+
return TaskExecutor
|
|
21
|
+
if name == 'ConfigManager':
|
|
22
|
+
from .config_manager import ConfigManager
|
|
23
|
+
return ConfigManager
|
|
24
|
+
if name == 'Logger':
|
|
25
|
+
from .logger import Logger
|
|
26
|
+
return Logger
|
|
27
|
+
if name == 'RodskiXmlValidator':
|
|
28
|
+
from .xml_schema_validator import RodskiXmlValidator
|
|
29
|
+
return RodskiXmlValidator
|
|
30
|
+
if name == 'XmlSchemaValidationError':
|
|
31
|
+
from .exceptions import XmlSchemaValidationError
|
|
32
|
+
return XmlSchemaValidationError
|
|
33
|
+
if name == 'DriverFactory':
|
|
34
|
+
from .driver_factory import DriverFactory
|
|
35
|
+
return DriverFactory
|
|
36
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""视觉断言模块 — lazy imports to avoid requiring cv2 at import time."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["BaseAssertion", "ImageMatcher", "VideoAnalyzer"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
if name == 'BaseAssertion':
|
|
8
|
+
from .base_assertion import BaseAssertion
|
|
9
|
+
return BaseAssertion
|
|
10
|
+
if name == 'ImageMatcher':
|
|
11
|
+
from .image_matcher import ImageMatcher
|
|
12
|
+
return ImageMatcher
|
|
13
|
+
if name == 'VideoAnalyzer':
|
|
14
|
+
from .video_analyzer import VideoAnalyzer
|
|
15
|
+
return VideoAnalyzer
|
|
16
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""断言基类 - 定义断言接口"""
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseAssertion(ABC):
|
|
8
|
+
"""视觉断言基类
|
|
9
|
+
|
|
10
|
+
所有断言实现必须继承此类并实现 match 方法。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def match(self, *args, **kwargs) -> Dict[str, Any]:
|
|
15
|
+
"""执行断言匹配
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
结构化结果字典,至少包含:
|
|
19
|
+
- matched: bool 是否匹配成功
|
|
20
|
+
- similarity: float 匹配度
|
|
21
|
+
- threshold: float 阈值
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def resolve_reference_path(reference: str, module_dir: Path) -> Path:
|
|
27
|
+
"""将相对路径解析为绝对路径
|
|
28
|
+
|
|
29
|
+
预期图片路径相对于 `images/assert/` 目录。
|
|
30
|
+
"""
|
|
31
|
+
images_dir = module_dir / "images" / "assert"
|
|
32
|
+
ref_path = Path(reference)
|
|
33
|
+
if ref_path.is_absolute():
|
|
34
|
+
return ref_path
|
|
35
|
+
return images_dir / reference
|