DreamShaderLang
插件参考贡献

测试

测试

测试

DreamShader » 贡献 » 测试

插件的 Unreal 自动化测试套件,以及其中十三个测试在运行时枚举的磁盘夹具语料库。

声明于Source/DreamShaderEditor/Private/Tests/ — 37 个翻译单元,全部位于 #if WITH_DEV_AUTOMATION_TESTS 中
类型Unreal 自动化测试
标志每个声明均带有 EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter;Custom Pass 渲染、生命周期和插槽注册表测试额外添加 NonNullRHI
语料库根目录<Plugin>/Tests/Corpus
编辑器界面Tools ▸ Session Frontend ▸ Automation,按测试名称前缀过滤

概要

& "<EngineDir>\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" --% "<ProjectDir>\<Project>.uproject" -ExecCmds="Automation RunTests <filter>; Quit" -nullrhi -unattended -nopause -nosplash -log

<filter> 是测试名称的任意前缀;DreamShader 运行所有内容。--% 是 PowerShell 的停止解析标记:其后的所有内容都会原样传递给可执行文件,因此 -ExecCmds= 周围的引号会保留。去掉 --% 的同一行就是粘贴到 cmd.exe 中的内容。

可选开关:-nullrhi、-DreamShaderUpdateGolden、-NoDreamShaderEditorBridge。参见命令行开关。

测试数量

数量项值
测试声明(IMPLEMENT_*_AUTOMATION_TEST)277
简单声明(每个一个测试)264
复杂声明(数据驱动运行器)13 — 参见语料库运行器
语料库夹具Tests/Corpus/ 下的 513 个源文件
.expected.json 基准文件508
可单独运行的测试945,在一次 DreamShader 运行中(2026-09-21)

复杂运行器在运行时枚举语料库树,因此可运行测试数量会随夹具数量变化,且添加夹具时无需更改任何 C++ 代码。DreamShader.Lang2.RoundtripIR 会扫描属于其他层的夹具,这就是可运行数量并非“简单测试 + 夹具”的原因。

测试分组

前缀层要求
DreamShader.Lang.Parse.*数据驱动解析语料库快速;无资源 I/O
DreamShader.Lang.Diagnostics.*纯诊断辅助函数毫秒级;无编辑器状态
DreamShader.Lang.Import.*纯导入说明符辅助函数毫秒级;无编辑器状态
DreamShader.Lang.ParameterExpressions.*1.x 的 Properties 接口,通过旧版前端快速;仅解析器
DreamShader.Commandlet.Args.*命令行程序参数解析毫秒级;纯函数
DreamShader.Commandlet.Compile.*命令行程序运行器冒烟测试慢速;编辑器;写入真实的 /Game 资源
DreamShader.Compiler.Parser.*1.x 源文件通过编译器端到端编译慢速;编辑器
DreamShader.Compiler.Generate.*1.x 源文件通过编译器端到端编译慢速;编辑器
DreamShader.Compiler.SourceHash.*重新生成跳过检查慢速;编辑器
DreamShader.Lang2.{Lexer,Expressions,Statements,Declarations,Printer,Trivia}.*2.0 前端,逐单元测试毫秒级;仅 Core
DreamShader.Lang2.{Binder,IR,IRCompare,InstanceSource,Migrate,LegacyRules}.*绑定器、IR、比较器、.dsi 文本、迁移器快速;手工构建的目录,无资源
DreamShader.Lang2.{IRLayout,SubstrateSugar,Format}.*三种 IR 图布局、Substrate 语法糖(每种写法与其代表的节点对照,并通过反编译器读回)、dsc fmt 的核心快速;手工构建的目录,无资源
DreamShader.Lang2.Corpus*.*数据驱动文本层:Lang/、IR/、Legacy/Parse、Legacy/IR、Decompile/、Migrate/快速;无资源 I/O
DreamShader.Lang2.RoundtripIR.*每个可降级的夹具:IR → 文本 → IR 等价快速;无资源 I/O
DreamShader.Compiler2.{Smoke,Instance,Decompile,Migrate,Provenance,Parity}.*2.0 管线、实例、反编译服务、dsc migrate、Adopt、与 1.x 捕获的图一致性慢速;编辑器
DreamShader.Compiler2.Corpus*.*数据驱动编译层:Compile/、Legacy/Compile、Instance/慢速;编辑器
DreamShader.Compiler2.Roundtrip.*.dss → 资源 → 反编译 → 资源,两个转储相等慢速;编辑器
DreamShader.Compiler.{Divergence,Atomic,Tweaked,Persistence,BuildKey,Order}.*手工编辑检测、原子重建、Ephemeral/Materialized、构建键慢速;编辑器
DreamShader.Browser.* · DreamShader.AssetRenameSync.* · DreamShader.Preview.*材质内容浏览器模型、资源重命名重写、预览探测(Preview.ProbePreview.RendersBinding 需要真实 RHI)混合
DreamShader.Gen.Graph.*对生成的图进行节点形状断言慢速;编辑器
DreamShader.Gen.Parameters.*参数节点创建和引脚连线慢速;编辑器
DreamShader.Gen.Wiring.*生成图中的条件连线慢速;编辑器
DreamShader.Gen.Layout.*所放置图的几何结构慢速;编辑器
DreamShader.DumpGraph.* (自 1.9.0 起)规范的图转储 — 参见图基线慢速;编辑器
DreamShader.Roundtrip.*反编译 → 重新生成保真度慢速;编辑器;其中两个还需要真实 RHI
DreamShader.Render.*像素一致性需要真实 RHI
DreamShader.Lang2.Pipeline.*.dsp:解析、打印、fmt、绑定、每条规则的代码、比较、Adopt 的重写、.dss 中的传递节点快速;仅 Core,手工构建的引擎事实
DreamShader.Lang2.Corpus.Pipeline.* · DreamShader.Lang2.CorpusIR.Pipeline.*Tests/Corpus/Lang/Pipeline 和 Tests/Corpus/IR/Pipeline 的 .dsp 夹具快速;无资源 I/O
DreamShader.Compiler2.Pipeline.*.dsp 到 UDreamPassPipeline:字段、渲染目标、依赖关系、往返;插槽注册表及其预检查慢速;编辑器;Registry 和 RegistryPrecheck 需要真实 RHI,并首先备份 <DShader>/.dreampass
DreamShader.Pass.Logic.*无渲染的 Custom Pass 运行时:写法、参数混合、插槽打包、资源的检查、哪些管线适用于某个视图、源文件快速
DreamShader.Pass.Render.* · DreamShader.Pass.Lifecycle.*Custom Pass 通道渲染到场景捕获并逐像素读回;帧中途的编辑、移除和世界销毁NonNullRHI:在 -nullrhi 下跳过,在下面的 RHI 门中运行

