The folded execution path in TryExecuteFoldedDataDrivenTestsAsync reuses the same TestContextImplementation across all DataRow iterations. PR #7926 fixes the immediate symptom (O(n²) output in TRX) by adding GetAndClearOut/Err/Trace, but the underlying problem is the shared context.
The unfolded path (Path 1, DataType == ITestDataSource) creates a fresh TestMethodRunner per DataRow — each gets its own TestContextImplementation, so there's no shared state between rows. The folded path loops all rows on the same TestMethodInfo with the same context. Every field on TestContextImplementation that accumulates state is a potential repeat of this bug.
Concrete example
Compare the two paths in TestMethodRunner.RunTestMethodAsync():
Unfolded (safe): each DataRow is a separate test case discovered individually. Each enters RunTestMethodAsync with its own TestMethodRunner → own TestMethodInfo → own TestContextImplementation. No sharing.
Folded: one test case enters RunTestMethodAsync, falls through to TryExecuteFoldedDataDrivenTestsAsync, which loops:
foreach (object?[] data in dataSource)
{
TestResult[] testResults = await ExecuteTestWithDataSourceAsync(testDataSource, data, ...);
results.AddRange(testResults);
}
All iterations share _testMethodInfo → same TestContext → same _stdOutStringBuilder, _stdErrStringBuilder, _traceStringBuilder. The GetAndClear fix clears these three, but any future accumulated field would need the same treatment.
Repro
[DynamicData] with a non-serializable type triggers folding (discovery can't serialize the data → TryUnfoldITestDataSource returns false):
public sealed class Payload
{
public int Id { get; init; }
public Func<int> GetId { get; init; } = null!;
public override string ToString() => $"Payload({Id})";
}
[TestClass]
public sealed class VerboseTests
{
public TestContext TestContext { get; set; } = null!;
public static IEnumerable<Payload[]> TestData
{
get
{
for (int i = 1; i <= 50; i++)
{
int capturedId = i;
yield return [new Payload { Id = i, GetId = () => capturedId }];
}
}
}
[TestMethod]
[DynamicData(nameof(TestData))]
public void VerboseDataDrivenTest(Payload p)
{
for (int i = 0; i < 10; i++)
Console.WriteLine($"[Row {p.Id:D3}] line {i:D3}: padding abcdefghijklmnop");
for (int i = 0; i < 10; i++)
TestContext.WriteLine($"[Row {p.Id:D3}] ctx {i:D3}: padding abcdefghijklmnop");
}
}
With MSTest 4.0.2, both VSTest and MTP produce a ~1.4 MB TRX (vs ~168 KB expected). Row 1 StdOut = 2 KB, Row 50 = 54 KB.
Suggestion
Instead of clearing individual fields after reading, create a fresh TestContextImplementation (or at least reset all accumulated state) at the start of each iteration in TryExecuteFoldedDataDrivenTestsAsync. This makes the folded path structurally equivalent to the unfolded path — new state bugs become impossible rather than requiring per-field GetAndClear methods.
Related
The folded execution path in
TryExecuteFoldedDataDrivenTestsAsyncreuses the sameTestContextImplementationacross all DataRow iterations. PR #7926 fixes the immediate symptom (O(n²) output in TRX) by addingGetAndClearOut/Err/Trace, but the underlying problem is the shared context.The unfolded path (Path 1,
DataType == ITestDataSource) creates a freshTestMethodRunnerper DataRow — each gets its ownTestContextImplementation, so there's no shared state between rows. The folded path loops all rows on the sameTestMethodInfowith the same context. Every field onTestContextImplementationthat accumulates state is a potential repeat of this bug.Concrete example
Compare the two paths in
TestMethodRunner.RunTestMethodAsync():Unfolded (safe): each DataRow is a separate test case discovered individually. Each enters
RunTestMethodAsyncwith its ownTestMethodRunner→ ownTestMethodInfo→ ownTestContextImplementation. No sharing.Folded: one test case enters
RunTestMethodAsync, falls through toTryExecuteFoldedDataDrivenTestsAsync, which loops:All iterations share
_testMethodInfo→ sameTestContext→ same_stdOutStringBuilder,_stdErrStringBuilder,_traceStringBuilder. TheGetAndClearfix clears these three, but any future accumulated field would need the same treatment.Repro
[DynamicData]with a non-serializable type triggers folding (discovery can't serialize the data →TryUnfoldITestDataSourcereturns false):With MSTest 4.0.2, both VSTest and MTP produce a ~1.4 MB TRX (vs ~168 KB expected). Row 1 StdOut = 2 KB, Row 50 = 54 KB.
Suggestion
Instead of clearing individual fields after reading, create a fresh
TestContextImplementation(or at least reset all accumulated state) at the start of each iteration inTryExecuteFoldedDataDrivenTestsAsync. This makes the folded path structurally equivalent to the unfolded path — new state bugs become impossible rather than requiring per-fieldGetAndClearmethods.Related