Skip to content

[Analyzers] Add analyzers for [EnvironmentRestorer] usage in tests#8402

Draft
lucaspimentel wants to merge 2 commits intomasterfrom
lpimentel/environmentrestorer-attribute-analyzer
Draft

[Analyzers] Add analyzers for [EnvironmentRestorer] usage in tests#8402
lucaspimentel wants to merge 2 commits intomasterfrom
lpimentel/environmentrestorer-attribute-analyzer

Conversation

@lucaspimentel
Copy link
Copy Markdown
Member

@lucaspimentel lucaspimentel commented Apr 2, 2026

Summary of changes

Add two new Roslyn analyzers to prevent environment variable leaks in xUnit tests:

  • DD0014: Flags Environment.SetEnvironmentVariable calls in test methods ([Fact], [Theory], [SkippableFact], etc.) that are not covered by a corresponding [EnvironmentRestorer] attribute at the method or class level
  • DD0013: Detects redundant [EnvironmentRestorer] attributes — either duplicated across methods (should be class-level) or already covered by a class-level attribute (should be removed)

Also extends EnvironmentRestorerAttribute to support AttributeTargets.Method in addition to AttributeTargets.Class.

Reason for change

PR #8388 fixed several test files where Environment.SetEnvironmentVariable was called without proper cleanup, causing env var state to leak between tests (flaky/order-dependent failures). These analyzers prevent the same issues from recurring by catching them at compile time.

