Godot GDScript 测试套件实战解析:集成测试脚本、输出对比与 Autocompletion 测试机制
【免费下载链接】godotGodot Engine – Multi-platform 2D and 3D game engine项目地址: https://gitcode.com/GitHub_Trending/go/godot
本文以 Godot 仓库中的 GDScript 测试说明文档 为主体,完整讲解modules/gdscript/tests/目录下两类测试的组织方式:一是scripts/中的 GDScript 集成测试(源码文件 + 期望输出文件),二是scripts/completion中的 GDScript 代码补全测试(.gd用例 +.cfg期望配置)。读完本文,你可以理解 Godot 如何在不启动完整编辑器的情况下,对 GDScript 的解析、类型检查、编译、运行及编辑器补全结果做自动化断言,并知道如何按规范新增测试用例。
一、测试目录结构与配套环境
GDScript 模块的测试代码位于 modules/gdscript/tests/,其核心文件分工如下:
| 文件 | 职责 |
|---|---|
| gdscript_test_runner.h / gdscript_test_runner.cpp | 集成测试运行器:扫描scripts/目录、执行脚本并比对输出 |
| test_completion.h | Autocompletion 测试运行器(仅在TOOLS_ENABLED下编译) |
| test_gdscript.h / test_lsp.h | 分阶段测试入口(Tokenizer / Parser / Compiler / Bytecode / LSP) |
| scripts/ | 测试脚本与期望输出,含analyzer/、parser/、runtime/、completion/、lsp/等子目录 |
| project.godot | 测试专用工程配置 |
其中 project.godot 开头明确写着:
; This is not an actual project. ; This config only exists to properly set up the test environment. ; It also helps for opening Godot to edit the scripts, but please don't ; let the editor changes be saved.它不是真正可运行的工程,而是为测试环境提供ProjectSettings(如test_input_action输入动作)。运行器初始化时会调用ProjectSettings::setup(p_base_path, ...)加载该目录,并初始化 GDScript 语言与 Autoload,见 gdscript_test_runner.cpp。
二、集成测试:脚本文件 + 输出文件
README 第一部分说明:scripts/目录中的集成测试以“GDScript 文件 + 输出文件”的形式存在。从源码可以确认其完整工作机制。
2.1 测试执行管线
每个测试脚本必须包含名为test的函数(运行器将test_function_name固定为StringName("test"),见 gdscript_test_runner.cpp)。GDScriptTest::execute_test_code()(gdscript_test_runner.cpp)按如下顺序处理每个脚本:
- 加载:读取脚本源码;若为二进制 token 模式,则用
GDScriptTokenizerBuffer::parse_code_string(code, COMPRESS_ZSTD)压缩为 token 缓冲再set_binary_tokens_source(); - 解析:
GDScriptParser::parse(),失败则记录第一条解析错误(后续错误可能是级联错误); - 类型检查:
GDScriptAnalyzer::analyze(),错误按start_line排序后逐行输出为>> ERROR at line N: ...; - 编译:
GDScriptCompiler::compile(); - 运行:
ClassDB::instantiate()创建宿主对象、set_script()挂载脚本,然后通过instance->callp("test", ...)调用测试函数。
执行过程中通过add_print_handler()/add_error_handler()接管标准输出与错误输出,print()的内容逐行追加到结果中,脚本错误则格式化为>> 类型: 错误信息 at 相对路径:行号 on 函数()的形式(仅ERR_HANDLER_SCRIPT类型附带文件/行号,以保证输出跨平台稳定)。
最终的判定逻辑是全文比对:实际输出(去掉首尾空白、末尾补一个换行,以适配 CI 静态检查)与同名.out文件内容逐字符比较,见 check_output()。
2.2 测试结果状态码
GDScriptTest定义了 6 种状态,写入输出文件首行,可直观区分脚本卡在哪一阶段(gdscript_test_runner.h):
| 状态 | 含义 |
|---|---|
GDTEST_OK | 成功执行到运行阶段 |
GDTEST_LOAD_ERROR | 源码加载失败或找不到test()函数 |
GDTEST_PARSER_ERROR | 解析阶段失败,随后输出第一条解析错误 |
GDTEST_ANALYZER_ERROR | 类型检查阶段失败,输出按行排序的错误列表 |
GDTEST_COMPILER_ERROR | 编译阶段失败 |
GDTEST_RUNTIME_ERROR | 运行期发生脚本错误 |
2.3 文件名约定与构建差异
目录扫描逻辑 make_tests_for_dir() 实现了若干文件名约定,编写集成测试时必须遵守:
*.notest.gd:被完全跳过,用于存放测试辅助代码(例如 utils.notest.gd 中定义的Utils类,提供了静态check()断言函数并打印失败时的调用栈,供运行期测试复用);*.norun.gd:只验证“解析 + 类型检查 + 编译”三个阶段,不要求存在test()函数,因此不执行运行期测试(gdscript_test_runner.cpp);*.bin.gd:同一脚本会先以文本 tokenizer 模式跑一遍,再强制以TOKENIZER_BUFFER(二进制 token)模式跑一遍,两种模式共享同一个.out期望文件;*.textonly.gd:在启用--use-binary-tokens的测试运行中会被跳过;- 首行为
#debug-only的脚本:仅在 release 构建(DEBUG_ENABLED未定义)中被跳过。
调试/发布构建的行为差异还包括警告处理:
- Debug 构建中,运行器会把所有
GDScriptWarning的级别强制设为Warn(便于测试原本默认是 Error 的警告),但UNTYPED_DECLARATION与INFERRED_DECLARATION两类默认保持关闭;若某个脚本需要测试这两类警告,可在源码中加入注释# enable UNTYPED_DECLARATION或# enable INFERRED_DECLARATION,运行器会据此动态打开对应设置(gdscript_test_runner.cpp 与 L545-L555); - 警告统一以
~~ WARNING at line N: (警告名) 消息的格式进入输出;在 release 构建中,期望文件里所有以~~开头的行会被 strip_warnings() 剔除后再比对,从而同一份.out文件能同时适配两类构建。
2.4 重新生成期望输出
集成测试采用“黄金输出(golden output)”模式:当 GDScript 行为变化导致输出变化时,可用命令行参数重新生成全部.out文件。handle_cmdline() 支持的用法为:
--gdscript-generate-tests [测试目录路径]- 省略路径参数时默认作用于
modules/gdscript/tests/scripts; - 追加
--print-filenames可逐个打印正在处理的文件名; - 生成模式(
generate_outputs())下不要求.out文件已存在,执行完每个脚本后将实际输出写入同目录的<脚本名>.out。
三、Autocompletion 测试:➡ 光标标记与 .cfg 期望文件
README 第二部分(也是本文档的重心)说明:scripts/completion目录存放 GDScript 代码补全测试,每个用例至少包含一个.gd文件(被测代码)和一个.cfg文件(期望结果与配置)。
3.1 光标位置标记➡
在 GDScript 文件中,字符➡(U+27A1)表示触发补全时的光标位置。由于该字符不是合法 GDScript 词法符号,且补全往往发生在代码不完整时,这些脚本本身并不可解析。运行器(test_completion.h)的处理方式是:
- 读取脚本后,把第一个
➡(0x27A1)替换为哨兵字符 0xFFFF(使用0x27A1而非 0xFFFF 是为了让文件对人类可读),并要求脚本中必须存在该哨兵,否则CHECK(location != -1)失败; - 当测试需要场景(owner 节点)时,删除包含哨兵字符的整行,重新
reload()脚本并set_script()挂到节点上,因此除该行外脚本必须合法——必要时可补一个pass语句; - 正由于脚本由运行器挂载到 owner 节点上,脚本不应再通过场景文件加载。
3.2 配置文件[input]段(测试环境配置)
.cfg是标准 INI 风格配置,用ConfigFile加载。[input]段的完整键位如下(与 README 一一对应,并补充了源码中的取值细节):
| 键 | 类型 | 默认值 | 作用 |
|---|---|---|---|
cs | boolean | false | 为true时,在非 C#(Mono)构建中跳过该测试。源码对应#ifndef MODULE_MONO_ENABLED下的判断(test_completion.h) |
use_single_quotes | boolean | false | 为本次测试设置编辑器选项text_editor/completion/use_single_quotes |
add_node_path_literals | boolean | false | 为本次测试设置编辑器选项text_editor/completion/add_node_path_literals |
add_string_name_literals | boolean | false | 为本次测试设置编辑器选项text_editor/completion/add_string_name_literals |
scene | String | 无 | 指定补全时打开的场景;未设置时运行器会查找与 GDScript 文件同基名的.tscn;两者都没有时,补全按“无场景打开”处理(test_completion.h) |
node_path | String | .(场景根节点) | 持有当前脚本的节点在场景中的路径(test_completion.h) |
3.3 配置文件[output]段(期望结果断言)
| 键 | 类型 | 说明 |
|---|---|---|
include | Array | 结果中应当出现的建议列表(无序)。每个条目是一个字典,支持display、insert_text、kind、location四个键,对应代码中建议结构ScriptLanguage::CodeCompletionOption的字段。运行器只测试条目中显式给出的键,因此多数情况下只写display即可 |
exclude | Array | 结果中不应出现的建议,条目格式与include相同 |
call_hint | String | 期望的调用提示(call hint) |
forced | boolean | 补全是否预期强制打开补全窗口 |
关键规则是:只针对[output]中显式给出的条目做断言(“Tests will only test against entries in[output]that were specified”)。这与源码实现一致:match_option() 对字典里缺失的键取实际值参与比较(即视为“不校验”),而call_hint/forced的缺省值就是运行器实际返回的值。
校验流程(test_directory()):调用GDScriptEditorLanguage::complete_code(code, res_path, owner, &options, forced, call_hint)得到建议列表后,先检查任何结果项不得命中exclude(命中则报 “Autocompletion suggests illegal option”),再把include中每一项逐一从实际结果中“认领”(未全部认领则CHECK(include.is_empty())失败),最后比对call_hint与forced。
3.4 一个完整用例:参数补全play_typed
以 scripts/completion/argument_options/play_typed.gd 为例,脚本内容为:
extends Node @onready var anim: AnimationPlayer = $AnimationPlayer func test(): anim.play(➡) pass➡位于anim.play(的参数位置。配套的 play_typed.cfg 为:
[input] scene="res://completion/argument_options/argument_options.tscn" [output] include=[ {"display": "\"bounce\""}, ]含义是:补全时加载argument_options.tscn场景(其中提供AnimationPlayer节点),且由于anim是明确标注为AnimationPlayer类型的变量,play()的第一个字符串参数应触发枚举/字面量建议,结果中必须出现display为"bounce"的条目。argument_options/目录下还有play_untyped.gd、play_inferred.gd、connect.gd等用例,分别覆盖参数类型为未标注、可推断、以及信号连接等场景,正是 README 所要求“同一行为在多个上下文中反复测试”的实例。
3.5 补全测试的初始化流程
整个补全测试套件由 doctest 套件[Modules][GDScript][Completion]下的用例[Editor] Check suggestion list驱动(test_completion.h),流程为:
- 将
text_editor/completion/use_single_quotes复位为false,保证起点状态一致; init_language("modules/gdscript/tests/scripts")加载测试工程配置并初始化 GDScript 语言;setup_global_classes()递归扫描scripts/completion目录,把带class_name的 GDScript 注册为全局类(并检查重名冲突,test_completion.h)。completion/目录下的 class_a.notest.gd、class_b.notest.gd等辅助类即通过此机制进入全局类索引,从而被补全测试引用;test_directory()递归遍历每个.gd用例(跳过*.notest.gd),按 3.2/3.3 节规则执行断言,结束后memdelete(scene)释放实例化的场景;finish_language()收尾。
四、编写 Autocompletion 测试的实践准则
README 最后强调:“为避免边缘用例失败,同一行为需要多次测试”,并给出两类必查维度。
覆盖所有可能的类型来源——针对被测试行为适用的每一种类型都要测一遍:
BUILTIN(内置类型);NATIVE(原生类);- GDScript 类(含带
class_name的与preload引入的两种形式); - C# 类(作为所有其他语言绑定的代表,同样区分
class_name与preload两种形式); - Autoload 单例。
README 还特别提醒:对最后几类,测试SCRIPT(GDScript 提供)与CLASS(可由 C# 提供)两种来源即可;不要依赖“Autoload 一定是 SCRIPT 类型”,因为这一行为未来可能变化。
覆盖可能的上下文——补全位置可能出现在程序的不同位置,例如:
- 类成员变量的初始化表达式中;
- 语句块(suite)内直接书写;
- 语句块内的赋值语句中;
- 作为函数调用的参数(如
play_typed.gd所示)。
从scripts/completion/的实际目录布局也能印证这些准则已被落实:types/ 下按local/、member/、hints/划分上下文,assignment_options/、argument_options/、index/、filter/、get_node/、global_enum/、enum_values_in_match/等目录分别对应不同的补全触发场景,types/中则覆盖本地变量、成员变量与类型提示的差异。
五、如何查看与运行这些测试
结合源码中可确认的信息,运行这些测试的途径是:
- Autocompletion 测试:随测试构建(test build,需
TOOLS_ENABLED使 test_completion.h 参与编译)一起通过 doctest 框架执行,用例标签为[Modules][GDScript][Completion]; - 集成测试:由 test_gdscript.h 定义的
TEST_TOKENIZER、TEST_TOKENIZER_BUFFER、TEST_PARSER、TEST_COMPILER、TEST_BYTECODE各阶段分别驱动,统一入口为GDScriptTests::test(TestType),内部构造GDScriptTestRunner(其--use-binary-tokens构造参数对应二进制 token 模式); - 重新生成期望输出:在测试可执行文件的命令行中传入
--gdscript-generate-tests(可选指定测试目录)与--print-filenames,运行器会把每个脚本的实际输出写回对应的.out文件。
需要说明的适用前提:警告相关输出(~~ WARNING ...行)仅在调试构建中产生,发布构建下会自动剔除;cs键标记的 C# 用例只有在启用 Mono 模块的构建中才会真正执行。
六、小结
modules/gdscript/tests/README.md用简短篇幅定义了 Godot 中 GDScript 的两种自动化测试范式:集成测试以“脚本 + 黄金输出文件”验证解析、类型检查、编译与运行全链路,并通过GDTEST_*状态码定位失败阶段;Autocompletion 测试则以➡光标哨兵加.cfg期望文件,对建议列表、call hint 与强制补全行为做精确断言,[input]段的 6 个配置键可精细控制编辑器选项与场景上下文。两者的实现分别落在 gdscript_test_runner.cpp 与 test_completion.h 中,文件名约定(.notest.gd、.norun.gd、.bin.gd、#debug-only)、警告开关机制与--gdscript-generate-tests再生成流程,共同构成了一套对构建类型稳健、可离线复现的 GDScript 质量保障体系。
【免费下载链接】godotGodot Engine – Multi-platform 2D and 3D game engine项目地址: https://gitcode.com/GitHub_Trending/go/godot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考