首页 每日推荐 OpenCSG.NET CSG几何内核测试体系实践笔记 - 我才是银古

OpenCSG.NET CSG几何内核测试体系实践笔记 - 我才是银古

本文记录为一个 CSG(构造实体几何)内核搭建测试体系的过程,包括用例组织、oracle 设计、黄金基线门禁、覆盖率与性能门禁、双平台 CI。内容不讨论项目本身,只整理过程中遇到的四类有代表性的工程问题及其处理方式:断言缺乏判别力的几种形态;oracle 自身存在误差时断言的写法;黄金基线测试中“通过但无保护”的漏检路径;以及跨平台浮点差异被阈值逻辑放大为输出结构差异的机制。文中给出的做法与取证方法均与具体业务无关,可迁移到数值、几何或图形类代码库。

1 背景:测试通过不等于测试有效

起因是一条新增的黄金基线(approval)用例。按惯例,首次运行没有基线时,框架把当前输出写成 pending 文件(以 _.stl / _.json 后缀标记),断言返回“无法判定”。问题在于多数测试框架不把“无法判定”计入失败:dotnet test 退出码为 0,CI 显示全绿。于是一条忘记升级基线的用例可以长期留在仓库中,不携带任何保护,外观上与健康用例没有区别。

同类问题还有三种形态:断言的期望值来自对被测输出的重算(恒真);断言查询的数据结构不携带被断言的信息(恒真);用例输入在数学上到不了被断言的分支(红绿均与实现无关)。它们的共同点是测试通过但不产生信息。

因此我们改用另一个指标衡量测试体系:注入一个已知错误后,有多少用例会失败。下文围绕这一点展开。

2 Oracle 分层:把正确性转化为可判定的命题

测试的有效性上限由 oracle(真值来源)决定。我们按判定强度把 oracle 分为五层:解析闭式解、数学不变量、结构不变量、量化命题(属性测试)、独立实现互证。上层给出唯一正确答案,下层给出不可违反的关系;体系的整体有效性取决于最弱一层是否仍可判定。

图 1 oracle 的五层判定形式:判定强度递减,输入覆盖面递增

2.1 解析闭式解:先确认恒等式的适用条件

球体积、立方体表面积、凸多面体的欧拉示性数 V−E+F=2 都是强 oracle。但选用公式时需要确认口径:散度定理计算的签名体积依赖参考点选取,同一网格更换参考点后数值会在接缝处波动;表面积则对平移与三角剖分不敏感。我们第一版体积恒等式属性测试的问题正在于此:恒等式本身成立,但被用于不满足前提的网格。做法上的调整是:写断言前先在注释中写明恒等式的适用条件。

2.2 不变量与守恒量

属性测试的谓词应选用对任意输入都成立的命题。两个例子:布尔并后的外轮廓周长等于用独立实现的单调链凸包算出的周长(守恒量);面积的包含-排斥关系 |A∪B| + |A∩B| = |A| + |B|(恒等式)。这类断言不需要知道正确答案,只需要知道哪些量不可能变化。

2.3 结构不变量:断言关系而非数值

包括覆盖(清理后不丢失输入)、幂等(重复清理结果不变)、单调(合并容差有上界)、封闭流形且绕序一致(焊接后的拓扑性质)。难点在容差推导:线段清理是两级投影,先把近共线成员投影到累加基线,再把基线投影到另一条基准线,每级各引入一次容差,总容差为 2×joinTolerance。第一版按单级容差编写,实测产生 72 个假警报。数值型断言的容差应当从实现的算法结构推导,而不是凭经验设定。

2.4 属性测试与语料:先规格后实体

生成器按“先规格、后实体”的方式构造:先采样带元数据的规格(尺寸、变换、期望拓扑特征),再按规格生成实体,使每个样本自带可断言信息,避免生成随机输入后依赖偶然覆盖。语料按坐标量级分三档,兼顾冒烟档速度与普查档深度;随机数使用固定种子以保证失败可复现。

2.5 独立实现互证

