Skip to content

Commit b4d8c0a

Browse files
authored
Fix hangdump not showing tests in progress (#3992) (#3999)
1 parent c06659e commit b4d8c0a

File tree

2 files changed

+162
-5
lines changed

2 files changed

+162
-5
lines changed

src/Platform/Microsoft.Testing.Extensions.HangDump/HangDumpActivityIndicator.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal sealed class HangDumpActivityIndicator : IDataConsumer, ITestSessionLif
3535
private readonly ManualResetEventSlim _signalActivity = new(false);
3636
private readonly ManualResetEventSlim _mutexCreated = new(false);
3737
private readonly bool _traceLevelEnabled;
38-
private readonly ConcurrentDictionary<string, (Type, DateTimeOffset)> _testsCurrentExecutionState = new();
38+
private readonly ConcurrentDictionary<TestNodeUid, (string Name, Type Type, DateTimeOffset StartTime)> _testsCurrentExecutionState = new();
3939

4040
private Task? _signalActivityIndicatorTask;
4141
private Mutex? _activityIndicatorMutex;
@@ -143,7 +143,7 @@ private async Task<IResponse> CallbackAsync(IRequest request)
143143
if (request is GetInProgressTestsRequest)
144144
{
145145
await _logger.LogDebugAsync($"Received '{nameof(GetInProgressTestsRequest)}'");
146-
return new GetInProgressTestsResponse(_testsCurrentExecutionState.Select(x => (x.Key, (int)_clock.UtcNow.Subtract(x.Value.Item2).TotalSeconds)).ToArray());
146+
return new GetInProgressTestsResponse(_testsCurrentExecutionState.Select(x => (x.Value.Name, (int)_clock.UtcNow.Subtract(x.Value.StartTime).TotalSeconds)).ToArray());
147147
}
148148
else if (request is ExitSignalActivityIndicatorTaskRequest)
149149
{
@@ -173,14 +173,14 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella
173173
await _logger.LogTraceAsync($"New in-progress test '{nodeChangedMessage.TestNode.DisplayName}'");
174174
}
175175

176-
_testsCurrentExecutionState.TryAdd(nodeChangedMessage.TestNode.DisplayName, (typeof(InProgressTestNodeStateProperty), _clock.UtcNow));
176+
_testsCurrentExecutionState.TryAdd(nodeChangedMessage.TestNode.Uid, (nodeChangedMessage.TestNode.DisplayName, typeof(InProgressTestNodeStateProperty), _clock.UtcNow));
177177
}
178178
else if (state is PassedTestNodeStateProperty or ErrorTestNodeStateProperty or CancelledTestNodeStateProperty
179179
or FailedTestNodeStateProperty or TimeoutTestNodeStateProperty or SkippedTestNodeStateProperty
180-
&& _testsCurrentExecutionState.TryRemove(nodeChangedMessage.TestNode.DisplayName, out (Type, DateTimeOffset) record)
180+
&& _testsCurrentExecutionState.TryRemove(nodeChangedMessage.TestNode.Uid, out (string Name, Type Type, DateTimeOffset StartTime) record)
181181
&& _traceLevelEnabled)
182182
{
183-
await _logger.LogTraceAsync($"Test removed from in-progress list '{nodeChangedMessage.TestNode.DisplayName}' after '{_clock.UtcNow.Subtract(record.Item2)}', total in-progress '{_testsCurrentExecutionState.Count}'");
183+
await _logger.LogTraceAsync($"Test removed from in-progress list '{record.Name}' after '{_clock.UtcNow.Subtract(record.StartTime)}', total in-progress '{_testsCurrentExecutionState.Count}'");
184184
}
185185

