From 5dcf0dceb55dab720e55061f9fd95d58f67827a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Mon, 4 Nov 2024 14:12:00 +0100 Subject: [PATCH] Fix hangdump not showing tests in progress (#3992) --- .../HangDumpActivityIndicator.cs | 10 +- .../HangDumpOutputTests.cs | 157 ++++++++++++++++++ 2 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs diff --git a/src/Platform/Microsoft.Testing.Extensions.HangDump/HangDumpActivityIndicator.cs b/src/Platform/Microsoft.Testing.Extensions.HangDump/HangDumpActivityIndicator.cs index c6bd66c240..aeff5caa2a 100644 --- a/src/Platform/Microsoft.Testing.Extensions.HangDump/HangDumpActivityIndicator.cs +++ b/src/Platform/Microsoft.Testing.Extensions.HangDump/HangDumpActivityIndicator.cs @@ -35,7 +35,7 @@ internal sealed class HangDumpActivityIndicator : IDataConsumer, ITestSessionLif private readonly ManualResetEventSlim _signalActivity = new(false); private readonly ManualResetEventSlim _mutexCreated = new(false); private readonly bool _traceLevelEnabled; - private readonly ConcurrentDictionary _testsCurrentExecutionState = new(); + private readonly ConcurrentDictionary _testsCurrentExecutionState = new(); private Task? _signalActivityIndicatorTask; private Mutex? _activityIndicatorMutex; @@ -143,7 +143,7 @@ private async Task CallbackAsync(IRequest request) if (request is GetInProgressTestsRequest) { await _logger.LogDebugAsync($"Received '{nameof(GetInProgressTestsRequest)}'"); - return new GetInProgressTestsResponse(_testsCurrentExecutionState.Select(x => (x.Key, (int)_clock.UtcNow.Subtract(x.Value.Item2).TotalSeconds)).ToArray()); + return new GetInProgressTestsResponse(_testsCurrentExecutionState.Select(x => (x.Value.Name, (int)_clock.UtcNow.Subtract(x.Value.StartTime).TotalSeconds)).ToArray()); } else if (request is ExitSignalActivityIndicatorTaskRequest) { @@ -173,14 +173,14 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella await _logger.LogTraceAsync($"New in-progress test '{nodeChangedMessage.TestNode.DisplayName}'"); } - _testsCurrentExecutionState.TryAdd(nodeChangedMessage.TestNode.DisplayName, (typeof(InProgressTestNodeStateProperty), _clock.UtcNow)); + _testsCurrentExecutionState.TryAdd(nodeChangedMessage.TestNode.Uid, (nodeChangedMessage.TestNode.DisplayName, typeof(InProgressTestNodeStateProperty), _clock.UtcNow)); } else if (state is PassedTestNodeStateProperty or ErrorTestNodeStateProperty or CancelledTestNodeStateProperty or FailedTestNodeStateProperty or TimeoutTestNodeStateProperty or SkippedTestNodeStateProperty - && _testsCurrentExecutionState.TryRemove(nodeChangedMessage.TestNode.DisplayName, out (Type, DateTimeOffset) record) + && _testsCurrentExecutionState.TryRemove(nodeChangedMessage.TestNode.Uid, out (string Name, Type Type, DateTimeOffset StartTime) record) && _traceLevelEnabled) { - await _logger.LogTraceAsync($"Test removed from in-progress list '{nodeChangedMessage.TestNode.DisplayName}' after '{_clock.UtcNow.Subtract(record.Item2)}', total in-progress '{_testsCurrentExecutionState.Count}'"); + await _logger.LogTraceAsync($"Test removed from in-progress list '{record.Name}' after '{_clock.UtcNow.Subtract(record.StartTime)}', total in-progress '{_testsCurrentExecutionState.Count}'"); } // Optimization, we're interested in test progression and eventually in the discovery progression diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs new file mode 100644 index 0000000000..e5bb4b6ff3 --- /dev/null +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HangDumpOutputTests.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests; + +[TestGroup] +public sealed class HangDumpOutputTests : AcceptanceTestBase +{ + private readonly TestAssetFixture _testAssetFixture; + + public HangDumpOutputTests(ITestExecutionContext testExecutionContext, TestAssetFixture testAssetFixture) + : base(testExecutionContext) => _testAssetFixture = testAssetFixture; + + [Arguments("Mini")] + public async Task HangDump_Outputs_HangingTests_EvenWhenHangingTestsHaveTheSameDisplayName(string format) + { + // This test makes sure that when tests have the same display name (e.g. like Test1 from both Class1 and Class2) + // they will still show up in the hanging tests. This was not the case before when we were just putting them into + // a dictionary based on DisplayName. In that case both tests were started at the same time, and only 1 entry was added + // to currently executing tests. When first test with name Test1 completed we removed that entry, but Class2.Test1 was still + // running. Solution is to use a more unique identifier. + string resultDirectory = Path.Combine(_testAssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N"), format); + var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, "HangDump", TargetFrameworks.NetCurrent.Arguments); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--hangdump --hangdump-timeout 8s --hangdump-type {format} --results-directory {resultDirectory} --no-progress", + new Dictionary + { + { "SLEEPTIMEMS1", "100" }, + { "SLEEPTIMEMS2", "600000" }, + }); + testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully); + testHostResult.AssertOutputContains("Test1"); + } + + [TestFixture(TestFixtureSharingStrategy.PerTestGroup)] + public sealed class TestAssetFixture(AcceptanceFixture acceptanceFixture) : TestAssetFixtureBase(acceptanceFixture.NuGetGlobalPackagesFolder) + { + private const string AssetName = "TestAssetFixture"; + + public string TargetAssetPath => GetAssetPath(AssetName); + + public override IEnumerable<(string ID, string Name, string Code)> GetAssetsToGenerate() + { + yield return (AssetName, AssetName, + Sources + .PatchTargetFrameworks(TargetFrameworks.All) + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); + } + + private const string Sources = """ +#file HangDump.csproj + + + + $TargetFrameworks$ + Exe + true + enable + preview + + + + + + + +#file Program.cs + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Globalization; + +using Microsoft.Testing.Platform; +using Microsoft.Testing.Platform.Extensions.TestFramework; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Requests; +using Microsoft.Testing.Platform.Services; + +public class Startup +{ + public static async Task Main(string[] args) + { + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + builder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_,__) => new DummyTestAdapter()); + builder.AddHangDumpProvider(); + using ITestApplication app = await builder.BuildAsync(); + return await app.RunAsync(); + } +} + +public class DummyTestAdapter : ITestFramework, IDataProducer +{ + public string Uid => nameof(DummyTestAdapter); + + public string Version => "2.0.0"; + + public string DisplayName => nameof(DummyTestAdapter); + + public string Description => nameof(DummyTestAdapter); + + public Task IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task CloseTestSessionAsync(CloseTestSessionContext context) + => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); + + public async Task ExecuteRequestAsync(ExecuteRequestContext context) + { + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Class1.Test1", + DisplayName = "Test1", + Properties = new PropertyBag(new InProgressTestNodeStateProperty()), + })); + + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Class2.Test1", + DisplayName = "Test1", + Properties = new PropertyBag(new InProgressTestNodeStateProperty()), + })); + + Thread.Sleep(int.Parse(Environment.GetEnvironmentVariable("SLEEPTIMEMS1")!, CultureInfo.InvariantCulture)); + + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Class1.Test1", + DisplayName = "Test1", + Properties = new PropertyBag(new PassedTestNodeStateProperty()), + })); + + Thread.Sleep(int.Parse(Environment.GetEnvironmentVariable("SLEEPTIMEMS2")!, CultureInfo.InvariantCulture)); + + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Class2.Test1", + DisplayName = "Test1", + Properties = new PropertyBag(new PassedTestNodeStateProperty()), + })); + + context.Complete(); + } +} +"""; + } +}