消隐(隐藏线消除)结果没有解析真值。我们另外实现了一个标量化的 Möller–Trumbore 射线投射器,与被测的 z-buffer 投影器互证。约束只有一条:oracle 不调用被测代码的任何成员。共享代码的互证只会让同一缺陷在两侧同时存在。

3 双侧判定:oracle 存在误差时用区间包含代替等值断言

射线投射 oracle 与被测投影器都工作在离散栅格上(900 格、深度偏置 1e-3 量级)。两者之间做逐样本等值比较会在边界处抖动:同一采样点在不同运行中可能被判为可见或隐藏。

处理方式是对每个采样点做双侧分类:robust(两侧证据一致,确定可见或确定隐藏)、possible(任一侧证据支持)、以及二者之间的 INDET 带(栅格量化最敏感的区域)。断言写为区间包含:

// 断言形式:区间包含,而非等值

Assert.That(proj, Is.SubsetOf(possible)); // 被测没有“凭空可见”

Assert.That(robust, Is.SubsetOf(proj)); // 被测没有“漏掉确证”

// 落在 INDET 带内的样本点不计入判定

不写等号有三个实测原因:圆弧不产生采样点,等值断言会形成空洞;重合边可互换见证,possible 是并集而非精确集;栅格量化使边界样本不可判定。将不可判定区域显式建模,比忽略它更便于解释失败。

图 2 双侧判定模型:robust ⊆ proj ⊆ possible,INDET 带内样本不计入判定

两条配套措施。其一,oracle 的关键常量(栅格分辨率、偏置系数)以字面量公开,并配一个守卫测试与被测常量比对;常量被修改而 oracle 未同步时,由守卫先失败。其二,每个视图设置空洞守卫(CheckedSamples > 0),避免“通过”实际是“未检查任何样本”。

4 三类无效断言与断言有效性自检

恒真断言(重言式)。可见性记账断言的第一版用视图自身数据重算后比较,恒真。改为由调用方提供手工地面真值(如 3 条可见 + 2 条隐藏)后才具备判别力。

恒真断言(数据结构不匹配)。拓扑封闭性检查的第一版从边表中统计有向边重复次数,但该边表是无向去重表,有向重复恒为 0。改为从三角形绕序重建有向边后,检查才生效。数据结构不携带的信息无法通过断言获得。

不可达用例。一条射线单测期望 miss,但所选原点与方向在几何上必然命中,结果与实现无关。这类用例提高覆盖率数字,同时给后人留下“该路径已测试”的错误印象。

4.1 自检三问

• 注入一个已知错误(修改一行产品代码或一个常量),该断言是否会失败?

• 断言的期望值来自独立来源,还是来自被测输出?

• 用例输入是否能到达被断言的分支?

第一问可以制度化:缺陷登记表配一个元测试,静态检查“每个登记缺陷有复现测试、每个复现测试在登记表中有记录”,双向索引缺失即失败;并对该流程做负对照——临时删除一条索引,确认元测试失败后恢复。守卫本身需要被验证,否则只是另一份会过期的文档。这份登记表后来不止是索引——它成了缺陷修复轮的工作载体,见第 11 节。

5 黄金基线测试的漏检路径与门禁

即第 1 节的问题:pending 文件 + “无法判定” + 退出码 0。门禁由四部分组成。

• 套件级守卫:在测试程序集内放置 SetUpFixture,其一次性清理钩子递归扫描结果目录下的 pending 文件,存在残留即失败,并列出相对路径与处置说明。递归是必要条件:第一版只扫描顶层目录,而 JSON 基线位于子目录,门禁未起作用。

• 升级脚本:要求显式确认参数,并输出逐文件字节数与内容预览,避免一次性升级把错误输出固化为基线。

• 版本控制排除 pending 文件,防止误提交。

• CI 上传结果目录产物,便于跨平台比对。

[SetUpFixture]

public class GoldenGuard

