Godot GDScript 测试套件实战解析:集成测试脚本、输出对比与 Autocompletion 测试机制
本文以 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 质量保障体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00