资讯动态

CAD Sketcher 开发测试指南:在 Blender 内驱动有状态操作符的完整测试体系

发布时间:2026/10/14 14:45:48 来源:尧图企业网站定制
桌面应用3D建模【免费下载链接】CAD_SketcherConstraint-based geometry sketcher for blender项目地址https://gitcode.com/gh_mirrors/ca/CAD_Sketcher点击查看免费下载CAD Sketcher 的官方开发文档docs/content/development.md系统性地介绍了这套参数化草图插件的测试方法论测试套件运行在Blender 进程内部依赖bpy与 Solvespace 求解器覆盖从 CI 全量回归到单模块快速迭代的各种运行方式并围绕有状态操作符stateful operator给出了一套不依赖视口的无头驱动方案OpHarness。读完本文你将掌握 CAD Sketcher 测试环境的搭建与运行命令、BgsTestCase/Sketch2dTestCase两类测试基类的用法、如何用OpHarness模拟用户点击驱动交互式工具、以及make_operator_double镜像机制的原理与边界从而能够为新增实体、约束或绘制工具编写可复现的单元测试。测试体系概览为什么测试必须跑在 Blender 内CAD Sketcher 的测试套件位于仓库根目录的 testing/ 目录下全部是遵循unittest规范的模块文件名以test_*.py命名。文档明确强调这些测试必须在 Blender 内部运行因为测试需要访问bpyBlender Python API来创建场景、激活草图测试需要调用求解器Solvespace 的slvs绑定仓库在 wheels/ 目录中为各平台内置了slvs-3.2的预编译 wheel来验证约束求解结果。因此测试文件内部大量使用相对导入例如from ..curve_solver import solve_system、from ..model.curve_ref import PointRef这也决定了测试模块必须以扩展包bl_ext.namespace.CAD_Sketcher子模块的方式被加载。运行完整测试套件CI 方式文档给出了 CI 运行测试的三条命令序列与仓库根目录的 .github 构建脚本 流程一一对应blender --background --command extension build --output-filepath ./CAD_Sketcher.zip blender --background --command extension install-file --repo user_default --enable ./CAD_Sketcher.zip blender --background --python ./scripts/ci_run_tests.py三步的含义分别是构建扩展extension build将当前目录打包为CAD_Sketcher.zip。需要注意的是blender_manifest.toml 的[build]段中paths_exclude_pattern明确排除了/testing/、/scripts/、/docs/等开发目录——发布构建是不带测试代码的安装并启用扩展extension install-file --repo user_default --enable把构建产物安装进 Blender 的扩展仓库user_default并启用扩展的顶层包名即bl_ext.user_default.CAD_Sketcher执行测试脚本scripts/ci_run_tests.py 会先调用addon_utils.modules()扫描已加载模块找到名字以.CAD_Sketcher结尾的那个即刚安装的扩展然后addon_utils.enable(pkg, default_setTrue)完成注册并激活内置 wheel。ci_run_tests.py的完整工作流对应文档的描述见 scripts/ci_run_tests.py由于发布构建剥离了测试目录脚本会检查已安装扩展的testing/目录是否存在若不存在则从源码检出环境变量CAD_SKETCHER_SRC指定的目录默认当前工作目录把整个testing/拷贝进安装目录并调用importlib.invalidate_caches()使新模块可被导入随后用glob扫描该目录下所有test_*.py按排序拼接成bl_ext.user_default.CAD_Sketcher.testing.module的完整模块名通过unittest.TestLoader().loadTestsFromNames(...)加载全部模块并用TextTestRunner(verbosity2)运行最后sys.exit(0 if result.wasSuccessful() else 1)——任何一条失败都会导致非零退出码这正是文档强调的exits non-zero on any failure也是 CI 门禁的判据。CAD_SKETCHER_SRC 环境变量如果你不在仓库根目录运行ci_run_tests.py需要显式指定源码检出位置CAD_SKETCHER_SRC/path/to/CAD_Sketcher blender --background --python ./scripts/ci_run_tests.py脚本在找不到testing/源目录时会抛出SystemExit(testing/ not found in checkout: ...)见 scripts/ci_run_tests.py。从源码检出运行单个模块快速本地迭代当插件以源码方式启用而非安装构建包时可以只运行某一个测试模块获得更快的反馈循环。文档给出的命令blender --background --addons CAD_Sketcher --python-expr import unittest from bl_ext.namespace.CAD_Sketcher.testing import test_operators suite unittest.TestLoader().loadTestsFromModule(test_operators) unittest.TextTestRunner(verbosity2).run(suite) --addons CAD_Sketcher让 Blender 从源目录加载插件源码检出的目录名必须与扩展 idCAD_Sketcher一致在--python-expr中先导入目标测试模块再构造TestSuite运行verbosity2输出每个用例的详细结果。仓库同时提供了现成的脚本化入口run_tests.sh 与 run_tests.bat 内容为blender --addons CAD_Sketcher --python ./testing/__init__.py -- --log_levelINFO即通过 testing/init.py 的run()函数用unittest.TestLoader().discover()自动发现并运行整个testing/下的全部用例脚本接受--log_levelINFO等参数透传给测试框架非交互模式下运行结束后会自动bpy.ops.wm.quit_blender()退出见 testing/init.py。命名空间问题bl_ext.namespace.CAD_Sketcher文档特别强调namespace取决于插件的安装方式普通安装为user_default而 dev/linked 检出则不同。因此永远不要在插件代码中硬编码bl_ext.namespace.CAD_Sketcher路径——一律使用相对导入。该约束有专门的守卫测试保障。在 testing/test_prefill.py 中TestNoHardcodedExtensionNamespace用正则\bbl_ext\.\w\.CAD_Sketcher\b扫描所有随插件发布排除testing、scripts、experiments、docs等构建期剔除目录的.py模块一旦发现硬编码命名空间的 import 即判定失败。守卫测试的 docstring 记录了真实事故选择预填充selection prefill曾因_matches_types里硬编码了开发环境的命名空间在用户安装环境下抛出ModuleNotFoundError。配套的test_the_scan_reaches_the_runtime_packages则断言扫描范围确实覆盖了operators、model、utilities、stateful_operator等核心运行时包见 testing/test_prefill.py防止扫描范围太窄导致漏检。测试基类BgsTestCase与Sketch2dTestCase文档指出两个基类均定义在 testing/utils.py 中BgsTestCase继承unittest.TestCase在setUpClass中为每个测试类创建一个全新场景bpy.data.scenes.new(cls.__name__)并切换为当前场景并对外暴露context、entities、constraints以及solve()辅助方法solve()内部调用from ..curve_solver import solve_system并断言求解成功见 testing/utils.py。tearDownClass会删除测试场景交互模式下保留场景以便人工检查由RUN_TESTS_INTERACTIVE环境变量或类属性interactive控制Sketch2dTestCase继承BgsTestCase在setUp中额外创建并激活一个 2D 草图——流程是先ensure_origin_elements建立 XY 原点平面entities.add_sketch(wp)创建草图实体再用 utilities/curve_data.py 的create_sketch_curve_object生成曲线对象最后用 model/sketch_ref.py 的Sketch访问器包装并set_active_sketch激活见 testing/utils.py。它还提供四个几何辅助方法每个都返回对应的CurveRef辅助方法返回类型底层实现add_point(co, **kwargs)PointRefPointRef.create(self.sketch, co, ...)add_line(p1, p2, **kwargs)LineRefLineRef.create(self.sketch, p1, p2, ...)add_arc(ct, start, end, **kwargs)ArcRefArcRef.create(self.sketch, ct, start, end, ...)add_circle(ct, radius, **kwargs)CircleRefCircleRef.create(self.sketch, ct, radius, ...)文档给出的用例模板展示了最典型的写法class TestMyThing(Sketch2dTestCase): def test_line(self): p1 self.add_point((0, 0)) p2 self.add_point((1, 0)) line self.add_line(p1, p2) self.assertTrue(line.valid)CurveRefPointRef、LineRef、ArcRef、CircleRef的基类定义在 model/curve_ref.py是 CAD Sketcher 中曲线引用的核心抽象其valid属性用于判断引用指向的实体是否仍然有效。测试有状态操作符为什么不能直接实例化CAD Sketcher 的交互工具都是有状态操作符其用户侧行为参见 docs/content/interaction_system.md 的 Stateful Operations 一节。文档指出这类操作符有两个硬性障碍导致无法在测试中直接op SomeOperator()元类阻塞构造注册后的操作符类形如class View3D_OT_...(bpy.types.Operator, Operator2d)而bpy.types.Operator的元类会拦截__new__使得类无法在模态 invoke 之外被实例化行为依赖模态事件循环真实操作符通过modal()处理鼠标事件驱动状态推进无头测试环境没有视口区域无法提供事件流。OpHarness不打开视口也能像用户一样驱动操作符testing/utils.py 的OpHarness正是为解决上述问题而生。它通过make_operator_double为真实操作符类构造一个非 bpy 的可实例化孪生twin把bpy.types.Operator从基类元组中剔除其余基类原样保留将真实类的__dict__包括states、main、init、各覆盖方法逐字复制到新类不复制任何逻辑——孪生运行的就是真实代码只是不再是一个 Blender 注册操作符见 testing/utils.py。OpHarness构造时__init__会为孪生实例准备好运行所需的内部状态_state_data {}、state_index 0、_active_sketch sketch、_undo False、continuous_draw False避免提交最后一个状态后链入新线段、initialized False约束类操作符依赖它门控sync_settings。文档给出的生命周期镜像表是理解OpHarness的关键方法镜像的真实行为prefill()invoke 时的选择预填充Select → Invoke 范式pick(ref)为当前状态点击一个已存在的实体place_point(co)点击空白处放置一个新点set_value(v)输入属性值如圆的半径finish()提交redo_statesmainfini具体实现中prefill()调用真实方法self.op.prefill_state_props(self.context)pick(ref)把实体的类型写入当前状态数据data[type] type(ref)、data[is_existing_entity] True通过set_state_pointer([ref.curve_id], indexi, implicitTrue)绑定指针再next_state推进到下一状态set_value(value)遍历当前状态的get_property()列出的属性名并赋值标记为非既有实体后推进状态place_point(co)本质是set_value(Vector(co))——用一个向量作为新建点的回退值对应真实流程中未拾取实体时按鼠标位置延迟创建点真正的点实体要等到finish()里redo_states才创建与真实交互的 deferred-creation 流程一致finish(run_finiTrue)依次执行redo_states重建延迟创建的元素→main真正构建操作结果→_run_fini返回main的结果。文档给出的完整示例可在 testing/test_operators.py 找到对应实现from .utils import Sketch2dTestCase, OpHarness class TestAddLine(Sketch2dTestCase): def test_two_clicks(self): from ..operators.add_line_2d import View3D_OT_slvs_add_line2d h OpHarness(View3D_OT_slvs_add_line2d, self.sketch, self.context) h.place_point((0, 0)).place_point((3, 0)) line h.finish() # real main() runs, auto-constraints applied self.assertEqual(h.op.target.p1.co.x, 0)注意place_point返回self支持链式调用h.op.target是操作符完成后的产物引用。test_operators.py中的test_line_from_two_placements断言了LineRef的端点坐标而test_axis_aligned_line_gets_horizontal_constraint则验证了自动约束绘制一条水平线段后草图中应恰好存在一个引用该线的SlvsHorizontal约束见 testing/test_operators.py。选择驱动的路径填充global_data.selected对于Select → Invoke范式先选实体再调用工具参见 docs/content/interaction_system.md 的 Selection vs Action 一节OpHarness的测试流程是先往global_data.selected中写入被选实体的curve_id十六进制字符串再调用prefill()from .. import global_data global_data.selected.clear() global_data.selected.append(pt.curve_id) h.prefill() # pt becomes the first pointer state这正是真实invoke对wait_for_input工具所走的路径prefill_state_props→gather_selection→parse_selection→_matches_types类型过滤→next_state见 testing/test_prefill.py 的模块 docstring。test_prefill.py提供了完整的端到端佐证test_selected_point_prefills_first_state对add_line2d、add_circle2d、add_arc2d、add_rectangle四个工具选中一个点后prefill()断言第一个指针状态被填充且get_point(0).curve_id等于所选点testing/test_prefill.pytest_selected_segment_prefills_first_state对offset、trim、bevel工具选中的线段填充第一个段指针状态test_prefilled_point_is_reused_as_line_start选中起点、prefill()后place_point放置终点finish()得到的线的p1正是所选点、p2是新创建的点testing/test_prefill.pytest_two_selected_points_complete_line_immediately同时选中两个点再prefill()两个指针状态全部填满h.op.check_props()为真——无需任何点击即可finish()这正是交互系统Immediate Execution立即执行的测试写照test_selected_line_does_not_prefill_point_state类型过滤验证——选中线段后调用prefill()圆的中心指针状态不会被填充因为_matches_types拒绝了错误类型的实体testing/test_prefill.py。约束操作符的测试范式与此相同见 testing/test_constraint_ops.py 的 docstring约束工具同样是有状态操作符但由约束的signature动态构建见 operators/base_constraint.py 的GenericConstraintOp每个实体槽位对应一个指针状态且use_createFalse实体只能拾取、不能创建。test_parallel_from_two_selected_lines选中两条线后prefill()断言entity1/entity2被填充、finish()生成SlvsParallel约束且其curve_id_placements()恰好是两条线的曲线 idtesting/test_constraint_ops.py。孪生的注意事项Caveats of the twinmake_operator_double通过反射复制真实类因此有两处 Blender 注册特性无法原样保留需要测试与 harness 自行处理文档明确列出实现见 testing/utils.pybpy 属性注解property annotations在孪生上是惰性的例如continuous_draw: BoolProperty(...)这类注解在普通类上不会生成属性行为。需要取值的地方要直接设置普通属性——OpHarness.__init__已代为设置常见项continuous_draw、initialized等零参super()的隐藏__class__闭包单元格被逐字复制的方法体内若出现super().main()如约束类的main调用基类的main其函数闭包中隐藏的__class__单元格仍指向原类直接实例化孪生会解析失败。make_operator_double的做法是对每个含__class__自由变量的方法用types.FunctionType在函数的副本上把该单元格重新绑定为孪生类原注册操作符完全不受影响从而让super()正确解析到孪生的基类见 testing/utils.py。交互式测试与 GUI 冒烟测试文档未展开、但仓库实际提供的两条补充路径值得说明交互式运行run_tests_interactive.sh 与 run_tests_interactive.bat 使用-- --interactive --log_levelINFO参数此时BgsTestCase.interactive TruetearDownClass保留场景不删除便于在打开的 Blender 窗口里人工检查测试生成的几何交互开关同样可由环境变量RUN_TESTS_INTERACTIVE控制见 testing/init.pyGUI 冒烟测试testing/gui_smoke.py 覆盖那些在--background下根本不执行、但在真实窗口下可能崩溃的路径draw handler、gizmo、view-layer 操作例如拾取 id 键控映射缺项导致绘制中崩溃、hide_set拒绝尚未进入视图层的对象等真实历史事故。它需要无头合成器环境mutter --headlessWAYLAND_DISPLAY以SMOKE OK/SMOKE FAILED作为结果并捕获 stderr 中的任何 traceback见 testing/gui_smoke.py。源码视角状态机如何支撑测试理解OpHarness为什么能以假乱真需要看它背后驱动的纯状态机 stateful_operator/state_machine.py_StateMachineMixin是一个不依赖 Blender 模态系统的纯状态机只要求子类提供states列表OperatorState的列表或可调用对象以及get_state_pointer/set_state_pointer由集成层提供。这意味着状态定义、状态数据存储_state_data按索引setdefault、属性/回调解析get_property、get_func的优先级状态字段可调用 状态字段字符串查找方法 操作符方法兜底都可以在无视口环境下纯 Python 运行stateful_operator/state_machine.pycheck_props()stateful_operator/state_machine.py遍历所有非optional状态指针状态检查指针是否已设置、属性状态检查properties.is_property_set——这就是OpHarness.finish()前可以判定所有状态是否就绪的依据也是立即执行测试断言h.op.check_props()的底层实现集成层 stateful_operator/integration.py 提供指针读写能力PointerKind一族处理器负责把拾取的实体身份写入/读回_state_data原生类型如ObjectPointer、MeshElementPointer分别按对象名、(object_name, mesh_index)存储见 stateful_operator/integration.py。OpHarness.pick()里的set_state_pointer([ref.curve_id], indexi, implicitTrue)正是经由这套处理器完成的绘制操作符基类 operators/base_stateful.py 的GenericEntityOp额外贡献了use_auto_constraints/add_auto_constraint推断的约束只有在求解结果为OKAY即既一致又确实消掉一个自由度时才保留REDUNDANT_OK或其它状态则回滚见 operators/base_stateful.py以及check_event中 Shift 旁路逻辑global_data.snap_bypass与skip_auto_constraints——test_prefill.py中的test_external_snap_creates_fixed_point、test_both_ends_snapped_skips_inferred_alignment正是利用placement_of(h.op.get_state_data(0)).snapped True模拟外部吸附后验证自动约束被正确抑制的用例testing/test_prefill.py。编写新测试的推荐工作流综合文档与仓库实践为 CAD Sketcher 新增功能编写测试时建议遵循以下流程判定测试类型普通实体/约束逻辑用Sketch2dTestCase有 2D 草图与几何辅助方法纯数据/序列化逻辑可直接用BgsTestCase需要驱动交互工具的用OpHarness创建测试模块在 testing/ 下新建test_你的模块.py类继承Sketch2dTestCase方法以test_开头内部使用相对导入引用被测操作符如from ..operators.add_line_2d import View3D_OT_slvs_add_line2d本地快速迭代blender --addons CAD_Sketcher --python-expr ...只跑当前模块批量跑用./run_tests.sh需保留场景人工检查时用./run_tests_interactive.sh提交前过 CI 三连extension build→extension install-file→ci_run_tests.py确保在安装后的干净命名空间下全部通过同时保证新增代码不含bl_ext.namespace.CAD_Sketcher硬编码导入由 testing/test_prefill.py 的守卫测试把关涉及绘制/拾取/Gizmo 的改动补充 testing/gui_smoke.py 步骤覆盖后台模式无法触达的路径。这套体系的设计核心在于不复制操作符逻辑而是复制操作符类本身——make_operator_double让测试永远运行真实的states、main、init代码OpHarness则把真实的生命周期预填充、拾取、放置、提交翻译成无头环境下的方法调用从而在保证测试保真度的同时把在 Blender 里用鼠标画草图这一复杂交互压缩为可断言的、可链式调用的几行测试代码。赞分享桌面应用3D建模【免费下载链接】CAD_SketcherConstraint-based geometry sketcher for blender项目地址https://gitcode.com/gh_mirrors/ca/CAD_Sketcher点击查看免费下载相关推荐CAD Sketcher在Blender 4.0.2中的Undo操作异常问题分析CAD Sketcher在Blender 4.0.2中的Undo操作异常问题分析 问题概述 CAD Sketcher作为Blender的约束式几何草图插件在B桌面应用3D建模RxJS 自定义 Observable 操作符实战从 Rx.Observable.create 到组合已有操作符与测试驱动开发RxJS 自定义 Observable 操作符实战从 Rx.Observable.create 到组合已有操作符与测试驱动开发 本文是 RxJS v4Rea后端WinJS测试驱动开发QUnit单元测试完整指南想要构建高质量的JavaScript应用程序 WinJS测试驱动开发 是你的最佳选择作为微软开发的现代浏览器UI工具包WinJS提供了完整的测试框架支持UI库/组件前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