dsh-origin-bridge 0.4.2 → 0.4.3
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.
- package/README.md +299 -295
- package/originlab.py +62 -4
- package/package.json +1 -5
- package/server.py +11 -3
- package/smoke_test.py +11 -3
- package/cases/catalogue.py +0 -636
- package/cases/data.py +0 -120
- package/render_realdata.py +0 -99
- package/run_cases.py +0 -304
package/cases/data.py
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"""Tiny deterministic datasets for the case bank.
|
|
2
|
-
|
|
3
|
-
Nothing here is real experimental data: every series is generated from a closed
|
|
4
|
-
formula so a re-run reproduces byte-identical files, and every file stays under
|
|
5
|
-
24 rows. `.bin` workstation samples are deliberately NOT covered here.
|
|
6
|
-
|
|
7
|
-
build(dir) writes the files and returns {placeholder: value}; the catalogue
|
|
8
|
-
references them as "{dat}", "{csv}", ... so cases never hardcode a path.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import math
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _rows(xs, ys):
|
|
16
|
-
return "\n".join("%g\t%g" % (x, y) for x, y in zip(xs, ys))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _write(path, text, encoding="utf-8"):
|
|
20
|
-
with open(path, "w", encoding=encoding, newline="\n") as handle:
|
|
21
|
-
handle.write(text)
|
|
22
|
-
return path
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _exp(n=24, k=0.16):
|
|
26
|
-
xs = [float(i) for i in range(n)]
|
|
27
|
-
ys = [round(1.8 * math.exp(k * x), 3) for x in xs]
|
|
28
|
-
return xs, ys
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def build(target):
|
|
32
|
-
os.makedirs(target, exist_ok=True)
|
|
33
|
-
j = lambda name: os.path.join(target, name) # noqa: E731
|
|
34
|
-
out = {"dir": target}
|
|
35
|
-
|
|
36
|
-
xs, ys = _exp()
|
|
37
|
-
|
|
38
|
-
out["dat"] = _write(j("curve.dat"),
|
|
39
|
-
"X\tY\n" + _rows(xs, ys))
|
|
40
|
-
out["units"] = _write(j("units.dat"),
|
|
41
|
-
"Long Name\tBinding Energy\tIntensity\n"
|
|
42
|
-
"Units\teV\tcps\n"
|
|
43
|
-
"Comments\tsynthetic\tpaired\n"
|
|
44
|
-
+ _rows([round(i * 0.9, 2) for i in range(20)],
|
|
45
|
-
[round(500 + 40 * math.sin(i / 3.0) + i * 6, 2) for i in range(20)]))
|
|
46
|
-
out["xps_file"] = _write(j("xps_survey.dat"),
|
|
47
|
-
"Binding Energy\tIntensity\n"
|
|
48
|
-
+ _rows([round(284 + i * 0.3, 2) for i in range(18)],
|
|
49
|
-
[round(900 - 12 * i + 40 * math.sin(i / 2.0)) for i in range(18)]))
|
|
50
|
-
out["csv"] = _write(j("curve.csv"),
|
|
51
|
-
"time,signal\n" + "\n".join("%g,%g" % p for p in zip(xs, ys)))
|
|
52
|
-
out["semi"] = _write(j("curve.txt"),
|
|
53
|
-
"time;signal\n" + "\n".join("%g;%g" % p for p in zip(xs, ys)))
|
|
54
|
-
out["spaced"] = _write(j("aligned.dat"),
|
|
55
|
-
"time signal\n" + "\n".join("%-6g %-9g" % p for p in zip(xs, ys)))
|
|
56
|
-
out["multi"] = _write(j("multi_header.dat"),
|
|
57
|
-
"run 1\nsample A\n\time\tmV\n" + _rows(xs[:12], ys[:12]))
|
|
58
|
-
out["na"] = _write(j("na_values.dat"),
|
|
59
|
-
"x\ty\n" + "\n".join(
|
|
60
|
-
"%g\t%s" % (x, ("NA" if i % 7 == 3 else ("-" if i % 11 == 5 else "%g" % y)))
|
|
61
|
-
for i, (x, y) in enumerate(zip(xs, ys))))
|
|
62
|
-
out["comment"] = _write(j("commented.dat"),
|
|
63
|
-
"# generated for the case bank\n# keep\nx\ty\n" + _rows(xs[:10], ys[:10]))
|
|
64
|
-
out["gbk"] = _write(j("gbk_units.dat"),
|
|
65
|
-
"名称\t电势\t电流\n单位\tmV\tuA\n" + _rows([-2.0, -1.0, 0.0, 1.0, 2.0],
|
|
66
|
-
[12.5, 40.0, 999.0, 38.0, 11.0]),
|
|
67
|
-
encoding="gbk")
|
|
68
|
-
out["bom"] = _write(j("bom.csv"), "\ufefftime,signal\n" + "\n".join("%g,%g" % p for p in zip(xs[:8], ys[:8])))
|
|
69
|
-
out["dup"] = _write(j("dup_names.dat"), "x\ty\ty\n" + "\n".join("%g\t%g\t%g" % (x, y, y / 2)
|
|
70
|
-
for x, y in zip(xs[:9], ys[:9])))
|
|
71
|
-
out["wide"] = _write(j("wide.dat"),
|
|
72
|
-
"x\ta\tb\tc\td\n" + "\n".join(
|
|
73
|
-
"%g\t%g\t%g\t%g\t%g" % (x, y, y * 0.8, y * 0.6, y * 0.4)
|
|
74
|
-
for x, y in zip(xs, ys)))
|
|
75
|
-
out["fit"] = _write(j("fit_pair.dat"),
|
|
76
|
-
"x\ty\n" + "\n".join("%g\t%g" % (x, 2.5 * x + 1.0 + (0.4 if i % 2 else -0.4))
|
|
77
|
-
for i, x in enumerate([float(i) for i in range(1, 15)])))
|
|
78
|
-
out["chi"] = _write(j("chi_lsv.txt"), (
|
|
79
|
-
"Mar. 02, 2024 09:41:07\r\n"
|
|
80
|
-
"Linear Sweep Voltammetry\r\n"
|
|
81
|
-
"File: d:\\lab\\chi_lsv.txt\r\n"
|
|
82
|
-
"Data Source: Experiment\r\n"
|
|
83
|
-
"Instrument Model: CHI660E\r\n"
|
|
84
|
-
"Header: \r\n"
|
|
85
|
-
"Note: \r\n"
|
|
86
|
-
"\r\n"
|
|
87
|
-
"Init E (V) = 0.0\r\n"
|
|
88
|
-
"Final E (V) = 0.6\r\n"
|
|
89
|
-
"Scan Rate (V/s) = 0.05\r\n"
|
|
90
|
-
"Sample Interval (V) = 0.001\r\n"
|
|
91
|
-
"Quiet Time (sec) = 2\r\n"
|
|
92
|
-
"Sensitivity (A/V) = 0.0001\r\n"
|
|
93
|
-
"\r\n"
|
|
94
|
-
"Results:\r\n"
|
|
95
|
-
"\r\n"
|
|
96
|
-
"Channel 1:\r\n"
|
|
97
|
-
"Segment 1:\r\n"
|
|
98
|
-
"\r\n"
|
|
99
|
-
"Potential/V, Current/A\r\n"
|
|
100
|
-
"\r\n" + "\r\n".join("%.3f, %.3e" % (i * 0.01, 1e-5 + 2e-6 * math.sin(i / 3.0))
|
|
101
|
-
for i in range(40)) + "\r\n"))
|
|
102
|
-
out["empty"] = _write(j("empty.dat"), "")
|
|
103
|
-
out["text"] = _write(j("text_only.dat"), "name\tnote\nalpha\tthis file has no numbers\nbeta\tnor does this\n")
|
|
104
|
-
out["onecol"] = _write(j("one_column.dat"), "y\n1\n2\n3\n")
|
|
105
|
-
out["huge"] = _write(j("long_line.dat"), "x\ty\n" + "\n".join("%g\t%g" % (i, i * i) for i in range(400)))
|
|
106
|
-
|
|
107
|
-
out["columns"] = {"t": [round(v, 2) for v in xs[:12]],
|
|
108
|
-
"v": [round(v, 2) for v in ys[:12]]}
|
|
109
|
-
out["wide_columns"] = {"t": [round(v, 2) for v in xs[:10]],
|
|
110
|
-
"a": [round(v, 2) for v in ys[:10]],
|
|
111
|
-
"b": [round(v * 0.7, 2) for v in ys[:10]],
|
|
112
|
-
"c": [round(v * 0.4, 2) for v in ys[:10]]}
|
|
113
|
-
out["headers"] = ["x", "y1", "y2"]
|
|
114
|
-
out["rows"] = [[round(x, 2), round(y, 2), round(y * 0.5, 2)] for x, y in zip(xs[:10], ys[:10])]
|
|
115
|
-
out["matrix_rows"] = [[round(math.sin(i / 2.0) + math.cos(j / 3.0), 4) for j in range(8)] for i in range(8)]
|
|
116
|
-
out["xps"] = {"binding_energy": [round(284.0 + i * 0.5, 2) for i in range(18)],
|
|
117
|
-
"intensity": [round(120 + 90 * math.exp(-((i - 9) ** 2) / 8.0), 2) for i in range(18)]}
|
|
118
|
-
out["cv"] = {"potential": [round(-0.2 + i * 0.02, 3) for i in range(24)],
|
|
119
|
-
"current": [round(1e-5 * math.sin(i / 2.4) * (1 + i / 12.0), 8) for i in range(24)]}
|
|
120
|
-
return out
|
package/render_realdata.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Render one PNG per real workstation file from cases/realdata/shiyan.
|
|
2
|
-
|
|
3
|
-
The data is the user's own lab export and is gitignored on purpose: this script
|
|
4
|
-
is the "真题" harness, the files themselves never ship with the plugin.
|
|
5
|
-
|
|
6
|
-
python render_realdata.py # 全部文件,一个一张图
|
|
7
|
-
python render_realdata.py --only cv # 只跑名字里含 cv 的
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import os
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
|
|
15
|
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
-
sys.path.insert(0, HERE)
|
|
17
|
-
import smoke_test as sm # noqa: E402
|
|
18
|
-
|
|
19
|
-
DATA = os.path.join(HERE, "cases", "realdata", "shiyan")
|
|
20
|
-
OUT = os.path.join(HERE, "out", "realdata")
|
|
21
|
-
PY = os.path.abspath(os.path.join(HERE, ".venv", "Scripts", "python.exe"))
|
|
22
|
-
SAFE = re.compile(r"[^\w.-]+")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def safe(name):
|
|
26
|
-
stem, ext = os.path.splitext(name)
|
|
27
|
-
return "%s__%s" % (SAFE.sub("_", stem) or "file", (ext.lstrip(".") or "dat").lower())
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def main():
|
|
31
|
-
ap = argparse.ArgumentParser()
|
|
32
|
-
ap.add_argument("--only", default="")
|
|
33
|
-
ap.add_argument("--keep-pages", action="store_true",
|
|
34
|
-
help="不清理 Origin 页面(默认每 3 个文件清一次,避免攒页面卡死)")
|
|
35
|
-
opts = ap.parse_args()
|
|
36
|
-
|
|
37
|
-
if not os.path.isdir(DATA):
|
|
38
|
-
print("没有 %s:把工作站导出文件放到这里再跑" % DATA)
|
|
39
|
-
return 2
|
|
40
|
-
os.makedirs(OUT, exist_ok=True)
|
|
41
|
-
files = [os.path.join(DATA, f) for f in sorted(os.listdir(DATA))
|
|
42
|
-
if os.path.splitext(f)[1].lower() in (".txt", ".dat", ".csv", ".tsv", ".bin")]
|
|
43
|
-
if opts.only:
|
|
44
|
-
files = [p for p in files if opts.only.lower() in os.path.basename(p).lower()]
|
|
45
|
-
|
|
46
|
-
rpc = sm.Rpc(PY, sm.SERVER)
|
|
47
|
-
rpc.request("initialize", {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
48
|
-
"clientInfo": {"name": "realdata", "version": "1"}})
|
|
49
|
-
rpc.notify("notifications/initialized")
|
|
50
|
-
|
|
51
|
-
rows = []
|
|
52
|
-
for n, path in enumerate(files, 1):
|
|
53
|
-
name = os.path.basename(path)
|
|
54
|
-
head = rpc.tool("origin_read_file", {"path": path}, timeout=300)
|
|
55
|
-
if not head.get("ok") and os.path.splitext(name)[1].lower() != ".bin":
|
|
56
|
-
rows.append((name, "读取失败", head.get("code"), ""))
|
|
57
|
-
print("%-26s 读取失败 %s" % (name, head.get("code")))
|
|
58
|
-
continue
|
|
59
|
-
target = os.path.join(OUT, safe(name) + ".png").replace("\\", "/")
|
|
60
|
-
out_dir = os.path.dirname(target)
|
|
61
|
-
fig = rpc.tool("origin_figure", {"source": path, "x": 1, "y": 2,
|
|
62
|
-
"title": os.path.splitext(name)[0],
|
|
63
|
-
"export_path": target,
|
|
64
|
-
"project_path": os.path.join(
|
|
65
|
-
OUT, safe(name) + ".opju").replace("\\", "/")},
|
|
66
|
-
timeout=900)
|
|
67
|
-
if not fig.get("ok") and fig.get("code") in ("bad_column", "sheet_empty"):
|
|
68
|
-
fig = rpc.tool("origin_figure", {"source": path, "x": 1, "y": 1,
|
|
69
|
-
"title": os.path.splitext(name)[0],
|
|
70
|
-
"export_path": target}, timeout=900)
|
|
71
|
-
if fig.get("ok"):
|
|
72
|
-
img = (fig.get("delivered") or {}).get("image", "")
|
|
73
|
-
size = os.path.getsize(img) if img and os.path.isfile(img) else 0
|
|
74
|
-
rows.append((name, "出图", "%d 行 / %d 字节" % (head.get("rows") or 0, size), img))
|
|
75
|
-
print("%-26s 出图 %s 行 -> %s" % (name, head.get("rows"), os.path.basename(img)))
|
|
76
|
-
else:
|
|
77
|
-
rows.append((name, "失败", fig.get("code"), str(fig.get("message"))[:60]))
|
|
78
|
-
print("%-26s 失败 %s %s" % (name, fig.get("code"), str(fig.get("message"))[:60]))
|
|
79
|
-
if not opts.keep_pages and n % 3 == 0:
|
|
80
|
-
rpc.tool("origin_close_pages", {"kind": "graph"}, timeout=300)
|
|
81
|
-
rpc.tool("origin_close_pages", {"kind": "workbook"}, timeout=300)
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
rpc.p.kill()
|
|
85
|
-
except Exception:
|
|
86
|
-
pass
|
|
87
|
-
print("\n共 %d 个文件,出图 %d 张,失败 %d 个,图片在 %s"
|
|
88
|
-
% (len(files), sum(1 for r in rows if r[1] == "出图"),
|
|
89
|
-
sum(1 for r in rows if r[1] != "出图"), OUT))
|
|
90
|
-
try:
|
|
91
|
-
import originlab
|
|
92
|
-
originlab.reclaim()
|
|
93
|
-
except Exception:
|
|
94
|
-
pass
|
|
95
|
-
return 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if __name__ == "__main__":
|
|
99
|
-
sys.exit(main())
|
package/run_cases.py
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
"""Run the case bank against the real stdio server and report.
|
|
2
|
-
|
|
3
|
-
python run_cases.py --list # 只打印题库,不跑
|
|
4
|
-
python run_cases.py --levels L0,L1 # 只跑只读层(安全、快)
|
|
5
|
-
python run_cases.py # 全跑
|
|
6
|
-
python run_cases.py --keep-data # 保留生成的数据文件(默认跑完删除)
|
|
7
|
-
|
|
8
|
-
The bank itself is validated first: every case's argument names are checked
|
|
9
|
-
against the live tools/list schema, so a typo in a case cannot masquerade as a
|
|
10
|
-
product bug.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import argparse
|
|
14
|
-
import json
|
|
15
|
-
import os
|
|
16
|
-
import re
|
|
17
|
-
import shutil
|
|
18
|
-
import sys
|
|
19
|
-
|
|
20
|
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
-
sys.path.insert(0, HERE)
|
|
22
|
-
sys.path.insert(0, os.path.join(HERE, "cases"))
|
|
23
|
-
|
|
24
|
-
import catalogue # noqa: E402
|
|
25
|
-
import data as case_data # noqa: E402
|
|
26
|
-
import server as bridge_server # noqa: E402
|
|
27
|
-
import smoke_test as sm # noqa: E402
|
|
28
|
-
|
|
29
|
-
DATA_DIR = os.path.join(HERE, "cases", "data_out")
|
|
30
|
-
HOUSEKEEPING_EVERY = 25
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
PLACEHOLDER = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
|
|
34
|
-
WHOLE = re.compile(r"^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def resolve(value, ctx):
|
|
38
|
-
"""Substitute {name} placeholders from ctx anywhere in a case's args.
|
|
39
|
-
|
|
40
|
-
A value that is exactly one placeholder resolves to the stored object as-is,
|
|
41
|
-
so {"columns": "{columns}"} passes a dict, not a JSON string. Unknown names
|
|
42
|
-
are rejected before substitution: doing it afterwards would trip over the
|
|
43
|
-
braces inside JSON values that were just inserted.
|
|
44
|
-
"""
|
|
45
|
-
if isinstance(value, str):
|
|
46
|
-
whole = WHOLE.match(value)
|
|
47
|
-
if whole:
|
|
48
|
-
if whole.group(1) not in ctx:
|
|
49
|
-
raise KeyError("题库里引用了未知的占位符 %s" % value)
|
|
50
|
-
return ctx[whole.group(1)]
|
|
51
|
-
for name in PLACEHOLDER.findall(value):
|
|
52
|
-
if name not in ctx:
|
|
53
|
-
raise KeyError("题库里引用了未知的占位符 {%s}" % name)
|
|
54
|
-
return PLACEHOLDER.sub(lambda m: str(ctx[m.group(1)]), value)
|
|
55
|
-
if isinstance(value, list):
|
|
56
|
-
return [resolve(v, ctx) for v in value]
|
|
57
|
-
if isinstance(value, dict):
|
|
58
|
-
return {k: resolve(v, ctx) for k, v in value.items()}
|
|
59
|
-
return value
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def schema_check(cases):
|
|
63
|
-
"""Report cases whose argument names are not in the live tool schema."""
|
|
64
|
-
props = {t["name"]: set(((t.get("inputSchema") or {}).get("properties") or {}).keys())
|
|
65
|
-
for t in bridge_server.TOOLS}
|
|
66
|
-
bad = []
|
|
67
|
-
for idx, c in enumerate(cases):
|
|
68
|
-
if c["tool"] not in props:
|
|
69
|
-
bad.append((idx, c["tool"], "工具不存在"))
|
|
70
|
-
continue
|
|
71
|
-
for key in c["args"]:
|
|
72
|
-
if key not in props[c["tool"]]:
|
|
73
|
-
bad.append((idx, c["tool"], "参数 %r 不在 schema 里" % key))
|
|
74
|
-
return bad
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def bootstrap(rpc, ctx):
|
|
78
|
-
"""Create the handles the bank assumes; not counted as cases.
|
|
79
|
-
|
|
80
|
-
Closing pages invalidates every stored handle, so this is also the
|
|
81
|
-
housekeeping step: it re-establishes all of them, never just some.
|
|
82
|
-
"""
|
|
83
|
-
imp = rpc.tool("origin_import", {"path": ctx["dat"]}, timeout=180)
|
|
84
|
-
ctx["ws"] = str(imp["worksheet"])
|
|
85
|
-
wide = rpc.tool("origin_import", {"path": ctx["wide"]}, timeout=180)
|
|
86
|
-
ctx["wsw"] = str(wide["worksheet"])
|
|
87
|
-
inl = rpc.tool("origin_write", {"columns": ctx["columns"]}, timeout=180)
|
|
88
|
-
ctx["wsi"] = str(inl["worksheet"])
|
|
89
|
-
fit = rpc.tool("origin_import", {"path": ctx["fit"]}, timeout=180)
|
|
90
|
-
ctx["wsf"] = str(fit["worksheet"])
|
|
91
|
-
p = rpc.tool("origin_plot", {"worksheet": ctx["ws"], "x": 1, "y": 2}, timeout=180)
|
|
92
|
-
ctx["gr"] = str(p["graph"])
|
|
93
|
-
two = rpc.tool("origin_plot", {"worksheet": ctx["wsw"], "x": 1, "y": [2, 3]}, timeout=180)
|
|
94
|
-
ctx["gr2"] = str(two["graph"])
|
|
95
|
-
# Cases that address layer 1 of {gr} must not depend on whichever layer-adding
|
|
96
|
-
# case happened to run earlier, so every batch starts with a two-layer page.
|
|
97
|
-
rpc.tool("origin_layer", {"graph": ctx["gr"], "layer_type": "right"}, timeout=180)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def rehousekeep(rpc, ctx):
|
|
101
|
-
"""Close accumulated pages, then rebuild every handle the bank points at."""
|
|
102
|
-
rpc.tool("origin_close_pages", {"kind": "graph"}, timeout=180)
|
|
103
|
-
rpc.tool("origin_close_pages", {"kind": "workbook"}, timeout=180)
|
|
104
|
-
rpc.tool("origin_close_pages", {"kind": "matrix"}, timeout=180)
|
|
105
|
-
bootstrap(rpc, ctx)
|
|
106
|
-
print(" -- 清理页面并重建句柄: ws=%s wsw=%s wsi=%s wsf=%s gr=%s gr2=%s"
|
|
107
|
-
% (ctx["ws"], ctx["wsw"], ctx["wsi"], ctx["wsf"], ctx["gr"], ctx["gr2"]))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def where_died(rpc, resp):
|
|
111
|
-
"""Dump the live handle table when a case blames a handle, to tell
|
|
112
|
-
'evicted from the table' apart from 'object still bound but page destroyed'."""
|
|
113
|
-
code = resp.get("code")
|
|
114
|
-
if code not in ("graph_not_found", "worksheet_not_found", "sheet_empty"):
|
|
115
|
-
return
|
|
116
|
-
try:
|
|
117
|
-
ins = rpc.tool("origin_inspect", {}, timeout=180)
|
|
118
|
-
print(" 现场 handles=%s pages=%s"
|
|
119
|
-
% (ins.get("handles"), [p.get("name") for p in (ins.get("pages") or [])]))
|
|
120
|
-
except Exception as exc:
|
|
121
|
-
print(" 现场取不到:%s" % exc)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def dig(resp, path):
|
|
125
|
-
"""Look up "columns.0.name" so a case can assert on nested values."""
|
|
126
|
-
cur = resp
|
|
127
|
-
for part in str(path).split("."):
|
|
128
|
-
if isinstance(cur, dict):
|
|
129
|
-
cur = cur.get(part)
|
|
130
|
-
elif isinstance(cur, list):
|
|
131
|
-
try:
|
|
132
|
-
cur = cur[int(part)]
|
|
133
|
-
except (ValueError, IndexError):
|
|
134
|
-
return None
|
|
135
|
-
else:
|
|
136
|
-
return None
|
|
137
|
-
return cur
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def run(cases, rpc, ctx, verbose):
|
|
141
|
-
import originlab
|
|
142
|
-
results, failures = [], []
|
|
143
|
-
since_housekeeping = 0
|
|
144
|
-
for idx, c in enumerate(cases):
|
|
145
|
-
# Housekeeping has to happen BEFORE the placeholders are resolved: closing
|
|
146
|
-
# pages evicts the handles, and a case that resolved "gr-43" first would
|
|
147
|
-
# then blame the product for a handle this script destroyed.
|
|
148
|
-
if c["level"] in ("L2", "L3"):
|
|
149
|
-
since_housekeeping += 1
|
|
150
|
-
if since_housekeeping >= HOUSEKEEPING_EVERY:
|
|
151
|
-
rehousekeep(rpc, ctx)
|
|
152
|
-
since_housekeeping = 0
|
|
153
|
-
try:
|
|
154
|
-
args = resolve(c["args"], ctx)
|
|
155
|
-
except KeyError as exc:
|
|
156
|
-
failures.append((idx, c, str(exc)))
|
|
157
|
-
continue
|
|
158
|
-
try:
|
|
159
|
-
resp = rpc.tool(c["tool"], args, timeout=240)
|
|
160
|
-
except Exception as exc: # transport died: the whole run is suspect
|
|
161
|
-
failures.append((idx, c, "调用异常 %s: %s" % (type(exc).__name__, exc)))
|
|
162
|
-
break
|
|
163
|
-
if c["tool"] == "origin_close_pages" and resp.get("closed"):
|
|
164
|
-
# Destroying a page invalidates every handle the bank points at, and
|
|
165
|
-
# the next case would blame the product for a stale handle.
|
|
166
|
-
rehousekeep(rpc, ctx)
|
|
167
|
-
if resp.get("code") in ("origin_unavailable", "connection_error"):
|
|
168
|
-
# The bridge dropped and rebuilt its connection; every handle this bank
|
|
169
|
-
# holds died with it, so rebuild before the next case or the whole tail
|
|
170
|
-
# fails for one wedge.
|
|
171
|
-
print(" -- #%s 掉线/坏状态(%s),重建句柄表后继续" % (idx, resp.get("code")))
|
|
172
|
-
rehousekeep(rpc, ctx)
|
|
173
|
-
problems = []
|
|
174
|
-
got_ok = resp.get("ok") is True
|
|
175
|
-
if c["ok"] and not got_ok:
|
|
176
|
-
problems.append("预期成功,实际 ok=%r code=%r msg=%r"
|
|
177
|
-
% (resp.get("ok"), resp.get("code"), str(resp.get("message"))[:120]))
|
|
178
|
-
if not c["ok"] and got_ok:
|
|
179
|
-
problems.append("预期失败,实际却成功了(静默失败的反面:静默成功)")
|
|
180
|
-
if c["code"] and resp.get("code") != c["code"]:
|
|
181
|
-
problems.append("错误码应为 %s,实际 %s" % (c["code"], resp.get("code")))
|
|
182
|
-
for key in c["has"]:
|
|
183
|
-
if key not in resp or resp[key] is None:
|
|
184
|
-
problems.append("返回里缺字段 %r" % key)
|
|
185
|
-
for key, want in c["eq"]:
|
|
186
|
-
got = dig(resp, key)
|
|
187
|
-
if got != want:
|
|
188
|
-
problems.append("字段 %r 应为 %r,实际 %r" % (key, want, got))
|
|
189
|
-
for key, floor in c.get("min", []):
|
|
190
|
-
got = resp.get(key)
|
|
191
|
-
if not isinstance(got, (list, dict, str)) or len(got) < floor:
|
|
192
|
-
problems.append("字段 %r 至少要有 %d 项,实际 %r" % (key, floor, got))
|
|
193
|
-
for frag in c.get("msg", []):
|
|
194
|
-
if frag not in str(resp.get("message", "")):
|
|
195
|
-
problems.append("错误信息里应当出现 %r,实际 %r"
|
|
196
|
-
% (frag, str(resp.get("message"))[:120]))
|
|
197
|
-
if not problems:
|
|
198
|
-
for name, key in c["save"].items():
|
|
199
|
-
if resp.get(key):
|
|
200
|
-
ctx[name] = str(resp[key])
|
|
201
|
-
results.append((idx, c, "pass"))
|
|
202
|
-
if verbose:
|
|
203
|
-
print(" ok #%-3d %-20s %s" % (idx, c["tool"], c["prompt"][:44]))
|
|
204
|
-
else:
|
|
205
|
-
results.append((idx, c, "fail"))
|
|
206
|
-
failures.append((idx, c, "; ".join(problems)))
|
|
207
|
-
print(" FAIL #%-3d %-20s %s\n %s"
|
|
208
|
-
% (idx, c["tool"], c["prompt"][:44], "; ".join(problems)))
|
|
209
|
-
where_died(rpc, resp)
|
|
210
|
-
return results, failures
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def main():
|
|
214
|
-
ap = argparse.ArgumentParser()
|
|
215
|
-
ap.add_argument("--levels", default="L0,L1,L2,L3,L4")
|
|
216
|
-
ap.add_argument("--tool", default="")
|
|
217
|
-
ap.add_argument("--list", action="store_true", help="只打印题库")
|
|
218
|
-
ap.add_argument("--keep-data", action="store_true")
|
|
219
|
-
ap.add_argument("--verbose", action="store_true")
|
|
220
|
-
opts = ap.parse_args()
|
|
221
|
-
|
|
222
|
-
levels = {l.strip() for l in opts.levels.split(",") if l.strip()}
|
|
223
|
-
cases = [c for c in catalogue.CASES
|
|
224
|
-
if c["level"] in levels and (not opts.tool or c["tool"] == opts.tool)]
|
|
225
|
-
|
|
226
|
-
if opts.list:
|
|
227
|
-
by_tool = {}
|
|
228
|
-
for c in cases:
|
|
229
|
-
by_tool.setdefault((c["level"], c["tool"]), []).append(c)
|
|
230
|
-
for (level, tool), items in sorted(by_tool.items()):
|
|
231
|
-
print("\n%s %-22s %d 例" % (level, tool, len(items)))
|
|
232
|
-
for c in items:
|
|
233
|
-
print(" - %s" % c["prompt"])
|
|
234
|
-
print("\n题库共 %d 例,覆盖 %d 个工具" % (len(cases), len({c["tool"] for c in cases})))
|
|
235
|
-
return 0
|
|
236
|
-
|
|
237
|
-
bad = schema_check(cases)
|
|
238
|
-
if bad:
|
|
239
|
-
print("题库与 schema 不符,先修题库:")
|
|
240
|
-
for idx, tool, why in bad[:20]:
|
|
241
|
-
print(" #%-3d %-20s %s" % (idx, tool, why))
|
|
242
|
-
return 2
|
|
243
|
-
|
|
244
|
-
shutil.rmtree(DATA_DIR, ignore_errors=True)
|
|
245
|
-
built = case_data.build(DATA_DIR)
|
|
246
|
-
ctx = dict(built)
|
|
247
|
-
ctx["dir"] = DATA_DIR.replace("\\", "/")
|
|
248
|
-
|
|
249
|
-
rpc = sm.Rpc(sm.PY, sm.SERVER)
|
|
250
|
-
rpc.request("initialize", {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
251
|
-
"clientInfo": {"name": "casebank", "version": "1"}})
|
|
252
|
-
rpc.notify("notifications/initialized")
|
|
253
|
-
bootstrap(rpc, ctx)
|
|
254
|
-
|
|
255
|
-
try:
|
|
256
|
-
results, failures = run(cases, rpc, ctx, opts.verbose)
|
|
257
|
-
finally:
|
|
258
|
-
try:
|
|
259
|
-
rpc.p.kill()
|
|
260
|
-
except Exception:
|
|
261
|
-
pass
|
|
262
|
-
|
|
263
|
-
per_tool = {}
|
|
264
|
-
per_level = {}
|
|
265
|
-
for idx, c, state in results:
|
|
266
|
-
per_tool.setdefault(c["tool"], [0, 0])[0] += state == "pass"
|
|
267
|
-
per_tool[c["tool"]][1] += state == "fail"
|
|
268
|
-
per_level.setdefault(c["level"], [0, 0])[0] += state == "pass"
|
|
269
|
-
per_level[c["level"]][1] += state == "fail"
|
|
270
|
-
|
|
271
|
-
print("\n=== 题库执行汇总 ===")
|
|
272
|
-
for level in sorted(per_level):
|
|
273
|
-
ok, bad_n = per_level[level]
|
|
274
|
-
print(" %s %d 通过 / %d 失败" % (level, ok, bad_n))
|
|
275
|
-
print("\n 每个工具的用例数(要求 ≥10):")
|
|
276
|
-
missing = []
|
|
277
|
-
for name in sorted(per_tool):
|
|
278
|
-
ok, bad_n = per_tool[name]
|
|
279
|
-
flag = "" if (ok + bad_n) >= 10 else " <-- 不足 10"
|
|
280
|
-
print(" %-22s %3d 通过 %2d 失败%s" % (name, ok, bad_n, flag))
|
|
281
|
-
if (ok + bad_n) < 10:
|
|
282
|
-
missing.append(name)
|
|
283
|
-
tools_in_bank = {c["tool"] for c in cases}
|
|
284
|
-
not_covered = sorted({t["name"] for t in bridge_server.TOOLS} - tools_in_bank)
|
|
285
|
-
total = len(results)
|
|
286
|
-
print("\n 测试了 %d 次:%d 通过 / %d 失败" % (total, total - len(failures), len(failures)))
|
|
287
|
-
if not_covered:
|
|
288
|
-
print(" 未覆盖的工具:", ", ".join(not_covered))
|
|
289
|
-
if missing:
|
|
290
|
-
print(" 用例数不足 10 的工具:", ", ".join(missing))
|
|
291
|
-
|
|
292
|
-
if not opts.keep_data:
|
|
293
|
-
shutil.rmtree(DATA_DIR, ignore_errors=True)
|
|
294
|
-
print(" 生成的数据文件已删除(题库保留)")
|
|
295
|
-
try:
|
|
296
|
-
import originlab
|
|
297
|
-
originlab.reclaim()
|
|
298
|
-
except Exception:
|
|
299
|
-
pass
|
|
300
|
-
return 1 if failures else 0
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if __name__ == "__main__":
|
|
304
|
-
sys.exit(main())
|