TiXL 新建项目构建失败的消息分诊设计:从手动测试规范看错误分类、友好提示与 Bug 上报机制
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
本文围绕仓库中的手动测试规范 .tests-manual/build-failure-messages.md 展开,结合 NewProjectDialog.cs、Compiler.cs、ProjectSetup.cs 等源码实现,系统拆解 TiXL 在「新建项目」这一首次用户必经路径上的构建失败消息设计:错误如何被分类(已知原因 / 未知原因)、如何给出可操作的修复提示、如何触发 Bug 上报,以及文件系统异常与配置缺失时的防御回退。读完本文,你将掌握这套错误分诊机制的完整测试用例、源码级判定逻辑,以及如何在真实环境下复现与验证每一个失败分支。
为什么「新建项目失败路径」是绝对不能搞错的地方
TiXL 是一个用于创建实时动态图形(realtime motion graphics)的开源软件,用户安装后的第一个动作往往就是File → New Project…创建项目。如果这一步就出现构建失败,弹出的对话框是用户对产品的第一印象——因此测试规范开篇就强调:
These paths are catastrophic to get wrong — a brand-new user whose first action is "create project" sees only this dialog, so the message has to be intelligible without context.
(这些路径一旦出错就是灾难性的——全新用户的第一动作就是「创建项目」,他只会看到这一个对话框,所以消息必须在没有任何上下文的情况下也能看懂。)
文档 front-matter 给出元数据:id: build-failure-messages、added-in-version: 4.3、scope: project-creation、tags: [dev, edge, essential],并把测试场景定位为「dev / 边界情况 / 必备」三个级别的组合。这是一份手动测试(manual test)规范,不是自动化测试脚本,因此它严格按照「Action / Expected / Cleanup」三段式编写,并要求测试者具备以下前置条件:
- TiXL 处于关闭状态——多个步骤需要修改 PATH 或在启动编辑器前移动文件;
- 管理员权限——部分步骤会临时重命名系统文件(
dotnet.exe); - 一个可安全删除的 scratch 项目目录——在
Settings → Project Directories下配置,测试后可整体删除。
测试的统一入口是Editor菜单 →File→New Project…,填写 name + namespace 后点击Create。所有场景共享同一条硬性验收底线:TiXL 绝不能崩溃,也绝不能把原始堆栈直接甩给用户。
基线验证:Happy Path 与对话框的表单校验
测试步骤与验收
Action:正常启动 TiXL(PATH 不变、.NET SDK 已安装),打开 New Project 对话框,填写唯一项目名(如TestHappyPath),命名空间留空,点击Create。
Expected:
- 对话框正常关闭;
- 不出现任何错误消息框;
- 新项目文件夹出现在配置的项目目录下,且包含
.csproj; - 编辑器 Console 窗口输出
Project "TestHappyPath" created successfully!。
源码佐证
对话框由 NewProjectDialog.cs 实现,它继承自ModalDialog,通过Draw()每帧绘制。在点击Create之前,表单会做多级校验:
- 命名空间校验:
GraphUtils.IsNamespaceValid(_newSubNamespace, true, out _)不通过时提示Namespace must be a valid and unique C# namespace; - 项目名校验:必须是合法 C# 标识符(否则提示
Name must be a valid C# identifier.)、不能包含点(Name must not contain dots.)、不能为空、不能与已有项目重名(DoesProjectWithNameExists遍历所有EditorSymbolPackage的AssemblyInformation.Name末段,忽略大小写比较); - 全名拼接:
fullName = $"{_userName}.{subNameSpaceWithSeparator}{_newProjectName}",预览文本用UiColors.TextMuted(合法)或UiColors.StatusError(非法)着色; - Share Resources 选项:默认勾选,取消时会以警告色提示「日后无法在不编辑项目代码的情况下修改该选项」。
只有allValid为真时Create按钮才可点击。成功路径调用:
if (ProjectSetup.TryCreateProject(fullName, _shareResources, out var project, out var failureLog)) { T3Ui.Save(false); ImGui.CloseCurrentPopup(); Log.Debug($"Project \"{project.DisplayName}\"created successfully! It can be opened from the project list."); }对应文档验收中的 Console 输出(源码实际输出带后续说明文字)。注意源码当前把成功消息放在Log.Debug,与文档描述的 Console 可见性一致。
对话框底部还有一行 hint 文本,用于回退场景下的路径可见性验证(见后文「空 ProjectDirectories」一节):
Creates a new project. Projects are used to group operators and resources. You can find your project in "{projectFolder}".其中projectFolder = Path.Combine(primaryProjectDirectory, _newProjectName)。
双分支架构:把构建失败「分诊」为已知原因与未知原因
这是整套机制的核心。ProjectSetup.TryCreateProject一旦失败,会通过out string? failureLog返回失败日志;对话框拿到日志后调用Compiler.ExplainBuildFailure(failureLog)做模式识别(见 NewProjectDialog.cs):
var explanation = Compiler.ExplainBuildFailure(failureLog); string message; string caption; if (explanation != null) { // Known cause(如缺少 .NET targeting pack)——给出可执行的下一步, // 而不是通用的「去提 Bug」。 caption = "Could not create new project"; message = $""" Failed to create project "{_newProjectName}" in "{_newSubNamespace}". {explanation} The empty project folder has been left on disk; you may want to delete it before retrying. """; BlockingWindow.Instance.ShowMessageBox(message, caption, "Ok"); } else { // Unknown cause——通用失败分支,引导用户上报。 caption = "Failed to create new project"; message = $""" Failed to create project "{_newProjectName}" in "{_newSubNamespace}". This should never happen - please file a bug report. ... """; var result = BlockingWindow.Instance.ShowMessageBox(message, caption, buttons: "Copy error and go to report page"); ... }两个分支的行为差异非常明确,也直接对应测试规范中的验收点:
| 维度 | 已知原因分支(known cause) | 未知原因分支(unknown cause) |
|---|---|---|
| 对话框标题 | Could not create new project | Failed to create new project |
| 按钮 | 仅Ok | Copy error and go to report page(及含环境变量的长变体) |
| 消息内容 | 具体修复提示 + 空文件夹清理提醒 | 「这不应该发生,请提交 Bug 报告」 |
| 是否上报 | 否 | 是(复制日志到剪贴板并打开 GitHub Issues 页面) |
ExplainBuildFailure的判定逻辑在 Compiler.cs:识别到已知模式返回人类可读提示,否则返回null落到通用分支。目前只有两类已知模式被识别,下面两节逐一讲解。
场景一:DOTNET_NOT_FOUND —— .NET SDK 缺失的友好提示
测试步骤与验收
Action:关闭 TiXL,在管理员命令提示符中临时重命名dotnetshim,使其在 PATH 上不再可解析:
ren "C:\Program Files\dotnet\dotnet.exe" dotnet.exe.disabled(或者从 PATH 环境变量中移除dotnet目录,并从一个不含它的新 shell 启动 TiXL。)
启动 TiXL,打开 New Project,填写唯一项目名,点击Create。
Expected:
- TiXL不崩溃;
- 弹出标题为
Could not create new project的对话框; - 正文同时包含三要素:
dotnet命令在 PATH 中未找到;- 指向
https://dotnet.microsoft.com/download的链接/URL; - 若刚安装完 dotnet,提示重新登录 / 重启以刷新 PATH;
- 对话框只有
Ok按钮(没有Copy error and go to report page——因为这是 known cause 分支,不是 unknown-failure 分支)。
Cleanup:测试后恢复 shim:
ren "C:\Program Files\dotnet\dotnet.exe.disabled" dotnet.exe源码佐证
这个分支的判定链值得完整追踪。TiXL 通过dotnetCLI 编译项目,在 Compiler.cs 的RunCommand中启动外部进程:
try { process.Start(); } catch (System.ComponentModel.Win32Exception e) { // Emit a marker ExplainBuildFailure recognises so the caller can show a useful hint. var marker = e.NativeErrorCode == 2 ? "DOTNET_NOT_FOUND" : "WIN32_EXEC_FAILED"; return ($"{marker}: {fileName}: {e.Message}", -1); }当进程无法启动且NativeErrorCode == 2(Windows 上对应「找不到指定文件」)时,会合成一个DOTNET_NOT_FOUND:前缀的标记行;其他启动失败则标记为WIN32_EXEC_FAILED。ExplainBuildFailure随后识别该前缀:
if (buildOutput.StartsWith("DOTNET_NOT_FOUND:", StringComparison.Ordinal)) { return "TiXL needs the .NET SDK to compile your projects, but the `dotnet` " + "command was not found in PATH.\n\n" + "Fix: install the .NET SDK from https://dotnet.microsoft.com/download. " + "If you just installed it, sign out and back in (or reboot) so PATH " + "refreshes — the installer doesn't push the change to already-running " + "processes."; }逐条对应文档验收:未找到 dotnet(was not found in PATH)、下载链接(https://dotnet.microsoft.com/download)、刚安装后需签出/重启(sign out and back in (or reboot))——三者全部命中,且没有任何「上报 Bug」的按钮。
顺带一提,RunCommand还会为子进程注入三个环境变量,这些细节在排查编译环境问题时很有用:
MSBUILDDISABLENODEREUSE=1:禁用 MSBuild 节点复用,避免跨编译复用陈旧节点;DOTNET_CLI_TELEMETRY_OPTOUT=1:关闭 .NET CLI 遥测;DOTNET_MULTILEVEL_LOOKUP=0:关闭多级查找,防止意外解析到非预期位置的 SDK。
此外编译设有3 分钟超时:process.WaitForExit(3*60*1000)超时后process.Kill(true)强杀进程树,并CancelOutputRead/CancelErrorRead防止管道阻塞。
场景二:NU1100 —— targeting pack 缺失的友好提示
测试步骤与验收
Action:将 .NET SDK 恢复到 PATH 后重启 TiXL。打开 New Project,填写唯一项目名(如TestNu1100),在点击 Create 之前,编辑磁盘上正在使用的.csproj模板(文档给出的路径是<repo>/Resources/default-home/或你环境中的等价路径),把<TargetFramework>改成你确定没有安装的 TFM,例如net99.0-windows。点击Create。
Expected:
- TiXL不崩溃;
- 弹出标题为
Could not create new project的对话框; - 正文同时包含三要素:
NuGet could not resolve ... for target framework 'net99.0-windows';.NET SDK / targeting pack for 'net99.0-windows'未安装;- 指向
https://dotnet.microsoft.com/download的链接/URL;
- 磁盘上存在空的
TestNu1100文件夹——对话框会提示用户重试前可能想删除它。
Cleanup:恢复.csproj模板,删除半创建的TestNu1100文件夹。
源码佐证
ExplainBuildFailure用一个编译期正则识别 NuGet 的 NU1100 错误码(Compiler.cs):
private static readonly Regex _nu1100Regex = new( @"NU1100:\s*Unable to resolve '([^']+)' for '([^']+)'", RegexOptions.Compiled | RegexOptions.IgnoreCase);匹配成功后,把捕获到的包名(pkg)与目标框架(tfm)动态填入提示模板:
var nu1100 = _nu1100Regex.Match(buildOutput); if (nu1100.Success) { var pkg = nu1100.Groups[1].Value; var tfm = nu1100.Groups[2].Value; return $"NuGet could not resolve '{pkg}' for target framework '{tfm}'.\n\n" + $"This usually means the .NET SDK / targeting pack for '{tfm}' " + $"is not installed on this machine.\n\n" + $"Fix: install the matching .NET SDK from https://dotnet.microsoft.com/download " + $"(make sure it includes the targeting pack for '{tfm}'), or update the project's " + $"<TargetFramework> to a TFM you do have installed."; }由此生成的正文与文档验收逐字对应。注意这里与 DOTNET_NOT_FOUND 一样,走的是known cause 分支——只有Ok按钮,不上报 Bug。
从编译链路看,NU1100 主要发生在dotnet restore阶段。TryCompile(Compiler.cs)将 restore 与 build 分离:先执行dotnet restore "<project>" --nologo -p:NuGetAudit=false(NuGetAudit=false是为了避免 restore 时下载审计数据库、在慢网络下拖慢数十秒且其警告用户也看不到),restore 失败即返回失败日志;成功后再执行dotnet build ... --no-restore --no-dependencies(Debug 构建跳过引用图遍历,直接针对编辑器已加载的依赖 DLL 编译)。restore 输出的错误会连同 build 输出一起进入failureLog,最终被ExplainBuildFailure识别。
关于「模板路径」的说明
文档中的模板编辑路径为<repo>/Resources/default-home/(或等价路径)。实际项目中,.csproj模板的具体落盘位置与安装/克隆布局相关,测试时以你环境中实际存放项目模板的位置为准——从源码结构看,最终创建文件的工作由CsProjectFile.CreateNewProject完成(见 ProjectSetup.cs),模板文件是它读取的输入。
场景三:通用构建失败 —— 回落到 Bug 上报分支
测试步骤与验收
Action:恢复模板后创建新项目(如TestGenericFail)。创建成功后,打开其主.cs源文件并引入一个语法错误(例如删除一个右大括号)。在编辑器中保存以触发重编译。
Expected:
- TiXL不崩溃;
- 弹出报告构建失败的对话框;
- 对话框包含
Copy error and go to report page按钮(以及含环境变量的长变体)——这是 unknown-cause 分支,用户被引导提交 Bug; - 点击该按钮会把失败日志复制到剪贴板,并在默认浏览器打开 GitHub Issues 页面。
Cleanup:恢复源文件,删除TestGenericFail文件夹。
源码佐证
语法错误不属于ExplainBuildFailure能识别的两类模式,因此explanation为null,落入未知分支。此时消息框提供两个按钮(见 NewProjectDialog.cs):
const string button = "Copy error and go to report page"; const string buttonWithEnvironmentVariables = "Copy error + environment variables and go to report page\n" + "(Most helpful, but check your list of variables before submitting to avoid leaking " + "sensitive information)"; var result = BlockingWindow.Instance.ShowMessageBox(message, caption, buttons: button); var hasResult = !string.IsNullOrWhiteSpace(result); ReportError(report: hasResult, includeEnvVars: result == buttonWithEnvironmentVariables || !hasResult, failureLog, fullName, primaryProjectDirectory);ReportError负责组装上报内容(同一文件的局部函数,第 189–226 行):
- Failure Log:完整构建输出(可选追加环境变量,
includeEnvVars为真时把Environment.GetEnvironmentVariables()拼接进## Environment variables:段落,取环境变量失败时会替换为异常消息); - Project details:TiXL 用户名、项目命名空间、项目名、项目全名、项目目录;
- Additional details:留空的注释占位符
<!--Insert any relevant additional details here, if any-->。
随后把组装好的文本SetClipboardText写入剪贴板,并用OpenWithDefaultApplication打开 GitHub Issues 页面。长按钮文案特意警告用户「提交前检查变量列表,避免泄露敏感信息」——这与长变体按钮的命名直接呼应。
场景四:文件系统失败 —— project files 创建失败的异常路由
测试步骤与验收
Action:关闭 TiXL。把项目目录配置为只读或不存在的路径——例如将Settings → Project Directories[0]指向一个已移除的 USB 驱动器上的路径,或指向C:\Windows\ReadOnlyTest\(普通用户通常无写权限)。启动 TiXL,打开 New Project,填写TestReadOnly,点击Create。
Expected:
- TiXL不崩溃;
- 弹出标题为
Failed to create new project的对话框; - 正文包含
Failed to create project files on disk:后跟底层 OS 错误; - 对话框包含
Copy error and go to report page按钮(通用失败分支——文件系统错误目前还没有 known-cause 提示)。
Cleanup:恢复项目目录。
源码佐证
文件系统失败走的是另一条通道:不是「编译失败」,而是创建文件本身抛异常。ProjectSetup.TryCreateProject用 try/catch 包住模板落盘操作(ProjectSetup.cs):
// Filesystem failures (OneDrive virtualisation, broken symlinks, antivirus, // missing/removed drives) surface as exceptions; route them through failureLog. CsProjectFile newCsProj; try { newCsProj = CsProjectFile.CreateNewProject(name, nameSpace, shareResources, UserSettings.Config.ProjectDirectories[0]); } catch (Exception e) { failureLog = $"Failed to create project files on disk: {e.Message}"; Log.Error(failureLog); newProject = null; return false; }源码注释直接列举了现实世界中会命中此分支的典型故障:OneDrive 虚拟化、损坏的符号链接、杀毒软件拦截、驱动器缺失/被移除。该失败日志以Failed to create project files on disk:开头,但ExplainBuildFailure并不识别它,所以落到通用失败分支——标题为Failed to create new project,并提供 Bug 上报按钮,与文档验收一致。也就是说,TryCreateProject的所有失败(文件系统异常、编译失败、release info 缺失、home guid 为空)最终都统一通过failureLog汇入同一个分诊入口,只是不同失败类型被识别为 known/unknown 的结局不同。
场景五:空 ProjectDirectories —— 防御性回退
测试步骤与验收
Action:关闭 TiXL。编辑userSettings.json(位于%APPDATA%\TiXL<version>\),把ProjectDirectories设为空数组[]。启动 TiXL——启动时应自动用默认文件夹填充它。打开 New Project,不点击 Create,观察对话框底部提示文本(Creates a new project. ... You can find your project in "...")。
Expected:
- TiXL 正常启动;
- 提示文本显示
Documents\TiXL<version>\下的有效路径(默认回退值),而不是缺失路径,也不崩溃; - 点击
Create成功。
Cleanup:无需清理——TiXL 已在启动时持久化了默认文件夹。
源码佐证
此场景防的是「用户设置文件损坏导致 ProjectDirectories 为空」的边界情况。对话框在 NewProjectDialog.cs 做了显式保护:
// ProjectDirectories is normally populated at startup but can be empty // on a corrupted user-settings file. Drawn every frame, so guard the indexer. var projectDirectories = UserSettings.Config.ProjectDirectories; var primaryProjectDirectory = projectDirectories.Count > 0 ? projectDirectories[0] : FileLocations.DefaultProjectFolder;由于Draw()每帧执行,若直接索引[0],空列表会抛异常导致崩溃——因此用Count > 0守卫并回退到默认目录。默认目录定义在 FileLocations.cs:
public static readonly string DefaultProjectFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), VersionedAppFolderName);其中VersionedAppFolderName形如TiXL4.3(preview 构建为TiXL4.3-alpha,也可通过环境变量TIXL_OVERRIDE_VERSION_ID或启动参数--override-version-id覆盖),与文档中的Documents\TiXL<version>\一致。设置目录同理:SettingsDirectory = %APPDATA%\TiXL<version>(FileLocations.cs),正是文档中编辑userSettings.json的位置。
此外 UserSettings.cs 在加载配置时会执行RemoveDuplicateProjectDirectories()去重,避免ProjectDirectories出现重复项影响[0]语义。文档中的 hint 文本(You can find your project in "...")正是上一节「Happy Path」部分展示的FormInputs.AddHint输出,其projectFolder使用的是上面计算出的primaryProjectDirectory——空列表时自然显示为默认回退路径。
这套机制的价值延伸:同一分诊器也在服务启动加载
值得注意的一点是,Compiler.ExplainBuildFailure并不只服务于「新建项目」。在 ProjectSetup.Startup.cs 的LoadProjects中,编辑器启动加载既有项目时若重编译失败,同样会调用它:
if (needsCompile && !csProjFile.TryRecompile(true, out var failureLog)) { Log.Error($"Failed to recompile project '{csProjFile.Name}:\n{failureLog}'"); var explanation = Compiler.ExplainBuildFailure(failureLog); if (explanation != null) Log.Warning($"Likely cause for '{csProjFile.Name}' compile failure:\n{explanation}"); return new ProjectLoadInfo(fileInfo, csProjFile, false, "The project failed to compile.", explanation ?? "See the log for details."); }也就是说,同一套「已知原因 → 可读提示 / 未知原因 → 详情记录」的分诊逻辑被复用于项目创建与项目加载两条路径。从源码结构可以推断,这正是测试规范把它标记为essential的原因:一套稳定的错误语义,同时降低了新建用户与老用户在升级后遇到 SDK 环境问题时的新手门槛。
测试要点总表
| 测试场景 | 触发方式 | 对话框标题 | 按钮 | 关键验收信息 |
|---|---|---|---|---|
| Happy Path | 正常环境创建 | 无错误框 | — | Console 输出created successfully!,.csproj落盘 |
| DOTNET_NOT_FOUND | 重命名/移除dotnet | Could not create new project | 仅Ok | 未找到 dotnet、下载链接、重启提示 |
| NU1100 | 模板 TFM 改为未安装版本 | Could not create new project | 仅Ok | NuGet 无法解析 TFM、targeting pack 提示、下载链接、空文件夹清理提醒 |
| 通用构建失败 | 源文件引入语法错误 | Failed to create new project | Copy error and go to report page | 剪贴板失败日志、浏览器打开 Issues 页 |
| 文件系统失败 | 只读/不存在项目目录 | Failed to create new project | Copy error and go to report page | Failed to create project files on disk:+ OS 错误 |
| 空 ProjectDirectories | userSettings.json置空数组 | 正常启动 | Create可用 | hint 显示Documents\TiXL<version>\回退路径 |
所有场景共享三条铁律:不崩溃、消息无需上下文即可读懂、known-cause 分支绝不把用户推向 Bug 上报。这套「分类 → 提示 → 上报」的错误分诊架构,加上 NewProjectDialog.cs、Compiler.cs 中可复现的判定逻辑与 ProjectSetup.cs 的异常路由,构成了 TiXL 新建项目体验的最后一道、也是最关键的一道防线。
【免费下载链接】t3TiXL is an open source software to create realtime motion graphics.项目地址: https://gitcode.com/GitHub_Trending/t3/t3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考