第15章:测试、性能优化与二次开发

本章聚焦工程质量与可扩展性——OpenCSG.NET 如何用五层测试体系守护正确性、如何用覆盖率门禁与双平台 CI 守住质量底线、如何用基准量化性能,以及当内置能力不够时,你如何给它做二次开发。学完本章,你不仅会”用”这个库,还能”改进”它、把它安全地融入自己的产品与 CI 流程。

15.1 测试体系全景

几何算法很难用”断言某个数值等于常量”验证——一次布尔运算产出成百上千个多边形,你不可能手写期望值。OpenCSG.NET 为此搭建了四个测试工程 + 五层测试的体系:

工程 目标框架 职责 规模(约)
tests/OpenCSG.NET.Tests net8.0;net10.0 主套件:单元/契约/黄金基线/缺陷回归 377 用例 ×2 TFM
tests/OpenCSG.NET.SystemTests net8.0;net10.0 系统层:生成器/属性/性能/稳健性/端到端 1257 用例 ×2 TFM
tests/OpenCSG.NET.TestKit net8.0 测试基础设施库(生成器、oracle、桥接器,被上两者引用) —
tests/OpenCSG.NET.ApiCompat netstandard2.0 编译期金丝雀:以 netstandard2.0 消费面引用核心库,验证公开 API 对旧平台可用 —

主套件的目录结构就是五层测试的物理形态:

层 目录 内容
① 单元测试 根目录 + Unit/ CubeTest/SphereTest/CylinderTest/ConeTest/UnionTest/… 形体、布尔、变换、求值的确定性用例
② 契约测试 Contracts/ 公开 API 的边界行为:throw 路径、退化输入、命名缺口
③ 黄金基线 Golden/ 几何等价比对(15.2)+ 三视图 JSON 基线(第 16 章)
④ 属性测试 Golden/、SystemTests/Property/ FsCheck 驱动的不变量验证(15.3)
⑤ 端到端 SystemTests/Integration/ 管线贯通、STL/JSON 闭环、真实模型(CPurlin)

另有两个专门目录:Defects/ 存放缺陷回归用例(15.6),Views//MeshIo//Tessellation/ 覆盖对应模块。运行方式:

dotnet test tests/OpenCSG.NET.Tests/OpenCSG.NET.Tests.csproj -c Release          # 主套件
dotnet test tests/OpenCSG.NET.SystemTests/OpenCSG.NET.SystemTests.csproj -c Release  # 系统套件
dotnet test tests/ -c Release -f net8.0      # 只跑 net8.0 一腿(双 TFM 会各跑一遍)

15.2 黄金基线测试(Golden Testing)

主套件里有一组黄金基线测试:把测试生成的 STL 与一份事先审核并”冻结”的基线文件比对。基线存放在 tests/OpenCSG.NET.Tests/Results/(60 个 .stl + Results/Views/ 下 15 个 .json),命名形如 ConeTest.Frustum.stl、ViewJsonGoldenTest.Cube_ThreeViews.json。

15.2.1 几何等价比较,不是逐字节比对

AssertAcceptedStl 的关键巧思是不做逐字节文本比较(那会因浮点格式化的微小差异误报),而是解析三角形后做几何等价判断(数值容差 STL_TOLERANCE = 1e-10)。它比较这些不变量:

比较项 含义
STL 头 名称/头部字符串一致
三角形数量 面片数相同
总表面积 所有三角形面积之和,容差内相等
包围盒 min/max 的 X/Y/Z 六界,容差内相等

用”面数 + 总面积 + 包围盒”这组几何指纹判等,既对无意义的浮点抖动免疫,又能敏锐捕捉真正的几何变化(多一个面、体积变了、位置偏了)。这是几何软件测试的实用范式,值得借鉴到你自己的项目。

15.2.2 基线升级工作流

