
CLI-Anything QGIS 测试体系PyQGIS 与 qgis_process 双层 pytest 验证方案【免费下载链接】CLI-AnythingCLI-Anything: Making ALL Software Agent-Native -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything本文以 CLI-Anything 仓库中 QGIS harness 的测试计划文档为核心完整解析其两级测试架构test_core.py直接驱动 PyQGIS 辅助模块做单元与模块级验证test_full_e2e.py则通过真实 QGIS 运行时与已安装的cli-anything-qgis可执行文件完成端到端工作流与子进程覆盖。读完后你将掌握如何在无显示环境下为 GIS 桌面软件编写可复现的 CLI 测试理解 PDF/PNG 魔数验证、native:buffer算法透传验证、_resolve_cli入口解析策略以及 QGIS 后端错误归一化的底层实现并可直接复制其中的执行命令在本机运行验证。一、测试范围与套件拆分QGIS harness 位于QGIS/agent-harness/cli_anything/qgis/其测试计划TEST.md将验证体系拆为两个文件test_core.py直接模块级测试针对 PyQGIS 辅助函数与后端封装backend wrapperstest_full_e2e.py真实工作流覆盖 已安装命令的子进程subprocess覆盖。对应实现文件为 test_core.py13 个测试与 test_full_e2e.py3 个真实工作流 6 个子进程测试共 22 个测试用例。测试的前提是环境中同时具备 QGIS 运行时PyQGIS Python 绑定与qgis_process命令行两个测试文件都在导入前设置QT_QPA_PLATFORMoffscreen保证无显示器环境CI、服务器下 QGIS 的 Qt 后端可以离屏运行。隔离机制fixture 与 sys.path 清理两层测试都通过 autouse fixture 保证用例间的状态隔离这是测试可靠性的关键设计test_core.py 中的clean_qgis_project每个用例前后调用backend.ensure_qgis_app()初始化单例QgsApplication并对QgsProject.instance()执行project.clear()与project.setFileName()确保每个用例从干净项目开始。test_full_e2e.py 中的clean_qgis_state更进一步用monkeypatch把HOME重定向到tmp_path避免污染真实用户配置强制QT_QPA_PLATFORMoffscreen并重置 CLI 层的三个全局状态qgis_cli._session、qgis_cli._json_output、qgis_cli._repl_mode保证子进程与 in-process 两条路径互不串扰。另外两个测试文件在导入被测模块前都会从sys.path移除cli_anything的命名空间根目录parents[2]配合 qgis_backend.py 中_import_qgs_application()的“影子模块”清理逻辑在导入qgis.core前临时弹出命名空间根路径避免本仓库包与系统安装的qgis模块相互遮蔽。仓库级配置 pytest.ini 中的addopts --import-modeimportlib也是为此类命名空间包导入服务的。二、第一层test_core.py 的单元与模块覆盖测试计划将直接模块测试划分为六组下面逐组说明计划条目、对应测试用例与源码实现印证。2.1 后端封装Backend helpers测试计划要求验证三个后端函数实现均位于 qgis_backend.pyfind_qgis_process()返回可用可执行文件路径或抛出清晰错误。test_find_qgis_process_missing_raises_clear_error 用mock.patch将shutil.which打桩为返回None断言抛出QgisBackendError且错误信息匹配qgis_process is not installed。源码侧L58-L66确认该函数仅在shutil.which(qgis_process)命中时返回路径否则携带安装提示如apt install qgis抛出异常——这正是 Agent 场景下“快速失败、错误可定位”的设计。project_path_argument()归一化项目路径。test_project_path_argument_normalizes 断言输入路径会被解析为绝对路径并包装成--PROJECT_PATH/abs/path/demo.qgz形式的qgis_process参数且None输入返回None。源码实现L133-L137中路径经过expanduser().resolve()双重归一化。run_process_json()归一化 JSON 与失败场景。test_run_process_json_normalizes_backend_failure 构造了一个returncode1、stdout 为{log: [{message: buffer failed}]}的subprocess.CompletedProcess假对象验证抛出QgisProcessError且消息提取到 payload 中的buffer failed异常对象保留returncode 1与完整payload便于上层程序做结构化诊断。对应源码 run_process_json 的完整失败处理链为非零退出码时优先从 payload 的log列表提取最后三条消息_extract_payload_message同时兼容results.error其次回退 stderr/stdout最后才生成“exited with status N”兜底消息退出码为零但 stdout 不是合法 JSON 时则抛出“returned non-JSON output in --json mode”异常。这套两级错误归一化是 harness 能被 Agent 可靠调用的核心保障。2.2 项目辅助Project helpers计划条目包括新建项目、打开项目、保存项目到指定路径、修改项目 CRS、推导默认数据存储路径、汇总项目元数据。创建/保存/打开/汇总test_project_create_save_open_and_info 完整走了一遍生命周期create_project返回path/title/crs/layer_count/layout_count/datastore_path断言默认 CRS 为EPSG:4326、默认数据仓库为{stem}_data.gpkgset_project_crs(EPSG:3857)后save_project到原路径与重命名路径再open_project重开并验证 CRS 与 title 均被持久化。源码印证见 project.pycreate_projectL76-L98会先project.clear()清空单例项目、校验 CRS 合法性非法 CRS 抛Invalid CRS并立即project.write()save_project支持改路径保存project_info汇总 path、title、crs、modified、layer/layout 计数与名称、datastore_path。默认数据仓库路径test_default_datastore_path 断言city.qgz对应city_data.gpkg与 default_datastore_path 中path.with_name(f{path.stem}_data.gpkg)的推导逻辑一致。注意 normalize_project_path 还会在无扩展名输入时自动补.qgz。2.3 图层辅助Layer helpers解析字段规格test_parse_field_and_param_specs 验证parse_field_specs([name:string, score:int, active:bool])映射为 QGIS 类型string/integer/bool并断言重名字段name:stringname:int与非法参数规格NOT_A_PARAM都会抛QgisBackendError。源码中 FIELD_TYPES 支持int/integer/double/float/string/str/bool/boolean等别名到QMetaType的映射parse_field_specs 用partition(:)解析并维护seen_names去重集合。创建 GeoPackage 支撑的矢量图层test_layer_create_list_info_and_remove 断言provider ogr、type vector、字段名存在layer_info的 source 以layers_data.gpkg|layernameplaces结尾即数据落在默认 GeoPackage 中删除后list_layers()[count] 0。源码 create_vector_layer 的实现路径为创建内存层 →addAttributes写字段 → 经QgsVectorFileWriter.writeAsVectorFormatV3以GPKG驱动落到default_datastore_path()→ 以ogrprovider 重新加载并addMapLayer保证图层是“可持久化的 GeoPackage 图层”而非一次性内存层。2.4 要素辅助Feature helpersWKT 类型化属性写入test_feature_add_and_list 向含name/count/rating/active四类型字段的点图层写入POINT(1 2)断言属性被正确类型化count为 int 7、rating为 float 2.5、active为 bool True并用limit1验证list_features的截断能力与geometry_wkt输出。源码 add_feature 中_coerce_valueL16-L33按字段元类型做 int/float/bool 转换写入后调用layer.updateExtents()保证图层范围及时更新——这一点直接服务于后文的布局自动扩展。非法布尔值拒绝test_feature_add_rejects_invalid_boolean 断言activemaybe抛QgisBackendError。源码中合法布尔字面量仅接受true/1/yes/on与false/0/no/off两组。非法属性规格拒绝测试计划中的“validate bad attr specifications”对应add_feature中partition()失败抛Invalid attribute specification、未知字段名抛Unknown field的两道校验features.py L64-L75。2.5 布局辅助Layout helpers计划覆盖按页面尺寸/方向创建布局、列出与查看布局、添加地图项、添加标签项、删除布局。test_layout_create_add_items_and_remove 在含多边形的演示项目上创建A4 portrait布局add_map_item(Main, 10, 20, 180, 120)后断言 items 中存在QgsLayoutItemMapadd_label_item后断言存在QgsLayoutItemLabel删除后计数归零。点要素项目自动扩展回归test_layout_add_map_accepts_point_only_project 专门覆盖一个回归场景——项目中只有零面积点要素时add_map_item也必须成功。其修复逻辑见 layouts.py 的_combined_project_extent()合并所有矢量/栅格图层范围后若extent.width() 0 and extent.height() 0则执行extent.grow(1.0)撑开一个最小范围否则点项目会因“空范围”无法渲染地图项。参数取值边界由 create_layout 强制约束页面尺寸仅接受PAGE_SIZES {A4, A3, A2, A1, A0, LETTER}大小写不敏感方向仅portrait/landscape重名布局直接报错。坐标/尺寸单位为毫米QgsUnitTypes.LayoutMillimeters。2.6 会话辅助Session helperstest_session_save_load_and_status 验证Session构造时从 JSON 文件加载状态set_project_path与record触发自动落盘重新加载后current_project_path、history_count、history(limit1)均正确status(modifiedTrue)返回current_project_path/project_name/modified/history_count四字段结构。源码 session.py 中_locked_save_jsonL12-L39使用fcntl.flock排他锁 truncate原子写 JSON在fcntl不可用的平台上优雅降级HistoryEntry以 UTC 时间戳记录每次命令、参数与结果status()L114-L120则输出给 Agent 消费的轻量状态快照。2.7 导出预设Export presetstest_export_presets_describe_supported_formats 断言预设表为pdf → native:printlayouttopdf、image → native:printlayouttoimage与 export.py 中export_presets()完全一致。这两个 QGIS 原生算法 ID 也是后文 e2e 工作流的执行通道。三、第二层真实端到端工作流TestRealCLIWorkflowstest_full_e2e.py不 mock 任何 PyQGIS 能力而是通过CliRunner直接调用 qgis_cli.py 的 Click 命令树--json模式对真实 QGIS 运行时发起请求。测试计划定义的三条工作流如下。3.1 Workflow 1scratch 项目到 PDF新建项目 → 2. 创建可写矢量图层 → 3. 添加要素 → 4. 创建布局 → 5. 添加地图项与标签 → 6. 导出 PDF → 7. 验证文件存在、非零大小、且以%PDF-开头。_build_cli_projectL103-L186用一条命令链搭出演示项目project new -o ... --title ...、layer create-vector --name areas --geometry polygon --field name:string --field score:int、feature add --layer areas --wkt POLYGON((0 0,0 5,5 5,5 0,0 0)) --attr nameZoneA --attr score5、layout create --name Main、layout add-map --x 10 --y 20 --width 180 --height 120、layout add-label --text Demo map --x 10 --y 8 --width 100 --height 10。导出命令为export pdf path --layout Main --overwrite对应 export_layout_pdf先get_layout校验布局存在、save_if_dirty确保项目落盘再组装参数LAYOUT/OUTPUT/FORCE_VECTOR/FORCE_RASTER/GEOREFERENCEGEOREFERENCE默认trueDPI可选交给qgis_process。PDF 的三重断言存在、st_size 0、read_bytes()[:5] b%PDF-在测试中体现为 test_scratch_project_to_pdf。3.2 Workflow 2scratch 项目到 PNG流程同 Workflow 1最终执行export image path --layout Main --overwrite对应 export_layout_image算法native:printlayouttoimage验证更严格test_scratch_project_to_png 除了检查文件存在外还比对 8 字节 PNG 魔数PNG_SIGNATURE b\x89PNG\r\n\x1a\n再用QImage解码图片断言width() 0且height() 0——即验证的不只是“文件是 PNG”而是“是一张可解码、有正尺寸位图的 PNG”。3.3 Workflow 3processing 透传native:buffer该工作流验证 harness 对qgis_process算法的透传能力创建矢量图层并添加要素后test_processing_passthrough_buffer 通过process run native:buffer传入完整参数组INPUTlayer_source DISTANCE1 SEGMENTS8 END_CAP_STYLE0 JOIN_STYLE0 MITER_LIMIT2 DISSOLVEfalse OUTPUTtmp/buffer.geojson其中INPUT取自layer create-vector返回的layer[source]GeoPackage 连接串实现了对“前一步命令输出作为后一步命令输入”的链式验证。结果断言分两层results.OUTPUT指向的文件必须存在随后以QgsVectorLayer(str(result_path), buffer, ogr)重新打开输出数据集断言layer.isValid()且featureCount() 0证明缓冲区结果确实是合法矢量数据而非空壳文件。参数解析侧由processing_mod.parse_param_specs保证KEYVALUE格式NOT_A_PARAM会抛错见 2.3 节。四、子进程覆盖已安装入口与 _resolve_cli 策略测试计划要求子进程测试必须针对已安装的可执行文件。test_full_e2e.py 中的_resolve_cli(cli-anything-qgis)实现了三级解析策略若与当前解释器同目录存在cli-anything-qgis虚拟环境安装后生成的 sibling 命令直接使用它否则查PATHshutil.which两者都没有时设置了CLI_ANYTHING_FORCE_INSTALLED1则抛RuntimeError提示python3 -m pip install -e .否则回退到python -m cli_anything.qgis.qgis_cli。这个入口正是 setup.py 中entry_points.console_scripts声明的cli-anything-qgiscli_anything.qgis.qgis_cli:main。计划列出的子进程检查项及实现如下计划检查项测试用例关键断言cli-anything-qgis --helptest_help退出码 0stdout 含QGIS CLI--json process help native:printlayouttopdftest_process_help_jsonpayload[algorithm][id]等于算法 ID--json project new -o ...test_project_new_json返回 path 为解析后的绝对路径且文件存在完整安装命令 PDF 工作流test_full_pdf_workflow同 3.1 节三重 PDF 断言完整安装命令 PNG 工作流test_full_png_workflowPNG 魔数校验点项目 add-map 回归test_point_only_project_add_map_without_extentitems 含QgsLayoutItemMap并能导出有效 PDF子进程统一通过_subprocess_jsonL91-L100以--json模式调用并解析 stdout任何非零退出码都会附带 stderr/stdout 让失败可诊断每个子进程环境都会注入隔离的HOME与QT_QPA_PLATFORMoffscreen。最后一项测试还额外验证仅含一个点要素POINT(116.397 39.907)的项目经layout add-map后同样能导出有效 PDF即 2.5 节所述的自动扩展修复在真实安装入口下同样生效。五、执行命令与运行环境测试计划给出的三条执行命令工作目录为QGIS/agent-harness/CLI_ANYTHING_FORCE_INSTALLED1 python3 -m pytest cli_anything/qgis/tests/test_core.py -v CLI_ANYTHING_FORCE_INSTALLED1 python3 -m pytest cli_anything/qgis/tests/test_full_e2e.py -v -s CLI_ANYTHING_FORCE_INSTALLED1 python3 -m pytest cli_anything/qgis/tests -v -s --tbno其中CLI_ANYTHING_FORCE_INSTALLED1使_resolve_cli严格走“已安装命令”路径找不到即报错从而把“入口可安装、可执行”本身也纳入回归范围-s让测试内print的工件路径PDF/PNG artifact直接可见--tbno用于快速看整包通过状态。适用前提Python ≥ 3.10setup.py 中python_requires与 classifiers 声明 3.10/3.11/3.12系统已安装 QGIS 且qgis_process在 PATH 中find_qgis_process 的错误提示以apt install qgis为例。开发依赖见 setup.py 的extras_require.devpytest7、pytest-cov、jinja2。六、实测结果与已知告警TEST.md 记录的完整一轮执行结果为22 passed, 4 warnings in 18.95s环境为platform linux -- Python 3.12.3, pytest-9.0.2configfile: pytest.ini。逐项结果如下引自 TEST.md 的pytest -v -s --tbno输出 test session starts platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0 cachedir: .pytest_cache plugins: cov-7.1.0, anyio-4.12.1 collecting ... collected 22 items cli_anything/qgis/tests/test_core.py::test_project_create_save_open_and_info PASSED cli_anything/qgis/tests/test_core.py::test_default_datastore_path PASSED cli_anything/qgis/tests/test_core.py::test_parse_field_and_param_specs PASSED cli_anything/qgis/tests/test_core.py::test_layer_create_list_info_and_remove PASSED cli_anything/qgis/tests/test_core.py::test_feature_add_and_list PASSED cli_anything/qgis/tests/test_core.py::test_feature_add_rejects_invalid_boolean PASSED cli_anything/qgis/tests/test_core.py::test_layout_create_add_items_and_remove PASSED cli_anything/qgis/tests/test_core.py::test_layout_add_map_accepts_point_only_project PASSED cli_anything/qgis/tests/test_core.py::test_export_presets_describe_supported_formats PASSED cli_anything/qgis/tests/test_core.py::test_session_save_load_and_status PASSED cli_anything/qgis/tests/test_core.py::test_project_path_argument_normalizes PASSED cli_anything/qgis/tests/test_core.py::test_find_qgis_process_missing_raises_clear_error PASSED cli_anything/qgis/tests/test_core.py::test_run_process_json_normalizes_backend_failure PASSED cli_anything/qgis/tests/test_full_e2e.py::TestRealCLIWorkflows::test_scratch_project_to_pdf PASSED cli_anything/qgis/tests/test_full_e2e.py::TestRealCLIWorkflows::test_scratch_project_to_png PASSED cli_anything/qgis/tests/test_full_e2e.py::TestRealCLIWorkflows::test_processing_passthrough_buffer PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_help PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_process_help_json PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_project_new_json PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_full_pdf_workflow PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_full_png_workflow PASSED cli_anything/qgis/tests/test_full_e2e.py::TestCLISubprocess::test_point_only_project_add_map_without_extent PASSED 22 passed, 4 warnings in 18.95s 覆盖小结与计划完全对齐直接核心测试覆盖了 project、layer、feature、layout、session 与后端错误归一化辅助函数并包含点项目布局自动扩展回归真实 E2E 覆盖在已安装 QGIS 运行时上执行了 PDF 导出、PNG 导出与native:buffer处理子进程覆盖验证了已安装入口的--help、process helpJSON 输出、项目创建、完整 PDF/PNG 工作流以及点项目layout add-map回归路径。已知告警QgsLayoutItemLabel.setFont()在当前 QGIS 构建中已弃用执行记录中告警位置为 layouts.py 的add_label_item当前源码中label.setFont(font)位于 L186-L194不影响导出与测试结果但属于后续维护需关注的 API 演进点。七、小结这份测试计划展示了“为 GUI GIS 软件构建 Agent-可用 CLI”时的完整质量保障思路用 mock 验证错误路径与参数解析find_qgis_process缺失、run_process_json失败归一化、字段/属性规格校验用真实运行时验证业务闭环项目 → 图层 → 要素 → 布局 → PDF/PNG → 算法透传再用子进程验证“安装后的命令”与 in-process 路径行为一致并以文件魔数%PDF-、PNG signature、QImage解码、QgsVectorLayer重开等可复现手段代替“看起来成功”的模糊判断。测试计划的六组单元覆盖、三条 E2E 工作流与五项子进程检查在 22 个用例中全部落地且结果章节保留了可追溯的完整执行记录可作为同类 QGIS/PyQGIS CLI 项目的测试组织范本。【免费下载链接】CLI-AnythingCLI-Anything: Making ALL Software Agent-Native -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考