{

[OneTimeTearDown]

public void FailOnUnacceptedGoldens()

{

var pending = FindPendingGoldens(ResultsDir); // 递归 **/*.stl | **/*.json

if (pending.Count > 0)

Assert.Fail($"{pending.Count} 个基线未升级:{string.Join(", ", pending)}");

}

}

图 3 漏检路径与门禁结构:守卫将“无法判定”转为套件级失败

门禁的局限需要记录:一次性清理钩子的失败属于 fixture 级错误,不计入用例失败数;用过滤器排除守卫测试时退出码仍为 0。

6 跨平台浮点差异:从 1 ULP 到输出结构变化

现象:同一代码、同一输入在 Windows 与 Linux 上产出的二进制网格字节流不同,逐字节基线在另一平台全部失败。排查结论与未定义行为无关,机制如下。

两个平台的数学库对 sin/cos/atan2 的实现最后 1 bit 可以不同(运行时不保证超越函数跨平台逐位一致);顶点坐标、法向、圆心、张角因此相差 ≤ 1 ULP;焊接容差、共圆判据、可见/隐藏比较均为阈值判断;阈值翻转导致两点焊接与否、边可见与否、弧拟合成功与否发生变化;最终三角形数、顶点序与字节流全部改变。误差量级未变(仍为 1e-16 相对量级),变化的是它穿过了阈值。

图 4 浮点误差经阈值判断放大为结构差异的过程

实测有四类夹具位于拓扑敏感路径上,差异可复现,不是随机噪声。处理采用分层策略:位精确比较只保留在单平台内(本平台做字节指纹);跨平台改用语义等价(拓扑同构 + 几何容差内一致);计数类断言从精确值改为实测稳健区间;耗时类断言从绝对预算改为同机比率。

一个可推广的结论:阈值判断是跨平台确定性的放大器。数值代码中把阈值比较集中、命名并文档化,对可移植性的改善大于提高计算精度。

后续一次涉及数十处读数的大规模内核修复(第 12、13 节)完成后,同一提交的首个 CI 运行即在两个平台上全部通过——分层策略与稳健读数的组合经受住了验证:大改动不再把「平台抖动」和「真实回归」混在一起。

7 性能测试的统计口径

• 统计量取 min-of-N 而非均值或中位数:均值受 GC 与调度干扰;中位数在双峰分布(分层 JIT 预热前后)下落在两峰之间;min-of-N 接近无干扰下限,稳定性最好。

• 基线在同一预热状态下采集:分层 JIT 与动态 PGO 使热循环存在约 3× 的阶段性差异,预热状态不一致时门禁比较的是 JIT 阶段而非代码。

• 绝对预算不可跨环境移植:CI runner 墙钟方差实测可达四分之一以上(同一用例 165 ms 与 205 ms 均属正常)。比率不变量(相对同机基线的倍数上界)保持严格,绝对预算仅在 CI 环境放宽系数。

• 覆盖率插桩会改变执行时间,插桩运行的墙钟数据不用于性能门禁。

• 复杂度门禁与常数门禁分离:用输入规模翻倍的时间比区分 O(n log n) 与 O(n²),比毫秒数字更稳定。

8 门禁的失败关闭原则

覆盖率门禁解析多次运行报告时取并集,阈值未达即非零退出。基础设施错误同样判失败:报告缺失、解析失败、守卫定位不到资源时均失败,并以不同退出码区分“未达标”与“门禁自身故障”,不采用“读不到即视为通过”。

配套做法是为每条门禁记录其失效模式(fixture 级失败不计入退出码、过滤器可绕过守卫、插桩使时间失真),评审时据此决定是否需要补充第二道检查。

9 并行执行暴露的全局状态问题

开启 fixture 级并行后的第一批失败指向两类共享状态:进程级静态字段(跨线程共享),以及线程静态字段误用(并行 fixture 复用同一线程时状态串扰)。处理:依赖全局状态的夹具显式标注不可并行(需使用特性,注释无效);可并行夹具保持无状态。

两个验证手段:用测试报告的时间区间分析确认并行确实生效;把并行作为检查手段而非性能手段,用于暴露隐藏的全局耦合。