算法有意改动(比如修复了某个缺陷、改了细分口径)时,基线需要显式升级,而不是悄悄通过:

  1. 测试运行后签名不一致,写出临时文件(*_.stl / *_.json 后缀)供 diff 检查;
  2. 人工查看 diff / 用查看器确认新结果正确;
  3. 用 tools/promote-goldens.sh 做升级前摘要提示并交互确认(--yes 跳过确认,适合脚本化),把临时文件提升为正式基线;
  4. 提交基线变更——diff 本身就是一次”几何行为变更评审”。

默认拒绝、显式批准是这个机制的灵魂:几何行为的变化永远会被看见。

15.2.3 跨平台分层

上一小节说”几何指纹”对浮点抖动免疫——但字节级基线做不到。Math.Sin/Cos/Atan2 在 Windows(UCRT)与 Linux(glibc)之间有 ≤1 ULP 的差异,经曲面细分、坐标旋转、圆弧拟合传播后,会把同一个几何放大成不同的数字文本,甚至让投影拓扑的计数发生翻转(登记为 DEFECT-011,裁定”不修但缓解”)。

CI 的处理是分层口径:

  • Windows:File.ReadAllBytes 逐字节比对基线(基线按 Windows 输出冻结);
  • 非 Windows(Linux):语义校验——JSON 的计数/可见性标志必须精确,数值容差 1e-6;STL 的 solid 名与三角形数必须精确,顶点容差 1e-4 绝对;
  • 两个已定性的平台拓扑敏感夹具走专门通道:跳过与 Windows 基线的数值比对,但输出带实测差量的 DEFECT-011 说明——响亮报告,绝不静默。

其余计数/拓扑不一致在两平台一律判红——若某平台转红且不在语义容差内,视为真发现,必须先调查取证并登记缺陷,不允许直接把平台从矩阵里删掉。

15.3 属性测试与 oracle

“举例测试”(给定输入断言输出)覆盖不了几何的组合爆炸。系统套件用 FsCheck 做属性测试——不写具体期望,而是断言”对任意输入都应成立的恒等式”,由框架生成大量随机用例:

层 主题 代表恒等式/oracle
Tier 1 布尔恒等式 A ∪ B = A + B − A ∩ B(面积恒等式,448 用例)
Tier 1 变换/序列化 变换等价、包围盒、JSON 往返、相等契约
Tier 2 解析解 与解析几何的独立对拍(sphere/cylinder 等)
Tier 3 结构不变量 绕序、流形/欧拉数、凸包恒等式
Tier 4 黄金语义 新模块语义基线(CsgHelpers、扇形柱、定向图元、细分容差)
Tier 5 遮挡 oracle 射线投射(Möller–Trumbore)+ 双侧判定的独立可见性 oracle

两个工程上的讲究值得学习:

  • 独立 oracle:验证消隐正确性时,不拿实现自己的中间结果当参照,而是另写一套独立算法(射线投射 + 探针)对拍——只有独立实现的”共识”才是有意义的正确性证据。
  • oracle 与被测实现共用输入、单独验证属性(如”遮挡关系必须满足反对称性”),比逐例比对更耐几何多样性。

测试基础设施中还有一组生成器(tests/OpenCSG.NET.TestKit/Generation/):节点树生成器(10 种节点 × 8 种截面,种子可复现)、线段集生成器、三档语料(64/512/环境变量驱动的更大规模,corpus.json)——用受控随机输入扩大覆盖面。

15.4 性能基准与回归门禁

15.4.1 BenchmarkDotNet 微基准

perf/OpenCSG.NET.PerfTest 用业界标准的 BenchmarkDotNet 做微基准测试,还挂了 [MemoryDiagnoser] 同时度量内存分配:

[MemoryDiagnoser]
public class PerfTest
{
    [Benchmark(Baseline = true)]
    public void Resolution10() => TestRes(10);

    [Benchmark]
    public void Resolution50() => TestRes(50);
}

基准负载是一个有代表性的重活:两个半径 1000 的球体做差集,分别在分辨率 10 和 50 下测量。运行:

dotnet run -c Release --project perf/OpenCSG.NET.PerfTest

15.4.2 墙钟预算测试与基线门禁