Implementation details

  • Both analyzers are disabled by default (isEnabledByDefault: false) and opt-in via .editorconfig
  • DD0014 uses CompilationStartAction to resolve Xunit.FactAttribute once, then checks all SetEnvironmentVariable invocations in test methods. It handles both constant variable names (checks exact [EnvironmentRestorer] coverage) and non-constant names (always flags, suggests using a constant or #pragma suppression)
  • DD0013 uses RegisterSymbolAction on INamedTypeSymbol to scan class members for duplicate or redundant [EnvironmentRestorer] attributes
  • A tracer/test/.editorconfig is added enabling both rules for Datadog.Trace.Tests

Test coverage

18 analyzer tests covering:

  • [Fact], [Theory], [SkippableFact] detection (semantic — checks attribute inheritance from FactAttribute)
  • Method-level and class-level [EnvironmentRestorer] suppression
  • Partial coverage (wrong variable name)
  • Non-constant variable names
  • Const field resolution
  • Manual try/finally (flagged — devs should use [EnvironmentRestorer] or #pragma)
  • DD0013: duplicate across methods, redundant with class-level, partial overlap

Other details

Related: #8388

The analyzer is intentionally shipped disabled. To enable, add to .editorconfig:

dotnet_diagnostic.DD0014.severity = warning
dotnet_diagnostic.DD0013.severity = suggestion

"I tried to set an environment variable in a test without restoring it. The analyzer said 'You've made your bed, now [EnvironmentRestorer] it.'" — Claude 🤖

DD0006 flags Environment.SetEnvironmentVariable calls in xUnit test
methods that lack a corresponding [EnvironmentRestorer] attribute,
preventing env var state from leaking between tests.

DD0013 detects redundant [EnvironmentRestorer] attributes — either
duplicated across methods (suggest class-level) or already covered
by a class-level attribute (suggest removal).

Both rules are disabled by default and enabled via .editorconfig.

🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
@lucaspimentel lucaspimentel added area:builds project files, build scripts, pipelines, versioning, releases, packages AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos labels Apr 2, 2026
@lucaspimentel lucaspimentel changed the title [Analyzers] Add DD0006/DD0013 for EnvironmentRestorer usage in tests [Analyzers] Add analyzers for [EnvironmentRestorer] in tests Apr 2, 2026
🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
@lucaspimentel lucaspimentel changed the title [Analyzers] Add analyzers for [EnvironmentRestorer] in tests [Analyzers] Add DD0014/DD0013 for EnvironmentRestorer usage in tests Apr 2, 2026
@lucaspimentel lucaspimentel changed the title [Analyzers] Add DD0014/DD0013 for EnvironmentRestorer usage in tests [Analyzers] Add analyzers for [EnvironmentRestorer] usage in tests Apr 2, 2026
@dd-trace-dotnet-ci-bot
Copy link
Copy Markdown

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (8402) and master.

✅ No regressions detected - check the details below

Full Metrics Comparison

FakeDbCommand

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration76.08 ± (76.02 - 76.40) ms72.11 ± (72.04 - 72.38) ms-5.2%
.NET Framework 4.8 - Bailout
duration80.67 ± (80.52 - 80.90) ms75.92 ± (75.94 - 76.26) ms-5.9%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1098.66 ± (1099.16 - 1104.78) ms1057.49 ± (1057.89 - 1063.64) ms-3.7%
.NET Core 3.1 - Baseline
process.internal_duration_ms23.20 ± (23.15 - 23.25) ms22.31 ± (22.28 - 22.34) ms-3.8%
process.time_to_main_ms89.25 ± (89.05 - 89.44) ms83.41 ± (83.25 - 83.57) ms-6.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.89 ± (10.89 - 10.90) MB10.93 ± (10.92 - 10.93) MB+0.3%✅⬆️
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms23.24 ± (23.19 - 23.30) ms22.29 ± (22.26 - 22.33) ms-4.1%
process.time_to_main_ms90.56 ± (90.34 - 90.78) ms85.06 ± (84.84 - 85.28) ms-6.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.93 ± (10.93 - 10.94) MB10.95 ± (10.95 - 10.96) MB+0.2%✅⬆️
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms233.52 ± (232.52 - 234.53) ms224.87 ± (223.58 - 226.16) ms-3.7%
process.time_to_main_ms538.04 ± (536.72 - 539.35) ms516.54 ± (515.39 - 517.70) ms-4.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed47.63 ± (47.60 - 47.65) MB47.73 ± (47.70 - 47.76) MB+0.2%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%
.NET 6 - Baseline
process.internal_duration_ms21.79 ± (21.75 - 21.83) ms20.97 ± (20.94 - 21.01) ms-3.7%
process.time_to_main_ms76.48 ± (76.26 - 76.70) ms72.23 ± (72.08 - 72.38) ms-5.6%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.62 ± (10.62 - 10.62) MB10.62 ± (10.62 - 10.63) MB+0.0%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms21.65 ± (21.60 - 21.71) ms20.96 ± (20.93 - 21.00) ms-3.2%
process.time_to_main_ms77.17 ± (76.98 - 77.35) ms73.71 ± (73.52 - 73.89) ms-4.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.73 ± (10.72 - 10.73) MB10.74 ± (10.73 - 10.74) MB+0.1%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms384.63 ± (382.52 - 386.74) ms385.02 ± (383.03 - 387.01) ms+0.1%✅⬆️
process.time_to_main_ms539.71 ± (538.63 - 540.78) ms521.74 ± (520.71 - 522.76) ms-3.3%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed49.93 ± (49.89 - 49.96) MB49.80 ± (49.77 - 49.84) MB-0.2%
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)-0.1%
.NET 8 - Baseline
process.internal_duration_ms20.01 ± (19.97 - 20.06) ms19.30 ± (19.26 - 19.33) ms-3.6%
process.time_to_main_ms75.99 ± (75.83 - 76.14) ms71.76 ± (71.61 - 71.91) ms-5.6%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.66 ± (7.66 - 7.67) MB7.67 ± (7.67 - 7.68) MB+0.1%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms20.05 ± (20.00 - 20.10) ms19.22 ± (19.18 - 19.26) ms-4.1%
process.time_to_main_ms77.45 ± (77.30 - 77.61) ms72.44 ± (72.30 - 72.58) ms-6.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.72 ± (7.71 - 7.73) MB7.75 ± (7.74 - 7.76) MB+0.4%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms306.40 ± (304.15 - 308.65) ms306.99 ± (304.86 - 309.12) ms+0.2%✅⬆️
process.time_to_main_ms498.65 ± (497.71 - 499.60) ms476.00 ± (475.20 - 476.80) ms-4.5%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed36.95 ± (36.93 - 36.97) MB36.93 ± (36.90 - 36.95) MB-0.1%
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)+1.2%✅⬆️

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration188.70 ± (188.76 - 189.41) ms189.03 ± (188.88 - 189.47) ms+0.2%✅⬆️
.NET Framework 4.8 - Bailout
duration192.42 ± (192.25 - 192.58) ms192.38 ± (192.30 - 192.67) ms-0.0%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1128.71 ± (1128.00 - 1134.47) ms1126.84 ± (1125.50 - 1131.27) ms-0.2%
.NET Core 3.1 - Baseline
process.internal_duration_ms184.42 ± (184.11 - 184.73) ms184.21 ± (184.01 - 184.41) ms-0.1%
process.time_to_main_ms79.28 ± (79.08 - 79.47) ms79.16 ± (79.00 - 79.32) ms-0.1%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.01 ± (15.93 - 16.10) MB16.17 ± (16.13 - 16.20) MB+0.9%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 20)20 ± (19 - 20)+0.3%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms183.49 ± (183.27 - 183.71) ms183.61 ± (183.36 - 183.86) ms+0.1%✅⬆️
process.time_to_main_ms80.41 ± (80.31 - 80.50) ms80.46 ± (80.37 - 80.56) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed15.80 ± (15.66 - 15.95) MB16.04 ± (15.94 - 16.15) MB+1.5%✅⬆️
runtime.dotnet.threads.count20 ± (20 - 20)20 ± (20 - 21)+2.6%✅⬆️
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms391.01 ± (389.77 - 392.24) ms390.23 ± (389.01 - 391.46) ms-0.2%
process.time_to_main_ms501.90 ± (500.88 - 502.92) ms500.28 ± (499.19 - 501.36) ms-0.3%
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed58.21 ± (58.04 - 58.38) MB57.66 ± (57.44 - 57.89) MB-0.9%
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.0%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms188.03 ± (187.78 - 188.28) ms188.42 ± (188.12 - 188.72) ms+0.2%✅⬆️
process.time_to_main_ms68.93 ± (68.80 - 69.07) ms69.12 ± (68.95 - 69.29) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.55 ± (15.37 - 15.72) MB16.23 ± (16.10 - 16.37) MB+4.4%✅⬆️
runtime.dotnet.threads.count17 ± (17 - 18)18 ± (18 - 18)+3.3%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms187.74 ± (187.60 - 187.88) ms187.05 ± (186.90 - 187.21) ms-0.4%
process.time_to_main_ms69.81 ± (69.76 - 69.85) ms69.88 ± (69.82 - 69.94) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.95 ± (15.79 - 16.11) MB15.55 ± (15.38 - 15.72) MB-2.5%
runtime.dotnet.threads.count19 ± (19 - 19)18 ± (18 - 19)-2.5%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms596.68 ± (593.95 - 599.41) ms593.55 ± (590.18 - 596.91) ms-0.5%
process.time_to_main_ms504.29 ± (503.37 - 505.22) ms503.11 ± (502.25 - 503.97) ms-0.2%
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed61.44 ± (61.35 - 61.54) MB61.30 ± (61.21 - 61.40) MB-0.2%
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)-0.3%
.NET 8 - Baseline
process.internal_duration_ms185.45 ± (185.21 - 185.70) ms185.86 ± (185.58 - 186.14) ms+0.2%✅⬆️
process.time_to_main_ms68.18 ± (68.05 - 68.30) ms68.41 ± (68.27 - 68.55) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.80 ± (11.75 - 11.86) MB11.76 ± (11.69 - 11.83) MB-0.3%
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (17 - 18)-2.9%
.NET 8 - Bailout
process.internal_duration_ms184.85 ± (184.67 - 185.03) ms185.30 ± (185.16 - 185.45) ms+0.2%✅⬆️
process.time_to_main_ms69.27 ± (69.21 - 69.33) ms69.36 ± (69.29 - 69.43) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.65 ± (11.54 - 11.75) MB11.66 ± (11.55 - 11.76) MB+0.1%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 19)19 ± (18 - 19)+1.3%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms523.27 ± (520.98 - 525.56) ms523.91 ± (521.44 - 526.39) ms+0.1%✅⬆️
process.time_to_main_ms457.03 ± (456.32 - 457.74) ms458.52 ± (457.84 - 459.19) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed50.67 ± (50.65 - 50.70) MB50.68 ± (50.66 - 50.71) MB+0.0%✅⬆️
runtime.dotnet.threads.count30 ± (30 - 30)30 ± (30 - 30)+0.4%✅⬆️
Comparison explanation

