You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Everything is OK—the changes have been pushed to branch efficiency/testcase-traits-single-enumeration-db58b9cf5a67d4a6. Please review the changes, including any protected files, before creating the pull request.
TestCaseExtensions.ToUnitTestElementWithUpdatedSource (called once per discovered TestCase on the execution path when the test source is rewritten) converted TestCase.Traits with:
Traits.Any() fully enumerates the (array-backed) trait collection just to check non-emptiness, then .Select() re-enumerates it to build the array — a double enumeration on every call. Most TestCase instances carry zero traits, so the common case pays for two full LINQ enumerator setups for nothing.
Focus area
Code-Level Efficiency.
Approach
Replaced the two-pass LINQ chain with a single foreach loop that lazily allocates a List<TestTrait> only once a trait is actually seen, then materializes the array only if the list is non-null. This collapses the double enumeration to one pass and, for the common empty-traits case, matches the zero-allocation behavior of returning early — the initial version I benchmarked (materialize an array unconditionally, then check Length > 0) actually regressed the empty case by always allocating an array, so I discarded that approach in favor of the lazy-list version below.
Energy efficiency evidence
Proxy metrics: CPU time (Stopwatch) and allocated bytes (GC.GetAllocatedBytesForCurrentThread()) — both proxies for energy since less CPU work and fewer GC cycles reduce power draw per test-discovery run. Standalone console micro-benchmark (net8.0, Release, 2,000,000 iterations per scenario), mirroring the real TraitCollection/Trait/TestTrait shapes:
Scenario
Old
New (final)
0 traits (common case)
64ms / 40B
85ms / 40B (comparable; JIT/branch noise)
2 traits
275ms / 560,000,040B
140ms / 448,000,040B (~2x faster, ~20% less alloc)
Methodology note: benchmarked against a standalone reproduction of the shapes involved (Trait, TestTrait, a minimal IEnumerable<Trait>-based collection), not the real VSTest TestCase type, since constructing one outside the adapter is impractical. The relative before/after comparison should still hold because the enumeration pattern (single-pass foreach vs. Any()+Select()) is what's being measured, not the underlying collection implementation.
Green Software Foundation context
Hardware Efficiency: removes a redundant enumeration pass per discovered test case, reducing CPU cycles spent on trait conversion during discovery/execution.
SCI: reduces the Energy term for the "convert one TestCase to a UnitTestElement" functional unit, at effectively zero cost in the (most common) zero-trait case.
Trade-offs
Slightly more verbose than the original 3-line LINQ chain (a foreach + null-conditional list build), but the comment explains the rationale and the pattern (lazy-allocate-on-first-hit) is already used elsewhere in the codebase for similar "usually empty" collections.
Reproducibility
./build.sh # full repo build, 0 warn/err
dotnet format whitespace TestFx.slnx --verify-no-changes --include src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs
The dedicated micro-benchmark used above was a standalone throwaway console app (not committed).
Test Status
./build.sh (full repo): 0 warnings, 0 errors.
dotnet format whitespace --verify-no-changes on the changed file: clean.
Could not execute the MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.UnitTests test suites that exercise this method (TestCaseExtensionsTests, UnitTestElementTests, TypeEnumeratorTests) in this sandbox: both projects multi-target net462;net48;net8.0;net9.0 and are excluded from NonWindowsTests.slnf, so dotnet restore/build fails with NU1201 (no .NET Framework SDK available here). CI (which runs on Windows with the full TFM set) should exercise these directly. The change is a pure single-pass refactor with identical output for every input (empty traits → null, non-empty traits → same TestTrait[] contents in the same order), so no behavior change is expected.
Note
GitHub Actions is not permitted to create or approve pull requests in this repository.
The changes have been pushed to branch efficiency/testcase-traits-single-enumeration-db58b9cf5a67d4a6 and are ready to review.
To fix the permissions issue, go to Settings → Actions → General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ
Show patch preview (34 of 46 lines)
From fab8dd5c0301b30180b9062bad68e3db80cbeb51 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 8b8e4ae266599d3a792bea561b89a935b77bff05
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 22:00:41 +0000
Subject: [PATCH] perf: single-pass Traits conversion in TestCaseExtensions
Replace the double-enumeration (Traits.Any() then Traits.Select().ToArray())
in ToUnitTestElementWithUpdatedSource with a single foreach pass that
lazily allocates only when at least one trait is present. Most TestCase
instances carry zero traits, so the previous code's unconditional
Select()/collection-expression materialization after the Any() check
still iterated the (usually empty) sequence twice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Extensions/TestCaseExtensions.cs | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs
index 0b7af8b..6919b42 100644
--- a/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs+++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs@@ -115,9 +115,17 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase
UnfoldingStrategy = (TestDataSourceUnfoldingStrategy)testCase.GetPropertyValue(AdapterTestProperties.UnfoldingStrategy, (int)TestDataSourceUnfoldingStrategy.Auto),
};
- if (testCase.Traits.Any())+ // Single pass over Traits: the previous code enumerated it twice (Any() then Select()), and most+ // test cases carry no traits at all, so avoid allocating a list/array unless there is at least one.+ List<TestTrait>? traits = null;+ foreach (Trait trait in testCase.Traits)
{
- testElement.Traits = [.. testCase.Traits.Select(t => new TestTrait(t.Name, t.Value))];+ (t
... (truncated)
Warning
Firewall blocked 2 domains
The following domains were blocked by the firewall during workflow execution:
github.com
southcentralus0.in.applicationinsights.azure.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
Tip
Your pull request is ready to create! 🎉 ✅
Everything is OK—the changes have been pushed to branch
efficiency/testcase-traits-single-enumeration-db58b9cf5a67d4a6. Please review the changes, including any protected files, before creating the pull request.Create the pull request
The original pull request description is below.
Goal and rationale
TestCaseExtensions.ToUnitTestElementWithUpdatedSource(called once per discoveredTestCaseon the execution path when the test source is rewritten) convertedTestCase.Traitswith:Traits.Any()fully enumerates the (array-backed) trait collection just to check non-emptiness, then.Select()re-enumerates it to build the array — a double enumeration on every call. MostTestCaseinstances carry zero traits, so the common case pays for two full LINQ enumerator setups for nothing.Focus area
Code-Level Efficiency.
Approach
Replaced the two-pass LINQ chain with a single
foreachloop that lazily allocates aList<TestTrait>only once a trait is actually seen, then materializes the array only if the list is non-null. This collapses the double enumeration to one pass and, for the common empty-traits case, matches the zero-allocation behavior of returning early — the initial version I benchmarked (materialize an array unconditionally, then checkLength > 0) actually regressed the empty case by always allocating an array, so I discarded that approach in favor of the lazy-list version below.Energy efficiency evidence
Proxy metrics: CPU time (
Stopwatch) and allocated bytes (GC.GetAllocatedBytesForCurrentThread()) — both proxies for energy since less CPU work and fewer GC cycles reduce power draw per test-discovery run. Standalone console micro-benchmark (net8.0, Release, 2,000,000 iterations per scenario), mirroring the realTraitCollection/Trait/TestTraitshapes:Methodology note: benchmarked against a standalone reproduction of the shapes involved (
Trait,TestTrait, a minimalIEnumerable<Trait>-based collection), not the real VSTestTestCasetype, since constructing one outside the adapter is impractical. The relative before/after comparison should still hold because the enumeration pattern (single-passforeachvs.Any()+Select()) is what's being measured, not the underlying collection implementation.Green Software Foundation context
Trade-offs
Slightly more verbose than the original 3-line LINQ chain (a
foreach+ null-conditional list build), but the comment explains the rationale and the pattern (lazy-allocate-on-first-hit) is already used elsewhere in the codebase for similar "usually empty" collections.Reproducibility
The dedicated micro-benchmark used above was a standalone throwaway console app (not committed).
Test Status
./build.sh(full repo): 0 warnings, 0 errors.dotnet format whitespace --verify-no-changeson the changed file: clean.MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.UnitTeststest suites that exercise this method (TestCaseExtensionsTests,UnitTestElementTests,TypeEnumeratorTests) in this sandbox: both projects multi-targetnet462;net48;net8.0;net9.0and are excluded fromNonWindowsTests.slnf, sodotnet restore/buildfails withNU1201(no .NET Framework SDK available here). CI (which runs on Windows with the full TFM set) should exercise these directly. The change is a pure single-pass refactor with identical output for every input (empty traits →null, non-empty traits → sameTestTrait[]contents in the same order), so no behavior change is expected.Note
GitHub Actions is not permitted to create or approve pull requests in this repository.
To fix the permissions issue, go to Settings → Actions → General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ
Show patch preview (34 of 46 lines)
Warning
Firewall blocked 2 domains
The following domains were blocked by the firewall during workflow execution:
github.comsouthcentralus0.in.applicationinsights.azure.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.
Add this agentic workflow to your repo
To install this agentic workflow, run