dsh-origin-bridge 0.4.5 → 0.4.7
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 +337 -303
- package/asciiio.py +184 -17
- package/cordis.patch.yml +6 -2
- package/fixtures/j_utf16_bom.dat +0 -0
- package/fixtures/k_utf16_nobom.dat +0 -0
- package/fixtures/l_cp1252_micro.dat +3 -0
- package/fixtures/m_cp1252_degree.dat +3 -0
- package/fixtures/make_fixtures.py +15 -0
- package/fixtures/n_detection_limit.dat +4 -0
- package/fixtures/o_plusminus.dat +3 -0
- package/fixtures/p_unit_in_cell.dat +3 -0
- package/fixtures/q_thousands.csv +3 -0
- package/fixtures/r_decimal_comma.txt +3 -0
- package/fixtures/s_star_flag.dat +3 -0
- package/fixtures/t_paren_negative.dat +3 -0
- package/fixtures/u_overflow.dat +3 -0
- package/fixtures/v_row_labels.dat +3 -0
- package/fixtures/w_two_numbers_cell.csv +3 -0
- package/originlab.py +199 -28
- package/package.json +13 -5
- package/requirements.txt +2 -0
- package/server.py +26 -10
- package/smoke_test.py +24 -2
- package/test_asciiio.py +118 -0
- package/test_json_safety.py +103 -0
- package/test_paths.py +142 -0
- package/test_readfile.py +104 -0
package/test_asciiio.py
CHANGED
|
@@ -82,6 +82,124 @@ d = load("i_bom_utf8.csv")
|
|
|
82
82
|
check("BOM stripped from first name", d["columns"][0]["name"] == "波长", view(d))
|
|
83
83
|
check("utf-8-sig reported", d["encoding"] == "utf-8-sig", d["encoding"])
|
|
84
84
|
|
|
85
|
+
# ---------------------------------------------------------------- other encodings
|
|
86
|
+
print()
|
|
87
|
+
print("encodings a real instrument writes")
|
|
88
|
+
|
|
89
|
+
d = load("j_utf16_bom.dat")
|
|
90
|
+
check("utf-16 with BOM parses", [(c["name"], c["numeric_rows"]) for c in d["columns"]]
|
|
91
|
+
== [("X", 2), ("Y", 2)], view(d))
|
|
92
|
+
check("utf-16 encoding named", d["encoding"] == "utf-16-le", d["encoding"])
|
|
93
|
+
|
|
94
|
+
d = load("k_utf16_nobom.dat")
|
|
95
|
+
check("utf-16 without BOM still parses", d["columns"][1]["values"] == [1.5, 2.5], d["columns"])
|
|
96
|
+
check("the guess is admitted", "无 BOM" in d["encoding"], d["encoding"])
|
|
97
|
+
|
|
98
|
+
d = load("l_cp1252_micro.dat")
|
|
99
|
+
check("cp1252 µ survives", d["columns"][1]["units"] == "µs", view(d))
|
|
100
|
+
check("not mojibaked into CJK", "祍" not in str(view(d)), view(d))
|
|
101
|
+
check("cp1252 named", d["encoding"] == "cp1252", d["encoding"])
|
|
102
|
+
|
|
103
|
+
d = load("m_cp1252_degree.dat")
|
|
104
|
+
check("cp1252 ° survives", d["columns"][0]["units"] == "°C", view(d))
|
|
105
|
+
check("° not read as 癈", "癈" not in str(view(d)), view(d))
|
|
106
|
+
|
|
107
|
+
# the discriminator itself
|
|
108
|
+
check("paired high bytes read as GBK",
|
|
109
|
+
all(r % 2 == 0 for r in asciiio.high_byte_runs("温度\t电导".encode("gbk"))),
|
|
110
|
+
asciiio.high_byte_runs("温度\t电导".encode("gbk")))
|
|
111
|
+
check("single high bytes read as cp1252",
|
|
112
|
+
asciiio.high_byte_runs("T/°C".encode("cp1252")) == [1],
|
|
113
|
+
asciiio.high_byte_runs("T/°C".encode("cp1252")))
|
|
114
|
+
check("gbk is not first when singles appear",
|
|
115
|
+
asciiio.candidate_encodings("T/°C".encode("cp1252")).index("cp1252")
|
|
116
|
+
< asciiio.candidate_encodings("T/°C".encode("cp1252")).index("gb18030"),
|
|
117
|
+
asciiio.candidate_encodings("T/°C".encode("cp1252")))
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------ decorated numbers
|
|
120
|
+
print()
|
|
121
|
+
print("decorated numbers, unwrapped and reported")
|
|
122
|
+
|
|
123
|
+
d = load("n_detection_limit.dat")
|
|
124
|
+
check("detection limits become their bound", d["columns"][1]["values"] == [0.001, 0.5, 1e6],
|
|
125
|
+
d["columns"][1]["values"])
|
|
126
|
+
check("and say so", any("检测限" in w for w in d["warnings"]), d["warnings"])
|
|
127
|
+
|
|
128
|
+
d = load("o_plusminus.dat")
|
|
129
|
+
check("mean±sd takes the mean", d["columns"][1]["values"] == [2.5, 3.5], d["columns"][1]["values"])
|
|
130
|
+
check("both spellings of +/-", any("± 写法" in w for w in d["warnings"]), d["warnings"])
|
|
131
|
+
|
|
132
|
+
d = load("p_unit_in_cell.dat")
|
|
133
|
+
check("unit in the cell does not kill the column",
|
|
134
|
+
d["columns"][1]["values"] == [25.0, 30.0], d["columns"][1]["values"])
|
|
135
|
+
check("and is reported", any("单位写在同一格" in w for w in d["warnings"]), d["warnings"])
|
|
136
|
+
|
|
137
|
+
d = load("q_thousands.csv")
|
|
138
|
+
check("quoted thousands stay one column", [c["name"] for c in d["columns"]] == ["time", "signal"],
|
|
139
|
+
view(d))
|
|
140
|
+
check("and keep their value", d["columns"][1]["values"] == [1234.5, 2345.6],
|
|
141
|
+
d["columns"][1]["values"])
|
|
142
|
+
check("thousands reported", any("千分位" in w for w in d["warnings"]), d["warnings"])
|
|
143
|
+
|
|
144
|
+
d = load("r_decimal_comma.txt")
|
|
145
|
+
check("European file sniffs ';'", d["delimiter"] == ";", d["delimiter"])
|
|
146
|
+
check("header split on the right delimiter",
|
|
147
|
+
[c["name"] for c in d["columns"]] == ["Zeit", "Signal"], view(d))
|
|
148
|
+
check("comma decimal parsed", d["columns"][1]["values"] == [1.5, 2.5], d["columns"][1]["values"])
|
|
149
|
+
check("decimal comma reported", any("小数点" in w for w in d["warnings"]), d["warnings"])
|
|
150
|
+
|
|
151
|
+
d = load("s_star_flag.dat")
|
|
152
|
+
check("star-flagged points keep their number",
|
|
153
|
+
d["columns"][1]["values"] == [2.5, 3.5], d["columns"][1]["values"])
|
|
154
|
+
check("flag reported", any("标记的值" in w for w in d["warnings"]), d["warnings"])
|
|
155
|
+
|
|
156
|
+
d = load("t_paren_negative.dat")
|
|
157
|
+
check("parenthesised negative goes negative",
|
|
158
|
+
d["columns"][1]["values"] == [-500.0, 250.0], d["columns"][1]["values"])
|
|
159
|
+
check("paren reported", any("括号表示负数" in w for w in d["warnings"]), d["warnings"])
|
|
160
|
+
|
|
161
|
+
d = load("u_overflow.dat")
|
|
162
|
+
check("overflowed cell becomes missing", d["columns"][1]["values"] == [None, 1.2e308],
|
|
163
|
+
d["columns"][1]["values"])
|
|
164
|
+
check("overflow reported", any("超出浮点范围" in w for w in d["warnings"]), d["warnings"])
|
|
165
|
+
check("nothing infinite escapes the parser",
|
|
166
|
+
all(v is None or (v == v and abs(v) != float("inf"))
|
|
167
|
+
for c in d["columns"] for v in c["values"]), d["columns"])
|
|
168
|
+
|
|
169
|
+
d = load("v_row_labels.dat")
|
|
170
|
+
check("row-label column skipped", [c["name"] for c in d["columns"]] == ["V", "I"], view(d))
|
|
171
|
+
check("and explained as text", any("文本" in w for w in d["warnings"]), d["warnings"])
|
|
172
|
+
|
|
173
|
+
# cell_number on its own, so a regression in one shape is not hidden by a file
|
|
174
|
+
print()
|
|
175
|
+
print("cell_number shapes")
|
|
176
|
+
for text, want, kind in (
|
|
177
|
+
("1.5", 1.5, None), ("1.5D+03", 1500.0, None), (".5", 0.5, None),
|
|
178
|
+
("NA", None, None), ("--", None, None), ("#N/A", None, None),
|
|
179
|
+
("<0.001", 0.001, "检测限"), ("≥ 2.5", 2.5, "检测限"), (">1e6", 1e6, "检测限"),
|
|
180
|
+
("2.5±0.1", 2.5, "不确定度"), ("2.5 +/- 0.1", 2.5, "不确定度"),
|
|
181
|
+
("25 mV", 25.0, "带单位的数"), ("1,234.5", 1234.5, "千分位"),
|
|
182
|
+
("2.5*", 2.5, "标记后缀"), ("(500)", -500.0, "括号负数"),
|
|
183
|
+
("1e400", None, "溢出"), ("Infinity", None, "溢出"),
|
|
184
|
+
("0,5", None, None), ("0,5;1,5", None, None), ("text", None, None)):
|
|
185
|
+
got, got_kind = asciiio.cell_number(text)
|
|
186
|
+
check("cell %r" % text, got == want and got_kind == kind, (got, got_kind))
|
|
187
|
+
check("cell '0,5' with decimal_comma", asciiio.cell_number("0,5", decimal_comma=True) == (0.5, "小数逗号"),
|
|
188
|
+
asciiio.cell_number("0,5", decimal_comma=True))
|
|
189
|
+
check("cell '\"2,5\"' quoted", asciiio.cell_number('"2,5"', decimal_comma=True) == (2.5, "小数逗号"),
|
|
190
|
+
asciiio.cell_number('"2,5"', decimal_comma=True))
|
|
191
|
+
check("cell '2.5 3.5' is two numbers, not a unit",
|
|
192
|
+
asciiio.cell_number("2.5 3.5") == (2.5, "一格两个数"), asciiio.cell_number("2.5 3.5"))
|
|
193
|
+
check("cell '25 mV' stays a unit",
|
|
194
|
+
asciiio.cell_number("25 mV") == (25.0, "带单位的数"), asciiio.cell_number("25 mV"))
|
|
195
|
+
|
|
196
|
+
# a mis-split column has to say so instead of quietly dropping every second value
|
|
197
|
+
d = load("w_two_numbers_cell.csv")
|
|
198
|
+
check("two numbers in one cell are reported", any("挤了两个数" in w for w in d["warnings"]),
|
|
199
|
+
d["warnings"])
|
|
200
|
+
check("the first number of each cell survives",
|
|
201
|
+
d["columns"][1]["values"] == [2.5, 4.0], d["columns"][1]["values"])
|
|
202
|
+
|
|
85
203
|
print()
|
|
86
204
|
if fails:
|
|
87
205
|
print("FAILED (%d): %s" % (len(fails), ", ".join(fails)))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Offline checks that the bridge can never emit non-standard JSON.
|
|
2
|
+
|
|
3
|
+
json.dumps writes a bare Infinity / NaN by default, which RFC 8259 does not allow:
|
|
4
|
+
a strict client (dsh, JS JSON.parse) fails on the whole response, so one overflowed
|
|
5
|
+
number looks like "the bridge is dead". Two guards, both checked here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
import originlab as ol # noqa: E402
|
|
15
|
+
|
|
16
|
+
PASSED = 0
|
|
17
|
+
FAILED = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ok(label, cond, detail=""):
|
|
21
|
+
global PASSED
|
|
22
|
+
if cond:
|
|
23
|
+
PASSED += 1
|
|
24
|
+
else:
|
|
25
|
+
FAILED.append("%s %s" % (label, detail))
|
|
26
|
+
print("%s %s%s" % ("PASS" if cond else "FAIL", label, (" | " + str(detail)[:90]) if detail else ""))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
INF = float("inf")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def strict(text):
|
|
33
|
+
"""Parse the way a JS client would: no Infinity, no NaN."""
|
|
34
|
+
return json.loads(text, parse_constant=lambda c: (_ for _ in ()).throw(ValueError(c)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
# ------------------------------------------------------- sanitize() mapping
|
|
39
|
+
ok("inf -> None", ol.sanitize(INF) is None, ol.sanitize(INF))
|
|
40
|
+
ok("-inf -> None", ol.sanitize(-INF) is None)
|
|
41
|
+
ok("nan -> None", ol.sanitize(float("nan")) is None)
|
|
42
|
+
ok("missing sentinel -> None", ol.sanitize(ol.MISSING_SENTINEL) is None)
|
|
43
|
+
ok("big finite stays", ol.sanitize(1.7976931348623157e308) == 1.7976931348623157e308)
|
|
44
|
+
ok("zero stays", ol.sanitize(0.0) == 0.0)
|
|
45
|
+
ok("int stays", ol.sanitize(42) == 42)
|
|
46
|
+
ok("string stays", ol.sanitize("ok") == "ok")
|
|
47
|
+
nested = ol.sanitize({"a": [1.0, INF, {"b": -INF, "c": float("nan")}], "d": "x"})
|
|
48
|
+
ok("nested cleaned",
|
|
49
|
+
json.dumps(nested, allow_nan=False) == '{"a": [1.0, null, {"b": null, "c": null}], "d": "x"}',
|
|
50
|
+
nested)
|
|
51
|
+
ok("tuple cleaned", ol.sanitize((INF, 1.0)) == [None, 1.0])
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------- the reply guard
|
|
54
|
+
# Simulate a handler that forgot to sanitize. The child server must still never
|
|
55
|
+
# write bytes a strict parser rejects, and must not die on the way.
|
|
56
|
+
import subprocess
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
json.dumps({"value": math.inf}, allow_nan=False)
|
|
60
|
+
raised = False
|
|
61
|
+
except ValueError:
|
|
62
|
+
raised = True
|
|
63
|
+
ok("allow_nan=False really rejects Infinity", raised)
|
|
64
|
+
|
|
65
|
+
child = (
|
|
66
|
+
"import io, json, math, os, sys\n"
|
|
67
|
+
"sys.path.insert(0, os.environ['BRIDGE_DIR'])\n"
|
|
68
|
+
"import server\n"
|
|
69
|
+
"server._handle = lambda msg: {'jsonrpc': '2.0', 'id': msg.get('id'),\n"
|
|
70
|
+
" 'result': {'value': math.inf}}\n"
|
|
71
|
+
"req = json.dumps({'jsonrpc': '2.0', 'id': 7, 'method': 'tools/list'}) + '\\n'\n"
|
|
72
|
+
"sys.stdin = io.TextIOWrapper(io.BytesIO(req.encode('utf-8')), encoding='utf-8')\n"
|
|
73
|
+
"sys.exit(server.main() or 0)\n"
|
|
74
|
+
)
|
|
75
|
+
env = dict(os.environ, BRIDGE_DIR=os.path.dirname(os.path.abspath(__file__)),
|
|
76
|
+
PYTHONIOENCODING="utf-8")
|
|
77
|
+
proc = subprocess.run([sys.executable, "-c", child], capture_output=True, env=env, timeout=120)
|
|
78
|
+
written = proc.stdout.decode("utf-8", "replace")
|
|
79
|
+
ok("child exited cleanly", proc.returncode == 0,
|
|
80
|
+
(proc.stderr.decode("utf-8", "replace") or "")[:160])
|
|
81
|
+
ok("guard logged why it downgraded", b"not strict JSON" in proc.stderr,
|
|
82
|
+
proc.stderr.decode("utf-8", "replace")[-160:])
|
|
83
|
+
ok("poisoned reply produced exactly one line", written.count("\n") == 1, repr(written[:160]))
|
|
84
|
+
ok("no bare Infinity on the wire", "Infinity" not in written, written[:160])
|
|
85
|
+
payload = {}
|
|
86
|
+
try:
|
|
87
|
+
payload = strict(written)
|
|
88
|
+
ok("client still gets a parseable answer", True)
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
ok("client still gets a parseable answer", False, "%s | %s" % (exc, written[:160]))
|
|
91
|
+
ok("answered with a JSON-RPC error", payload.get("error", {}).get("code") == -32603, payload)
|
|
92
|
+
ok("error keeps the request id", payload.get("id") == 7, payload)
|
|
93
|
+
|
|
94
|
+
print("\n%d/%d json-safety checks passed" % (PASSED, PASSED + len(FAILED)))
|
|
95
|
+
if FAILED:
|
|
96
|
+
for f in FAILED:
|
|
97
|
+
print(" FAILED:", f)
|
|
98
|
+
return 1
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
sys.exit(main())
|
package/test_paths.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Offline checks for the export/save target-path helpers (no Origin needed).
|
|
2
|
+
|
|
3
|
+
Everything here was measured against the live bridge first (2026-10-12): Origin's
|
|
4
|
+
export filter rewrites '#' into a numbering digit, Win32 strips trailing spaces
|
|
5
|
+
and dots out of path components, and a path with no real filename used to turn
|
|
6
|
+
into a dotfile named ".png" inside the plugin's own install folder.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import stat
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import originlab as ol
|
|
15
|
+
|
|
16
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
BASE = os.path.join(os.environ.get("TEMP", HERE), "origin_bridge_path_checks")
|
|
18
|
+
|
|
19
|
+
PASSED = 0
|
|
20
|
+
FAILED = []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ok(label, cond, detail=""):
|
|
24
|
+
global PASSED
|
|
25
|
+
if cond:
|
|
26
|
+
PASSED += 1
|
|
27
|
+
else:
|
|
28
|
+
FAILED.append("%s %s" % (label, detail))
|
|
29
|
+
print("%s %s%s" % ("PASS" if cond else "FAIL", label, (" | " + str(detail)) if detail else ""))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def code_of(fn):
|
|
33
|
+
try:
|
|
34
|
+
fn()
|
|
35
|
+
except ol.OriginError as exc:
|
|
36
|
+
return exc.code
|
|
37
|
+
except Exception as exc: # noqa: BLE001 — an unwrapped crash is itself the failure
|
|
38
|
+
return "UNWRAPPED:" + type(exc).__name__
|
|
39
|
+
return "no-raise"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fresh_dir(rel=""):
|
|
43
|
+
path = os.path.join(BASE, rel) if rel else BASE
|
|
44
|
+
if os.path.isdir(path):
|
|
45
|
+
for root, dirs, files in os.walk(path):
|
|
46
|
+
for name in files + dirs:
|
|
47
|
+
try:
|
|
48
|
+
os.chmod(os.path.join(root, name), stat.S_IWRITE)
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
52
|
+
os.makedirs(path, exist_ok=True)
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
fresh_dir()
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------- naming
|
|
60
|
+
ok("blank path means default", ol._norm_target(" ", "png") == "")
|
|
61
|
+
ok("dot is a directory", code_of(lambda: ol._norm_target(".", "png")) == "bad_path")
|
|
62
|
+
ok("bare repo dir rejected",
|
|
63
|
+
code_of(lambda: ol._norm_target(HERE, "png")) == "bad_path")
|
|
64
|
+
ok("dotfile name rejected",
|
|
65
|
+
code_of(lambda: ol._norm_target(os.path.join(BASE, " .png "), "png")) == "bad_path")
|
|
66
|
+
ok("drive root rejected", code_of(lambda: ol._norm_target("C:\\", "png")) == "bad_path")
|
|
67
|
+
ok("trailing space stripped from component",
|
|
68
|
+
os.path.basename(os.path.dirname(
|
|
69
|
+
ol._norm_target(os.path.join(BASE, "trailsp ", "y.png")))) == "trailsp")
|
|
70
|
+
ok("trailing dot stripped from file",
|
|
71
|
+
ol._norm_target(os.path.join(BASE, "a.png.")) == os.path.join(BASE, "a.png"))
|
|
72
|
+
ok("fmt wins over the extension",
|
|
73
|
+
ol._norm_target(os.path.join(BASE, "x.tiff"), "tif") == os.path.join(BASE, "x.tif"))
|
|
74
|
+
ok("no fmt keeps the name",
|
|
75
|
+
ol._norm_target(os.path.join(BASE, "x.opju")) == os.path.join(BASE, "x.opju"))
|
|
76
|
+
ok("chinese preserved",
|
|
77
|
+
ol._norm_target(os.path.join(BASE, "中文测试", "图谱.png"))
|
|
78
|
+
== os.path.join(BASE, "中文测试", "图谱.png"))
|
|
79
|
+
ok("tilde expanded", os.path.expanduser("~") in ol._norm_target("~/a.png"))
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------- writability
|
|
82
|
+
ok("unmapped drive is bad_path",
|
|
83
|
+
code_of(lambda: ol._prepare_write(ol._norm_target(r"Q:\nope\x.png"))) == "bad_path")
|
|
84
|
+
ok("dead UNC host is bad_path",
|
|
85
|
+
code_of(lambda: ol._prepare_write(ol._norm_target(r"\\nope-not-real\x.png"))) == "bad_path")
|
|
86
|
+
adir = fresh_dir("adir")
|
|
87
|
+
ok("directory target is bad_path", code_of(lambda: ol._prepare_write(adir)) == "bad_path")
|
|
88
|
+
ro = os.path.join(fresh_dir("ro"), "locked.png")
|
|
89
|
+
with open(ro, "wb") as fh:
|
|
90
|
+
fh.write(b"marker")
|
|
91
|
+
os.chmod(ro, 0o444)
|
|
92
|
+
ok("read-only target is target_locked",
|
|
93
|
+
code_of(lambda: ol._prepare_write(ro)) == "target_locked")
|
|
94
|
+
ok("read-only file untouched", open(ro, "rb").read() == b"marker")
|
|
95
|
+
os.chmod(ro, 0o666)
|
|
96
|
+
ok("writable existing passes", code_of(lambda: ol._prepare_write(ro)) == "no-raise")
|
|
97
|
+
deep = os.path.join(BASE, "made", "by", "us")
|
|
98
|
+
ol._prepare_write(os.path.join(deep, "x.png"))
|
|
99
|
+
ok("missing dirs get created", os.path.isdir(deep))
|
|
100
|
+
|
|
101
|
+
# -------------------------------------------------- Origin's '#' rewriting
|
|
102
|
+
real = os.path.join(BASE, "100%_a1b[1].png")
|
|
103
|
+
want = os.path.join(BASE, "100%_a#b[1].png")
|
|
104
|
+
with open(real, "wb") as fh:
|
|
105
|
+
fh.write(b"PNGDATA")
|
|
106
|
+
got, note = ol._written_or_mangled(want)
|
|
107
|
+
ok("# result renamed back to what was asked", got == want and os.path.isfile(want))
|
|
108
|
+
ok("mangled sibling is gone", not os.path.exists(real))
|
|
109
|
+
ok("warning explains the filter", bool(note) and "滤镜" in note, note)
|
|
110
|
+
two_real = os.path.join(BASE, "graph01final02.png")
|
|
111
|
+
two_want = os.path.join(BASE, "graph##final###.png")
|
|
112
|
+
with open(two_real, "wb") as fh:
|
|
113
|
+
fh.write(b"PNGDATA")
|
|
114
|
+
got2, _n2 = ol._written_or_mangled(two_want)
|
|
115
|
+
ok("multi-digit # runs handled", got2 == two_want and os.path.isfile(two_want))
|
|
116
|
+
amb = fresh_dir("amb")
|
|
117
|
+
for name in ("g1.png", "g2.png"):
|
|
118
|
+
with open(os.path.join(amb, name), "wb") as fh:
|
|
119
|
+
fh.write(b"x")
|
|
120
|
+
got3, _n3 = ol._written_or_mangled(os.path.join(amb, "g#.png"))
|
|
121
|
+
ok("ambiguous match is not guessed", got3 is None)
|
|
122
|
+
ok("plain missing file stays a failure",
|
|
123
|
+
ol._written_or_mangled(os.path.join(BASE, "never.png")) == (None, None))
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------ CWD litter
|
|
126
|
+
before = set(os.listdir(HERE))
|
|
127
|
+
ok("relative path warns", bool(ol._rel_note("out.png")))
|
|
128
|
+
ok("absolute path silent", ol._rel_note(os.path.join(BASE, "a.png")) is None)
|
|
129
|
+
ok("blank path silent", ol._rel_note(" ") is None)
|
|
130
|
+
ok("no file written into the package dir by these checks",
|
|
131
|
+
set(os.listdir(HERE)) == before, sorted(set(os.listdir(HERE)) - before))
|
|
132
|
+
|
|
133
|
+
print("\n%d/%d path checks passed" % (PASSED, PASSED + len(FAILED)))
|
|
134
|
+
if FAILED:
|
|
135
|
+
for f in FAILED:
|
|
136
|
+
print(" FAILED:", f)
|
|
137
|
+
return 1
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
sys.exit(main())
|
package/test_readfile.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Offline checks for origin_read_file's shaping (no Origin needed).
|
|
2
|
+
|
|
3
|
+
Two behaviours that cost a real debugging cycle:
|
|
4
|
+
- a `.xlsx` used to be decoded as text and reported as "找不到数值数据行(只有表头?)",
|
|
5
|
+
which blames the user's file for our mistake;
|
|
6
|
+
- the response used to carry no values at all, so a model could not tell a correctly
|
|
7
|
+
parsed column from one whose values slid into the neighbouring column.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
import originlab as ol # noqa: E402
|
|
17
|
+
|
|
18
|
+
PASSED = 0
|
|
19
|
+
FAILED = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ok(label, cond, detail=""):
|
|
23
|
+
global PASSED
|
|
24
|
+
if cond:
|
|
25
|
+
PASSED += 1
|
|
26
|
+
else:
|
|
27
|
+
FAILED.append("%s %s" % (label, detail))
|
|
28
|
+
print("%s %s%s" % ("PASS" if cond else "FAIL", label,
|
|
29
|
+
(" | " + str(detail)[:110]) if detail else ""))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write(dirname, name, text, encoding="utf-8"):
|
|
33
|
+
path = os.path.join(dirname, name)
|
|
34
|
+
with open(path, "w", encoding=encoding, newline="") as fh:
|
|
35
|
+
fh.write(text)
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def code_of(fn):
|
|
40
|
+
try:
|
|
41
|
+
fn()
|
|
42
|
+
except ol.OriginError as exc:
|
|
43
|
+
return exc.code
|
|
44
|
+
except Exception as exc: # noqa: BLE001 — an unwrapped crash is the failure
|
|
45
|
+
return "UNWRAPPED:" + type(exc).__name__
|
|
46
|
+
return "no-raise"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
work = tempfile.mkdtemp(prefix="readfile_checks_")
|
|
51
|
+
try:
|
|
52
|
+
csv = write(work, "a.csv", "x,y\n1,2\n3,\n5,6\n")
|
|
53
|
+
res = ol.read_file(csv)
|
|
54
|
+
ok("csv parses", res.get("rows") == 3, res.get("rows"))
|
|
55
|
+
ok("every column carries a 3-value sample",
|
|
56
|
+
[c["sample"] for c in res["columns"]] == [[1.0, 3.0, 5.0], [2.0, None, 6.0]],
|
|
57
|
+
[c["sample"] for c in res["columns"]])
|
|
58
|
+
ok("sample keeps nulls so a gap is visible", None in res["columns"][1]["sample"])
|
|
59
|
+
ok("sample is explained", "原始值" in (res.get("sample_note") or ""),
|
|
60
|
+
res.get("sample_note"))
|
|
61
|
+
ok("full values still stay out of the payload",
|
|
62
|
+
all("values" not in c for c in res["columns"]))
|
|
63
|
+
|
|
64
|
+
# the binary-spreadsheet guard
|
|
65
|
+
for name, head in (("b.xlsx", b"PK\x03\x04"), ("c.xls", b"\xd0\xcf\x11\xe0"),
|
|
66
|
+
("d.wks", b"\x00\x00\x02\x00")):
|
|
67
|
+
path = os.path.join(work, name)
|
|
68
|
+
with open(path, "wb") as fh:
|
|
69
|
+
fh.write(head + b"\x00" * 64)
|
|
70
|
+
ok("%s is refused as a binary sheet" % name,
|
|
71
|
+
code_of(lambda p=path: ol.read_file(p)) == "unsupported_file_type",
|
|
72
|
+
code_of(lambda p=path: ol.read_file(p)))
|
|
73
|
+
|
|
74
|
+
# and the guard says what to do instead, with the file name in the message
|
|
75
|
+
path = os.path.join(work, "b.xlsx")
|
|
76
|
+
try:
|
|
77
|
+
ol.read_file(path)
|
|
78
|
+
except ol.OriginError as exc:
|
|
79
|
+
ok("message names the format", ".xlsx" in exc.message, exc.message)
|
|
80
|
+
ok("next action points at origin_import",
|
|
81
|
+
any("origin_import" in a for a in exc.next_actions), exc.next_actions)
|
|
82
|
+
|
|
83
|
+
# import_file must still accept Excel: only read_file refuses it
|
|
84
|
+
import inspect
|
|
85
|
+
src = inspect.getsource(ol.import_file)
|
|
86
|
+
ok("import_file still allows .xlsx", "二进制表格" not in src)
|
|
87
|
+
|
|
88
|
+
ok("a .bin is still routed to binio, not refused",
|
|
89
|
+
code_of(lambda: ol.read_file(os.path.join(
|
|
90
|
+
os.path.dirname(os.path.abspath(__file__)), "fixtures", "not_a_bin.bin")))
|
|
91
|
+
in ("parse_failed", "no-raise"), "binio path")
|
|
92
|
+
|
|
93
|
+
print("\n%d/%d read_file checks passed" % (PASSED, PASSED + len(FAILED)))
|
|
94
|
+
if FAILED:
|
|
95
|
+
for f in FAILED:
|
|
96
|
+
print(" FAILED:", f)
|
|
97
|
+
return 1
|
|
98
|
+
return 0
|
|
99
|
+
finally:
|
|
100
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
sys.exit(main())
|