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