drun 2.1.2__tar.gz → 2.2.2__tar.gz
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.
- {drun-2.1.2 → drun-2.2.2}/PKG-INFO +1 -1
- {drun-2.1.2 → drun-2.2.2}/drun/__init__.py +1 -1
- {drun-2.1.2 → drun-2.2.2}/drun/cli.py +11 -17
- {drun-2.1.2 → drun-2.2.2}/drun/reporter/html_reporter.py +21 -7
- {drun-2.1.2 → drun-2.2.2}/drun/runner/assertions.py +15 -0
- {drun-2.1.2 → drun-2.2.2}/drun/runner/runner.py +8 -2
- {drun-2.1.2 → drun-2.2.2}/drun/scaffolds/__init__.py +2 -0
- {drun-2.1.2 → drun-2.2.2}/drun/scaffolds/templates.py +249 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/curl.py +20 -6
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/PKG-INFO +1 -1
- {drun-2.1.2 → drun-2.2.2}/pyproject.toml +1 -1
- {drun-2.1.2 → drun-2.2.2}/LICENSE +0 -0
- {drun-2.1.2 → drun-2.2.2}/README.md +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/db/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/db/database_proxy.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/db/generate_mysql_config.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/engine/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/engine/http.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/exporters/curl.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/importers/base.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/importers/curl.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/importers/har.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/importers/openapi.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/importers/postman.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/loader/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/loader/collector.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/loader/env.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/loader/hooks.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/loader/yaml_loader.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/case.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/config.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/report.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/request.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/step.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/models/validators.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/base.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/dingtalk.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/emailer.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/feishu.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/notifier/format.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/reporter/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/reporter/allure_reporter.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/reporter/json_reporter.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/runner/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/runner/extractors.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/templating/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/templating/builtins.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/templating/context.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/templating/engine.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/__init__.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/errors.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/logging.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/mask.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun/utils/timeit.py +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/SOURCES.txt +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/dependency_links.txt +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/entry_points.txt +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/requires.txt +0 -0
- {drun-2.1.2 → drun-2.2.2}/drun.egg-info/top_level.txt +0 -0
- {drun-2.1.2 → drun-2.2.2}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "2.
|
|
2
|
+
__version__ = "2.2.2"
|
|
@@ -1677,7 +1677,7 @@ def init_project(
|
|
|
1677
1677
|
from drun import scaffolds
|
|
1678
1678
|
|
|
1679
1679
|
# Display version
|
|
1680
|
-
typer.echo(f"Drun v{_get_drun_version()}
|
|
1680
|
+
typer.echo(f"Drun v{_get_drun_version()}")
|
|
1681
1681
|
|
|
1682
1682
|
# 确定目标目录
|
|
1683
1683
|
if name:
|
|
@@ -1752,6 +1752,9 @@ def init_project(
|
|
|
1752
1752
|
# testcases/test_stream.yaml
|
|
1753
1753
|
_write_template("testcases/test_stream.yaml", scaffolds.STREAM_TESTCASE)
|
|
1754
1754
|
|
|
1755
|
+
# testcases/test_assertions.yaml
|
|
1756
|
+
_write_template("testcases/test_assertions.yaml", scaffolds.ASSERTIONS_TESTCASE)
|
|
1757
|
+
|
|
1755
1758
|
# data/users.csv
|
|
1756
1759
|
_write_template("data/users.csv", scaffolds.CSV_USERS_SAMPLE)
|
|
1757
1760
|
|
|
@@ -1806,7 +1809,8 @@ def init_project(
|
|
|
1806
1809
|
("│ ├── ", "test_api_health.yaml", "健康检查示例"),
|
|
1807
1810
|
("│ ├── ", "test_stream.yaml", "流式响应 (SSE) 示例"),
|
|
1808
1811
|
("│ ├── ", "test_db_assert.yaml", "数据库断言示例"),
|
|
1809
|
-
("│
|
|
1812
|
+
("│ ├── ", "test_import_users.yaml", "CSV 参数化示例"),
|
|
1813
|
+
("│ └── ", "test_assertions.yaml", "断言操作符完整示例"),
|
|
1810
1814
|
("├── ", "testsuites/", ""),
|
|
1811
1815
|
("│ ├── ", "testsuite_smoke.yaml", "冒烟测试套件"),
|
|
1812
1816
|
("│ └── ", "testsuite_csv.yaml", "CSV 示例套件"),
|
|
@@ -1841,7 +1845,7 @@ def init_project(
|
|
|
1841
1845
|
else:
|
|
1842
1846
|
typer.echo(full)
|
|
1843
1847
|
typer.echo("")
|
|
1844
|
-
typer.echo("9 directories,
|
|
1848
|
+
typer.echo("9 directories, 18 files")
|
|
1845
1849
|
|
|
1846
1850
|
if skipped_files:
|
|
1847
1851
|
typer.echo("")
|
|
@@ -1862,20 +1866,10 @@ def init_project(
|
|
|
1862
1866
|
if name:
|
|
1863
1867
|
typer.echo(f" cd {name}")
|
|
1864
1868
|
typer.echo(" drun run testcases/test_api_health.yaml")
|
|
1865
|
-
|
|
1866
|
-
typer.echo(" drun run testcases/
|
|
1867
|
-
typer.echo(" drun run testcases/
|
|
1868
|
-
typer.echo(" drun run testcases/
|
|
1869
|
-
typer.echo("")
|
|
1870
|
-
typer.echo("格式转换 (查看 converts/README.md 获取详细说明):")
|
|
1871
|
-
typer.echo(" - cURL 转用例:")
|
|
1872
|
-
typer.echo(" drun convert converts/curl/sample.curl --outfile testcases/new_test.yaml")
|
|
1873
|
-
typer.echo(" - Postman 转用例:")
|
|
1874
|
-
typer.echo(" drun convert converts/postman/sample_collection.json --split-output --suite-out testsuites/new_suite.yaml")
|
|
1875
|
-
typer.echo(" - HAR 转用例:")
|
|
1876
|
-
typer.echo(" drun convert converts/har/sample_recording.har --exclude-static --only-2xx --outfile testcases/from_har.yaml")
|
|
1877
|
-
typer.echo(" - OpenAPI 转用例:")
|
|
1878
|
-
typer.echo(" drun convert-openapi converts/openapi/sample_openapi.json --split-output --outfile testcases/from_openapi.yaml")
|
|
1869
|
+
typer.echo(" drun run testcases/test_stream.yaml")
|
|
1870
|
+
typer.echo(" drun run testcases/test_db_assert.yaml")
|
|
1871
|
+
typer.echo(" drun run testcases/test_import_users.yaml")
|
|
1872
|
+
typer.echo(" drun run testcases/test_assertions.yaml")
|
|
1879
1873
|
typer.echo("")
|
|
1880
1874
|
typer.echo("文档: https://github.com/Devliang24/drun")
|
|
1881
1875
|
|
|
@@ -44,14 +44,28 @@ def _escape_html(text: str) -> str:
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _format_assert_value(value: Any) -> str:
|
|
48
|
+
"""Format assertion value: strings without quotes, others with JSON"""
|
|
49
|
+
if value is None:
|
|
50
|
+
return "null"
|
|
51
|
+
if isinstance(value, str):
|
|
52
|
+
return value
|
|
53
|
+
if isinstance(value, bool):
|
|
54
|
+
return "true" if value else "false"
|
|
55
|
+
if isinstance(value, (int, float)):
|
|
56
|
+
return str(value)
|
|
57
|
+
# For lists and dicts, use JSON formatting
|
|
58
|
+
return json.dumps(value, ensure_ascii=False)
|
|
59
|
+
|
|
60
|
+
|
|
47
61
|
def _build_assert_table(asserts: List[AssertionResult]) -> str:
|
|
48
62
|
rows = []
|
|
49
63
|
for a in asserts or []:
|
|
50
64
|
cells = [
|
|
51
65
|
f"<td><code>{_escape_html(str(a.check))}</code></td>",
|
|
52
66
|
f"<td><code>{_escape_html(str(a.comparator))}</code></td>",
|
|
53
|
-
f"<td><code>{_escape_html(
|
|
54
|
-
f"<td><code>{_escape_html(
|
|
67
|
+
f"<td><code>{_escape_html(_format_assert_value(a.expect))}</code></td>",
|
|
68
|
+
f"<td><code>{_escape_html(_format_assert_value(a.actual))}</code></td>",
|
|
55
69
|
("<td><span class='ok'>✓</span></td>" if a.passed else f"<td><span class='err' title='{_escape_html(a.message or '')}'>✗</span></td>")
|
|
56
70
|
]
|
|
57
71
|
rows.append("<tr " + ("data-pass=1" if a.passed else "data-pass=0") + ">" + "".join(cells) + "</tr>")
|
|
@@ -428,14 +442,14 @@ def write_html(report: RunReport, outfile: str | Path) -> None:
|
|
|
428
442
|
.panel .p-head { padding:6px 8px; background:var(--panel-head-bg); color:var(--muted); font-size:12px; display:flex; justify-content:space-between; align-items:center; }
|
|
429
443
|
.panel .p-head .actions { display:flex; gap:6px; }
|
|
430
444
|
.panel pre, .panel table { margin:0; padding:10px; overflow:auto; max-height: 360px; }
|
|
431
|
-
.panel[data-section='curl'] pre { white-space: pre-
|
|
445
|
+
.panel[data-section='curl'] pre { white-space: pre; overflow-x: auto; word-break: normal; }
|
|
432
446
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
433
447
|
th { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; text-align: left; font-weight: 600; }
|
|
434
448
|
td { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; word-break: break-word; }
|
|
435
|
-
.assert-table th:nth-child(1), .assert-table td:nth-child(1) { width:
|
|
436
|
-
.assert-table th:nth-child(2), .assert-table td:nth-child(2) { width:
|
|
437
|
-
.assert-table th:nth-child(3), .assert-table td:nth-child(3) { width:
|
|
438
|
-
.assert-table th:nth-child(4), .assert-table td:nth-child(4) { width:
|
|
449
|
+
.assert-table th:nth-child(1), .assert-table td:nth-child(1) { width: 23%; }
|
|
450
|
+
.assert-table th:nth-child(2), .assert-table td:nth-child(2) { width: 15%; }
|
|
451
|
+
.assert-table th:nth-child(3), .assert-table td:nth-child(3) { width: 23%; }
|
|
452
|
+
.assert-table th:nth-child(4), .assert-table td:nth-child(4) { width: 24%; }
|
|
439
453
|
.assert-table th:nth-child(5), .assert-table td:nth-child(5) { width: 15%; text-align: center; }
|
|
440
454
|
.ok { color: var(--ok); }
|
|
441
455
|
.err { color: var(--fail); }
|
|
@@ -23,6 +23,19 @@ def op_ge(a: Any, b: Any) -> bool: return a >= b
|
|
|
23
23
|
def op_len_eq(a: Any, b: Any) -> bool: return _len(a) == int(b)
|
|
24
24
|
def op_in(a: Any, b: Any) -> bool: return a in b if b is not None else False
|
|
25
25
|
def op_not_in(a: Any, b: Any) -> bool: return a not in b if b is not None else True
|
|
26
|
+
def op_contains_all(a: Any, b: Any) -> bool:
|
|
27
|
+
"""Check if all elements in list a contain string b"""
|
|
28
|
+
if not isinstance(a, list):
|
|
29
|
+
return False
|
|
30
|
+
if not b:
|
|
31
|
+
return False
|
|
32
|
+
return all(str(b) in str(item) for item in a)
|
|
33
|
+
def op_match_regex_all(a: Any, b: Any) -> bool:
|
|
34
|
+
"""Check if all elements in list a match regex pattern b"""
|
|
35
|
+
if not isinstance(a, list):
|
|
36
|
+
return False
|
|
37
|
+
pattern = str(b)
|
|
38
|
+
return all(bool(re.search(pattern, str(item))) for item in a)
|
|
26
39
|
|
|
27
40
|
|
|
28
41
|
OPS: Dict[str, Callable[[Any, Any], bool]] = {
|
|
@@ -38,6 +51,8 @@ OPS: Dict[str, Callable[[Any, Any], bool]] = {
|
|
|
38
51
|
"len_eq": op_len_eq,
|
|
39
52
|
"in": op_in,
|
|
40
53
|
"not_in": op_not_in,
|
|
54
|
+
"contains_all": op_contains_all,
|
|
55
|
+
"match_regex_all": op_match_regex_all,
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
|
|
@@ -563,9 +563,15 @@ class Runner:
|
|
|
563
563
|
step_failed = False
|
|
564
564
|
for v in step.validators:
|
|
565
565
|
rendered_check = self._render(v.check, variables, funcs, envmap)
|
|
566
|
-
|
|
566
|
+
# If rendered_check is not a string, it's already a value (e.g., extracted variable)
|
|
567
|
+
# Use it directly as actual instead of trying to resolve from response
|
|
568
|
+
if not isinstance(rendered_check, str):
|
|
569
|
+
actual = rendered_check
|
|
570
|
+
check_str = str(v.check)
|
|
571
|
+
else:
|
|
572
|
+
check_str = rendered_check
|
|
573
|
+
actual = self._resolve_check(check_str, resp_obj)
|
|
567
574
|
expect_rendered = self._render(v.expect, variables, funcs, envmap)
|
|
568
|
-
actual = self._resolve_check(check_str, resp_obj)
|
|
569
575
|
passed, err = compare(v.comparator, actual, expect_rendered)
|
|
570
576
|
msg = err
|
|
571
577
|
if not passed and msg is None:
|
|
@@ -5,6 +5,7 @@ Drun 项目脚手架模板集合。
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .templates import (
|
|
8
|
+
ASSERTIONS_TESTCASE,
|
|
8
9
|
CONVERTS_README,
|
|
9
10
|
CSV_DATA_TESTCASE,
|
|
10
11
|
CSV_DATA_TESTSUITE,
|
|
@@ -27,6 +28,7 @@ from .templates import (
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
__all__ = [
|
|
31
|
+
"ASSERTIONS_TESTCASE",
|
|
30
32
|
"CONVERTS_README",
|
|
31
33
|
"CSV_DATA_TESTCASE",
|
|
32
34
|
"CSV_DATA_TESTSUITE",
|
|
@@ -1775,5 +1775,254 @@ steps:
|
|
|
1775
1775
|
- gt: [$time_to_last, $time_to_first] # 末包晚于首包
|
|
1776
1776
|
"""
|
|
1777
1777
|
|
|
1778
|
+
# 断言完整示例用例模板
|
|
1779
|
+
ASSERTIONS_TESTCASE = """config:
|
|
1780
|
+
name: 断言操作符完整示例
|
|
1781
|
+
base_url: ${ENV(BASE_URL)}
|
|
1782
|
+
tags: [demo, assertions]
|
|
1783
|
+
variables:
|
|
1784
|
+
expected_username: test_user
|
|
1785
|
+
max_response_time: 2000
|
|
1786
|
+
min_items: 5
|
|
1787
|
+
search_keyword: embedding
|
|
1788
|
+
|
|
1789
|
+
steps:
|
|
1790
|
+
# ==================== 基础比较断言 ====================
|
|
1791
|
+
- name: 基础比较断言示例
|
|
1792
|
+
request:
|
|
1793
|
+
method: GET
|
|
1794
|
+
path: /get?page=1&limit=10
|
|
1795
|
+
headers:
|
|
1796
|
+
User-Agent: Drun-Test-Client
|
|
1797
|
+
extract:
|
|
1798
|
+
page_num: $.args.page
|
|
1799
|
+
limit_num: $.args.limit
|
|
1800
|
+
validate:
|
|
1801
|
+
# eq: 等于
|
|
1802
|
+
- eq: [status_code, 200]
|
|
1803
|
+
- eq: [$page_num, "1"]
|
|
1804
|
+
- eq: [$.args.limit, "10"]
|
|
1805
|
+
|
|
1806
|
+
# ne: 不等于
|
|
1807
|
+
- ne: [status_code, 404]
|
|
1808
|
+
- ne: [$limit_num, "20"]
|
|
1809
|
+
|
|
1810
|
+
# lt: 小于
|
|
1811
|
+
- lt: [$elapsed_ms, $max_response_time]
|
|
1812
|
+
- lt: [$page_num, "10"]
|
|
1813
|
+
|
|
1814
|
+
# le: 小于等于
|
|
1815
|
+
- le: [status_code, 299]
|
|
1816
|
+
- le: [$limit_num, "10"]
|
|
1817
|
+
|
|
1818
|
+
# gt: 大于
|
|
1819
|
+
- gt: [status_code, 100]
|
|
1820
|
+
- gt: [$elapsed_ms, 0]
|
|
1821
|
+
|
|
1822
|
+
# ge: 大于等于
|
|
1823
|
+
- ge: [status_code, 200]
|
|
1824
|
+
- ge: [$limit_num, "1"]
|
|
1825
|
+
|
|
1826
|
+
# ==================== 字符串断言 ====================
|
|
1827
|
+
- name: 字符串断言示例
|
|
1828
|
+
request:
|
|
1829
|
+
method: POST
|
|
1830
|
+
path: /anything
|
|
1831
|
+
headers:
|
|
1832
|
+
Content-Type: application/json
|
|
1833
|
+
User-Agent: Drun-Test-Client/v1.0
|
|
1834
|
+
body:
|
|
1835
|
+
username: $expected_username
|
|
1836
|
+
email: test@example.com
|
|
1837
|
+
description: This is a test user account
|
|
1838
|
+
extract:
|
|
1839
|
+
response_json: $.json
|
|
1840
|
+
user_agent: $.headers.User-Agent
|
|
1841
|
+
validate:
|
|
1842
|
+
# contains: 包含子字符串
|
|
1843
|
+
- contains: [headers.Content-Type, application/json]
|
|
1844
|
+
- contains: [$user_agent, Drun-Test-Client]
|
|
1845
|
+
- contains: [$.json.description, test user]
|
|
1846
|
+
|
|
1847
|
+
# not_contains: 不包含子字符串
|
|
1848
|
+
- not_contains: [$.json.username, admin]
|
|
1849
|
+
- not_contains: [$.json.email, .cn]
|
|
1850
|
+
|
|
1851
|
+
# regex: 正则表达式匹配
|
|
1852
|
+
- regex: [$.json.email, '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$']
|
|
1853
|
+
- regex: [$.json.username, '^[a-z_]+$']
|
|
1854
|
+
- regex: [$user_agent, 'Drun.*v\d+\.\d+']
|
|
1855
|
+
|
|
1856
|
+
# ==================== 集合与长度断言 ====================
|
|
1857
|
+
- name: 集合与长度断言示例
|
|
1858
|
+
request:
|
|
1859
|
+
method: POST
|
|
1860
|
+
path: /anything
|
|
1861
|
+
body:
|
|
1862
|
+
tags: [python, testing, automation, api]
|
|
1863
|
+
permissions: [read, write, delete]
|
|
1864
|
+
metadata:
|
|
1865
|
+
count: 42
|
|
1866
|
+
active: true
|
|
1867
|
+
extract:
|
|
1868
|
+
tags_list: $.json.tags
|
|
1869
|
+
permissions: $.json.permissions
|
|
1870
|
+
tag_count: $.json.tags
|
|
1871
|
+
first_permission: $.json.permissions[0]
|
|
1872
|
+
validate:
|
|
1873
|
+
# in: 值在列表中
|
|
1874
|
+
- in: [python, $tags_list]
|
|
1875
|
+
- in: [testing, $tags_list]
|
|
1876
|
+
- in:
|
|
1877
|
+
- $first_permission
|
|
1878
|
+
- [read, write, admin]
|
|
1879
|
+
|
|
1880
|
+
# not_in: 值不在列表中
|
|
1881
|
+
- not_in: [java, $tags_list]
|
|
1882
|
+
- not_in: [admin, $permissions]
|
|
1883
|
+
|
|
1884
|
+
# len_eq: 长度等于
|
|
1885
|
+
- len_eq: [$tags_list, 4]
|
|
1886
|
+
- len_eq: [$permissions, 3]
|
|
1887
|
+
- len_eq: [$.json.metadata, 2]
|
|
1888
|
+
|
|
1889
|
+
# ==================== 批量断言(列表)====================
|
|
1890
|
+
- name: 批量断言示例 - 模型列表
|
|
1891
|
+
# 注意:实际使用时应该用 JSONPath 从响应中提取列表
|
|
1892
|
+
# 例如:extract: { all_model_names: "$.data.items[*].model_name" }
|
|
1893
|
+
# 这里为了演示,使用预定义的变量
|
|
1894
|
+
variables:
|
|
1895
|
+
all_model_names:
|
|
1896
|
+
- Deepexi-Embedding-V1
|
|
1897
|
+
- Deepexi-Embedding-V2
|
|
1898
|
+
- Deepexi-Embedding-V3
|
|
1899
|
+
all_model_ids:
|
|
1900
|
+
- emb-001
|
|
1901
|
+
- emb-002
|
|
1902
|
+
- emb-003
|
|
1903
|
+
request:
|
|
1904
|
+
method: GET
|
|
1905
|
+
path: /anything/models
|
|
1906
|
+
params:
|
|
1907
|
+
category: embedding
|
|
1908
|
+
vendor: deepexi
|
|
1909
|
+
validate:
|
|
1910
|
+
- eq: [status_code, 200]
|
|
1911
|
+
|
|
1912
|
+
# contains_all: 列表中所有元素都包含指定字符串
|
|
1913
|
+
- contains_all: [$all_model_names, Deepexi]
|
|
1914
|
+
- contains_all: [$all_model_names, Embedding]
|
|
1915
|
+
- contains_all: [$all_model_ids, emb-]
|
|
1916
|
+
|
|
1917
|
+
# match_regex_all: 列表中所有元素都匹配正则表达式
|
|
1918
|
+
- match_regex_all: [$all_model_names, '^Deepexi-Embedding-V\d+$']
|
|
1919
|
+
- match_regex_all: [$all_model_ids, '^emb-\d{3}$']
|
|
1920
|
+
|
|
1921
|
+
# ==================== 复杂场景组合 ====================
|
|
1922
|
+
- name: 复杂场景组合示例
|
|
1923
|
+
request:
|
|
1924
|
+
method: GET
|
|
1925
|
+
path: /get
|
|
1926
|
+
params:
|
|
1927
|
+
search: $search_keyword
|
|
1928
|
+
page: 1
|
|
1929
|
+
page_size: 20
|
|
1930
|
+
headers:
|
|
1931
|
+
Accept: application/json
|
|
1932
|
+
X-Request-ID: ${short_uid(16)}
|
|
1933
|
+
extract:
|
|
1934
|
+
items_list: $.args
|
|
1935
|
+
request_id: $.headers.X-Request-ID
|
|
1936
|
+
validate:
|
|
1937
|
+
# 状态码检查
|
|
1938
|
+
- eq: [status_code, 200]
|
|
1939
|
+
- ge: [status_code, 200]
|
|
1940
|
+
- lt: [status_code, 300]
|
|
1941
|
+
|
|
1942
|
+
# 响应时间检查
|
|
1943
|
+
- lt: [$elapsed_ms, 3000]
|
|
1944
|
+
- gt: [$elapsed_ms, 0]
|
|
1945
|
+
|
|
1946
|
+
# Content-Type 检查
|
|
1947
|
+
- contains: [headers.Content-Type, application/json]
|
|
1948
|
+
- not_contains: [headers.Content-Type, text/html]
|
|
1949
|
+
|
|
1950
|
+
# 参数回显检查
|
|
1951
|
+
- eq: [$.args.search, $search_keyword]
|
|
1952
|
+
- regex: [$.args.page, '^\d+$']
|
|
1953
|
+
- regex: [$.args.page_size, '^\d+$']
|
|
1954
|
+
|
|
1955
|
+
# Request ID 格式检查
|
|
1956
|
+
- regex: [$request_id, '^[a-f0-9]{16}$']
|
|
1957
|
+
- len_eq: [$request_id, 16]
|
|
1958
|
+
|
|
1959
|
+
# ==================== JSONPath 提取 + 批量断言 ====================
|
|
1960
|
+
- name: JSONPath 提取 + 批量断言
|
|
1961
|
+
# 注意:实际使用时应该用 JSONPath 从响应中提取列表
|
|
1962
|
+
# 例如:extract: { all_usernames: "$.json.users[*].username" }
|
|
1963
|
+
# 这里为了演示,使用预定义的变量模拟提取的数据
|
|
1964
|
+
variables:
|
|
1965
|
+
# 模拟从响应中提取的用户名列表
|
|
1966
|
+
all_usernames:
|
|
1967
|
+
- alice
|
|
1968
|
+
- bob
|
|
1969
|
+
- carol
|
|
1970
|
+
# 模拟从响应中提取的邮箱列表
|
|
1971
|
+
all_emails:
|
|
1972
|
+
- alice@example.com
|
|
1973
|
+
- bob@example.com
|
|
1974
|
+
- carol@example.com
|
|
1975
|
+
request:
|
|
1976
|
+
method: POST
|
|
1977
|
+
path: /anything/api/users
|
|
1978
|
+
body:
|
|
1979
|
+
users:
|
|
1980
|
+
- username: alice
|
|
1981
|
+
email: alice@example.com
|
|
1982
|
+
role: admin
|
|
1983
|
+
- username: bob
|
|
1984
|
+
email: bob@example.com
|
|
1985
|
+
role: member
|
|
1986
|
+
- username: carol
|
|
1987
|
+
email: carol@example.com
|
|
1988
|
+
role: member
|
|
1989
|
+
validate:
|
|
1990
|
+
- eq: [status_code, 200]
|
|
1991
|
+
|
|
1992
|
+
# 验证所有用户名都是小写字母
|
|
1993
|
+
- match_regex_all: [$all_usernames, '^[a-z]+$']
|
|
1994
|
+
|
|
1995
|
+
# 验证所有邮箱格式正确
|
|
1996
|
+
- match_regex_all: [$all_emails, '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$']
|
|
1997
|
+
|
|
1998
|
+
# 验证所有邮箱都来自 example.com 域
|
|
1999
|
+
- contains_all: [$all_emails, example.com]
|
|
2000
|
+
|
|
2001
|
+
# 验证列表长度
|
|
2002
|
+
- len_eq: [$all_usernames, 3]
|
|
2003
|
+
- len_eq: [$all_emails, 3]
|
|
2004
|
+
|
|
2005
|
+
# ==================== 性能和负值断言 ====================
|
|
2006
|
+
- name: 性能和边界值断言
|
|
2007
|
+
request:
|
|
2008
|
+
method: GET
|
|
2009
|
+
path: /delay/0
|
|
2010
|
+
extract:
|
|
2011
|
+
response_time: $elapsed_ms
|
|
2012
|
+
validate:
|
|
2013
|
+
- eq: [status_code, 200]
|
|
2014
|
+
|
|
2015
|
+
# 性能断言
|
|
2016
|
+
- lt: [$response_time, 1000] # 响应时间 < 1秒
|
|
2017
|
+
- gt: [$response_time, 0] # 响应时间 > 0
|
|
2018
|
+
- le: [$response_time, 1000] # 响应时间 <= 1秒
|
|
2019
|
+
- ge: [$response_time, 0] # 响应时间 >= 0
|
|
2020
|
+
|
|
2021
|
+
# 状态码范围
|
|
2022
|
+
- ge: [status_code, 200]
|
|
2023
|
+
- lt: [status_code, 300]
|
|
2024
|
+
- ne: [status_code, 204]
|
|
2025
|
+
"""
|
|
2026
|
+
|
|
1778
2027
|
# .gitkeep 文件内容(用于保留空目录)
|
|
1779
2028
|
GITKEEP_CONTENT = "# This file keeps the directory in version control\n"
|
|
@@ -5,12 +5,18 @@ from typing import Any, Dict
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def to_curl(method: str, path: str, *, headers: Dict[str, str] | None = None, data: Any | None = None) -> str:
|
|
8
|
-
"""Build a curl command string.
|
|
8
|
+
"""Build a curl command string with multi-line formatting.
|
|
9
9
|
|
|
10
10
|
- Uses --data-raw for request body to keep payload untouched.
|
|
11
11
|
- Pretty prints JSON body with indent=2 when data is dict/list for readability.
|
|
12
|
+
- Formats output with each parameter on a separate line using backslash continuation.
|
|
12
13
|
"""
|
|
13
|
-
|
|
14
|
+
lines = ["curl \\"]
|
|
15
|
+
|
|
16
|
+
# Method and URL
|
|
17
|
+
lines.append(f" -X {method.upper()} \\")
|
|
18
|
+
lines.append(f" {shlex.quote(path)} \\")
|
|
19
|
+
|
|
14
20
|
# Prepare headers (case-insensitive handling)
|
|
15
21
|
hdrs: Dict[str, str] = dict(headers or {})
|
|
16
22
|
has_ct = any(k.lower() == "content-type" for k in hdrs.keys())
|
|
@@ -21,14 +27,22 @@ def to_curl(method: str, path: str, *, headers: Dict[str, str] | None = None, da
|
|
|
21
27
|
is_json_like = s.startswith("{") or s.startswith("[")
|
|
22
28
|
if is_json_like:
|
|
23
29
|
hdrs["Content-Type"] = "application/json"
|
|
30
|
+
|
|
31
|
+
# Headers (each on separate line)
|
|
24
32
|
for k, v in hdrs.items():
|
|
25
|
-
|
|
33
|
+
lines.append(f" -H {shlex.quote(f'{k}: {v}')} \\")
|
|
34
|
+
|
|
35
|
+
# Data (if exists)
|
|
26
36
|
if data is not None:
|
|
27
37
|
if isinstance(data, (dict, list)):
|
|
28
38
|
import json
|
|
29
39
|
payload = json.dumps(data, ensure_ascii=False, indent=2)
|
|
30
40
|
else:
|
|
31
41
|
payload = str(data)
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
# Last line without backslash
|
|
43
|
+
lines.append(f" --data-raw {shlex.quote(payload)}")
|
|
44
|
+
else:
|
|
45
|
+
# Remove trailing backslash from last line
|
|
46
|
+
lines[-1] = lines[-1].rstrip(' \\')
|
|
47
|
+
|
|
48
|
+
return '\n'.join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|