10 CI 流水线与发布凭据

CI 采用双平台矩阵且互不快速失败,夜间长档运行大语料与性能门禁:快档覆盖每次提交,长档覆盖回归趋势。发布工作流从长期 API key 改为 OIDC 可信发布:工作流以身份令牌换取一小时临时密钥,一个令牌对应一把密钥,仓库中不保留长期凭据。配置面为四项(账号、仓库、工作流文件名、可选环境),与代码侧权限声明对应。

凭据生命周期与测试预算同理:越短,失效范围越小。

11 缺陷登记表与修复轮:把「知道有坑」变成可执行的工作流

测试体系的价值不止于发现缺陷,更在于让每个缺陷以可检索、可复现、可审计的形态沉淀下来,并在获得修复授权后按固定节奏转入修复。这套流程由三部分组成。

11.1 登记表

每个缺陷一条记录,包含:现象(可观察读数)、最小复现(一行命令或独立代码片段)、期望与实际、影响面、处置状态。最小复现是硬要求——它把口口相传的坑变成任何人五分钟内能看到结果的实验。

11.2 双向索引

每个缺陷编号必须能在测试代码里检索到,每个标注了缺陷编号的用例必须在登记表里有对应行。这条约束由元测试自动校验(见 4.1),且对校验本身做过负对照:临时删掉一条索引,确认元测试失败后恢复。

11.3 修复轮的节奏

默认策略是只登记、不改产品代码;修复按批次获得明确授权。每轮修复按固定顺序执行:先写复现(失败),再定位根因,再做最小修复,再让复现转绿并纳入常规回归;随后把登记表行从「未修复」迁到「已修复」,注明修复方式与实测证据。两个配套纪律很重要:其一,旧的行为基线(golden)按新输出重基线化时逐文件人工核对,只接受解释得通的差异;其二,确实无法在本轮闭环的残余按「有据豁免」处理——豁免集合本身是代码里的一张显式列表,逐条附注理由,不允许静默跳过。

流程的价值在于可预期:任何时刻,「已知但未修的问题有几条、分别是什么状态」都只有一个答案来源。

12 内核缺陷案例(一):数值与语义

以下五个案例选自修复轮中根因最有代表性的一批。每个案例按同样的结构记录:现象、根因、修复、验证;具体的复现命令与代码位置不在本文展开。

12.1 平面变换:一次大坐标平移如何破坏共面判定

现象:同一实体在原点附近布尔运算完全正确,平移到 |坐标| ≈ 1e6 后,自并集开始产出十来个面(正常六个),含逐位相同的重复面与同平面薄片,体积偏差可达 47%。

定位:探针把坐标量级逐级放大,发现阈值在 1e5 与 1e6 之间;再做「平移出去、平移回来」的往返实验,顶点几乎无损,但平面方程漂移了 5e-5——而共面判定的容差是 1e-5,误差刚好跨过阈值。

机制:平面表示为「法向 + 偏移量」,变换实现采用「在平面上取三个点、变换三点、再用新三点重建平面」。平移量到 1e6 量级时,重建切向量时的相减发生灾难性抵消,切向量的低有效位被抹掉,重建法向的相对误差被放大到 ~1e-10;再乘坐标量级 1e6,平面偏移的绝对误差达到 5e-5。误差不来自变换本身,而来自为变换选择的计算路径。

图 5 大坐标平移下的平面误差链:抵消发生在「变换后相减」时刻,5e-5 的漂移越过 1e-5 的判定容差

修复:平面按精确仿射映射变换——n′ = A⁻ᵀ·n、W′ = (W + n′·t)/|n′|,一次乘加完成,平移、旋转、缩放、镜像统一处理(镜像由负行列式自然表达,不需要额外翻转)。修复后同尺度往返的平面偏移回到 ~1e-15;1e0 到 1e7 全尺度上自并集精确等于自身,不相交并集的体积相对偏差从 9e-4 降到 4e-12。