Execution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:

  • Welch test with statistical test for significance of 5%
  • Only results indicating a difference greater than 5% and 5 ms are considered.

Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard.

Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph).

Duration charts
FakeDbCommand (.NET Framework 4.8)
gantt
    title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (72ms)  : 70, 75
    master - mean (76ms)  : 73, 79

    section Bailout
    This PR (8402) - mean (76ms)  : 74, 78
    master - mean (81ms)  : 78, 83

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (1,061ms)  : 1020, 1102
    master - mean (1,102ms)  : 1061, 1143

Loading
FakeDbCommand (.NET Core 3.1)
gantt
    title Execution time (ms) FakeDbCommand (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (112ms)  : 109, 115
    master - mean (120ms)  : 116, 124

    section Bailout
    This PR (8402) - mean (114ms)  : 111, 116
    master - mean (121ms)  : 118, 124

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (780ms)  : 760, 799
    master - mean (810ms)  : 786, 835

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (99ms)  : 97, 102
    master - mean (105ms)  : 100, 109

    section Bailout
    This PR (8402) - mean (101ms)  : 99, 103
    master - mean (105ms)  : 103, 108

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (934ms)  : 904, 964
    master - mean (950ms)  : 919, 982

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (99ms)  : 96, 102
    master - mean (104ms)  : 101, 107

    section Bailout
    This PR (8402) - mean (99ms)  : 97, 102
    master - mean (105ms)  : 103, 108

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (815ms)  : 772, 859
    master - mean (835ms)  : 795, 876

Loading
HttpMessageHandler (.NET Framework 4.8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (189ms)  : 186, 192
    master - mean (189ms)  : 186, 192

    section Bailout
    This PR (8402) - mean (192ms)  : 191, 194
    master - mean (192ms)  : 191, 194

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (1,128ms)  : 1087, 1170
    master - mean (1,131ms)  : 1085, 1177

Loading
HttpMessageHandler (.NET Core 3.1)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (271ms)  : 268, 275
    master - mean (272ms)  : 268, 276

    section Bailout
    This PR (8402) - mean (272ms)  : 269, 274
    master - mean (272ms)  : 269, 275

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (918ms)  : 895, 941
    master - mean (921ms)  : 892, 951

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (265ms)  : 260, 271
    master - mean (265ms)  : 261, 269

    section Bailout
    This PR (8402) - mean (265ms)  : 263, 267
    master - mean (265ms)  : 263, 267

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (1,124ms)  : 1064, 1183
    master - mean (1,131ms)  : 1092, 1170

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (8402) - mean (263ms)  : 259, 268
    master - mean (263ms)  : 259, 266

    section Bailout
    This PR (8402) - mean (264ms)  : 261, 266
    master - mean (263ms)  : 260, 266

    section CallTarget+Inlining+NGEN
    This PR (8402) - mean (1,016ms)  : 977, 1055
    master - mean (1,012ms)  : 967, 1058

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos area:builds project files, build scripts, pipelines, versioning, releases, packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant