Fix log accumulation across DynamicData test invocations#7925
Fix log accumulation across DynamicData test invocations#7925Evangelink merged 3 commits intomainfrom
Conversation
GetOut(), GetErr(), and GetTrace() on TestContextImplementation returned accumulated buffer contents but never cleared them. When DynamicData tests reuse the same TestContext across multiple invocations, each subsequent result contained all logs from all previous invocations, causing exponential memory growth (>20 GB for simple projects). Replace these with GetAndClearOutput(), GetAndClearError(), and GetAndClearTrace() that clear the StringBuilder after extracting the value, matching the existing GetAndClearDiagnosticMessages() pattern. Fixes #7846
There was a problem hiding this comment.
Pull request overview
Fixes MSTest adapter log buffer accumulation across multiple DynamicData invocations by switching stdout/stderr/trace retrieval to a get-then-clear pattern, preventing exponential growth in captured output and memory usage.
Changes:
- Replace
GetOut/GetErr/GetTracewithGetAndClearOutput/GetAndClearError/GetAndClearTraceinTestContextImplementation. - Update execution pipeline call sites (test method invoke; assembly/class init & cleanup) to use the new draining APIs.
- Add regression unit tests to ensure output/error/trace don’t accumulate across multiple invocations.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs | Updates background-thread write test to use new get-and-clear APIs. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs | Adds regression tests validating buffers don’t accumulate across repeated InvokeAsync calls. |
| src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs | Introduces get-and-clear methods for stdout/stderr/trace buffers. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs | Updates assembly init/cleanup log capture to drain buffers. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs | Updates per-test invocation log capture to drain buffers. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs | Updates class init/cleanup log capture to drain buffers. |
Copilot's findings
- Files reviewed: 6/6 changed files
- Comments generated: 1
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-04-29
Repository: microsoft/testfx
Key Findings
-
[Coverage — Minor]
WritesFromBackgroundThreadShouldNotThrowinTestContextImplementationTests.cswas updated forOutputandErrorbut not forTrace.GetAndClearTrace()also callsClear()under concurrency and should be included in the thread-safety test. -
[Coverage — Important] No test in
TestContextImplementationTests.csdirectly verifies the clearing contract of the newGetAndClear*methods. The only test for these methods is the thread-safety check (which only asserts no exception). A dedicated read-then-clear test would document and protect the core invariant introduced by this fix. -
[Coverage — Minor] Regression tests for log accumulation are scoped to
TestMethodInfo.InvokeAsync. TheTestClassInfo(class init/cleanup) andUnitTestRunner(assembly init/cleanup) call sites that were also changed have no equivalent accumulation tests.
Recommendations
- Add a
GetAndClearOutput_ShouldReturnContentThenClearBuffer-style test (and equivalents for Error/Trace) inTestContextImplementationTests.csto directly validate the clearing behavior. - Extend
WritesFromBackgroundThreadShouldNotThrowto also callGetAndClearTrace()for completeness. - Consider adding accumulation regression tests for the class/assembly init-cleanup paths, or add a comment noting they're exercised by acceptance tests.
The existing accumulation tests in TestMethodInfoTests.cs are well-structured and correctly detect the regression: result2.LogOutput.Should().Be("invocation_output") would fail with the old non-clearing code (it would produce "invocation_outputinvocation_output"), so the core fix is properly verified.
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer
Date: 2026-04-29
Repository: microsoft/testfx
Key Findings
[Threading — Minor/Accepted] GetAndClearOutput/Error/Trace each perform two individually-synchronized calls (ToString() + Clear()). Content written by a background thread in the TOCTOU window is silently discarded. This matches the pre-existing GetAndClearDiagnosticMessages() pattern and is an accepted design trade-off in this codebase. See inline comment.
Positive Observations
- Root cause addressed correctly: The
TestContextImplementationoutput buffers now clear after each read, consistent with the establishedGetAndClearDiagnosticMessagespattern. The exponential memory growth scenario is fully closed. - Bonus fix in multi-assembly cleanup: In
UnitTestRunner.RunAssemblyCleanupAsync, theforeachover multipleTestAssemblyInfoentries previously accumulated logs across iterations (eachGetOut()returned all prior logs); the newGetAndClearOutput()correctly scopes each iteration to only its own cleanup logs. - Regression tests are valid: The three new
public async Tasktests inTestMethodInfoTests.csare properly discoverable byTestFramework.ForTestingMSTest(which discovers all public parameterless methods returningvoidorTaskinTestContainersubclasses — no[TestMethod]attribute required). - Exclusive control flow: In both
TestClassInfo.DoRunAsyncandRunAssemblyCleanupAsync, the failure path (creates a newTestResultand returns) and the success path (lastResult +=) are mutually exclusive, so there is no double-clear or double-append risk. - No public API surface changed: All three renamed methods remain
internal.
Recommendations
- Consider adding a brief comment to the three new methods noting the intentional TOCTOU (consistent with
GetAndClearDiagnosticMessages), to signal future maintainers that this is deliberate rather than an oversight.
Generated by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
Address review feedback: ToString() + Clear() as two separate synchronized calls could lose appended text if another thread writes between them. Add a single synchronized GetAndClear() method to SynchronizedStringBuilder that performs both operations under one lock.
- Add focused GetAndClear{Output,Error,Trace} unit tests in
TestContextImplementationTests that verify the buffer is cleared after
the first call (returns empty on second call).
- Add GetAndClearTrace() call to WritesFromBackgroundThreadShouldNotThrow
to complete thread-safety coverage for all three buffer types.
- Consolidate regression test comments: single block comment noting that
TestClassInfo and UnitTestRunner call sites use the same GetAndClear*
methods tested in isolation.
|
/backport to rel/4.2 |
|
Started backporting to rel/4.2: https://github.com/microsoft/testfx/actions/runs/25104582574 |
Summary
Fixes #7846
When running
DynamicDatatests with multiple invocations, theTestContextImplementationoutput buffers (_stdOutStringBuilder,_stdErrStringBuilder,_traceStringBuilder) were never cleared between invocations. Since DynamicData tests reuse the sameTestContextinstance across all invocations, logs accumulated exponentially:This caused >20 GB RAM usage for simple projects and truncated/broken log output in test summaries.
Root Cause
GetOut(),GetErr(), andGetTrace()onTestContextImplementationreturned theStringBuildercontents viaToString()but never cleared them. In contrast,GetAndClearDiagnosticMessages()andGetResultFiles()already followed a get-then-clear pattern.Fix
Replace
GetOut(),GetErr(),GetTrace()withGetAndClearOutput(),GetAndClearError(),GetAndClearTrace()that clear theStringBuilderafter extracting the value — matching the existingGetAndClearDiagnosticMessages()pattern.Changes
TestContextImplementation.cs— Replace non-clearing getters withGetAndClearOutput/Error/TracemethodsTestMethodInfo.cs— Update call site inInvokeAsyncfinally blockUnitTestRunner.cs— Update call sites in assembly init/cleanupTestClassInfo.cs— Update call sites in class init/cleanupTestMethodInfoTests.cs— Add 3 regression tests verifying buffers don't accumulate across invocationsTestContextImplementationTests.cs— Update existing thread-safety test to use renamed methods