教训:几何代码里「先构造、再重建」的路径隐含数值代价;对阈值敏感的对象(平面、法向、包围盒)应尽量用一次性的精确公式直接变换。

12.2 空集的布尔语义:∅ 的传播

现象:两个不相交实体求交得到空集;把空集继续参与运算,减法返回了被减数本身(法线朝内、有向体积为负),交集返回了另一个操作数——等价于把空集当成了全空间。

根因:布尔路径用「树取反」表达补集,而空的 BSP 树取反在实现上等于全空间;这一约定对并集有显式短路,减法和交集没有对齐——空实体的语义在库内没有成文契约。

修复:在三条路径上显式定义空集的代数性质——∅ − X = ∅、X − ∅ = X、任一操作数为空的交集为 ∅,全空的并集走与无参并集相同的路径。修复后,包含随机深树的属性测试从「十几个种子产出非正体积」收敛为全部通过。多余的短路还顺带消除了一段「平移回原点再平移回来」的往返——这段往返在大坐标下会让体积读数漂移。

教训:空集合、单位元这类退化输入要有成文语义,并在所有运算路径上一致实现;穷举不出的路径,交给「对任意规格成立」的属性测试。

12.3 惰性缓存的失效边界

现象:实体暴露公共可变的顶点列表,包围盒与包围球以惰性缓存保存。调用方在原地修改顶点(合法操作)后,缓存过期;随后布尔运算的粗筛层用陈旧包围盒误判两实体不相交,跳过裁剪直接拼接——静默产出重叠几何。

处理:第一轮修掉实体级缓存(包围盒每次现算);随后的审计又在多边形对象上发现同模式的两处缓存,一并去除。另留一个用文档写清楚的边界:多边形上还有只读的快照数据(平面方程),原地改顶点后它与顶点不再自洽,因此「读操作」全面支持现算语义,「原地改顶点后继续做布尔运算」明确不支持——边界可以存在,但必须成文。

教训:可见的可变状态加隐式缓存,等于必然的失效问题。缓存只有三种出路:去掉、可失效化、或在契约上排除触发路径。

12.4 深树序列化:栈溢出与可捕获的深度边界

现象:把程序构造的、超过一千层的节点树序列化为 JSON,进程直接中止(栈溢出),任何 try/catch 都接不住;而读路径有 64 层守卫——两侧不对称。

根因:写出路径逐层递归,唯一的防线是序列化器自身的 64 层嵌套限制;递归深度先于守卫触达栈容量。

修复:在写出入口加显式深度守卫(线程局部的深度计数,try/finally 复位),超过 63 层立即抛可捕获的序列化异常。63 有明确推导:写出器的嵌套上限是 64,能成功写出的最深链恰好是 63 层节点。

教训:「异常可捕获」是深递归代码的最低契约;守卫阈值与写出器上限对齐比拍一个数更稳。修复也让一条原本无法在测试宿主内验证的崩溃路径,变成了普通的异常断言。

12.5 浅斜肩板:由覆盖条件推导出的几何缺陷

现象:一款带圆头凸耳板的生成函数,在「斜角小于 30°」的参数域内静默多出一块水平平台与竖壁,净体积多出约 0.4%。

根因:拼接肩部矩形时其跨度取 zT = R·sinθ;推导覆盖条件发现,圆盘外的台面段需要覆盖到 R − R·sinθ,覆盖成立当且仅当 sinθ ≥ 1/2——恰好是 θ ≥ 30°。图纸常用参数(约 60°–70°)都在安全侧,缺陷因此潜伏了很久。

修复:跨度改为条件式——θ < 30° 时扩展到 R,其余情况保持原值。条件式选择不只是保守:几何生成在多边形分解上并不规范,改动默认路径会改变所有既有输出的逐字节形态;把行为变化严格限制在缺陷参数域内,回归验证就从「全部重基线」缩小为「受影响域的重基线」。

教训:参数域推导比盲目的参数扫掠更能定位这类边界;修复的影响面要主动设计。

13 内核缺陷案例(二):图形管线