除微基准外,系统套件里还有一组标注 [Category("Perf")] 的墙钟预算用例(BoundaryProjectionTest、ComplexityBudgetTest、LargeInputBudgetTest 等),对 CsgEvaluator、ViewProjector.Project、ViewArcFitter.Fit、ViewSegmentCleanup.Clean 等关键操作设时间上限。两个工程口径:

  • CI 环境系数:CI 机器(共享、噪声大)会把墙钟预算放大约 10 倍——但比率守卫(相对基线/相对同类操作的比值)保持收紧,噪声不至于让”规模放大 × 复杂度变坏”这类真实信号漏网。
  • 基线 JSON:perf/baseline.json 冻结了本机基线(Union/Intersect/Evaluate 等操作的中位耗时与内存字节数);nightly 工作流对其实施 25% 回归门禁——超过即报警。

15.4.3 性能特征与优化建议

结合第 13 章的内核剖析与这些基准,可以总结出用好性能的实用经验:

  • 分辨率是最大的旋钮:球/圆柱面数随分辨率增长,布尔的比较成本与面数相关。够用就好——预览用低分辨率,出图再调高;长半径曲面用第 12 章的弦高容差自适应替代手工调参。
  • 善用包围盒短路:相距很远、不相交的实体做并集走 MayOverlap 快速通道,几乎零成本(第 13 章)。装配大量分离零件时很划算。
  • 靠近原点建模:远离原点的大坐标会削弱 1e-5 容差的有效性;必要时要先 Translate 到原点附近(第 13 章)。
  • 减少不必要的中间清理:Subtract/Intersect 的多参数重载只在最后一步做 Retesselate/Canonicalize。把多次减料写成一次 a.Subtract(b, c, d) 通常比链式调用更省。
  • 复用与缓存:变换用 Tag 缓存(第 7、13 章),重复子件建一次多次 Translate 比重新构造便宜。
  • 空集短路:把可能为空的工具实体(零尺寸孔)直接参与布尔是安全的,短路语义避免无谓的 BSP 往返(第 6 章)。

15.5 稳健性、并行与跨运行时

系统套件专门有一组 Robustness/ 用例,覆盖了工程落地时常被忽略的边角:

  • 并行执行:整套件并行、跨线程 scope、跨线程静态状态——Tessellation.ChordTolerance、ViewArcFitter.DiagnosticsEnabled 等都是 [ThreadStatic],正是为并行测试的隔离性设计的(第 12、16 章)。
  • soak(浸泡)测试:长时间重复运算检查无内存泄漏/状态漂移,nightly 工作流用 OPENCSG_FUZZ_CASES=20000 跑模糊用例。
  • 跨运行时一致性:tools/OpenCSG.NET.CrossRuntime 把同一份源码分别编到 net8.0 与 net10.0,对固定输入(语料求值、三视图、JSON 往返、STL 字节)产物做 SHA256 比对——同一操作系统内两个运行时的输出必须逐位一致。

这组测试回答的问题是:”库在单机演示里对,在生产的多线程/长跑/多运行时环境里还对吗?”

15.6 覆盖率门禁与缺陷登记制度

15.6.1 覆盖率门禁

tools/coverage-gate.py 是 CI 的覆盖率闸门(fail-closed:出错即红,绝不静默通过):

  • src/ 整体行覆盖率 ≥ 85%;
  • 五个重点分组各 ≥ 90%:View*(三视图子系统)、CsgHelpers、StlWriter、CsgMesh、Tessellation——正是第 12、16 章介绍的新增模块;
  • 统计口径:两个测试套件的 cobertura 报告取并集(任一套件命中即算覆盖),排除 [Category("Perf")] 的墙钟用例(插桩会改变计时)。

它与普通”覆盖率数字”的区别在于分组阈值:整体达标不够,五个新模块必须各自达标——防止”总量漂亮、新代码裸奔”。

15.6.2 缺陷登记制度

tests/DEFECTS.md 是一份双向索引的缺陷登记表:每个缺陷一个 DEFECT-nnn 编号,对应测试用例标注 [Explicit("DEFECT-nnn: ...")](使套件保持绿色、缺陷可一行复现),并由 DefectRegistryTest 强制校验”文档里有编号 ⇔ 代码里有标注”。