源码中记录的划分:快速的 DreamShader.Lang.* 和 DreamShader.Lang2.* 层作为拉取请求的门禁,慢速的 DreamShader.Compiler* 和 DreamShader.Gen.* 层每晚运行。整个套件无头运行大约需要一分半钟。

[!WARNING] 请先关闭编辑器。 一次运行会在 /Game/DreamShaderTests 下编译源文件并写入资源;如果同一项目上打开着编辑器,两者会争夺桥接的写入所有权和包文件。

无头运行

命令行程序驱动程序的按所有权清理可以在没有 Unreal 的情况下测试:

pwsh -NoProfile -File Tools/Tests/Test-DscAssetCleanup.ps1

这会在一个临时的假项目中练习驱动程序的真实辅助函数,包括并发无关写入、预先存在的资源、保存后编辑、Content 边界、无效和已消耗的保存清单,以及无法列出的 Content 文件夹。

目标过滤器
快速门禁 — 1.x 解析等价语料库、纯辅助函数、预处理器DreamShader.Lang.
快速门禁 — 2.0 前端、绑定器、IR、反编译和迁移文本层DreamShader.Lang2
仅命令行程序参数辅助函数DreamShader.Commandlet.Args
1.x 源文件端到端编译(原 Generate 语料库)DreamShader.Compiler2.CorpusLegacy
2.0 管线端到端DreamShader.Compiler2
1.x 时代的端到端测试DreamShader.Compiler.
反编译往返DreamShader.Roundtrip
图转储确定性DreamShader.DumpGraph
Custom Pass,无渲染器DreamShader.Lang2.Pipeline+DreamShader.Compiler2.Pipeline+DreamShader.Pass.Logic
Custom Pass,已渲染(UE 5.8,真实 RHI — 见下文)DreamShader.Pass.Render+DreamShader.Pass.Lifecycle+DreamShader.Compiler2.Pipeline.Registry
所有内容DreamShader

Custom Pass 渲染测试需要渲染设备但不需要窗口:将它们作为独立门禁运行,不使用 -nullrhi,并使用 -RenderOffscreen -d3d12:

& "<EngineDir>\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" --% "<ProjectDir>\<Project>.uproject" -ExecCmds="Automation RunTests DreamShader.Pass.Render+DreamShader.Pass.Lifecycle; Quit" -RenderOffscreen -d3d12 -unattended -nopause -nosplash -NoDreamShaderEditorBridge -log

Tools/Tests/Invoke-DreamShaderTests.ps1 在独立宿主项目上运行两个门控 (Tools/TestHost):-nullrhi 下的 Suite 预设,以及离屏的 Rhi 预设。

Custom Pass 门控的两侧

在 UE 5.8 之前,DREAMSHADER_WITH_CUSTOM_PASS 为 0,插件在构建时不包含 Custom Pass 的渲染器部分。当向 UBT 传入 -ProjectDefine:DREAMSHADER_FORCE_NO_CUSTOM_PASS 时,5.8 的机器也会构建该部分, 测试脚本的 -WithoutCustomPass 会传入该参数:

pwsh -NoProfile -File Tools\Tests\Invoke-DreamShaderTests.ps1 -Preset Suite -WithoutCustomPass -Filter DreamShader.Lang2.Pipeline+DreamShader.Compiler2.Pipeline+DreamShader.Pass.Logic

在 0 下,管线会测试构建资源是否被编译排除、 DreamShader.Compiler2.Pipeline.NeedsCustomPass 是否被编译包含:一次 .dsp 构建会报告 DSH8300 并且 不写入任何内容。渲染和生命周期测试会随渲染器一起被编译排除。

