拓展阅读
从契约到补丁与反例测试
定义一个虚构的字面目录前缀匹配契约,展示仅比较顶层目录的基线缺陷,以及规范化后的修复。
沿着对象与状态理解本章
先写出要守住的行为,再用会失败的反例证明确实需要修改。测试通过必须绑定被测代码版本;之后再改文件,旧绿色结果不再证明当前交付。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
动态图解 · F34
测试是找反例,不是给代码盖绿章
为什么“测试都通过”仍可能交付错东西?
测试只能验证它实际检查的行为和版本。一条只断言“不报错”的测试无法证明排除规则正确。好的测试围绕需求构造能击中错误的输入;代码修改以后,旧绿色结果也不再自动属于新版本。
反例能让错误实现暴露问题的具体输入。
回归修改破坏了本来成立的行为。
交付证据当前版本对应的检查命令、结果与范围。
输入与适用条件:构造的文件筛选任务;通配符语义须由需求明确,不在动画里偷偷改变“匹配文件名”与“匹配完整路径”的规则。
1 / 6 · 明确需求
第1步,共6步
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
不先说匹配对象与目录语义,无法判断哪个输出才正确。
本步状态由图形与说明共同给出。默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
减少返工和虚假完成,而非让审查循环无限增长。
怎样实现
围绕可观察行为、反例和当前版本组织验证。
可能的代价与误解
测试与实现共用同一错误假设、只测顺利路径、频繁审查无实际修复。
如何判断是否做对
故意引入一处已知错误,测试应失败;恢复后再通过。
怎样观察与验证
明确区分src/generated与src/generated2,保持大小写敏感,拒绝父目录穿越和glob。它不代表任何真实构建系统的匹配语义。
运行命令
python3 reading-notes/experiments/09_exclude_contract.py完整教学代码
"""虚构exclude契约:相对路径、大小写敏感、字面目录前缀,不支持glob。
不读写真实仓库;目标是演示需求、最小修复和反例测试。
"""
import re
def normalize(path: str) -> str:
if not isinstance(path,str) or not path:
raise ValueError("路径必须是非空字符串")
if any(c in path for c in ['\x00','*','?','[',']']):
raise ValueError("本教学契约不接受NUL或glob字符")
text=path.replace('\\','/')
if text.startswith('/') or re.match(r'^[A-Za-z]:',text):
raise ValueError("仅接受相对路径")
parts=text.split('/')
if '..' in parts:
raise ValueError("禁止父目录穿越")
normalized='/'.join(p for p in parts if p and p!='.')
if not normalized:
raise ValueError("根目录/空路径不在本契约中")
return normalized
def excluded(path: str, rules: list[str]) -> bool:
p=normalize(path)
normalized=[normalize(r) for r in rules]
return any(p==r or p.startswith(r+'/') for r in normalized)
def filter_files(files: list[str],rules: list[str]) -> list[str]:
# 保留调用者原始字符串;仅比较时规范化。
return [p for p in files if not excluded(p,rules)]
def buggy_excluded(path: str,rules: list[str]) -> bool:
# 故意的基线缺陷:只比较顶层目录,无法处理嵌套规则。
return normalize(path).split('/')[0] in [normalize(r) for r in rules]
def self_test() -> None:
assert excluded('src/generated/a.py',['src/generated'])
assert not excluded('src/generated2/a.py',['src/generated'])
assert excluded(r'src\generated\a.py',['./src/generated/'])
assert excluded('build/a.txt',['build'])
assert not excluded('Build/a.txt',['build']) # 明确的大小写契约
files=['src/a.py','tests/a.py']
assert filter_files(files,[])==files
assert not buggy_excluded('src/generated/a.py',['src/generated'])
for bad in ['../secret','/etc/passwd','C:\\secret','a/../b','*','']:
try:
normalize(bad)
except ValueError:
pass
else:
raise AssertionError(f"应该拒绝: {bad}")
def run_demo() -> dict:
files=['src/generated/a.py','src/generated2/keep.py','src/main.py','build/out.txt']
rules=['src/generated','build']
return {"性质":"虚构契约,不代表PSBuild或任何真实项目",
"输入":files,"规则":rules,
"有缺陷基线保留":[p for p in files if not buggy_excluded(p,rules)],
"修复后保留":filter_files(files,rules),
"关键反例":"src/generated2 不能被 src/generated 误排除"}
if __name__ == "__main__":
import json
self_test()
print(json.dumps(run_demo(),ensure_ascii=False,indent=2))
运行结果示例
以下结果由随包脚本产生。它们不是LLM或产品性能数据。
{
"性质": "虚构契约,不代表PSBuild或任何真实项目",
"输入": [
"src/generated/a.py",
"src/generated2/keep.py",
"src/main.py",
"build/out.txt"
],
"规则": [
"src/generated",
"build"
],
"有缺陷基线保留": [
"src/generated/a.py",
"src/generated2/keep.py",
"src/main.py"
],
"修复后保留": [
"src/generated2/keep.py",
"src/main.py"
],
"关键反例": "src/generated2 不能被 src/generated 误排除"
}