Skip to content

Commit cb1cda4

Browse files
committed
pathlib-ref
1 parent 275c05e commit cb1cda4

7 files changed

Lines changed: 675 additions & 0 deletions

File tree

devel/1004.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ Windows 专属用例包在 `(when (os-windows?) ...)` 内,跨平台自动跳过
7878

7979
## 提交记录(最新在上)
8080

81+
### 2026/06/23 建立 pathlib 对照工具 + 边界审计修复
82+
**新增 `pathlib-ref/` 对照工具**(根目录):把"goldfish 行为是否等于 pathlib"从依赖手写断言,升级为可复现的逐项对照。
83+
- `posix_ref.py` / `windows_ref.py``PurePosixPath` / `PureWindowsPath` 真值,覆盖全部 API 的边界用例。
84+
- `posix_audit.scm` / `windows_audit.scm` — goldfish 实际行为审计,label 与 ref 脚本一一对应。
85+
- `normalize.py` — 一键归一化对照(`python3 pathlib-ref/normalize.py posix|windows`),按 label 比对两边输出,只打印真差异。
86+
- `README.md` — 用法与平台说明(Windows 审计须在 Windows 平台跑)。
87+
88+
**边界审计发现并修复的 POSIX bug**(经 normalize 对照 148 项,0 差异):
89+
1. `path-stem`/`path-suffix`/`path-suffixes` 对末尾点文件 `foo.` 错误切分:pathlib `foo.` 的 stem=`foo.`、suffix=空、suffixes=空(末尾点不构成后缀);原实现给 stem=`foo`、suffix=`.`。修 `split-name-dots``path-suffixes`:末段为空时不切。
90+
2. `path-join` 空串段错误追加 `.`:pathlib `P('/a').joinpath('')` => `/a`(空串是 no-op)、`P('').joinpath('b')` => `b`(空 base 不引入 `.`);原实现给 `/a/.``./b`。修 `path-join`:loop 跳过空串段,`append-parts` 过滤 `.` 段。
91+
3. `path-parts` 对当前目录 `.` 返回 `#(".")`:pathlib `PurePath('.').parts` => `()`;原返回 `#(".")`。修 `path-parts`:纯当前目录返回空向量。
92+
93+
**对照结论**:POSIX 平台 goldfish 与 `PurePosixPath` **148 项 0 差异,完全一致**;Windows 待换平台跑 `normalize.py windows` 确认。
94+
8195
### 2026/06/23 全面对齐 pathlib 的平台相关行为
8296
**核心认知**:pathlib 行为本身平台相关(`Path("C:\\a")` 在 POSIX 是 `PurePosixPath`,在 Windows 是 `PureWindowsPath`)。goldfish 已正确按 `os-windows?` 分流 type(已验证 macOS 上 `(path "C:\\a")` type 为 `'posix`)。本轮把所有测试断言对齐到 pathlib 在对应平台的真值(全部经 `python3` 实跑验证)。
8397

