TiXL 新建项目构建失败的消息分诊设计:从手动测试规范看错误分类、友好提示与 Bug 上报机制
2026/9/19 6:39:57 网站建设 项目流程

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-messagesadded-in-version: 4.3scope: project-creationtags: [dev, edge, essential],并把测试场景定位为「dev / 边界情况 / 必备」三个级别的组合。这是一份手动测试(manual test)规范,不是自动化测试脚本,因此它严格按照「Action / Expected / Cleanup」三段式编写,并要求测试者具备以下前置条件:

  • TiXL 处于关闭状态——多个步骤需要修改 PATH 或在启动编辑器前移动文件;
  • 管理员权限——部分步骤会临时重命名系统文件(dotnet.exe);
  • 一个可安全删除的 scratch 项目目录——在Settings → Project Directories下配置,测试后可整体删除。

测试的统一入口是Editor菜单 →FileNew 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遍历所有EditorSymbolPackageAssemblyInformation.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 projectFailed to create new project
按钮OkCopy 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_FAILEDExplainBuildFailure随后识别该前缀:

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=falseNuGetAudit=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能识别的两类模式,因此explanationnull,落入未知分支。此时消息框提供两个按钮(见 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重命名/移除dotnetCould not create new projectOk未找到 dotnet、下载链接、重启提示
NU1100模板 TFM 改为未安装版本Could not create new projectOkNuGet 无法解析 TFM、targeting pack 提示、下载链接、空文件夹清理提醒
通用构建失败源文件引入语法错误Failed to create new projectCopy error and go to report page剪贴板失败日志、浏览器打开 Issues 页
文件系统失败只读/不存在项目目录Failed to create new projectCopy error and go to report pageFailed to create project files on disk:+ OS 错误
空 ProjectDirectoriesuserSettings.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询