下一次不带该开关的构建会自行恢复渲染器:更改后的 UBT 命令行 会使 UBT 的 makefile 失效,因此规则会被重新求值。由 规则文件读取的环境变量不会被看到——UBT 会保留它上次求值的规则,直到规则文件发生变化,并且 在 5.8 上,即使在 -NoUBTMakefiles 下它也会加载其 makefile。运行前,脚本会从 UBT 为 DreamShaderPass 写入的定义中读取 二进制文件位于哪一侧,并在与所要求的一侧不符时停止, 例如在构建了另一种类型之后出现 -NoBuild 的情况。

命令行开关

开关对测试套件的影响
-nullrhi无渲染设备。DreamShader.Render.ThinCustomVsGraphParity 和 DreamShader.Roundtrip.MTestToonRenderParity 自动跳过;NonNullRHI 测试(Custom Pass 渲染、生命周期、槽注册表)完全不运行。其他所有测试仍会运行。
-DreamShaderUpdateGolden每个语料运行器都会根据实际结果重写每个 .expected.json,而不是对其进行断言。参见重新生成黄金文件。
-NoDreamShaderEditorBridge跳过创建编辑器桥接和材质内容浏览器:没有目录监视器、启动时没有临时生成阶段、127.0.0.1:17864 上没有 WebSocket 监听器。当一次运行不能与桥接竞争相同源文件时很有用。
-unattended -nopause -nosplash标准无头标志;没有模态对话框、没有启动画面、退出时不按键。
-log / -stdout将日志路由到控制台。

[!WARNING] 被跳过的测试会报告成功。 测试套件中的每条跳过路径都会调用 AddInfo(...),然后 调用 return true。因此一次 -nullrhi 运行会显示 DreamShader.Render.ThinCustomVsGraphParity 和 DreamShader.Roundtrip.MTestToonRenderParity 为绿色,却没有比较过一个像素,而 在 UE 5.3 上的运行会显示 DreamShader.Compiler.Generate.SubstrateMaterial 为绿色,却没有 编译过 Substrate 材质。要证明渲染一致性,请在不带 -nullrhi 的情况下运行,并阅读日志中的信息 行。

跳过条件

测试在未断言任何内容的情况下返回成功的所有条件。

测试条件消息
DreamShader.Compiler.Generate.SubstrateMaterialDREAMSHADER_WITH_SUBSTRATE_BUILTINS 为 0 — UE < 5.4DreamShader Substrate builtins are not available for this Unreal Engine version; skipping the compile test.
DreamShader.Render.ThinCustomVsGraphParityGUsingNullRHI 或 !FApp::CanEverRender()Skipping ThinCustom-vs-Graph render parity: no usable RHI (-nullrhi). Run without -nullrhi for the full pixel comparison.
DreamShader.Roundtrip.MTestToonRenderParityGUsingNullRHI 或 !FApp::CanEverRender()Skipping M_Test_Toon round-trip render parity: no usable RHI (run without -nullrhi).
DreamShader.Roundtrip.MTestToonRenderParity它进行往返转换的项目资源不存在Skipping M_Test_Toon round-trip: '{ObjectPath}' is not present in this project.

本页每个消息表中,运行时替换都显示为 {Placeholder}。

共享测试框架

编辑器级测试都使用相同的脚手架。

辅助工具行为
MakeUniqueTestAssetName(Prefix)<Prefix>_<GUID digits> — 两次运行不会冲突。
GetAutomationSourceDirectory()<SourceDirectory>/Tests/Automation,即默认情况下为 DShader/Tests/Automation。
WriteAutomationSourceFile(...)以不带 BOM 的 UTF-8 格式将测试的 .dsm / .dsf 写入那里。失败消息:Failed to write DreamShader automation source file '{Path}'.
MakeAutomationObjectPath(Name)/Game/DreamShaderTests/Automation/<Name>.<Name>
FScopedDreamShaderAutomationArtifactsRAII 清理:使用 ObjectTools::DeleteObjectsUnchecked 删除每个已注册资源,然后删除每个已注册源文件。
AddExpectedNewAssetProbeWarnings(...)抑制 SkipPackage: <pkg> 和对象路径探测警告,以出现次数 -1 注册(如果存在则抑制,不要求存在),因为较新的引擎并不总是发出它们。
AddExpectedAutomationCleanupWarnings(...)抑制 package was marked as deleted in editor, but has been modified on disk。
FDreamShaderQuietAutomationTestBase基类,其 SuppressLogErrors() / SuppressLogWarnings() 返回 true;需要它是因为图自动布局会触发一个良性的引擎 SlowTask ensure。
FScopedDreamShaderGraphBackendPinRAII:在作用域内强制 UDreamShaderSettings::DefaultBackend = Graph 并恢复先前的值。

[!NOTE] Compile/ 语料黄金文件编码的是 Graph-backend 语义,因此该运行器在每个用例期间都固定 DefaultBackend = Graph。Legacy/Compile/ 是例外:它的 黄金文件来自 2026-09-15 的 1.x 捕获,并固定为 ThinCustom,即项目默认值。

