Aspire 测试解除隔离政策(Unquarantine Policy)详解:21 天全平台零失败判定标准与修复流程
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
本文基于 Aspire 仓库的 docs/unquarantine-policy.md 官方政策文档,系统讲解被隔离(Quarantined)的 flaky 测试在何种条件下可以解除隔离、修复者应该做什么与不该做什么,并结合仓库中的隔离测试属性实现、隔离 CI 工作流、复现工作流与 QuarantineTools 源码,给出可直接落地的实操流程与底层原理。读完本文,你将掌握 Aspire 仓库中"修复偶发失败测试 → 验证修复 → 等待观测窗口 → 解除隔离"的完整闭环,以及其中 21 天观察期背后的统计依据。
一、从隔离到解除隔离:先理解测试隔离机制
在进入解除隔离政策之前,需要先建立对"隔离测试"的完整认知。Aspire 仓库将"当前已知不稳定(flaky)但尚未修复"的测试标记为隔离测试,使其从常规 CI 中排除,避免产生误报(false negatives),这一机制的详细说明见 docs/quarantined-tests.md。
隔离的载体是[QuarantinedTest]特性,其源码实现位于 tests/Aspire.TestUtilities/QuarantinedTestAttribute.cs。该特性实现了 xUnit 的ITraitAttribute,在编译期把quarantined=true这一 trait 附加到测试方法(或类、程序集)上,随后 CI 脚本与命令行过滤器就可以据此包含或排除这些测试:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] public sealed class QuarantinedTestAttribute : Attribute, ITraitAttribute { public string? Reason { get; } public QuarantinedTestAttribute(string? reason = null) { Reason = reason; } public IReadOnlyCollection<KeyValuePair<string, string>> GetTraits() => [new KeyValuePair<string, string>("quarantined", "true")]; }Reason参数通常用于存放追踪 issue 的链接或说明文字。例如 docs/quarantined-tests.md 中给出的用法:
[Fact] [QuarantinedTest("https://github.com/microsoft/aspire/issues/7920")] public async Task FlakyTest() { // Test implementation }常规 CI 通过--filter-not-trait "quarantined=true"排除隔离测试;而隔离 CI(quarantine CI)则用--filter-trait "quarantined=true"反向只跑隔离测试,用于持续观测其稳定性。隔离测试的完整生命周期为:标记为隔离 → 在隔离 CI 中持续运行观测 → 修复底层问题 → 满足解除隔离条件后移除特性。
而"何时可以移除[QuarantinedTest]特性"正是 docs/unquarantine-policy.md 这份解除隔离政策文档要回答的核心问题。
二、核心政策:21 天 × 全平台零失败
解除隔离政策的正文只有一条硬性标准(docs/unquarantine-policy.md):
一个被隔离的测试,只有在隔离 CI 运行中,于所有操作系统(Windows、Linux、macOS)上连续至少 21 天保持零失败,才允许被解除隔离。
这里有两个关键限定词:
- "所有操作系统":不是"修复者本地通过"、也不是"某一个平台通过",而是 Windows、Linux、macOS 三个平台全部通过。因为不少 flaky 问题具有平台特异性——例如路径分隔符、文件系统行为、Docker/容器运行时差异、密钥链(keychain)行为等,只在特定平台暴露。
- "连续 21 天":强调的是持续稳定性(sustained reliability),而非单次或短时间的观察结果。一次偶然的绿色不能证明修复是持久的。
政策文档同时强调了一个容易被误解的前提:仅仅提交代码修复是不足以解除隔离的("A code fix alone is not sufficient")。修复只是必要条件,而不是充分条件;充分条件是在观测窗口内被数据证明稳定。
观测数据从哪来
政策文档明确指出:每个测试对应的 GitHub issue(从 meta-issue 8813 关联而来)中的失败跟踪数据会按操作系统分别记录通过/失败率。也就是说,解除隔离的决策依据是"每个平台的通过/失败率数据",而非代码审查或主观判断。
关于观测频率的补充说明
政策文档原文表述隔离 CI 工作流(tests-quarantine.yml)每 6 小时运行一次。从当前仓库的实际工作流配置 .github/workflows/tests-quarantine.yml 看,其调度 cron 表达式为'0 */2 * * *'(即每 2 小时一次),并在注释中说明"频繁运行隔离测试是为了捕获低失败率的 flaky 测试"。无论按文档的 6 小时还是当前配置的 2 小时计算,观测数据点的数量级是一致的,且更高的运行频率只会让数据更充分。
三、为什么是 21 天:低频偶发失败的统计依据
21 天不是拍脑袋的数字,政策文档给出了明确的计算依据:
部分 flaky 测试的失败率很低(1%~5%),或者只在特定条件下失败(负载、时段、runner 硬件差异)。
这类低频偶发失败的特点是:单次或几次运行很难"撞见"失败,因此修复是否真正生效也无法用少量样本判断。按"每 6 小时运行一次 × 21 天"计算,每个操作系统可积累约84 个数据点,三个平台合计约252 个数据点。即便某个测试的潜在失败率低至 1%~5%,在 252 个样本中,如果修复有效(真实失败率归零),出现连续全绿的概率极高;反过来,如果修复无效,残留的偶发失败几乎必然会在如此多的样本中暴露出来。这就是 21 天窗口能够提供"高置信度"(high confidence)的原因——它把"修复是否持久"这一判断建立在统计学上有意义的样本量之上。
四、修复隔离测试的正确姿势:DO 与 DO NOT
政策文档对"修复者"给出了非常具体的操作纪律,共四条,前两条是"应该做",后两条是"绝对不要做":
| 类别 | 操作 | 说明 |
|---|---|---|
| ✅ DO | 应用修复并推送(apply your fix and push it) | 正常提交代码修复 |
| ✅ DO | 通过复现工作流验证修复(reproduce-flaky-tests.yml) | 在提交前用专门工作流压测验证 |
| ❌ DO NOT | 在同一个 PR 里解除隔离 | 必须保留[QuarantinedTest]特性 |
| ❌ DO NOT | 关闭跟踪 issue | 21 天窗口的监控由独立流程负责,达标后由其关闭 issue |
修复后的真实走向
政策文档描述了解除隔离的完整时序:
- 修复者推送修复代码,测试继续留在隔离 CI 中运行;
- 如果修复正确,失败率会逐步下降到 0%;
- 连续 21 天零失败后,由独立流程(或人工)执行解除隔离操作并关闭跟踪 issue。
这里的关键设计是职责分离:修复者只负责修代码和验证,而"判定是否达标"由独立的监控流程根据跟踪数据完成。这正是第一节政策文档中"决策由审查跟踪数据的一方做出,而不是由提交修复的 agent 做出"的体现——防止"自己修复、自己判定、自己解除"带来的利益冲突与误判风险。
五、21 天窗口背后的 CI 机制:隔离测试如何持续运行与观测
解除隔离政策依赖的观测数据,来自隔离 CI 的持续运行。这一机制的实现细节在 .github/workflows/tests-quarantine.yml 中,包含三种触发方式:
- 定时调度(schedule):cron 每 2 小时运行一次,用于频繁捕获低失败率的 flaky 测试;
- 手动触发(workflow_dispatch):可按需手动执行;
- PR 触发(pull_request):路径过滤仅限
tests-quarantine.yml、specialized-test-runner.yml、run-tests.yml、build-cli-e2e-image.yml等编排类工作流文件本身——即只有改动 CI 基础设施时才触发一次隔离测试作为 sanity check,避免每次 CI/eng 改动都触发全量隔离/外环测试(工作流注释中明确记录了此前因路径过滤过宽导致磁盘空间问题的历史)。
隔离测试的执行管线
quarantine_tests作业复用了专门的specialized-test-runner.yml工作流,关键参数为:
testRunnerName: "QuarantinedTestRunsheetBuilder":使用隔离测试专用的 runsheet 生成器;attributeName: "QuarantinedTest":按该特性筛选测试;extraRunSheetBuilderArgs: "-p:RunQuarantinedTests=true":开启隔离测试运行模式;extraTestArgs: "--filter-trait quarantined=true":只运行带quarantined=truetrait 的测试;ignoreTestFailures: true:隔离测试的失败不导致流水线变红。
最后一项是理解"21 天观测"的关键:隔离测试本来就是为了容忍失败而存在的,所以其失败会被ignoreTestFailures吞掉,不会阻塞 PR 合并;失败数据通过测试结果文件(TRX)被记录和跟踪,用于后续的稳定性评估。
Runsheet 生成器的 MSBuild 实现位于 eng/QuarantinedTestRunsheetBuilder/QuarantinedTestRunsheetBuilder.targets,它导入共享基类 eng/SpecializedTestRunsheetBuilderBase.targets,并配置:
<PropertyGroup> <SpecializedTestType>quarantined</SpecializedTestType> <SpecializedTestProperty>/p:RunQuarantinedTests=true</SpecializedTestProperty> <SpecializedTestRunsheetBuilderName>QuarantinedTestRunsheetBuilder</SpecializedTestRunsheetBuilderName> </PropertyGroup>共享基类负责:先以--list-tests列出匹配的测试(用退出码 8 表示"无匹配测试"),再为 Windows(build.ps1)与 Linux/macOS(build.sh)分别生成 JSON runsheet,供 GitHub Actions 在三个平台矩阵上执行。整个流程还会跳过 Playground 类测试项目。
基础设施失败与测试失败的分流
工作流中还有一个值得注意的设计:report_infra_failure作业在隔离测试作业失败时触发(仅限定时调度且仓库为上游时)。由于隔离测试失败被ignoreTestFailures吞掉,一个失败的隔离运行只能意味着基础设施坏了,此时会去创建/追加一个去重后的 "automation-broken" issue;而测试本身的失败不会生成 issue——失败数据只作为观测记录存在。这保证了 21 天零失败统计的纯净性:观测到的每一次失败都是测试自身的失败,而不是 CI 基础设施故障的噪声。
六、修复后的第一道验证:reproduce-flaky-tests 工作流
政策文档明确要求修复者在推送后用reproduce-flaky-tests.yml验证修复。该工作流的完整配置见 .github/workflows/reproduce-flaky-tests.yml,其设计目标是在多个 OS runner 上反复运行指定测试,以压测方式确认修复的有效性。
使用步骤(官方注释)
- 修改文件顶部
CONFIGURATION段的 env 变量,填入要复现的测试; - 推送到自己的分支;
- 手动触发:
gh workflow run reproduce-flaky-tests.yml --repo <repo> --ref <your-branch>; - 在多个 OS 与 runner 上检查结果。
可配置参数
| 环境变量 | 作用 | 示例/取值范围 |
|---|---|---|
TEST_PROJECT | 测试项目短名(映射到tests/Aspire.{name}.Tests/或tests/{name}.Tests/) | Hosting、Core、Dashboard、Components.Common、Cli.EndToEnd |
TEST_FILTER | 传给dotnet test的过滤参数(放在--之后) | --filter-method "*.TestMethodName"、--filter-class "*.TestClassName",可多个叠加 |
TARGET_OSES | 目标操作系统(逗号分隔) | ubuntu-latest,windows-latest,macos-latest |
RUNNERS_PER_OS | 每个 OS 的并行 runner 数 | 正整数,总作业数 = RUNNERS_PER_OS × OS 数(≤ 256) |
ITERATIONS_PER_RUNNER | 每个 runner 执行的迭代次数 | 正整数 |
工作流的setup作业会先校验配置合法性:RUNNERS_PER_OS与ITERATIONS_PER_RUNNER必须是正整数、TARGET_OSES只能包含三个已知 runner、TEST_FILTER不能仍是占位符YourTestMethodName,否则直接报错退出,避免浪费 runner 资源。
迭代执行与零测试检测
reproduce作业按"OS × runner 序号"生成矩阵,每个 runner 独立循环执行指定迭代次数。每次迭代之间会清理testresults目录并杀掉残留的dcp进程,避免上次运行的状态污染本次结果。每次迭代使用:
dotnet test --project <项目路径> --no-restore --no-build -- \ --timeout 10m \ --results-directory .../testresults \ <TEST_FILTER>值得注意的是零测试检测逻辑:由于仓库在Testing.props中配置了--ignore-exit-code 8,即使一个测试都没匹配上,进程退出码也可能是 0。因此工作流会解析输出日志,若退出码为 0 但日志中没有total: [1-9]之类的计数,就判定为"零测试执行"并以退出码 8 处理——避免"假通过"掩盖过滤配置错误。整个运行结束后会输出汇总表(项目、过滤条件、OS、runner 数、迭代数),任一 runner 有失败则整体失败。
七、真正移除隔离标记:QuarantineTools 命令行工具
当 21 天零失败的观测期结束、由独立流程判定达标后,解除隔离的具体操作——从源码中移除[QuarantinedTest]特性——可由仓库自带的命令行工具完成。其实现位于 tools/QuarantineTools/Quarantine.cs,基于 Roslyn(Microsoft.CodeAnalysis)对测试源码做结构化的安全编辑,而不是粗暴的文本替换。
命令格式
# 隔离一个测试(必须提供 issue URL) quarantine -q Namespace.Type.Method -i <issue-url> # 解除隔离一个或多个测试 quarantine -u Namespace.Type.Method核心参数
| 参数 | 说明 |
|---|---|
-q/--quarantine | 隔离模式,为指定测试添加[QuarantinedTest("<issue-url>")] |
-u/--unquarantine | 解除隔离模式,移除指定测试上的[QuarantinedTest]特性 |
-i/--url | 隔离时必填的 issue URL(必须是合法的 http/https 地址) |
-m/--mode | 模式:quarantine(默认)或activeissue(操作Xunit.ActiveIssueAttribute) |
-a/--attribute | 自定义要增删的特性全名,默认按 mode 推断 |
-r/--root | 要扫描的 tests 根目录,默认<repo>/tests |
工具的工作原理
从 tools/QuarantineTools/Quarantine.cs 源码看,其执行流程为:
- 定位仓库根:向上查找
.git目录,默认扫描tests下的全部.cs文件(跳过bin、obj、.git、artifacts、node_modules等目录); - 预过滤:先用正则快速判断文件是否包含目标方法名或特性名,避免对所有文件做昂贵的 Roslyn 解析;
- 解析语法树:对候选文件构建 C# 语法树,定位方法声明,并计算其所在的命名空间与嵌套类型链,与用户传入的
Namespace.Type.Method(支持+嵌套类型语法)精确匹配; - 增删特性:隔离时若方法上尚无该特性则追加
[QuarantinedTest("issue-url")],并确保文件包含using Aspire.TestUtilities;;解除隔离时移除特性,且仅在文件中不再有任何QuarantinedTest特性残留时才移除 using 指令; - 幂等与安全:若目标已处于期望状态则不做任何修改;编辑时保留文件原有换行风格(CRLF/LF),只写回发生变化的文件,并打印更新清单。
这种"按语法树编辑"的方式避免了文本替换可能破坏代码格式、注释或错误命中同名标识符的问题,是解除隔离操作值得推荐的安全路径。对应的单元测试见 tests/QuarantineTools.Tests/QuarantineScriptTests.cs。
八、日常排查与过滤命令速查
无论是验证修复还是调试隔离测试,都需要掌握基于 trait 的过滤命令。以下命令整理自 docs/quarantined-tests.md:
排除隔离测试运行(常规 CI 的做法):
dotnet test --filter-not-trait "quarantined=true"使用直接测试运行器:
dotnet exec YourTestAssembly.dll --filter-not-trait "quarantined=true"只运行隔离测试(排查/调试用):
dotnet test --filter-trait "quarantined=true"dotnet exec YourTestAssembly.dll --filter-trait "quarantined=true"对于在自动化环境(如 Copilot agent)中运行测试的场景,docs/quarantined-tests.md 特别强调始终使用隔离过滤以避免误报:dotnet test --filter-not-trait "quarantined=true"是正确的打开方式,而裸的dotnet test会把隔离测试一并跑进来,产生无意义的失败信号。
九、例外情形:何时可以缩短观察期
政策文档在严格标准之外给出了两条例外,均需 maintainer 酌情判断(at maintainer discretion):
- 基础设施缺陷导致的隔离:如果测试是因为测试基础设施的 bug(而非时序/竞态问题)被隔离,且修复明显正确,则可以采用更短的观察期。理由很直观:基础设施缺陷的修复通常是一次性的、确定性的,不需要用大量样本去排除低频偶发因素。
- 超高失败率骤降为零:如果隔离前失败率极高(>80%),且修复后立即降到 0%,maintainer 可以选择提前解除隔离。>80% 的失败率意味着问题几乎必然复现,修复后"立即归零"本身就具有很高的证据价值。
但无论哪种例外,政策都强调一条不可动摇的原则:解除隔离的最终决策由审查跟踪数据的一方做出,而不是由提交修复的 agent 决定("In all cases, the decision to unquarantine is made by reviewing the tracking data, not by the agent that applied the fix")。这是整个政策体系中防止"既当运动员又当裁判"的关键防线。
十、总结:解除隔离的完整决策链
综合 docs/unquarantine-policy.md 与仓库实现,Aspire 的解除隔离政策可以概括为一条完整、可审计的决策链:
- 隔离:不稳定测试通过
[QuarantinedTest("reason")](实现于 tests/Aspire.TestUtilities/QuarantinedTestAttribute.cs)打上quarantined=truetrait,退出常规 CI; - 持续观测:隔离 CI(.github/workflows/tests-quarantine.yml)按 cron 定时在 Windows、Linux、macOS 三平台运行全部隔离测试,失败不阻塞合并、只记录数据,基础设施故障与测试失败严格分流;
- 修复与验证:修复者提交修复后,用 .github/workflows/reproduce-flaky-tests.yml 在多个 OS/runner 上反复压测验证,但不移除隔离标记、不关闭跟踪 issue;
- 数据达标:连续 21 天、三平台零失败(约 252 个数据点)后,由独立流程/维护者根据 per-OS 通过率数据判定达标;
- 解除隔离:使用 tools/QuarantineTools/Quarantine.cs 提供的
-u模式结构化移除[QuarantinedTest]特性并清理 using 指令,测试回归常规 CI 队列。
这套政策的核心价值在于:把"修复 flaky 测试"这种容易陷入主观判断的工程活动,转化为由统计样本量、跨平台观测与职责分离共同保证的客观流程——对维护者、对自动化的 Copilot 类 agent、对 CI 系统本身,都给出了清晰且可执行的规则边界。相关配套文档可进一步阅读 docs/quarantined-tests.md(隔离机制与过滤命令)以及 docs/ci/specialized-test-failure-issues.md(专项测试失败 issue 的分流策略)。
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考