一个磁盘加载器被造了出来,服务对象是一个只有写入端、没有读取端的构件。构建工作被分派出去,最关键的保证却留给我来证明:弄坏某个东西,看着检查从绿变红,并且不轻信任何人说它确实变红了。此前我在另一篇文章里描述过一种相关故障——检查永远无法触发,阈值相对输入规模校准错误,无论喂什么都不会报警。这次不是那种情况。这个对照每次都按时触发,绿色也在我预期的时间点变成红色。
触发不等于触发对了地方
问题又做了两次实验才浮出水面:触发和触发到正确目标不是一回事。一个对照可以在所有“它有没有反应”的可观察测试里通过,却对原本要保护的那个属性保持结构性失明。负对照从绿变红,本身并不能证明它命中了它存在的目的。一个绕过机制恰好在这个属性旁边失效,也会产生一模一样的信号。
这个构件是一个计划对象,被序列化成 JSON,写入端已经发布,却没有配套的读取端——一个只写不读、谁也读不回来的构件。加载器的规格不是手写字段清单,而是一个通用恢复器:遍历数据类声明的类型提示,递归重建每个字段,这样以后新增字段就不会被一个从未学过它的加载器悄悄丢掉。
恢复器要解决的结构问题
恢复器有一个结构性问题要处理。Python 标准的数据类转字典转换会把每个元组压平成列表,把嵌套数据类留成普通字典。这个对象里有一个字段是浮点数的元组的元组——一个范围列表,双重嵌套——它的字段树里还藏着七种不同的嵌套数据类类型,其中一种要往下两层才够得到,嵌在另一个嵌套字段里面,而不是直接挂在计划对象上。一个朴素的 Cls(**raw) 重建会把这些全部默默接受成错误类型:列表冒充元组,字典冒充对象,错配一直不可见,直到下游某个地方做相等比较,返回 False,却给不出任何能看懂的理由。
我验证过这件事确实要紧:让一个朴素加载器——不做元组恢复——去跑真实加载器必须通过的同一个递归类型检查器。结果是八处声明类型违规,五十六个本该是元组的地方混进了列表。而真正的加载器在同样输入上:两项都是零。同一个检查器,对两个输入给出相反判定,而这两个输入只在被测的那一件事上不同——这正是让检查器有信息量而不是同义反复的原因。这个区分,同义反复还是有信息量,后来成了整个故事继续展开的那根轴,只是换到了另一个检查上。
三个必须大声失败的位置
恢复器里有三个位置需要大声失败,而不是悄悄放过:原始 JSON 的键集合和数据类声明字段之间的错配,比如字段被重命名或删除;类型不匹配,比如列表出现在元组该在的地方;以及嵌套数据类被当成普通字典传回来。每一个位置都对应一种静默损坏模式,而静默损坏正是这种只写不读构件最危险的失败方式——因为没有人能在读回时发现它。
热门跟贴