规则里有两条工程智慧:

  • 产品代码默认只报告不改——测试发现的缺陷先登记、先复现,只有经过批准的最小修复批次才动产品代码。避免”测试写着写着顺手把库改了”的失控。
  • 修复后迁移:删掉 [Explicit] 标注、从”未修复”区移到”已修复”区并注明修复方式——缺陷的完整生命史留在仓库里。

仓库的最终状态(截至 2026-10-14 的缺陷修复轮):15 条已登记缺陷中 14 条已修复(A1/A2/A4 与 001~010、012),唯一未修复的 DEFECT-011 是跨平台 libm 的字节级不可移植(15.2.3 的分层口径即其缓解方案)。这套制度解释了本书各章”历史提示”的出处——每一条都对应一次真实的修复。

15.7 CI 与发布流水线

15.7.1 CI:双平台矩阵

.github/workflows/ci.yml 在 ubuntu-latest 与 windows-latest 两个平台上:全解决方案构建(10 个工程)、双 TFM 测试(net8.0;net10.0)、覆盖率门禁。fail-fast: false——两个平台各自出结论,一个平台失败不取消另一个(避免”掩盖第二个平台的真实问题”)。SDK 装 10.0.x + 8.0.x 两个版本(与 global.json 的钉版和双 TFM 目标一致)。

.github/workflows/nightly.yml 承担”慢测试”:长模糊用例(OPENCSG_FUZZ_CASES=20000)与性能回归门禁(对 perf/baseline.json 的 25% 阈值)。

双平台为什么必要? 第 15.2.3 节的跨平台 ULP 差异、路径/换行机制、文件系统大小写敏感度——这些都只在真实双平台上跑一遍才暴露。本地全绿的代码在 Linux CI 上红是常态,别把 CI 红当成噪声。

15.7.2 发布:NuGet 可信发布(Trusted Publishing)

.github/workflows/nuget-publish.yml 在推送 v* 版本标签时自动发布到 NuGet.org。当前用的是 NuGet Trusted Publishing(OIDC 临时密钥)——弃用了长期 NUGET_API_KEY:

GitHub 颁发 OIDC 令牌 → NuGet/login 换取临时 API 密钥(有效期 1 小时)→ dotnet nuget push