图形管线的缺陷有个共同特点:输出(视图、线、弧)没有解析真值,判错与判对的差别藏在几何语义里。四个案例都先用探针把现象拆成机制,再逐一隔离验证。

13.1 圆弧识别:千条碎段到三个整圆

现象:曲面的三视图轮廓没有还原为圆,而是上千条 0.01 毫米级碎段与两条 14° 开口小弧;圆柱轴向视图的整圈边缘只还原到 330°,缺一段无法补圆。

探针把机制拆成两层。其一,成链算法对「边的朝向」做了隐含假设:链按「队尾边的终点就是前进尖端」推进,一旦某条边以相反的端点接入(三角面片的环绕序决定边的起止,与链的走向无关),尖端停在原地,链当场截断;纯几何见证复现为:48 边圆环在两条反向边处被切成 4 条链,各自拟合为开口弧。其二,低于拟合容差的「退化段」被整体排除出成链图,而掠射轮廓被栅格切成的一格级碎段恰好全部低于该阈值——即使朝向修复,碎段也无法成环。

修复:链的成员携带取向(下标 + 是否反向行进),前向与后向延伸各自要求新成员的入点/出点接在尖端;退化阈值从「拟合容差」收敛为「接链容差」——点状边(真退化)依旧排除,长度超过接链容差的碎段可以成链。

图 6 成链取向修复前后:反向边不再截断链;叠加退化阈值收敛后,碎段首尾相接成环

结果:球的三视图从 1482 条碎段还原为 3 个闭合整圆;圆柱与扇形柱的轴向弧分别从 330°、247.5°、82.5° 补齐为整圆或完整扇角。回归侧新增了一个不依赖投影管线的纯几何用例:混合取向的圆环必须还原为整圆。

13.2 深度栅格:格内采样与自遮挡误判

现象:随机语料中近九成种子的斜掠面视图出现「几何可见、投影判隐藏」的错误——表现为一格级虚线碎段,最长可达数十格;射线 oracle 与投影器在同一点上给出相反判定。

根因:深度栅格每格只记录「格中心最近的一次深度」,而倾斜表面在同一格内的深度变化上界是 cell·tanφ。判定某条边的采样点是否被遮挡时拿格心深度做比较,当格内变化超过全局偏置(深度跨度 ×1e-3)时,边点会被自身所在的倾斜面判成隐藏——量化误差直接进了布尔判断。

图 7 格内深度模型与两次判定口径;下方是两个被实测否决的替代口径

修复:把误差项显式记账——每格在记录最近深度的同时,记录写入面在该格的深度变化上界(cell 乘深度梯度绝对值之和),判定改为 d ≤ d_c + bias + 上界。修复过程有两轮被实测否决的尝试,两个错误方向都值得记录:其一,用写入面的平面方程把深度精确外推到采样点,看似更精确,但当写入三角形不覆盖采样点所在的格区时外推没有意义,反而制造大量伪「可见」;其二,只放大上界而不控制分布,会把真正隐藏的段判成可见。最终口径是上界记账与离群约束双条件。

结果:随机语料从 59/64 个种子失败收敛到 11 个残余,且残余形态收敛为单一窄类——贯通孔膛缘的刀口可见性:孔膛屏面开度处于 1~2 个栅格量级时,远侧孔缘恰好「透过孔膛可见」,格量化判定相反。这类问题需要亚格分辨率才能根治,成本不划算,按有据豁免显式登记,不再计入失败;覆盖门限相应重校准。

13.3 伪弧拟合:几何不可分时,用分布判别

现象:一条「两端圆角碎片 + 长直弦」的折线链被整体拟合成一条巨半径圆弧(R ≈ 279),弧体在中段外拱 9.2 毫米,越过真实几何包络。

分析:拟合判据要求每个顶点距圆不超过容差、每条弦的中点距圆不超过 5%·R。难点在于——单条长弦与「粗采样真弧」在几何上不可分:两者的弦矢比同为 L²/8R,逐条弦比较永远无法区分。