pathlib-ref/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# pathlib 真值参考
2+
3+
为 goldfish `(liii path)` 库的对齐工作提供 Python `pathlib` 的权威行为基准,
4+
并附带 goldfish 审计探针与一键对照工具。
5+
6+
## 文件
7+
8+
- `posix_ref.py``PurePosixPath` 真值(对应 POSIX 平台 / macOS / Linux)
9+
- `windows_ref.py``PureWindowsPath` 真值(对应 Windows 平台)
10+
- `posix_audit.scm` — goldfish POSIX 实际行为审计(label 与 posix_ref.py 一一对应)
11+
- `windows_audit.scm` — goldfish Windows 实际行为审计(label 与 windows_ref.py 一一对应)
12+
- `normalize.py` — 一键对照工具,归一化两边输出后只打印差异
13+
14+
## 背景
15+
16+
pathlib 的行为是平台相关的:`Path("C:\\a")` 在 POSIX 上是 `PurePosixPath`(整体当一段),
17+
在 Windows 上是 `PureWindowsPath`(drive=`C:`、有解析)。goldfish 已按 `os-windows?` 分流
18+
`path-type` 复刻此差异。
19+
20+
因此审计分两套:
21+
- **POSIX** — 在 macOS/Linux 上跑,对照 `PurePosixPath`
22+
- **Windows** — 须在 Windows 平台跑(`(os-windows?)=>#t` 时字符串才解析为 windows record),
23+
对照 `PureWindowsPath`。macOS 上跑 `windows_audit.scm` 字符串会走 posix 解析,结果无意义。
24+
25+
## 一键对照(推荐)
26+
27+
```bash
28+
# POSIX(任意平台均可)
29+
python3 pathlib-ref/normalize.py posix
30+
31+
# Windows(须在 Windows 平台)
32+
python3 pathlib-ref/normalize.py windows
33+
```
34+
35+
输出 `=== posix: 共 N 项,差异 0 ===` 即完全对齐;否则列出每个 `[差异]` / `[仅 python]` /
36+
`[仅 scheme]`,据此修正实现或测试。
37+
38+
## 手动流程
39+
40+
1. 跑参考脚本拿到 pathlib 真值:
41+
```bash
42+
python3 pathlib-ref/posix_ref.py
43+
python3 pathlib-ref/windows_ref.py
44+
```
45+
46+
2. 跑 goldfish 审计探针:
47+
```bash
48+
bin/gf pathlib-ref/posix_audit.scm
49+
bin/gf pathlib-ref/windows_audit.scm # Windows 平台
50+
```
51+
52+
3. 逐行 diff:
53+
```bash
54+
diff <(python3 pathlib-ref/posix_ref.py) <(bin/gf pathlib-ref/posix_audit.scm)
55+
```
56+
57+
## 输出格式与归一化
58+
59+
每行 `label => value`。Python 用 `repr`(`'...'`/`[...]`/`True`/`False`),
60+
Scheme 用 `write`(`"..."`/`(...)`/`#t`/`#f`)。`normalize.py` 会把两边 value 归一化
61+
(去引号、list 括号统一为 `()`、布尔统一为 `#t`/`#f`)后按 label 比对,只打印真差异。
62+
63+
## 验证范围
64+
65+
覆盖 path 库全部 API 的边界用例:`path->string``name``stem``suffix``suffixes`
66+
`parent``parents``parts``join``match``absolute?``relative-to`
67+
`with-name/stem/suffix``as-posix`,以及 Windows 的 `drive`/`root`、UNC、drive-relative 等。
68+
69+
边界包括:`.``..`、空串、尾斜杠、连续点(`a..b`/`a...b`)、末尾点(`foo.`)、
70+
隐藏文件(`.bashrc`)、多后缀(`a.tar.gz`)、绝对模式 match、空串 join 等。

pathlib-ref/normalize.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
# 规范化 diff:对比 pathlib 真值(posix_ref.py / windows_ref.py)与 goldfish 审计
3+
# (posix_audit.scm / windows_audit.scm)的输出。
4+
#
5+
# 用法: python3 pathlib-ref/normalize.py posix
6+
# python3 pathlib-ref/normalize.py windows
7+
#
8+
# 按 label 对齐两边,把 value 归一化后逐行比较,只输出不一致的行(并标注差异)。
9+
# value 归一化:去引号、统一 list 括号为 (...)、布尔 True/False -> #t/#f。
10+
11+
import subprocess
12+
import sys
13+
import re
14+
15+
ROOT = subprocess.DEVNULL # placeholder
16+
# resolve repo root from this file's location
17+
import os
18+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19+
GF = os.path.join(REPO, "bin", "gf")
20+
21+
22+
def norm(v):
23+
s = v.strip()
24+
# python bool
25+
if s in ("True", "False"):
26+
return "#t" if s == "True" else "#f"
27+
# list:[a,b](python) 或 ()(scheme) -> 统一为 (a b)
28+
if (s.startswith("[") and s.endswith("]")) or (s.startswith("(") and s.endswith(")")):
29+
inner = s[1:-1].strip()
30+
if not inner:
31+
return "()"
32+
items = [norm(x.strip()) for x in split_top(inner)]
33+
return "(" + " ".join(items) + ")"
34+
# python str '...' / "..." -> 去引号
35+
if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
36+
return python_unescape(s[1:-1])
37+
# scheme str "..." -> 去引号
38+
if s.startswith('"') and s.endswith('"'):
39+
return s[1:-1]
40+
return s
41+
42+
43+
def split_top(s):
44+
"""按逗号或空格切分顶层(忽略括号/引号内)。python 用逗号,scheme 用空格分隔 list 元素。"""
45+
out, cur, depth, in_str, q = [], "", 0, False, None
46+
for ch in s:
47+
if in_str:
48+
cur += ch
49+
if ch == q:
50+
in_str = False
51+
elif ch in "'\"":
52+
in_str = True
53+
q = ch
54+
cur += ch
55+
elif ch in "[(":
56+
depth += 1
57+
cur += ch
58+
elif ch in "])":
59+
depth -= 1
60+
cur += ch
61+
elif ch == "," and depth == 0:
62+
out.append(cur)
63+
cur = ""
64+
elif ch == " " and depth == 0:
65+
if cur:
66+
out.append(cur)
67+
cur = ""
68+
else:
69+
cur += ch
70+
if cur.strip():
71+
out.append(cur)
72+
return out
73+
74+
75+
def python_unescape(s):
76+
# 把 python repr 的转义还原(\', \\, \n ...)
77+
return s.encode().decode("unicode_escape") if "\\" in s else s
78+
79+
80+
def parse(path):
81+
"""解析 `label => value` 行为 (label, raw_value)。"""
82+
rows = {}
83+
with open(path) as f:
84+
for line in f:
85+
line = line.rstrip("\n")
86+
if " => " not in line:
87+
continue
88+
label, val = line.split(" => ", 1)
89+
rows[label.strip()] = val
90+
return rows
91+
92+
93+
def run(cmd):
94+
return subprocess.check_output(cmd, cwd=REPO, text=True)
95+
96+
97+
def main(flavor):
98+
py_file = os.path.join(REPO, "pathlib-ref", f"{flavor}_ref.py")
99+
scm_file = os.path.join(REPO, "pathlib-ref", f"{flavor}_audit.scm")
100+
py_out = run(["python3", py_file])
101+
scm_out = run([GF, scm_file])
102+
103+
py_rows = {}
104+
scm_rows = {}
105+
for line in py_out.splitlines():
106+
if " => " in line:
107+
lab, val = line.split(" => ", 1)
108+
py_rows[lab.strip()] = val
109+
for line in scm_out.splitlines():
110+
if " => " in line:
111+
lab, val = line.split(" => ", 1)
112+
scm_rows[lab.strip()] = val
113+
114+
labels = list(dict.fromkeys(list(py_rows.keys()) + list(scm_rows.keys())))
115+
diffs = 0
116+
only_py, only_scm = 0, 0
117+
for lab in labels:
118+
if lab not in scm_rows:
119+
print(f"[仅 python] {lab} => {py_rows[lab]}")
120+
only_py += 1
121+
diffs += 1
122+
continue
123+
if lab not in py_rows:
124+
print(f"[仅 scheme] {lab} => {scm_rows[lab]}")
125+
only_scm += 1
126+
diffs += 1
127+
continue
128+
a = norm(py_rows[lab])
129+
b = norm(scm_rows[lab])
130+
if a != b:
131+
print(f"[差异] {lab}\n python = {a}\n scheme = {b}")
132+
diffs += 1
133+
print(f"\n=== {flavor}: 共 {len(labels)} 项,差异 {diffs} (仅python {only_py}, 仅scheme {only_scm}) ===")
134+
135+
136+
if __name__ == "__main__":
137+
main(sys.argv[1] if len(sys.argv) > 1 else "posix")

