dsh-origin-bridge 0.4.2 → 0.4.4

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/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
@@ -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())