测试索引

按翻译单元列出的每个声明。

DreamShaderAutomationTests.cpp — 30 个声明

除非注释另有说明,否则其中每一个都会向 DShader/Tests/Automation 写入一个源文件, 从中生成,针对生成的资源进行断言,并在退出时删除两者。

测试宏备注
DreamShader.Compiler.Parser.MinimalMaterialSIMPLE—
DreamShader.Compiler.Generate.MinimalMaterialSIMPLE—
DreamShader.Compiler.Generate.DsfWithImportSIMPLE—
DreamShader.Compiler.Generate.SubstrateMaterialSIMPLE在 UE 5.4 以下跳过
DreamShader.Compiler.SourceHash.SkipUnchangedMaterialSIMPLE断言对未更改源码的第二次编译会被跳过
DreamShader.Commandlet.Compile.SingleSourceSmokeSIMPLE运行 commandlet 编译路径,该路径会持久化一个真实的 /Game 资产
DreamShader.Gen.Wiring.TruthyConditionCUSTOM_SIMPLE,quiet base—
DreamShader.Roundtrip.MaterialDecompilesCUSTOM_SIMPLE,quiet base—
DreamShader.Roundtrip.SubstrateMaterialRegeneratesCUSTOM_SIMPLE,quiet base—
DreamShader.Roundtrip.SwitchTypedAppendRegeneratesCUSTOM_SIMPLE,quiet base—
DreamShader.Roundtrip.CustomAdditionalOutputRegeneratesCUSTOM_SIMPLE,quiet base—
DreamShader.Roundtrip.StaticBoolFunctionInputRegeneratesCUSTOM_SIMPLE,quiet base—
DreamShader.Gen.Graph.ArithmeticNodesCUSTOM_SIMPLE,quiet base—
DreamShader.Gen.Graph.MathBuiltinNodesCUSTOM_SIMPLE,quiet base—
DreamShader.Gen.Parameters.NodeCreationCUSTOM_SIMPLE,quiet base声明带内联默认值和不带内联默认值的参数,并断言创建了匹配的 UMaterialExpression*Parameter 节点
DreamShader.Gen.Parameters.OtherNodeCreationCUSTOM_SIMPLE,quiet base对 Scalar / Vector / Dynamic 之外的参数类型采用相同轴线。纹理对象和资产必需类型不在范围内
DreamShader.Gen.Parameters.InputWiringCUSTOM_SIMPLE,quiet baseParam(InputPin = expr) 调用形式;断言命名引脚最终被连接
DreamShader.Gen.Layout.NoOverlapCUSTOM_SIMPLE,quiet base生成一个四输出材质,并断言没有两个已放置节点重叠,且没有节点超出其注释框;使用与放置时相同的 EstimateMaterialNodeSize 测量每个节点。该夹具需要平衡树(rank 是到汇点的距离,因此链式结构每列只留一个节点,没有可碰撞对象),还需要一对八参数 Function 调用点——普通数学节点默认 bCollapsed,不绘制预览且高度约为 124,可避开任何合理的固定行距;调用点生成的 Custom 节点约为 416
DreamShader.Compiler.Generate.InstanceAliasSIMPLE—
DreamShader.Compiler.Generate.ThinCustomBackendSIMPLE—
DreamShader.Render.ThinCustomVsGraphParityCUSTOM_SIMPLE,quiet base需要真实的 RHI。 参见 渲染一致性
DreamShader.Roundtrip.MTestToonRenderParityCUSTOM_SIMPLE,quiet base需要真实 RHI 以及项目专用资产;两者任一缺失时跳过
DreamShader.Roundtrip.RenamedChannelUsesMaskCUSTOM_SIMPLE,quiet base—
DreamShader.Compiler.Generate.ThinCustomTextureSIMPLE—
DreamShader.Compiler.Generate.ThinCustomUISIMPLE—
DreamShader.Compiler.Generate.ThinCustomPostProcessSIMPLE—
DreamShader.Compiler.Generate.ThinCustomSceneReadsSIMPLE—
DreamShader.Compiler.Generate.ThinCustomMaterialAttributesSIMPLE—
DreamShader.Compiler.Generate.InstanceAliasStateReadsSIMPLE—
DreamShader.Compiler.Generate.InstanceAliasImportedFunctionSIMPLE—
DreamShader.Compiler.Generate.InstanceAliasBaseOverridesSIMPLE—

DreamShaderParameterTests.cpp — 3 个声明

测试覆盖内容
DreamShader.Lang.ParameterExpressions.ParseAll24 个声明,覆盖 23 个不同的参数节点关键字;ScalarParameter、VectorParameter 和 TextureObjectParameter 各出现两次,一次带内联默认值,一次不带
DreamShader.Lang.ParameterExpressions.GroupScopeGroup("X") { … } 标记分组;松散属性保持未分组且未排序;自动 SortPriority 是从 0 开始、步长为 10 的全局计数器;显式 SortPriority 优先且不占用计数器槽位;Slider(0, 1) 会展开为恰好两个反射属性
DreamShader.Lang.ParameterExpressions.NestedGroupScope嵌套分组通过 | 组合——Group("Surface") { Group("SS") { … } } 产生 Surface|SS;字面量 Group("Manual|Literal") 原样传递