pathlib-ref/posix_audit.scm

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
;; goldfish (liii path) POSIX 实际行为审计探针
2+
;;
3+
;; 用途:与 pathlib-ref/posix_ref.py 的 PurePosixPath 真值逐行对照。
4+
;; label 与 posix_ref.py 一一对应(顺序、用例相同),便于 diff。
5+
;;
6+
;; 运行: bin/gf pathlib-ref/posix_audit.scm
7+
;; 对照: diff <(python3 pathlib-ref/posix_ref.py) <(bin/gf pathlib-ref/posix_audit.scm)
8+
;;
9+
;; 注意 value 格式差异:python repr 给 '...' / [..] / True,False;
10+
;; scheme write 给 "..." / (..) / #t,#f。label 对齐即可定位差异。
11+
12+
(import (liii path) (liii os) (liii base) (liii string))
13+
14+
(define (show label v)
15+
(display label) (display " => ") (write v) (newline))
16+
17+
(define (list->show label lst)
18+
;; 与 python list 对应:scheme 用 (...)
19+
(display label) (display " => ") (write lst) (newline))
20+
21+
;; ---- str (path->string) ----
22+
(for-each
23+
(lambda (s) (show (string-append "str " s) (path->string (path s))))
24+
'("." "" ".." "a/b" "a//b" "a/./b" "./a" "a/" "/a/" "a/../b" "/.." "/." "/" "/a/b/c" "tmp/demo.txt" "a.b.c" ".hidden" "foo.")
25+
) ;for-each
26+
27+
;; ---- name ----
28+
(for-each
29+
(lambda (s) (show (string-append "name " s) (path-name (path s))))
30+
'("." ".." "" "a/" "/a/b/" "a..b" "foo." "a...b" "file.txt" ".hidden")
31+
) ;for-each
32+
33+
;; ---- stem / suffix / suffixes ----
34+
(for-each
35+
(lambda (s)
36+
(show (string-append "stem " s) (path-stem (path s)))
37+
(show (string-append "suffix " s) (path-suffix (path s)))
38+
(list->show (string-append "suffixes " s) (vector->list (path-suffixes (path s))))
39+
) ;lambda
40+
'("file.txt" "archive.tar.gz" ".hidden" "noext" "" "." ".." "foo." "a..b" "a...b" "a.b.c" "config.yaml.bak")
41+
) ;for-each
42+
43+
;; ---- parent ----
44+
(for-each
45+
(lambda (s) (show (string-append "parent " s) (path->string (path-parent (path s)))))
46+
'("." ".." "" "/" "/a" "/a/b" "/a/b/c" "a" "a/b" "a/" "/a/" "a/../b")
47+
) ;for-each
48+
49+
;; ---- parents ----
50+
(for-each
51+
(lambda (s)
52+
(list->show (string-append "parents " s)
53+
(map path->string (vector->list (path-parents (path s))))))
54+
'("/a/b/c" "/a/b" "/a" "/" "a/b/c" "a" "." "..")
55+
) ;for-each
56+
57+
;; ---- parts ----
58+
(for-each
59+
(lambda (s) (list->show (string-append "parts " s) (vector->list (path-parts (path s)))))
60+
'("." "" ".." "/" "/a/b" "a/b" "/a/" "a/" "/.." "a//b")
61+
) ;for-each
62+
63+
;; ---- joinpath ----
64+
;; 用例与 posix_ref.py 的 joins 表一致,label 为 "join {base} {segs空格连接}"
65+
(for-each
66+
(lambda (c)
67+
(let* ((base (car c)) (segs (cadr c))
68+
(joined-segs (apply string-append (map (lambda (x) (string-append " " x)) segs))))
69+
(show (string-append "join " base joined-segs)
70+
(path->string (apply path-join (cons (path base) (map path segs))))))
71+
) ;lambda
72+
'(("/a" ("/b")) ("/a" ("b")) ("a" ("/b")) ("/a" ("b" "/c" "d"))
73+
("/a" ("")) ("a" ("")) ("" ("b")) ("" ("")) ("a" ())
74+
("/" ("tmp" "demo.txt")) ("/a/" ("b")))
75+
) ;for-each
76+
77+
;; ---- match ----
78+
(for-each
79+
(lambda (c)
80+
(show (string-append "match " (car c) " " (cadr c))
81+
(path-match (path (car c)) (cadr c))))
82+
'(("a/b/c" "b/c") ("a/b/c" "c") ("a/b/c" "b") ("a/b/c" "a/b/c")
83+
("a/b/c" "*/c") ("/a/b/c" "/a/b/c") ("/a/b/c" "a/b/c")
84+
("/x/a/b/c" "/a/b/c") ("/a/b" "a/b") ("a/b/c" "a/c")
85+
("foo.txt" "*.txt") ("foo.TXT" "*.txt") ("foo.txt" "foo.???"))
86+
) ;for-each
87+
88+
;; ---- is_absolute ----
89+
(for-each
90+
(lambda (s) (show (string-append "abs? " s) (path-absolute? (path s))))
91+
'("/a" "a" "." "/" "/a/b" "a/b")
92+
) ;for-each
93+
94+
;; ---- relative_to ----
95+
(for-each
96+
(lambda (c)
97+
(show (string-append "rel-to " (car c) " " (cadr c))
98+
(path->string (path-relative-to (path (car c)) (path (cadr c))))))
99+
'(("/a/b" "/a") ("/a" "/a") ("/a/b/c" "/a") ("/a/b" "/") ("a/b/c" "a/b") ("a/b" "a/b"))
100+
) ;for-each
101+
102+
;; ---- with_name / with_stem / with_suffix ----
103+
(for-each
104+
(lambda (c)
105+
(show (string-append "with-name " (car c) " " (cadr c))
106+
(path->string (path-with-name (path (car c)) (cadr c)))))
107+
'(("a.txt" "c.md") ("/a/b.txt" "c.md") ("x.txt" "y"))
108+
) ;for-each
109+
(for-each
110+
(lambda (c)
111+
(show (string-append "with-stem " (car c) " " (cadr c))
112+
(path->string (path-with-stem (path (car c)) (cadr c)))))
113+
'(("a.tar.gz" "new") ("a.b.c" "new") ("a.txt" "b") ("README" "new") (".bashrc" "new") ("foo." "new"))
114+
) ;for-each
115+
(for-each
116+
(lambda (c)
117+
(show (string-append "with-suffix " (car c) " " (cadr c))
118+
(path->string (path-with-suffix (path (car c)) (cadr c)))))
119+
'(("a.txt" ".md") ("README" ".md") ("a.tar.gz" ".md") ("a.txt" "") ("a.tar.gz" "") (".bashrc" ".bak"))
120+
) ;for-each
121+
122+
;; ---- as_posix ----
123+
(for-each
124+
(lambda (s) (show (string-append "as-posix " s) (path-as-posix (path s))))
125+
'("/a/b/c" "a/b" "/" ".")
126+
) ;for-each

0 commit comments

Comments
 (0)