px普贤教程PuXian
普贤教程 › 生态包

测试与对比

四个包解决「怎么证明代码是对的」:testkit 断言、tdtest 表驱动、quickcheck 属性测试、diff 差异。它们都是纯函数、零依赖,px test 之外也能直接当库用。

testkit:断言返回 Result,不打印不计数

这是最容易用错的地方:tk_equal 之类不做任何输出,成功返 Ok(null)、失败返 Err(msg)。累加与打印是调用方的事。

px
import testkit

var total = 0
var failed = 0

var r1 = tk_equal(1 + 1, 2, "加法")
total += 1
if r1.is_err():
    failed += 1
    print(r1)

var r2 = tk_equal(1 + 1, 3, "加法")
total += 1
if r2.is_err():
    failed += 1
    print(r2)

tk_summary("demo", total, failed)
text
Err(期望 3,实际 2(加法))
FAIL: demo(1/2 失败)
tk_summary(name, total, failed) 是三个参数
它不自己统计,全部靠你传。传 failed=0 就打印 PASS: name(N 断言),否则 FAIL: name(M/N 失败),并返回 bool——可以直接 if not tk_summary(...): exit(1)。

可用断言:tk_true tk_false tk_equal tk_not_equal tk_contains tk_not_contains tk_approx tk_is_ok tk_is_err tk_type tk_len tk_nil tk_not_nil tk_match tk_greater tk_less tk_fail,另加 tk_tempdir(prefix) 建临时目录。

tdtest:表驱动

每条用例是一个 dict,必须带 fn(单参函数)与 args,断言二选一:want(相等)或 check(谓词)。

px
import tdtest

def twice(x):
    return x * 2

def threetimes(x):
    return x * 3

var cases = [
    {"name": "2*2", "fn": twice,      "args": 2, "want": 4},
    {"name": "3*2", "fn": threetimes, "args": 2, "want": 100}
]

var r = td_run(cases).ok()
print(r["pass"], r["fail"], r["total"])
print(str(r["failures"]))
text
1 1 2
[{got: 6, name: 3*2, want: 100}]
td_run 只有一个形参
写成 td_run(cases, check_fn) 会得到 R1005: 调用 td_run 参数过多(2 个实参,1 个形参)。 断言函数要放进每条用例的 check 里。另外 fn 只接受单参——多参场景要自己包一层。

quickcheck:属性测试,但反例不可捕获

成功时返回跑了多少次(不是 true):

px
import quickcheck

def in_range(n):
    return n >= 0 and n <= 100

print(qc_for_int(0, 100, in_range, 50))   # 50
text
50
反例直接终结进程
qc_for_int 内部对 qc_forall 的结果调 .unwrap(),而 PuXian 没有 try/catch:
px
def bad(n):
    return n < 10

print("start")
print(qc_for_int(0, 100, bad, 20))
text
start
运行时错误: 错误 [R1004] 67:53: unwrap 失败: Err(quickcheck: 反例(第 1 次): 52)

所以要「跑属性测试但不让进程死」只能用底层的 qc_forall(gen, prop, n),它返 Result:Err 里带反例值与第几次。qc_int(lo,hi) / qc_str(maxlen) / qc_list(gen, maxlen) / qc_bool() 是生成器。

diff:行级差异

px
import diff

var a = "a\nb\nc"
var b = "a\nB\nc"
print(str(df_lines(a, b)))     # 前缀表示 同/删/增
print(df_unified(a, b))
print(df_ratio(a, b))
text
[ a, -b, +B,  c]
--- a
+++ b
@@ -1,4 @@
 a
-b
+B
 c

0.6666666666666666
hunk 头的行数比实际多 1
a 只有 3 行,头写的却是 @@ -1,4 @@。单行输入 x 也会写成 @@ -1,2 @@。 patch 命令对「行数偏大但内容完整」的 hunk 通常能容忍,但严格校验的工具有可能拒收。要用标准 unified diff,把头部行数自己减 1 重写。

API:df_lines(带前缀的行列表)、df_opcodes(equal/replace/insert/delete 段)、df_unified(补丁文本,context 默认 3)、df_ratio(相似度,按行/词判等,不是字符级)、df_inline(行内差异)。