Skip to content

Commit

Permalink
Fix hangdump not showing tests in progress (#3992)
Browse files Browse the repository at this point in the history
  • Loading branch information
nohwnd authored Nov 4, 2024
1 parent 9d5248f commit 6594ac5
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (Type, DateTimeOffset)> _testsCurrentExecutionState = new();
private readonly ConcurrentDictionary<TestNodeUid, (string Name, Type Type, DateTimeOffset StartTime)> _testsCurrentExecutionState = new();

private Task? _signalActivityIndicatorTask;
private Mutex? _activityIndicatorMutex;
Expand Down Expand Up @@ -143,7 +143,7 @@ private async Task<IResponse> 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)
{
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string>
{
{ "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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$TargetFrameworks$</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseAppHost>true</UseAppHost>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" Version="$MicrosoftTestingPlatformVersion$" />
</ItemGroup>
</Project>
#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<int> 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<bool> IsEnabledAsync() => Task.FromResult(true);
public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) };
public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });
public Task<CloseTestSessionResult> 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();
}
}
""";
}
}

0 comments on commit 6594ac5

Please sign in to comment.