流程要点:

  1. 检出 + 装 SDK(10.0.x + 8.0.x——因为测试前置门禁要跑双 TFM)。
  2. 先跑测试:dotnet test tests/OpenCSG.NET.Tests/——测试不过就不发布,质量门禁前置。
  3. 从标签解析版本号:v1.2.3 → VERSION=1.2.3。
  4. 打包:dotnet pack src/OpenCSG.NET/OpenCSG.NET.csproj -c Release -o out -p:Version=$VERSION。
  5. OIDC 换临时密钥(NuGet/login@v1,用仓库 Secret NUGET_USER 指向 nuget.org 用户名),紧挨着推送使用(密钥 1 小时有效、一次性)。
  6. 推送:dotnet nuget push out/*.nupkg --skip-duplicate。

所以为这个库(或你 fork 的版本)发新版,只需:确保测试通过 → 打一个 vX.Y.Z 标签并推送 → CI 自动测试、打包、上传。相比”长期 API 密钥”的老路子,OIDC 换取的密钥不落地、不共享、自动过期——这是 .NET 生态当前的推荐做法,值得照搬到你自己的发布流水线。

15.8 二次开发:三个典型扩展点

15.8.1 添加自定义截面

假设你需要一个内置八种之外的特殊截面(比如带孔的多腔型材轮廓)。回顾第 10 章的拉伸管线,添加一个 Profile2D 需要三步:

  1. 定义记录(在 Profiles.cs):
    public record ZBeamProfile(double Height, double FlangeWidth, double Thickness) : Profile2D;
    

    并在 Profiles 工厂加一个便捷方法。

  2. 在 CsgEvaluator.ExpandProfile 的 switch 里加一个分支,把参数展开成一圈顶点。绕序不必手工保证——自 PolygonProfile 起,展开路径会按有向面积自动规整为 CCW(第 10 章);但简单不自交仍是硬性要求(耳切三角化的前提)。
  3. 带孔截面不要试图塞进一个 Profile2D——像 SquareTubeProfile 那样只给外轮廓,用 SubtractNode 在节点层挖空。

更轻的替代方案:如果截面已经是现成的点列表,直接用 new PolygonProfile(pts)(第 10 章),无需改库。

15.8.2 扩展序列化

如果你新增了节点或截面类型,并希望它们能被 JSON 持久化,别忘了同步第 11 章的判别式。在 CsgSerialization.cs 对应的转换器里,Read 和 Write 两处 switch 都要加上你的新 $type:

// Profile2DConverter.Read 的 switch
"ZBeam" => JsonSerializer.Deserialize<ZBeamProfile>(json, options)!,
// Profile2DConverter.Write 的 switch
ZBeamProfile => "ZBeam",

漏掉任意一处,序列化或反序列化就会因”未知 $type“而抛 JsonException。两处对称添加,才能保证第 11 章说的往返保真与幂等。顺带记得把新类型纳入 DefectRegistryTest 之外的往返测试(CsgSerializationTest 的模式照抄即可)。

15.8.3 扩展辅助函数与投影后处理

库的工程辅助层(CsgHelpers,第 12 章)与投影后处理层(View*,第 16 章)是扁平结构——新增一个”回转体”辅助函数、一个新的投影后处理步骤,通常只需:

  1. 在对应静态类加一个方法(纯函数、无状态是硬约束——注意 [ThreadStatic] 的既有先例);
  2. 补一组单元测试(含退化输入);
  3. 如果属于 coverage-gate.py 的五个分组之一,新代码要被 ≥90% 覆盖率门禁追着走——先写测试再写实现是这里的现实选择。

15.9 本章小结与教程结语

本章要点:

  • 测试体系是四工程 + 五层:单元/契约/黄金基线/属性/端到端;黄金基线用”面数 + 总面积 + 包围盒”的几何指纹判等,基线升级必须显式批准。
  • 属性测试用 FsCheck + 独立 oracle(如射线投射)验证恒等式与不变量;Tier 1~5 覆盖布尔恒等式到遮挡 oracle。
  • 性能:BenchmarkDotNet + [MemoryDiagnoser] 微基准、[Category("Perf")] 墙钟预算、perf/baseline.json 25% 回归门禁;优化要点是分辨率、包围盒短路、原点居中、减少中间清理。
  • 质量制度:覆盖率门禁(src ≥85%、五分组 ≥90%,fail-closed)、缺陷登记双向索引(DEFECTS.md ⇔ [Explicit])、产品代码默认只报告不改。
  • CI/发布:双平台矩阵 + nightly 长测;发布走 NuGet Trusted Publishing(OIDC 临时密钥),v* 标签触发、测试前置。
  • 二次开发三方向:添加自定义截面(或直接用 PolygonProfile)、扩展序列化(Read/Write 两处 $type 对称)、扩展辅助/后处理层(无状态 + 测试先行)。

至此,这套 OpenCSG.NET 使用与开发教程 主线十五章讲完。我们从 CSG 与 BSP 的第一性原理出发(第 1–3 章),掌握了数学基础与命令式建模(第 4–8 章),进阶到声明式节点树、参数化型材与序列化(第 9–11 章),补全了网格导出与工程辅助(第 12 章),深入内核算法(第 13 章),完成了冷弯 C 型钢实战(第 14 章),最后落到测试、质量与发布(本章)。第 16 章另辟专题,把库最新的三视图投影与隐藏线消除子系统完整讲透。

希望这套教程既能让你快速上手用几行代码造出三维实体,也能让你看透原理、在需要时改进甚至扩展这个库。愿 OpenCSG.NET 成为你参数化建模、BIM 二次开发、增材制造工具链中一件称手的利器。


← 上一章 目录 下一章 →