图 8 真弧链与伪链的角步分布对比,以及被三轮校准敲定的离群判据

但两者在整体分布上可分:真弧链的顶点按镶嵌步长均匀分布(最大角步约等于平均角步),伪链的角步集中在小段簇、长弦一步跨过大幅张角(实测最大步 29° 对平均约 3°)。修复即增加一条守卫:角步按弧序统计,最大步超过平均步 2.2 倍、且超过次大步 1.8 倍时拒绝整链拟合、回退折线——「宁缺勿滥」与拟合器原有设计原则一致。

这条守卫的校准用了三轮:(一)按排序表逐差统计是错的——跨 0° 的长弦步会被排序拆散而看不见,必须沿弧序(自最大间隙起、跨 0° 回绕)累进;(二)只卡最大/平均会伪拒真弧——栅格化的掠射真弧由 1~2 格碎段混成,比值本就可到 2;(三)加入离群判据(同时超过次大步一定倍数)后,伪弧与真弧才稳定分开。可见的效应:巨弧族消失,包络回到几何尺度;同时一条被拒的窗拟合暴露出它此前把两处真弧藏在链里,修复后帽圆弧由「90° + 90°」补全为「180° + 180°」。

13.4 包络与分解缝:两处「结构信息丢失」类缺陷

包络。视图投影在识别出圆弧后会把折线替换成弧实体,而包络最初仍只由折线导出——全弧视图(如圆柱轴向)的包络退化为空哨兵 (0,0,0,0),与「无几何」不可分。修复:把弧的极值并入包络,采用精确弧盒(整圆取外接盒;开口弧取两端点加扫掠范围内的象限极值点),而不是保守全圆盒——后者在巨半径浅弧上会把几十毫米宽的视图撑到数百毫米,破坏落图定位。这里的原则值得记录:容差与保守方向的选择要跟随真实需求,而不是「保守总是安全」。

分解缝。布尔运算把大面按小特征切成多片后,切缝在三角汤里成为单侧边,投影器按「边界边无条件放行」,视图里于是出现贯穿整板的凭空长实线。修复思路:对单侧边做区分——沿同一直线、被共面邻居覆盖的子区间是「同一曲面内部的分解缝」,剔除;未被覆盖、或覆盖方不共面的,是真实边界与棱线,保留。判据里「共面」是关键:实验证明真实棱线与幻线在接缝形态上同构(长边加多段覆盖),唯一区别就是覆盖方与自身共面与否。修复后幻线从视图中消失,而板顶角线等真实棱线原样保留——两个方向都有读数钉。

14 小结:可迁移的做法清单

主题

做法

oracle 分层

先确定最强可判定命题再写断言;恒等式注明适用条件

oracle 独立

不调用被测代码;关键常量配漂移守卫

区间断言

双侧分类 + INDET 带 + 每视图空洞守卫

断言自检

注入错误是否失败;期望值是否独立;输入是否可达

基线门禁

递归扫描 + 显式确认升级 + 记录失效模式

跨平台

位精确限单平台;跨平台用语义等价与稳健区间

性能

min-of-N、同预热状态、比率不变量、复杂度门禁

门禁失败关闭

资源缺失判失败;退出码区分原因

并行

用于暴露全局状态;不可并行用特性标注

凭据

OIDC 临时密钥替代长期 key

缺陷工作流

登记表 + 双向索引 + 批次授权;复现先行、最小修复、有据豁免

可变状态

公共可变列表 + 惰性缓存 ⇒ 去缓存或成文排除;空集语义全路径一致

数值稳健

阈值敏感的几何对象用精确变换公式;误差按量级实测定位

判别式设计

几何不可分时用分布/离群判据;校准要防伪阳也防伪阴

修复影响面

条件式修复把行为变化限制在缺陷域内;重基线逐文件核对

用例数量不是这套体系的主要价值。更实际的作用是:每次测试通过时,都能回答“如果此处出错,哪条用例会失败”。这也是我们后续新增用例时的验收标准。

项目地址

https://github.com/znlgis/OpenCSG.NET