DreamShaderPureFunctionTests.cpp — 8 个声明

不依赖编辑器、世界或资产;这些测试在毫秒级内运行。

测试覆盖内容
DreamShader.Lang.Diagnostics.ParseErrorLocationTryParseErrorLocation,包括将行/列限制到 >= 1 并拒绝非数字坐标
DreamShader.Lang.Diagnostics.BuildGenerateDiagnosticsBuildGenerateErrorDiagnostics 行拆分
DreamShader.Lang.Diagnostics.TextWireUtils诊断传输文本在 en-US 和 zh-Hans 编辑器区域设置下相同
DreamShader.Lang.Diagnostics.NoFile无文件、以空路径报告的诊断不携带位置信息:其记录或编译错误中都没有编造的文件
DreamShader.Lang.Import.ExtractImportPathTryExtractImportPathFromLine:引号规则、注释拒绝、尾部垃圾拒绝
DreamShader.Lang.Import.NormalizeSpecifierNormalizeImportSpecifier:无扩展名的说明符获得 .dsh,反斜杠和前导 ./ 被剥离
DreamShader.Commandlet.Args.SplitAndGetCommandlet 键/值规范化以及 Params → Switches → Tokens 搜索顺序,基于来自 UCommandlet::ParseCommandLine 的列表
DreamShader.Commandlet.Args.FlagValuesHasCommandletFlag:逐行验证 布尔标志 表格行,任何其他值均为 DSH9110

语料运行器 — 13 个声明

