From 19e7dff9d0529af2a463d67d61f7775c6ddf3245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Mon, 25 May 2026 16:07:32 +0200 Subject: [PATCH 1/9] Fix race condition: skip hang dump when testhost hasn't launched yet When the inactivity timer fires before the testhost process has launched, _testHostProcessId is 0 (default int). Previously this caused ProcessDumpUtility.StartHangBasedProcessDump to attempt to dump PID 0 (the Idle process on Windows / Swapper on Linux), resulting in an empty or incorrect dump file. The fix adds an early-return guard in CollectDumpAndAbortTesthost: if _testHostProcessId == 0, log a warning and skip the dump/kill. Also updates three existing hang dump unit tests to properly simulate the happy-path scenario (testhost launches before the timer fires) by: - Using a 50 ms timeout instead of 0 ms so the TestHostLaunched event can be raised before the timer callback runs - Raising TestHostLaunched with PID 1234 before the timer fires Adds a new test that verifies StartHangBasedProcessDump is NOT called when the timer fires before TestHostLaunched. Fixes #15588 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameCollector.cs | 8 +++ .../Resources/Resources.Designer.cs | 9 +++ .../Resources/Resources.resx | 3 + .../Resources/xlf/Resources.cs.xlf | 5 ++ .../Resources/xlf/Resources.de.xlf | 5 ++ .../Resources/xlf/Resources.es.xlf | 5 ++ .../Resources/xlf/Resources.fr.xlf | 5 ++ .../Resources/xlf/Resources.it.xlf | 5 ++ .../Resources/xlf/Resources.ja.xlf | 5 ++ .../Resources/xlf/Resources.ko.xlf | 5 ++ .../Resources/xlf/Resources.pl.xlf | 5 ++ .../Resources/xlf/Resources.pt-BR.xlf | 5 ++ .../Resources/xlf/Resources.ru.xlf | 5 ++ .../Resources/xlf/Resources.tr.xlf | 5 ++ .../Resources/xlf/Resources.zh-Hans.xlf | 5 ++ .../Resources/xlf/Resources.zh-Hant.xlf | 5 ++ .../BlameCollectorTests.cs | 57 +++++++++++++++++-- 17 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs index 97774c907e..74f2c36c0d 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs @@ -244,6 +244,14 @@ private void CollectDumpAndAbortTesthost() EqtTrace.Verbose("Inactivity timer is already disposed."); } + // If testhost has not launched yet, we cannot dump or kill it. + if (_testHostProcessId == 0) + { + EqtTrace.Warning("BlameCollector.CollectDumpAndAbortTesthost: Test host process has not launched yet. Skipping hang dump."); + _logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.TestHostNotLaunchedCannotCollectHangDump); + return; + } + if (_collectProcessDumpOnCrash) { // Detach the dumper from the testhost process to prevent crashing testhost process. When the dumper is procdump.exe diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs index 2c32d6d3df..1f67259901 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs @@ -206,5 +206,14 @@ internal static string UnexpectedValueForInactivityTimespanValue { return ResourceManager.GetString("UnexpectedValueForInactivityTimespanValue", resourceCulture); } } + + /// + /// Looks up a localized string similar to Test host process has not launched yet. Cannot collect hang dump. + /// + internal static string TestHostNotLaunchedCannotCollectHangDump { + get { + return ResourceManager.GetString("TestHostNotLaunchedCannotCollectHangDump", resourceCulture); + } + } } } diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx index b1a62c3790..3f40dcda9c 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx @@ -174,4 +174,7 @@ This test may, or may not be the source of the crash. Invalid 'DumpDirectoryPath' for postmortem debugger monitor + + Test host process has not launched yet. Cannot collect hang dump. + diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf index dafafdf58d..2c5bc1438d 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf @@ -88,6 +88,11 @@ Tento test může a nemusí být příčinou chybového ukončení. Neplatná vlastnost DumpDirectoryPath pro monitorování ladicího programu postmortem + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf index 5c21b44514..680804b50e 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf @@ -88,6 +88,11 @@ Dieser Test kann, muss aber nicht unbedingt die Absturzursache sein. Ungültiger "DumpDirectoryPath" für Postmortemdebuggermonitor + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf index de67d8af2c..f642d0308c 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf @@ -88,6 +88,11 @@ Esta prueba puede ser el origen del bloqueo o no serlo. 'DumpDirectoryPath' no válido para el monitor del depurador post mortem + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf index ae354a94d3..0fe56ee7f9 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf @@ -88,6 +88,11 @@ Ce test est éventuellement à l'origine du plantage. 'DumpDirectoryPath' non valide pour l’analyse du débogueur post-mortem + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf index 6fb6917ae9..5a73d11797 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf @@ -88,6 +88,11 @@ Il test potrebbe essere l'origine dell'arresto anomalo. Il valore di 'DumpDirectoryPath' non è valido per il monitoraggio del debugger effettuato dopo che l'applicazione è terminata. + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf index 6342208092..4f5a224eb6 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf @@ -88,6 +88,11 @@ This test may, or may not be the source of the crash. 事後デバッガー モニターの 'DumpDirectoryPath' が無効です + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf index 17e587362a..fd911e121a 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf @@ -88,6 +88,11 @@ This test may, or may not be the source of the crash. 사후 디버거 모니터에 대한 잘못된 'DumpDirectoryPath' + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf index bc99526529..a16bb4f580 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf @@ -88,6 +88,11 @@ Ten test może, ale nie musi być źródłem awarii. Nieprawidłowy element „DumpDirectoryPath” dla monitora debugera postmortem + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf index 5b39153cf7..b46a6142bb 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf @@ -88,6 +88,11 @@ Esse teste pode ser a origem da falha ou não. 'DumpDirectoryPath' inválido para o monitor do depurador postmortem + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf index fc5cb4b297..640b54e3f2 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf @@ -88,6 +88,11 @@ This test may, or may not be the source of the crash. Недопустимый DumpDirectoryPath для монитора отладчика с разбором итогов + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf index 1ef6ca4a85..bfb150b414 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf @@ -88,6 +88,11 @@ Bu test, kilitlenmenin kaynağı olmayabilir veya olmayabilir. Postmortem hata ayıklayıcısı izleyicisi için geçersiz 'DumpDirectoryPath' + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf index 38702a9594..aa9930eeb6 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf @@ -88,6 +88,11 @@ This test may, or may not be the source of the crash. 事后分析调试程序监视器的 “DumpDirectoryPath” 无效 + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf index d28661032e..dc636e9724 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf @@ -88,6 +88,11 @@ This test may, or may not be the source of the crash. 事後剖析偵錯工具監視的 'DumpDirectoryPath' 無效 + + Test host process has not launched yet. Cannot collect hang dump. + Test host process has not launched yet. Cannot collect hang dump. + + \ No newline at end of file diff --git a/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs b/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs index b48dd14294..ddeb77fe7b 100644 --- a/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs @@ -182,13 +182,16 @@ public void InitializeWithDumpForHangShouldCaptureADumpOnTimeout() _mockDataCollectionSink.Setup(x => x.SendFileAsync(It.IsAny())).Callback(() => hangBasedDumpcollected.Set()); _blameDataCollector.Initialize( - GetDumpConfigurationElement(false, false, true, 0), + GetDumpConfigurationElement(false, false, true, 50), _mockDataColectionEvents.Object, _mockDataCollectionSink.Object, _mockLogger.Object, _context); - hangBasedDumpcollected.Wait(1000, TestContext.CancellationToken); + // Simulate testhost launching before the timer fires. + _mockDataColectionEvents.Raise(x => x.TestHostLaunched += null, new TestHostLaunchedEventArgs(_dataCollectionContext, 1234)); + + hangBasedDumpcollected.Wait(2000, TestContext.CancellationToken); _mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); _mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny()), Times.Once); _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.Is(y => y.Path == dumpFile)), Times.Once); @@ -216,13 +219,16 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfGet _mockProcessDumpUtility.Setup(x => x.GetDumpFiles(true, It.IsAny())).Callback(() => hangBasedDumpcollected.Set()).Throws(new Exception("Some exception")); _blameDataCollector.Initialize( - GetDumpConfigurationElement(false, false, true, 0), + GetDumpConfigurationElement(false, false, true, 50), _mockDataColectionEvents.Object, _mockDataCollectionSink.Object, _mockLogger.Object, _context); - hangBasedDumpcollected.Wait(1000, TestContext.CancellationToken); + // Simulate testhost launching before the timer fires. + _mockDataColectionEvents.Raise(x => x.TestHostLaunched += null, new TestHostLaunchedEventArgs(_dataCollectionContext, 1234)); + + hangBasedDumpcollected.Wait(2000, TestContext.CancellationToken); _mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); _mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny()), Times.Once); } @@ -251,18 +257,57 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfAtt _mockDataCollectionSink.Setup(x => x.SendFileAsync(It.IsAny())).Callback(() => hangBasedDumpcollected.Set()).Throws(new Exception("Some other exception")); _blameDataCollector.Initialize( - GetDumpConfigurationElement(false, false, true, 0), + GetDumpConfigurationElement(false, false, true, 50), _mockDataColectionEvents.Object, _mockDataCollectionSink.Object, _mockLogger.Object, _context); - hangBasedDumpcollected.Wait(1000, TestContext.CancellationToken); + // Simulate testhost launching before the timer fires. + _mockDataColectionEvents.Raise(x => x.TestHostLaunched += null, new TestHostLaunchedEventArgs(_dataCollectionContext, 1234)); + + hangBasedDumpcollected.Wait(2000, TestContext.CancellationToken); _mockProcessDumpUtility.Verify(x => x.StartHangBasedProcessDump(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); _mockProcessDumpUtility.Verify(x => x.GetDumpFiles(true, It.IsAny()), Times.Once); _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.Is(y => y.Path == dumpFile)), Times.Once); } + /// + /// If the inactivity timer fires before testhost has launched, the hang dump should not be + /// attempted (which would target PID 0 — the Idle process on Windows / Swapper on Linux). + /// + [TestMethod] + public void InitializeWithDumpForHangShouldSkipDumpIfTestHostHasNotLaunchedYet() + { + _blameDataCollector = new TestableBlameCollector( + _mockBlameReaderWriter.Object, + _mockProcessDumpUtility.Object, + null, + _mockFileHelper.Object, + _mockProcessHelper.Object); + + // Signal that fires once the timer callback completes (warning logged). + var warningLogged = new ManualResetEventSlim(); + _mockLogger + .Setup(x => x.LogWarning(It.IsAny(), It.IsAny())) + .Callback(() => warningLogged.Set()); + + _blameDataCollector.Initialize( + GetDumpConfigurationElement(false, false, true, 0), + _mockDataColectionEvents.Object, + _mockDataCollectionSink.Object, + _mockLogger.Object, + _context); + + // Do NOT raise TestHostLaunched — _testHostProcessId stays 0. + warningLogged.Wait(1000, TestContext.CancellationToken); + + // The hang dump must not have been attempted against PID 0. + _mockProcessDumpUtility.Verify( + x => x.StartHangBasedProcessDump(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never); + } + /// /// The trigger session ended handler should write to file if test start count is greater. /// From 16a2275ad08ecad5839c15e651d6c0c2a581fcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Mon, 25 May 2026 16:23:28 +0200 Subject: [PATCH 2/9] fix: mark _testHostProcessId as volatile for cross-thread visibility Ensures the hang-dump guard added in the parent commit sees a fresh value on the timer-callback thread (ARM64 / weakly-ordered CPUs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameCollector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs index 74f2c36c0d..ffd32dde05 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs @@ -59,7 +59,7 @@ public class BlameCollector : DataCollector, ITestExecutionEnvironmentSpecifier private IInactivityTimer? _inactivityTimer; private TimeSpan _inactivityTimespan = TimeSpan.FromMinutes(DefaultInactivityTimeInMinutes); - private int _testHostProcessId; + private volatile int _testHostProcessId; private string? _testHostProcessName; private string? _targetFramework; private readonly List> _environmentVariables = new(); From 7180aef41d0831ef1a5d81d9d00c2fd115cf7063 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 26 May 2026 13:51:38 +0200 Subject: [PATCH 3/9] fix: start hang-dump timer only after testhost launches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of guarding against PID 0 in the timer callback, don't start the inactivity timer until TestHostLaunched fires. This eliminates the race at the source — the timer can never fire before we know which process to dump. - Remove ResetInactivityTimer() call from Initialize; timer is created but not started until TestHostLaunchedHandler sets the PID and calls ResetInactivityTimer. - Swap order in TestHostLaunchedHandler: set PID before starting timer. - Remove PID==0 guard and resource string (no longer needed). - Revert volatile — no cross-thread visibility concern with proper ordering. - Update tests to reflect that timer starts on TestHostLaunched, not Initialize. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameCollector.cs | 17 ++++------ .../Resources/Resources.Designer.cs | 9 ----- .../Resources/Resources.resx | 3 -- .../Resources/xlf/Resources.cs.xlf | 7 +--- .../Resources/xlf/Resources.de.xlf | 7 +--- .../Resources/xlf/Resources.es.xlf | 7 +--- .../Resources/xlf/Resources.fr.xlf | 7 +--- .../Resources/xlf/Resources.it.xlf | 7 +--- .../Resources/xlf/Resources.ja.xlf | 7 +--- .../Resources/xlf/Resources.ko.xlf | 7 +--- .../Resources/xlf/Resources.pl.xlf | 7 +--- .../Resources/xlf/Resources.pt-BR.xlf | 7 +--- .../Resources/xlf/Resources.ru.xlf | 7 +--- .../Resources/xlf/Resources.tr.xlf | 7 +--- .../Resources/xlf/Resources.zh-Hans.xlf | 7 +--- .../Resources/xlf/Resources.zh-Hant.xlf | 7 +--- .../BlameCollectorTests.cs | 33 ++++++++++--------- 17 files changed, 38 insertions(+), 115 deletions(-) diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs index ffd32dde05..a6f24b331f 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs @@ -59,7 +59,7 @@ public class BlameCollector : DataCollector, ITestExecutionEnvironmentSpecifier private IInactivityTimer? _inactivityTimer; private TimeSpan _inactivityTimespan = TimeSpan.FromMinutes(DefaultInactivityTimeInMinutes); - private volatile int _testHostProcessId; + private int _testHostProcessId; private string? _testHostProcessName; private string? _targetFramework; private readonly List> _environmentVariables = new(); @@ -200,7 +200,8 @@ public override void Initialize( if (_collectProcessDumpOnHang) { _inactivityTimer ??= new InactivityTimer(CollectDumpAndAbortTesthost); - ResetInactivityTimer(); + // Don't start the timer here — wait until TestHostLaunched so we know + // which process to dump. ResetInactivityTimer is called from TestHostLaunchedHandler. } } @@ -244,13 +245,9 @@ private void CollectDumpAndAbortTesthost() EqtTrace.Verbose("Inactivity timer is already disposed."); } - // If testhost has not launched yet, we cannot dump or kill it. - if (_testHostProcessId == 0) - { - EqtTrace.Warning("BlameCollector.CollectDumpAndAbortTesthost: Test host process has not launched yet. Skipping hang dump."); - _logger.LogWarning(_context.SessionDataCollectionContext, Resources.Resources.TestHostNotLaunchedCannotCollectHangDump); - return; - } + // If testhost has not launched yet, the timer should not have been started, + // so this callback should not fire. Assert to catch unexpected paths. + TPDebug.Assert(_testHostProcessId != 0, "CollectDumpAndAbortTesthost called but testhost has not launched yet."); if (_collectProcessDumpOnCrash) { @@ -638,9 +635,9 @@ private void SessionEndedHandler(object? sender, SessionEndEventArgs args) /// TestHostLaunchedEventArgs private void TestHostLaunchedHandler(object? sender, TestHostLaunchedEventArgs args) { - ResetInactivityTimer(); _testHostProcessId = args.TestHostProcessId; _testHostProcessName = _processHelper.GetProcessName(args.TestHostProcessId); + ResetInactivityTimer(); if (!_collectProcessDumpOnCrash) { diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs index 1f67259901..2c32d6d3df 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.Designer.cs @@ -206,14 +206,5 @@ internal static string UnexpectedValueForInactivityTimespanValue { return ResourceManager.GetString("UnexpectedValueForInactivityTimespanValue", resourceCulture); } } - - /// - /// Looks up a localized string similar to Test host process has not launched yet. Cannot collect hang dump. - /// - internal static string TestHostNotLaunchedCannotCollectHangDump { - get { - return ResourceManager.GetString("TestHostNotLaunchedCannotCollectHangDump", resourceCulture); - } - } } } diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx index 3f40dcda9c..b1a62c3790 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/Resources.resx @@ -174,7 +174,4 @@ This test may, or may not be the source of the crash. Invalid 'DumpDirectoryPath' for postmortem debugger monitor - - Test host process has not launched yet. Cannot collect hang dump. - diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf index 2c5bc1438d..05cf1e4042 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.cs.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Tento test může a nemusí být příčinou chybového ukončení. Neplatná vlastnost DumpDirectoryPath pro monitorování ladicího programu postmortem - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf index 680804b50e..cb707603fa 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.de.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Dieser Test kann, muss aber nicht unbedingt die Absturzursache sein. Ungültiger "DumpDirectoryPath" für Postmortemdebuggermonitor - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf index f642d0308c..082bc817e7 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.es.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Esta prueba puede ser el origen del bloqueo o no serlo. 'DumpDirectoryPath' no válido para el monitor del depurador post mortem - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf index 0fe56ee7f9..95bc78f7cd 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.fr.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Ce test est éventuellement à l'origine du plantage. 'DumpDirectoryPath' non valide pour l’analyse du débogueur post-mortem - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf index 5a73d11797..b8343d82a1 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.it.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Il test potrebbe essere l'origine dell'arresto anomalo. Il valore di 'DumpDirectoryPath' non è valido per il monitoraggio del debugger effettuato dopo che l'applicazione è terminata. - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf index 4f5a224eb6..6c00d0a78c 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ja.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ This test may, or may not be the source of the crash. 事後デバッガー モニターの 'DumpDirectoryPath' が無効です - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf index fd911e121a..d48e2ac432 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ko.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ This test may, or may not be the source of the crash. 사후 디버거 모니터에 대한 잘못된 'DumpDirectoryPath' - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf index a16bb4f580..176832f588 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pl.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Ten test może, ale nie musi być źródłem awarii. Nieprawidłowy element „DumpDirectoryPath” dla monitora debugera postmortem - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf index b46a6142bb..70917a127d 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.pt-BR.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Esse teste pode ser a origem da falha ou não. 'DumpDirectoryPath' inválido para o monitor do depurador postmortem - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf index 640b54e3f2..5ac9e2d81c 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.ru.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ This test may, or may not be the source of the crash. Недопустимый DumpDirectoryPath для монитора отладчика с разбором итогов - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf index bfb150b414..dcadc8956c 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.tr.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ Bu test, kilitlenmenin kaynağı olmayabilir veya olmayabilir. Postmortem hata ayıklayıcısı izleyicisi için geçersiz 'DumpDirectoryPath' - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf index aa9930eeb6..e50417ea86 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ This test may, or may not be the source of the crash. 事后分析调试程序监视器的 “DumpDirectoryPath” 无效 - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf index dc636e9724..28e001dac8 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/Resources/xlf/Resources.zh-Hant.xlf @@ -1,4 +1,4 @@ - + @@ -88,11 +88,6 @@ This test may, or may not be the source of the crash. 事後剖析偵錯工具監視的 'DumpDirectoryPath' 無效 - - Test host process has not launched yet. Cannot collect hang dump. - Test host process has not launched yet. Cannot collect hang dump. - - \ No newline at end of file diff --git a/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs b/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs index ddeb77fe7b..f6f1eaba86 100644 --- a/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Extensions.BlameDataCollector.UnitTests/BlameCollectorTests.cs @@ -111,7 +111,7 @@ public void InitializeWithDumpForHangDisabledShouldNotInitializeInactivityTimerO /// /// Initializing with collect dump for hang should configure the timer with the right values and should - /// not call the reset method if no events are received. + /// start the timer when testhost launches (not during Initialize). /// [TestMethod] public void InitializeWithDumpForHangShouldInitializeInactivityTimerAndCallResetOnce() @@ -127,12 +127,17 @@ public void InitializeWithDumpForHangShouldInitializeInactivityTimerAndCallReset _mockLogger.Object, _context); - Assert.AreEqual(1, resetCalledCount, "Should have called InactivityTimer.Reset exactly once since no events were received"); + Assert.AreEqual(0, resetCalledCount, "Should not have called InactivityTimer.Reset during Initialize — timer starts on TestHostLaunched"); + + // Simulate testhost launching — this should start the timer. + _mockDataColectionEvents.Raise(x => x.TestHostLaunched += null, new TestHostLaunchedEventArgs(_dataCollectionContext, 1234)); + + Assert.AreEqual(1, resetCalledCount, "Should have called InactivityTimer.Reset exactly once after TestHostLaunched"); } /// /// Initializing with collect dump for hang should configure the timer with the right values and should - /// reset for each event received + /// reset for each event received (including TestHostLaunched which starts the timer). /// [TestMethod] public void InitializeWithDumpForHangShouldInitializeInactivityTimerAndResetForEachEventReceived() @@ -151,6 +156,9 @@ public void InitializeWithDumpForHangShouldInitializeInactivityTimerAndResetForE _mockLogger.Object, _context); + // Simulate testhost launching — this starts the timer (1st reset). + _mockDataColectionEvents.Raise(x => x.TestHostLaunched += null, new TestHostLaunchedEventArgs(_dataCollectionContext, 1234)); + TestCase testcase = new("TestProject.UnitTest.TestMethod", new Uri("test:/abc"), "abc.dll"); _mockDataColectionEvents.Raise(x => x.TestCaseStart += null, new TestCaseStartEventArgs(testcase)); @@ -273,11 +281,11 @@ public void InitializeWithDumpForHangShouldCaptureKillTestHostOnTimeoutEvenIfAtt } /// - /// If the inactivity timer fires before testhost has launched, the hang dump should not be - /// attempted (which would target PID 0 — the Idle process on Windows / Swapper on Linux). + /// If testhost has not launched, the inactivity timer should not be started, so no hang dump + /// should be attempted even after the configured timeout elapses. /// [TestMethod] - public void InitializeWithDumpForHangShouldSkipDumpIfTestHostHasNotLaunchedYet() + public void InitializeWithDumpForHangShouldNotStartTimerIfTestHostHasNotLaunchedYet() { _blameDataCollector = new TestableBlameCollector( _mockBlameReaderWriter.Object, @@ -286,12 +294,6 @@ public void InitializeWithDumpForHangShouldSkipDumpIfTestHostHasNotLaunchedYet() _mockFileHelper.Object, _mockProcessHelper.Object); - // Signal that fires once the timer callback completes (warning logged). - var warningLogged = new ManualResetEventSlim(); - _mockLogger - .Setup(x => x.LogWarning(It.IsAny(), It.IsAny())) - .Callback(() => warningLogged.Set()); - _blameDataCollector.Initialize( GetDumpConfigurationElement(false, false, true, 0), _mockDataColectionEvents.Object, @@ -299,10 +301,11 @@ public void InitializeWithDumpForHangShouldSkipDumpIfTestHostHasNotLaunchedYet() _mockLogger.Object, _context); - // Do NOT raise TestHostLaunched — _testHostProcessId stays 0. - warningLogged.Wait(1000, TestContext.CancellationToken); + // Do NOT raise TestHostLaunched — timer should never start. + // Wait long enough that a started timer with timeout 0 would have fired. + Thread.Sleep(100); - // The hang dump must not have been attempted against PID 0. + // The hang dump must not have been attempted. _mockProcessDumpUtility.Verify( x => x.StartHangBasedProcessDump(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Never); From c74e1d07dd8f78b2cec40cd6e5e59dcfb596ce35 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 26 May 2026 15:52:11 +0200 Subject: [PATCH 4/9] test: add integration test for hang dump when testhost fails to start Poisons runtimeconfig.json with net9999.0 so testhost can't launch, then runs with CollectHangDump. Verifies vstest exits cleanly without hanging or producing a dump against PID 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameDataCollectorTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs index cdec3a8de8..378b2e7ba9 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs @@ -238,6 +238,33 @@ public void HangDumpChildProcesses(RunnerInfo runnerInfo) ValidateDump(2); } + [TestMethod] + [NetCoreTargetFrameworkDataSource] + public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) + { + // When testhost can't start (e.g. wrong runtime version), the inactivity timer + // must not try to dump PID 0. It should exit cleanly because the timer only starts + // after TestHostLaunched, which never fires here. + SetTestEnvironment(_testEnvironment, runnerInfo); + const string testAssetProjectName = "SimpleTestProjectMessedUpTargetFramework"; + var assemblyPath = GetTestDllForFramework(testAssetProjectName + ".dll", Core11TargetFramework); + + // Make testhost fail immediately by targeting a non-existent runtime. + var runtimeConfigJson = Path.Combine(Path.GetDirectoryName(assemblyPath)!, testAssetProjectName + ".runtimeconfig.json"); + var fileContent = File.ReadAllText(runtimeConfigJson); + var updatedContent = fileContent.Replace("\"version\": \"11.0.0", "\"version\": \"9999.0.0"); + File.WriteAllText(runtimeConfigJson, updatedContent); + + var arguments = PrepareArguments(assemblyPath, GetTestAdapterPath(), string.Empty, string.Empty, runnerInfo.InIsolationValue); + arguments = string.Concat(arguments, $" /ResultsDirectory:{TempDirectory.Path}"); + arguments = string.Concat(arguments, $@" /Blame:""CollectHangDump;HangDumpType=mini;TestTimeout=30s"""); + InvokeVsTest(arguments); + + // vstest should exit with failure (testhost didn't start), but not hang and not crash. + ExitCodeEquals(1); + Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); + } + [TestMethod] [TestCategory("Windows-Review")] [DoNotParallelize] // Installs/uninstalls procdump as machine-wide postmortem debugger via HKLM registry. From 6e1864e5616f861d54560a4a8877a35297a5dbad Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 26 May 2026 16:28:41 +0200 Subject: [PATCH 5/9] fix: restore runtimeconfig.json after integration test The test mutates SimpleTestProjectMessedUpTargetFramework's runtimeconfig.json, which is shared with ProcessesInteractionTests. Wrap in try/finally to restore the original content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameDataCollectorTests.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs index 378b2e7ba9..5dc043eef9 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs @@ -251,18 +251,25 @@ public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) // Make testhost fail immediately by targeting a non-existent runtime. var runtimeConfigJson = Path.Combine(Path.GetDirectoryName(assemblyPath)!, testAssetProjectName + ".runtimeconfig.json"); - var fileContent = File.ReadAllText(runtimeConfigJson); - var updatedContent = fileContent.Replace("\"version\": \"11.0.0", "\"version\": \"9999.0.0"); - File.WriteAllText(runtimeConfigJson, updatedContent); + var originalContent = File.ReadAllText(runtimeConfigJson); + try + { + var updatedContent = originalContent.Replace("\"version\": \"11.0.0", "\"version\": \"9999.0.0"); + File.WriteAllText(runtimeConfigJson, updatedContent); - var arguments = PrepareArguments(assemblyPath, GetTestAdapterPath(), string.Empty, string.Empty, runnerInfo.InIsolationValue); - arguments = string.Concat(arguments, $" /ResultsDirectory:{TempDirectory.Path}"); - arguments = string.Concat(arguments, $@" /Blame:""CollectHangDump;HangDumpType=mini;TestTimeout=30s"""); - InvokeVsTest(arguments); + var arguments = PrepareArguments(assemblyPath, GetTestAdapterPath(), string.Empty, string.Empty, runnerInfo.InIsolationValue); + arguments = string.Concat(arguments, $" /ResultsDirectory:{TempDirectory.Path}"); + arguments = string.Concat(arguments, $@" /Blame:""CollectHangDump;HangDumpType=mini;TestTimeout=30s"""); + InvokeVsTest(arguments); - // vstest should exit with failure (testhost didn't start), but not hang and not crash. - ExitCodeEquals(1); - Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); + // vstest should exit with failure (testhost didn't start), but not hang and not crash. + ExitCodeEquals(1); + Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); + } + finally + { + File.WriteAllText(runtimeConfigJson, originalContent); + } } [TestMethod] From b179a7d98bf67b9959893c64b4ab6e2ad8cde163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 28 May 2026 09:10:52 +0200 Subject: [PATCH 6/9] Use Interlocked.Exchange to set _testHostProcessId Reviewer preference: use Interlocked for cross-thread visibility instead of plain int assignment, as it is more common and explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameCollector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs index a6f24b331f..1756e5ed13 100644 --- a/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs +++ b/src/Microsoft.TestPlatform.Extensions.BlameDataCollector/BlameCollector.cs @@ -635,7 +635,7 @@ private void SessionEndedHandler(object? sender, SessionEndEventArgs args) /// TestHostLaunchedEventArgs private void TestHostLaunchedHandler(object? sender, TestHostLaunchedEventArgs args) { - _testHostProcessId = args.TestHostProcessId; + Interlocked.Exchange(ref _testHostProcessId, args.TestHostProcessId); _testHostProcessName = _processHelper.GetProcessName(args.TestHostProcessId); ResetInactivityTimer(); From a30e0c2a1a14ea11d19aca9fbbe2c112f2d2da6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 29 May 2026 17:50:17 +0200 Subject: [PATCH 7/9] test: use regex for version replacement, add DoNotParallelize, assert runtime-not-found error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameDataCollectorTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs index 5dc043eef9..c8cfec2b30 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs @@ -239,6 +239,7 @@ public void HangDumpChildProcesses(RunnerInfo runnerInfo) } [TestMethod] + [DoNotParallelize] // Modifies the test asset's runtimeconfig.json on disk. [NetCoreTargetFrameworkDataSource] public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) { @@ -254,7 +255,7 @@ public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) var originalContent = File.ReadAllText(runtimeConfigJson); try { - var updatedContent = originalContent.Replace("\"version\": \"11.0.0", "\"version\": \"9999.0.0"); + var updatedContent = Regex.Replace(originalContent, @"""version""\s*:\s*""[\d.]+""", @"""version"": ""9999.0.0"""); File.WriteAllText(runtimeConfigJson, updatedContent); var arguments = PrepareArguments(assemblyPath, GetTestAdapterPath(), string.Empty, string.Empty, runnerInfo.InIsolationValue); @@ -264,6 +265,8 @@ public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) // vstest should exit with failure (testhost didn't start), but not hang and not crash. ExitCodeEquals(1); + // Verify the failure was specifically because the runtime wasn't found, not some other error. + Assert.MatchesRegex(new Regex(@"framework.*9999\.0\.0|9999\.0\.0.*not found|compatible framework version", RegexOptions.IgnoreCase), StdOut + Environment.NewLine + StdErr, "testhost should fail because the runtime version was not found"); Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); } finally From 5895ebd4de2176e8f09c5ca45c918510cd8a7342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sun, 31 May 2026 08:14:15 +0200 Subject: [PATCH 8/9] test: use StdErrorRegexIsMatch for runtime-not-found assertion Use the same assertion approach as ProcessesInteractionTests which is proven to work on Windows. The previous Assert.MatchesRegex on combined StdOut+StdErr may fail due to multiline regex behavior differences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameDataCollectorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs index c8cfec2b30..36e564c08a 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs @@ -266,7 +266,7 @@ public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) // vstest should exit with failure (testhost didn't start), but not hang and not crash. ExitCodeEquals(1); // Verify the failure was specifically because the runtime wasn't found, not some other error. - Assert.MatchesRegex(new Regex(@"framework.*9999\.0\.0|9999\.0\.0.*not found|compatible framework version", RegexOptions.IgnoreCase), StdOut + Environment.NewLine + StdErr, "testhost should fail because the runtime version was not found"); + StdErrorRegexIsMatch("9999\\.0\\.0"); Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); } finally From ecb07c847de2a03cdc3ae1a503322c5fba00e139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Mon, 1 Jun 2026 08:52:50 +0200 Subject: [PATCH 9/9] test: check both stdout and stderr for runtime-not-found assertion The .NET runtime-not-found error message can appear in either stdout or stderr depending on the platform and vstest host configuration. Check both streams to make the assertion robust across environments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlameDataCollectorTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs index 36e564c08a..7b0b2db2f9 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/BlameDataCollectorTests.cs @@ -266,7 +266,10 @@ public void HangDumpShouldNotHangWhenTestHostFailsToStart(RunnerInfo runnerInfo) // vstest should exit with failure (testhost didn't start), but not hang and not crash. ExitCodeEquals(1); // Verify the failure was specifically because the runtime wasn't found, not some other error. - StdErrorRegexIsMatch("9999\\.0\\.0"); + // The .NET runtime error can appear in stderr or stdout depending on the platform/host. + Assert.IsTrue( + Regex.IsMatch(StdErr, "9999\\.0\\.0") || Regex.IsMatch(StdOut, "9999\\.0\\.0"), + $"Expected '9999.0.0' in output.\nStdErr: {StdErr}\nStdOut: {StdOut}"); Assert.DoesNotContain(".dmp", StdOut, "no dump should be collected when testhost never launched"); } finally