186186
// Optimization, we're interested in test progression and eventually in the discovery progression
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers;
5+
using Microsoft.Testing.Platform.Helpers;
6+
7+
namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests;
8+
9+
[TestGroup]
10+
public sealed class HangDumpOutputTests : AcceptanceTestBase
11+
{
12+
private readonly TestAssetFixture _testAssetFixture;
13+
14+
public HangDumpOutputTests(ITestExecutionContext testExecutionContext, TestAssetFixture testAssetFixture)
15+
: base(testExecutionContext) => _testAssetFixture = testAssetFixture;
16+
17+
[Arguments("Mini")]
18+
public async Task HangDump_Outputs_HangingTests_EvenWhenHangingTestsHaveTheSameDisplayName(string format)
19+
{
20+
// This test makes sure that when tests have the same display name (e.g. like Test1 from both Class1 and Class2)
21+
// they will still show up in the hanging tests. This was not the case before when we were just putting them into
22+
// a dictionary based on DisplayName. In that case both tests were started at the same time, and only 1 entry was added
23+
// to currently executing tests. When first test with name Test1 completed we removed that entry, but Class2.Test1 was still
24+
// running. Solution is to use a more unique identifier.
25+
string resultDirectory = Path.Combine(_testAssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N"), format);
26+
var testHost = TestInfrastructure.TestHost.LocateFrom(_testAssetFixture.TargetAssetPath, "HangDump", TargetFrameworks.NetCurrent.Arguments);
27+
TestHostResult testHostResult = await testHost.ExecuteAsync(
28+
$"--hangdump --hangdump-timeout 8s --hangdump-type {format} --results-directory {resultDirectory} --no-progress",
29+
new Dictionary<string, string>
30+
{
31+
{ "SLEEPTIMEMS1", "100" },
32+
{ "SLEEPTIMEMS2", "600000" },
33+
});
34+
testHostResult.AssertExitCodeIs(ExitCodes.TestHostProcessExitedNonGracefully);
35+
testHostResult.AssertOutputContains("Test1");
36+
}
37+
38+
[TestFixture(TestFixtureSharingStrategy.PerTestGroup)]
39+
public sealed class TestAssetFixture(AcceptanceFixture acceptanceFixture) : TestAssetFixtureBase(acceptanceFixture.NuGetGlobalPackagesFolder)
40+
{
41+
private const string AssetName = "TestAssetFixture";
42+
43+
public string TargetAssetPath => GetAssetPath(AssetName);
44+
45+
public override IEnumerable<(string ID, string Name, string Code)> GetAssetsToGenerate()
46+
{
47+
yield return (AssetName, AssetName,
48+
Sources
49+
.PatchTargetFrameworks(TargetFrameworks.All)
50+
.PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion));
51+
}
52+
53+
private const string Sources = """
54+
#file HangDump.csproj
55+
56+
<Project Sdk="Microsoft.NET.Sdk">
57+
<PropertyGroup>
58+
<TargetFrameworks>$TargetFrameworks$</TargetFrameworks>
59+
<OutputType>Exe</OutputType>
60+
<UseAppHost>true</UseAppHost>
61+
<Nullable>enable</Nullable>
62+
<LangVersion>preview</LangVersion>
63+
</PropertyGroup>
64+
65+
<ItemGroup>
66+
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" Version="$MicrosoftTestingPlatformVersion$" />
67+
</ItemGroup>
68+
</Project>
69+
70+
#file Program.cs
71+
72+
using System;
73+
using System.Threading;
74+
using System.Threading.Tasks;
75+
using System.Globalization;
76+
77+
using Microsoft.Testing.Platform;
78+
using Microsoft.Testing.Platform.Extensions.TestFramework;
79+
using Microsoft.Testing.Platform.Builder;
80+
using Microsoft.Testing.Platform.Capabilities.TestFramework;
81+
using Microsoft.Testing.Extensions;
82+
using Microsoft.Testing.Platform.Extensions.Messages;
83+
using Microsoft.Testing.Platform.Requests;
84+
using Microsoft.Testing.Platform.Services;
85+
86+
public class Startup
87+
{
88+
public static async Task<int> Main(string[] args)
89+
{
90+
ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args);
91+
builder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_,__) => new DummyTestAdapter());
92+
builder.AddHangDumpProvider();
93+
using ITestApplication app = await builder.BuildAsync();
94+
return await app.RunAsync();
95+
}
96+
}
97+
98+
public class DummyTestAdapter : ITestFramework, IDataProducer
99+
{
100+
public string Uid => nameof(DummyTestAdapter);
101+
102+
public string Version => "2.0.0";
103+
104+
public string DisplayName => nameof(DummyTestAdapter);
105+
106+
public string Description => nameof(DummyTestAdapter);
107+
108+
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
109+
110+
public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) };
111+
112+
public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
113+
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });
114+
115+
public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
116+
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });
117+
118+
public async Task ExecuteRequestAsync(ExecuteRequestContext context)
119+
{
120+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
121+
{
122+
Uid = "Class1.Test1",
123+
DisplayName = "Test1",
124+
Properties = new PropertyBag(new InProgressTestNodeStateProperty()),
125+
}));
126+
127+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
128+
{
129+
Uid = "Class2.Test1",
130+
DisplayName = "Test1",
131+
Properties = new PropertyBag(new InProgressTestNodeStateProperty()),
132+
}));
133+
134+
Thread.Sleep(int.Parse(Environment.GetEnvironmentVariable("SLEEPTIMEMS1")!, CultureInfo.InvariantCulture));
135+
136+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
137+
{
138+
Uid = "Class1.Test1",
139+
DisplayName = "Test1",
140+
Properties = new PropertyBag(new PassedTestNodeStateProperty()),
141+
}));
142+
143+
Thread.Sleep(int.Parse(Environment.GetEnvironmentVariable("SLEEPTIMEMS2")!, CultureInfo.InvariantCulture));
144+
145+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
146+
{
147+
Uid = "Class2.Test1",
148+
DisplayName = "Test1",
149+
Properties = new PropertyBag(new PassedTestNodeStateProperty()),
150+
}));
151+
152+
context.Complete();
153+
}
154+
}
155+
""";
156+
}
157+
}

0 commit comments

Comments
 (0)