测试基名语料子树
DreamShader.Lang.ParseParse/ — 1.x 解析等价集,由旧版前端读取
DreamShader.Lang2.CorpusLang/
DreamShader.Lang2.CorpusIRIR/(包含其 Instances/*.dsi)
DreamShader.Lang2.CorpusLegacyParseLegacy/Parse/
DreamShader.Lang2.CorpusLegacyIRLegacy/IR/
DreamShader.Lang2.CorpusDecompileDecompile/
DreamShader.Lang2.CorpusMigrateMigrate/
DreamShader.Lang2.RoundtripIR扫描 IR/、Lang/Examples/ 和 Compile/,不使用文本黄金文件
DreamShader.Lang2.Trivia.CommentInvariant两种语言中每个可解析的夹具,每个语料目录一个子测试:没有注释在解析 → 打印过程中丢失
DreamShader.Compiler2.CorpusCompile/
DreamShader.Compiler2.CorpusLegacyLegacy/Compile/ — 原 Generate/ 夹具
DreamShader.Compiler2.CorpusInstanceInstance/
DreamShader.Compiler2.Roundtrip.CorpusRoundtrip/

四个 Compiler2 运行器是带 quiet base 的 IMPLEMENT_CUSTOM_COMPLEX_AUTOMATION_TEST(图自动布局会触发一个良性的引擎 ensure);其余为 IMPLEMENT_COMPLEX_AUTOMATION_TEST。

图基线 (自 1.9.0 起)

自动化套件说编译仍然成功。它并没说编译器仍然产生相同的图——而对于编译器重写来说,这是唯一重要的问题。dump-graph commandlet 动词填补了这一空白:它为每个生成的资产写出一份规范化 JSON(节点类、反射属性、连接、引脚顺序;不含坐标、颜色或 GUID),然后通过普通文本 diff 比较两次捕获。

为整个项目捕获基线。编译步骤不是可选的——dump-graph 从不写入资产,因此磁盘上已有的资产会按现状转储,而不是重新构建:

& $UnrealEditorCmd $Project -run=DreamShader compile -All -Force -unattended -nopause -nullrhi -nosplash -stdout -log
& $UnrealEditorCmd $Project -run=DreamShader dump-graph -All -Out="I:/Baseline/before" -unattended -nopause -nullrhi -nosplash -stdout -log

然后,在更改之后:

git diff --no-index -- I:/Baseline/before I:/Baseline/after

每个差异块都是一个行为差异。./dsc.ps1 dump-graph -All -Out … 是同样两条命令的简短写法。

三个自动化测试守护转储本身,而第一个是无法通过阅读代码替代的——它生成同一份源码两次并断言两个 JSON 文件逐字节相同,这是发现泄漏的 GUID、以路径名作种子的节点颜色或对象名后缀的唯一方法:

测试断言
DreamShader.DumpGraph.Determinism对同一份源码的两次完整生成转储出相同的字节;LF 结尾;一个尾部换行
DreamShader.DumpGraph.ExcludesEditorState没有任何 MaterialExpressionGuid、EditorX/Y、NodeColor、Desc、bCollapsed、VariableGuid… 键留存——而行为键则会留存
DreamShader.DumpGraph.NodeCount微小夹具转储三个节点,图中的每个表达式恰好出现一次,且 id 从 n0…n(N-1)

三者都将项目的 DefaultBackend 固定为 Graph 与 FScopedDreamShaderGraphBackendPin,并清理其源文件、资产和临时输出树。

渲染一致性

两种像素测试都会渲染两个材质并逐帧比较。 ThinCustomVsGraphParity 从同一主体构建成对内容,每个后端一个(Graph 和 ThinCustom);MTestToonRenderParity 将项目材质与其自身反编译源码重新生成的材质进行比较 (Original 对 RoundTripTwin)。

标准值
渲染尺寸256 × 256
网格sphere
环绕偏航角 / 俯仰角-157.5 / -11.25
每通道容差2
允许的差异像素数PixelCount / 1000,即 0.1 %
内容哨兵 — ThinCustom 一致性超过 5 % 的像素必须以绿色为主(G > R + 30 和 G > B + 30)
内容哨兵 — MTestToonRenderParity超过 5 % 的像素与清除颜色 (44, 44, 48) 的差异必须大于 8
预热每个孪生体先进行一次丢弃渲染,然后 FAssetCompilingManager::FinishCompilationForObjects、GShaderCompilingManager->FinishAllCompilation() 和 FlushRenderingCommands()
失败产物<Project>/Saved/DreamShaderTests/Parity_<Case>_<Backend>.png 用于 ThinCustomVsGraphParity,<Project>/Saved/DreamShaderTests/MTestToon_<Label>.png 用于 MTestToonRenderParity

一致性用例 — 同一主体的 ThinCustom 与 Graph 孪生体,仅在 Backend 上不同:

用例测试内容
FlatParams在 Unlit、Opaque 材质上按名称绑定 ScalarParameter / VectorParameter
UvTextureTextureObjectParameter 加上 UE.TexCoord(Index=0) 和 SampleTexture2D — 插值器和纹理路径
LitAttributesDefaultLit 材质上的 MaterialAttributes 输出 — MakeMaterialAttributes 路径

语料库

Tests/Corpus 位于插件之下,而不是项目的 DShader 树之下,因此它随插件一起分发,并且永远不会被普通的源文件发现机制拾取。 Tests/Corpus/README.md 是该树、每个黄金基准模式以及每个运行器的参考;本节是地图。

层源文件用例做什么需求
Parse/361.x 文本由旧版前端完全按照 1.x 的方式接受或拒绝Core
Lang/72一个 ParseDreamShaderLang,结构计数,打印 → 解析 → 打印字节完全一致Core
IR/167绑定 → 构建 → 通过 → 验证;黄金基准是 IR 转储Core,手工制作的目录
Legacy/Parse/1211.x 文本 → 旧版前端(带琐碎内容)→ 其打印出的 2.0 文本,该文本必须能作为 2.0 解析Core
Legacy/IR/54每条文档化的 1.x 规则(L2–L27)对应一个夹具,以及 Substrate 语法糖按 1.x 的写法Core,手工制作的目录
Decompile/25.dss → IR → AST → 文本,并且该文本可降低为等价 IRCore,手工制作的目录
Migrate/221.x 文本 → 迁移器 → 2.0 文本:无注释丢失,可构建,IR 等价Core,手工制作的目录
Compile/12整个管线到资产;黄金基准保存图转储编辑器
Legacy/Compile/121.x 源文件同理编辑器
Instance/10一个 .dsi,其 Parent 命名的同级 .dss编辑器
Roundtrip/11.dss → 资产 → 反编译服务 → 文本 → 资产;两个转储相等编辑器
Parity/—仅黄金基准:Lang/Examples 源文件必须复现的 1.x 图捕获编辑器

发现按层的扩展名递归进行,然后排序。

[!WARNING] Tests/ 不在发布存档中。从发布 zip 安装的插件没有语料库,因此十三个运行器枚举到零个子测试,测试套件会静默收缩为其简单声明。请从仓库检出中运行语料库。

命名约定

约定规则
层前缀每个目录一个短前缀(L_ 词法,T_ 顶层,S_ 节,Ty_ 类型,E_ 表达式,D_ 声明或反编译,Rf_ 反射,Cu_ 自定义,M_ 材质,I_ 实例,H_ 头文件……)
负向用例文件名包含 .bad.,不区分大小写匹配。没有黄金基准时,默认预期翻转为“这失败”
黄金基准名称仅剥离最后一个扩展名,因此 X.bad.dsm 与 X.bad.expected.json 配对,X.dsm 与 X.expected.json 配对
黄金基准存在性可选 — 缺失意味着默认值适用:正向 ⇒ 必须成功,.bad. ⇒ 必须失败
子测试名称相对于层目录的路径,剥离扩展名并将 / 和 \ 替换为 .

生成的完整测试名称:

DreamShader.Lang.Parse.TopLevel.T_MinimalShader
DreamShader.Lang2.CorpusIR.Reflected.Rf_LateBoundPin
DreamShader.Compiler2.CorpusLegacy.Material.M_Surface

每层的入口点

层入口点
Parse/,Lang/ParseDreamShaderLang — Auto 为 .dsm / .dsf 选择旧版前端,并在 .dsh 中按声明选择
IR/,Legacy/IR/BindDreamShaderLang → BuildDreamShaderIR → RunDreamShaderIRPasses → ValidateDreamShaderIR
Legacy/Parse/ParseDreamShaderLang 带 bKeepTrivia → PrintDreamShaderLang,并将打印出的文本再次作为 2.0 解析
Decompile/RaiseDreamShaderIR → BuildDreamShaderAstFromIR → 打印 → 再次降低 → AreDreamShaderIRModulesEquivalent
Migrate/旧版解析 + 绑定 → MigrateDreamShaderLegacyModule → 打印 → 注释计数 → 作为 2.0 构建 → IR 等价
Compile/,Legacy/Compile/,Instance/,Parity/CompileDreamShaderTestAssets — 编译器服务的 CompileAssets 之上的测试门面 —— 然后 dump-graph 对每个资产的转储
Roundtrip/编译 → 对整个源文件执行 RunDreamShaderDecompileRequest → 在另一个临时根目录下编译文本 → 比较两个转储

编译层会将每个夹具复制到临时源根目录并构建到 /Game/DreamShaderTests/... 中,然后清理两者。

.expected.json

每个字段都是选择加入的;缺失的字段不做任何断言。下表是 Parse/ 层的黄金基准("entryPoint": "parse"):由于 1.x 解析器已在 2.0 中退役,其 definition 字段是从旧版前端的 AST 和 FLegacyMigrationInfo 中读取的,而不是从 FTextShaderDefinition 中读取,字段名相同。其他每一层都有自己的形态 — lang、ir、 legacy、legacy-ir、decompile、migrate、compile、roundtrip — 记录在 Tests/Corpus/README.md 中。它们共享两个约定: errorContains / warningsContain / infosContain 保存 DSHnnnn 代码而非消息文本, 以及 ...Pending: true 标志(textPending、irPending、graphPending)会跳过尚未有人审核的黄金基准的字节比较 — 移除该标志是审核者的行为,绝不是编写者的行为。

字段类型层含义
entryPointstring—在 Parse/ golden 中仅供参考:其解码器从不读取它。Parse/ writer 会输出 "parse"
outcomestringall"error",以不区分大小写方式比较,期望失败;任何其他值,包括 "ok",都期望成功。覆盖 .bad. 文件名默认值
errorContainsstring[]all在预期失败时检查:每个子字符串都必须出现在某个错误诊断中,以不区分大小写方式比较——实际中即 DSHnnnn 代码。messageContains,即 1.x Generate 层的拼写,会被读入同一列表
warningsContainstring[]all每个子字符串都必须出现在某个警告诊断中,不区分大小写
definition.namestringParse第一个产品块的 Name=
definition.settingsobjectParse字符串 → 字符串。通过 TryGetSetting 断言:键不区分大小写,值精确匹配;列出的每个键都必须存在
definition.outputDeclarationsnumberParse输出声明的数量
definition.outputsnumberParse输出绑定的数量
definition.materialFunctionsnumberParseShaderFunction / ShaderLayer / ShaderLayerBlend 块的数量
definition.materialFunction0KindstringParse值之一:ShaderFunction、ShaderLayer、ShaderLayerBlend。仅当至少存在一个材质函数时检查
definition.virtualFunctionsnumberParseVirtualFunction 块的数量
definition.codeNotEmptyboolParseDefinition.Code 是否非空

[!WARNING] outcome 仅与 "error" 进行比较(不区分大小写,因此 "Error" 和 "ERROR" 都没问题)。一个笔误——"fail"、带尾随空格的 "error "、"errors"——会让 golden 期望成功,而一个本应 固定失败结果的夹具就会悄然开始断言相反的结果。请只使用 "ok" 或 "error"。

添加夹具

  1. 将源文件放入该层的目录——Tests/Corpus/IR/<Area>/X.dss、 Tests/Corpus/Legacy/Compile/Material/M_X.dsm、…… 使用层前缀;对负向用例在名称主干中添加 .bad.。
  2. 可选地添加 <same stem>.expected.json,并包含上表中字段的任意子集。没有 golden 的正向用例 仍断言“必须成功”;没有 golden 的 .bad. 用例断言“必须失败”。
  3. 不要编写 C++,也不要重新编译。 每个运行器都会在下次运行时枚举其目录树。
  4. 运行该层一次以查看新的子测试出现: -ExecCmds="Automation RunTests DreamShader.Lang.Parse; Quit"。
  5. 要从实际结果引导生成 golden,请添加 -DreamShaderUpdateGolden 并再次运行,然后在提交前 手动审查写出的 JSON。新的文本、IR 或图形 golden 在写入时会设置 其 ...Pending 标志;在逐行阅读 golden 后删除该标志。

重新生成 golden

在命令行中加上 -DreamShaderUpdateGolden 时,每个运行器会写出 golden 而不是 对其进行断言,并记录 Updated golden '{Path}'.;若写入失败,则记录 Failed to write golden '{Path}'.

Parse/ writer 输出什么:entryPoint: "parse"、outcome,然后是 errorContains: [<the codes>] 或一个 definition 对象,包含 name(为空时省略)、 outputDeclarations、outputs、materialFunctions、materialFunction0Kind(仅当至少存在一个时)、 virtualFunctions、codeNotEmpty 和 settings(仅当非空时);另外,当解析产生警告时, 会有一个顶层 warningsContain。其他层的 writer 及其 schema 在 语料库 README 中描述。

输出为格式化美观的 JSON。

[!WARNING] -DreamShaderUpdateGolden 会按代码当前的行为重写 golden。在未审查的更改之后运行它 会把回归当作新的基线接受。提交前请对每个被触及的 .expected.json 进行差异对比,并且绝不要把该开关放入 CI 任务。

诊断

运行器发出的断言和基础设施消息。运行时替换显示为 {Placeholder};{Case} 是夹具的绝对源路径,而不是其子测试名称。

消息层原因
[{Case}] the legacy front end should REFUSE this sourceParse夹具被接受但预期失败 (自 2.0.0 起)
[{Case}] the legacy front end should ACCEPT this source but reported: {Error}Parse夹具被拒绝但预期成功 (自 2.0.0 起)
[{Case}] an error contains '{Substring}' (actual: {Error})Parse未找到 errorContains 条目
[{Case}] a warning contains '{Substring}' (actual: {Warnings})ParsewarningsContain 条目未匹配任何警告
[{Case}] name == '{Expected}' (actual '{Actual}')Parsedefinition.name 不匹配
[{Case}] outputDeclarationsParsedefinition.outputDeclarations 不匹配
[{Case}] outputsParsedefinition.outputs 不匹配
[{Case}] materialFunctionsParsedefinition.materialFunctions 不匹配
[{Case}] materialFunction0KindParsedefinition.materialFunction0Kind 不匹配
[{Case}] virtualFunctionsParsedefinition.virtualFunctions 不匹配
[{Case}] codeNotEmpty == {Value}Parsedefinition.codeNotEmpty 不匹配
[{Case}] setting '{Key}' presentParsedefinition.settings 中的某个键缺失
[{Case}] setting '{Key}' == '{Expected}' (actual '{Actual}')Parse该键的值不同
Cannot read corpus source '{Path}'.all无法读取夹具文件
Cannot read golden '{Path}'.all.expected.json 存在但无法读取
Malformed golden '{Path}': {Reason}allgolden 不是有效 JSON(invalid JSON)或无法解码
Updated golden '{Path}'.all-DreamShaderUpdateGolden 写入了 golden
Failed to write golden '{Path}'.all-DreamShaderUpdateGolden 无法写入它
DreamShader {layer} corpus test invoked without a source path.every runner子测试以空命令参数启动;{layer} 是 parse、lang、IR、legacy parse、legacy IR、legacy compile、compile、instance、decompile、migrate 或 roundtrip(该运行器为 DreamShader RoundtripIR test …)
Failed to write DreamShader automation source file '{Path}'.harness测试无法写入其临时源文件

1.x Generate 运行器及其 generation should … 消息已消失 (自 2.0.0 起):其夹具 为 Legacy/Compile/,通过 2.0 管线编译。其他层的断言消息位于 其运行器中,以及 Tests/Corpus/README.md 中。

示例

随附的负向夹具,用于固定“每个解析单元一个 Shader”规则。 Tests/Corpus/Parse/TopLevel/T_TwoShaders.bad.dsm:

Shader(Name="DreamShaderTests/Corpus/M_First")
{
    Settings = { Domain = "UI"; ShadingModel = "Unlit"; }
    Outputs = { vec3 Color; Base.EmissiveColor = Color; }
    Graph = { Color = vec3(1.0, 0.0, 0.0); }
}

Shader(Name="DreamShaderTests/Corpus/M_Second")
{
    Settings = { Domain = "UI"; ShadingModel = "Unlit"; }
    Outputs = { vec3 Color; Base.EmissiveColor = Color; }
    Graph = { Color = vec3(0.0, 1.0, 0.0); }
}

仅 .bad. 名称主干本身会断言“解析必须失败”。其 golden, T_TwoShaders.bad.expected.json,还固定了旧版前端为第二个 Shader 块抛出的代码,DSH2250:

{
	"entryPoint": "parse",
	"outcome": "error",
	"errorContains": ["DSH2250"]
}

运行它:

& "C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" --% "D:\Work\MyProject\MyProject.uproject" -ExecCmds="Automation RunTests DreamShader.Lang.Parse.TopLevel; Quit" -nullrhi -unattended -nopause -nosplash -log

新的子测试显示为:

DreamShader.Lang.Parse.TopLevel.T_TwoShaders.bad

另请参阅

  • 贡献 — 构建插件及源码树布局
  • 发布 — 发布归档中为何不含 Tests/
  • Commandlet — -run=DreamShader,另一个无头入口点
  • 编辑器桥接 — -NoDreamShaderEditorBridge 会关闭什么
  • 预览 — 一致性测试所驱动的渲染器
  • 后端 — Graph 与 ThinCustom,一致性用例所比较的维度
  • 项目设置 — DefaultBackend,Compile/ 运行器将其固定为 Graph
  • DreamShaderLang — ParseDreamShaderLang 以及文本层调用的所有其他入口点
  • 诊断索引 — 测试夹具所断言的各类消息
  • 版本兼容性 — DREAMSHADER_WITH_SUBSTRATE_BUILTINS 与 UE 5.4 跳过

本页目录