dsh-origin-bridge 0.4.4 → 0.4.6
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 +329 -303
- package/asciiio.py +184 -17
- package/docs/shot-cv.png +0 -0
- package/docs/shot-eis.png +0 -0
- package/docs/shot-heatmap.png +0 -0
- package/docs/shot-workstation.png +0 -0
- 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 +164 -22
- package/package.json +89 -76
- package/screenshots.json +7 -0
- package/scripts/make_screenshots.py +187 -0
- package/server.py +628 -612
- 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/smoke_test.py
CHANGED
|
@@ -27,6 +27,21 @@ PY = sys.executable
|
|
|
27
27
|
STARTED_AT = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class NonStrictJSON(ValueError):
|
|
31
|
+
"""The server wrote bare Infinity/NaN/-Infinity.
|
|
32
|
+
|
|
33
|
+
Python's json accepts those, RFC 8259 does not: dsh's client would fail on the
|
|
34
|
+
whole response and the tool would look dead rather than slightly wrong. A test
|
|
35
|
+
harness that tolerates it hides a real protocol defect, so we reject it here.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def strict_loads(text):
|
|
40
|
+
def _reject(const):
|
|
41
|
+
raise NonStrictJSON("非标准 JSON 常量: %s" % const)
|
|
42
|
+
return json.loads(text, parse_constant=_reject)
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
class Rpc:
|
|
31
46
|
def __init__(self, python, script):
|
|
32
47
|
self.p = subprocess.Popen([python, "-u", script], cwd=HERE,
|
|
@@ -64,7 +79,11 @@ class Rpc:
|
|
|
64
79
|
with self.lock:
|
|
65
80
|
for raw in self.lines:
|
|
66
81
|
try:
|
|
67
|
-
msg =
|
|
82
|
+
msg = strict_loads(raw)
|
|
83
|
+
except NonStrictJSON:
|
|
84
|
+
# A reply carrying bare Infinity/NaN parses in Python but not
|
|
85
|
+
# in a strict client, so dsh would report the tool as dead.
|
|
86
|
+
raise SystemExit("服务器写出了非法 JSON:%s" % raw[:160])
|
|
68
87
|
except ValueError:
|
|
69
88
|
continue
|
|
70
89
|
if msg.get("id") == mine:
|
|
@@ -82,7 +101,10 @@ class Rpc:
|
|
|
82
101
|
blocks = res.get("content", [])
|
|
83
102
|
text = "".join(c.get("text", "") for c in blocks if c.get("type") == "text")
|
|
84
103
|
try:
|
|
85
|
-
payload =
|
|
104
|
+
payload = strict_loads(text)
|
|
105
|
+
except NonStrictJSON as exc:
|
|
106
|
+
payload = {"ok": False, "code": "bad_payload",
|
|
107
|
+
"message": "工具返回体不是合法 JSON(%s):%s" % (exc, text[:300])}
|
|
86
108
|
except ValueError:
|
|
87
109
|
payload = {"ok": False, "code": "bad_payload", "message": text[:400]}
|
|
88
110
|
payload["_isError"] = res.get("isError", False)
|
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())
|