拓展阅读

从契约到补丁与反例测试

定义一个虚构的字面目录前缀匹配契约,展示仅比较顶层目录的基线缺陷,以及规范化后的修复。

状态:自检已实际通过 · 环境:Python标准库 · 性质:合成/模拟教学实验
对应讲义 11.9 · 保存Python脚本

沿着对象与状态理解本章

先写出要守住的行为,再用会失败的反例证明确实需要修改。测试通过必须绑定被测代码版本;之后再改文件,旧绿色结果不再证明当前交付。

下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。

动态图解 · F34

测试是找反例,不是给代码盖绿章

为什么“测试都通过”仍可能交付错东西?

测试只能验证它实际检查的行为和版本。一条只断言“不报错”的测试无法证明排除规则正确。好的测试围绕需求构造能击中错误的输入;代码修改以后,旧绿色结果也不再自动属于新版本。

反例能让错误实现暴露问题的具体输入。
回归修改破坏了本来成立的行为。
交付证据当前版本对应的检查命令、结果与范围。

输入与适用条件:构造的文件筛选任务;通配符语义须由需求明确,不在动画里偷偷改变“匹配文件名”与“匹配完整路径”的规则。

1 / 6 · 明确需求

第1步,共6步

窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。

测试是找反例,不是给代码盖绿章:明确需求明确需求排除*.tmp,但保留其余文件构造小反例a.py、b.tmp、nested/c.tmp先看原始失败确认旧代码确实没有正确排除实施最小修改修复匹配语义,不改无关API测试针对行为检查保留、排除、嵌套和空规则绑定版本交付绿色结果属于当前代码,而非旧文件教学流程;高亮表示当前步骤,不表示真实耗时比例。

不先说匹配对象与目录语义,无法判断哪个输出才正确。

本步状态由图形与说明共同给出。

默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。

为什么需要

减少返工和虚假完成,而非让审查循环无限增长。

怎样实现

围绕可观察行为、反例和当前版本组织验证。

可能的代价与误解

测试与实现共用同一错误假设、只测顺利路径、频繁审查无实际修复。

如何判断是否做对

故意引入一处已知错误,测试应失败;恢复后再通过。

怎样观察与验证

明确区分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 误排除"
}

输入关键词即可检索。

↑ ↓ 选择 Enter 打开Esc 关闭 · 仅本地搜索

阅读设置

阅读设置仅保存在本机浏览器,不上传任何数据。