diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs index 3f6c408568..7f93edbea5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.cs @@ -458,9 +458,9 @@ async Task DoRunAsync() { // Assembly initialize and class initialize logs are pre-pended to the first result. var testContextImpl = testContext as TestContextImplementation; - result.LogOutput = initializationLogs + testContextImpl?.GetOut(); - result.LogError = initializationErrorLogs + testContextImpl?.GetErr(); - result.DebugTrace = initializationTrace + testContextImpl?.GetTrace(); + result.LogOutput = initializationLogs + testContextImpl?.GetAndClearOutput(); + result.LogError = initializationErrorLogs + testContextImpl?.GetAndClearError(); + result.DebugTrace = initializationTrace + testContextImpl?.GetAndClearTrace(); result.TestContextMessages = initializationTestContextMessages + testContext.GetAndClearDiagnosticMessages(); } @@ -680,9 +680,9 @@ async Task DoRunAsync() Outcome = UnitTestOutcome.Failed, DisplayName = $"[{ClassType.FullName} ClassCleanup]", TestFailureException = ex, - LogOutput = testContextImpl?.GetOut(), - LogError = testContextImpl?.GetErr(), - DebugTrace = testContextImpl?.GetTrace(), + LogOutput = testContextImpl?.GetAndClearOutput(), + LogError = testContextImpl?.GetAndClearError(), + DebugTrace = testContextImpl?.GetAndClearTrace(), TestContextMessages = testContext.GetAndClearDiagnosticMessages(), }; } @@ -690,9 +690,9 @@ async Task DoRunAsync() if (results.Length > 0) { TestResult lastResult = results[results.Length - 1]; - lastResult.LogOutput += testContextImpl?.GetOut(); - lastResult.LogError += testContextImpl?.GetErr(); - lastResult.DebugTrace += testContextImpl?.GetTrace(); + lastResult.LogOutput += testContextImpl?.GetAndClearOutput(); + lastResult.LogError += testContextImpl?.GetAndClearError(); + lastResult.DebugTrace += testContextImpl?.GetAndClearTrace(); lastResult.TestContextMessages += testContext.GetAndClearDiagnosticMessages(); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs index ee240aab96..e45ed74e5f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs @@ -148,9 +148,9 @@ public virtual async Task InvokeAsync(object?[]? arguments) if (result != null) { var testContextImpl = TestContext as TestContextImplementation; - result.LogOutput = testContextImpl?.GetOut(); - result.LogError = testContextImpl?.GetErr(); - result.DebugTrace = testContextImpl?.GetTrace(); + result.LogOutput = testContextImpl?.GetAndClearOutput(); + result.LogError = testContextImpl?.GetAndClearError(); + result.DebugTrace = testContextImpl?.GetAndClearTrace(); result.TestContextMessages = TestContext?.GetAndClearDiagnosticMessages(); result.ResultFiles = TestContext?.GetResultFiles(); result.Duration = watch.Elapsed; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs index ab36a72657..8bee0a4a4d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs @@ -265,9 +265,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet finally { var testContextImpl = testContext.Context as TestContextImplementation; - result!.LogOutput = testContextImpl?.GetOut(); - result.LogError = testContextImpl?.GetErr(); - result.DebugTrace = testContextImpl?.GetTrace(); + result!.LogOutput = testContextImpl?.GetAndClearOutput(); + result.LogError = testContextImpl?.GetAndClearError(); + result.DebugTrace = testContextImpl?.GetAndClearTrace(); result.TestContextMessages = testContext.GetAndClearDiagnosticMessages(); } @@ -288,9 +288,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet { Outcome = UnitTestOutcome.Failed, TestFailureException = ex, - LogOutput = testContextImpl?.GetOut(), - LogError = testContextImpl?.GetErr(), - DebugTrace = testContextImpl?.GetTrace(), + LogOutput = testContextImpl?.GetAndClearOutput(), + LogError = testContextImpl?.GetAndClearError(), + DebugTrace = testContextImpl?.GetAndClearTrace(), TestContextMessages = testContext.GetAndClearDiagnosticMessages(), }; } @@ -298,9 +298,9 @@ private static async Task RunAssemblyInitializeIfNeededAsync(TestMet if (results.Length > 0) { TestResult lastResult = results[results.Length - 1]; - lastResult.LogOutput += testContextImpl?.GetOut(); - lastResult.LogError += testContextImpl?.GetErr(); - lastResult.DebugTrace += testContextImpl?.GetTrace(); + lastResult.LogOutput += testContextImpl?.GetAndClearOutput(); + lastResult.LogError += testContextImpl?.GetAndClearError(); + lastResult.DebugTrace += testContextImpl?.GetAndClearTrace(); lastResult.TestContextMessages += testContext.GetAndClearDiagnosticMessages(); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs index 78bce06425..c7460abf34 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs @@ -46,6 +46,15 @@ internal void AppendLine(string? value) internal void Clear() => _builder.Clear(); + [MethodImpl(MethodImplOptions.Synchronized)] + internal string GetAndClear() + { + string result = _builder.ToString(); + _builder.Clear(); + + return result; + } + [MethodImpl(MethodImplOptions.Synchronized)] public override string ToString() => _builder.ToString(); @@ -380,12 +389,12 @@ private SynchronizedStringBuilder GetTestContextMessagesStringBuilder() return _testContextMessageStringBuilder; } - internal string? GetOut() - => _stdOutStringBuilder?.ToString(); + internal string? GetAndClearOutput() + => _stdOutStringBuilder?.GetAndClear(); - internal string? GetErr() - => _stdErrStringBuilder?.ToString(); + internal string? GetAndClearError() + => _stdErrStringBuilder?.GetAndClear(); - internal string? GetTrace() - => _traceStringBuilder?.ToString(); + internal string? GetAndClearTrace() + => _traceStringBuilder?.GetAndClear(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs index 98e17cfcb3..e2d249bdea 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodInfoTests.cs @@ -1501,6 +1501,45 @@ public void ResolveArgumentsShouldReturnPopulatedParamsWithAllProvided() ((string[])expectedArguments[1]).SequenceEqual((string[])resolvedArguments[1]!).Should().BeTrue(); } + // Regression tests for https://github.com/microsoft/testfx/issues/7846 + // Verify that log output buffers are cleared between invocations to prevent + // exponential memory growth with DynamicData tests. + // NOTE: The TestClassInfo (class init/cleanup) and UnitTestRunner (assembly init/cleanup) + // call sites use the same GetAndClear* methods tested in isolation in + // TestContextImplementationTests.GetAndClear{Output,Error,Trace}_ShouldReturnContentThenClearBuffer. + public async Task InvokeAsync_ShouldNotAccumulateLogOutputAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteConsoleOut("invocation_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.LogOutput.Should().Be("invocation_output"); + result2.LogOutput.Should().Be("invocation_output"); + } + + public async Task InvokeAsync_ShouldNotAccumulateLogErrorAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteConsoleErr("error_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.LogError.Should().Be("error_output"); + result2.LogError.Should().Be("error_output"); + } + + public async Task InvokeAsync_ShouldNotAccumulateDebugTraceAcrossMultipleInvocations() + { + DummyTestClass.TestMethodBody = _ => _testContextImplementation.WriteTrace("trace_output"); + + TestResult result1 = await _testMethodInfo.InvokeAsync(null); + TestResult result2 = await _testMethodInfo.InvokeAsync(null); + + result1.DebugTrace.Should().Be("trace_output"); + result2.DebugTrace.Should().Be("trace_output"); + } + #region helper methods private static async Task RunWithTestablePlatformService(TestablePlatformServiceProvider testablePlatformServiceProvider, Func action) diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs index 1c900fae88..a0ad10507b 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs @@ -347,6 +347,42 @@ public void DisplayMessageShouldForwardToIMessageLogger() messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Error, "ErrorMessage"), Times.Once); } + public void GetAndClearOutput_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteConsoleOut("hello"); + + string? first = _testContextImplementation.GetAndClearOutput(); + string? second = _testContextImplementation.GetAndClearOutput(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + + public void GetAndClearError_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteConsoleErr("hello"); + + string? first = _testContextImplementation.GetAndClearError(); + string? second = _testContextImplementation.GetAndClearError(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + + public void GetAndClearTrace_ShouldReturnContentThenClearBuffer() + { + _testContextImplementation = CreateTestContextImplementation(); + _testContextImplementation.WriteTrace("hello"); + + string? first = _testContextImplementation.GetAndClearTrace(); + string? second = _testContextImplementation.GetAndClearTrace(); + + first.Should().Be("hello"); + second.Should().BeEmpty(); + } + public void WritesFromBackgroundThreadShouldNotThrow() { TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); @@ -360,8 +396,9 @@ public void WritesFromBackgroundThreadShouldNotThrow() }); t.Start(); - _ = testContextImplementation.GetOut(); - _ = testContextImplementation.GetErr(); + _ = testContextImplementation.GetAndClearOutput(); + _ = testContextImplementation.GetAndClearError(); + _ = testContextImplementation.GetAndClearTrace(); t.Join(); } }