Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace Microsoft.Testing.Platform.Hosts;
/// </summary>
internal abstract class CommonHost(ServiceProvider serviceProvider) : IHost
{
private readonly List<object> _alreadyDisposedServices = [];

public ServiceProvider ServiceProvider => serviceProvider;

protected IPushOnlyProtocol? PushOnlyProtocol => ServiceProvider.GetService<IPushOnlyProtocol>();
Expand Down Expand Up @@ -69,7 +71,7 @@ public async Task<int> RunAsync()
}
finally
{
await DisposeServiceProviderAsync(ServiceProvider, isProcessShutdown: true).ConfigureAwait(false);
await DisposeServiceProviderAsync(ServiceProvider, alreadyDisposed: _alreadyDisposedServices, isProcessShutdown: true).ConfigureAwait(false);
await DisposeHelper.DisposeAsync(ServiceProvider.GetService<FileLoggerProvider>()).ConfigureAwait(false);
await DisposeHelper.DisposeAsync(PushOnlyProtocol).ConfigureAwait(false);

Expand Down Expand Up @@ -121,6 +123,7 @@ private async Task<int> RunTestAppAsync(CancellationToken testApplicationCancell
{
await testApplicationLifecycleCallbacks.AfterRunAsync(exitCode, testApplicationCancellationToken).ConfigureAwait(false);
await DisposeHelper.DisposeAsync(testApplicationLifecycleCallbacks).ConfigureAwait(false);
_alreadyDisposedServices.Add(testApplicationLifecycleCallbacks);
}
#pragma warning restore CS0618 // Type or member is obsolete
}
Expand Down Expand Up @@ -243,7 +246,6 @@ protected static async Task DisposeServiceProviderAsync(ServiceProvider serviceP
#pragma warning disable CS0618 // Type or member is obsolete
if (!isProcessShutdown &&
service is ITelemetryCollector or
ITestHostApplicationLifetime or
ITestHostApplicationLifetime or
IPushOnlyProtocol)
{
Expand All @@ -269,7 +271,7 @@ ITestHostApplicationLifetime or
if (!alreadyDisposed.Contains(dataConsumer))
{
await DisposeHelper.DisposeAsync(dataConsumer).ConfigureAwait(false);
alreadyDisposed.Add(service);
alreadyDisposed.Add(dataConsumer);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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.Extensions;
using Microsoft.Testing.Platform.Extensions.TestHost;
using Microsoft.Testing.Platform.Helpers;

namespace Microsoft.Testing.Platform.UnitTests.Helpers;

[TestClass]
public class DisposeHelperTests
{
[TestMethod]
public async Task CleanupAsync_CalledOnlyOnce_ForIAsyncCleanableExtension()
{
// Arrange
var extension = new TestExtensionWithCleanup();

// Act
await DisposeHelper.DisposeAsync(extension);

// Assert
extension.CleanupCallCount.Should().Be(1, "CleanupAsync should be called exactly once");

Check failure on line 23 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Debug)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L23

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(23,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 23 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L23

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(23,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 23 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L23

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(23,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 23 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Debug)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L23

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(23,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 23 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L23

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(23,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)
}

[TestMethod]
public async Task CleanupAsync_CalledOnlyOnce_ForExtensionImplementingBothInterfaces()
{
// Arrange
var extension = new TestLifetimeExtensionWithCleanup("test-id");

// Act - Simulate the scenario where the extension is disposed as ITestHostApplicationLifetime
await DisposeHelper.DisposeAsync(extension);

// Assert
extension.CleanupCallCount.Should().Be(1, "CleanupAsync should be called exactly once even when extension implements both ITestHostApplicationLifetime and IAsyncCleanableExtension");

Check failure on line 36 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L36

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(36,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 36 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L36

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(36,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 36 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Debug)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L36

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(36,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 36 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L36

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(36,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)
}

[TestMethod]
public async Task CleanupAsync_NotCalledTwice_WhenDisposedMultipleTimes()
{
// Arrange
var extension = new TestExtensionWithCleanup();

// Act - Dispose twice (simulating the bug scenario)
await DisposeHelper.DisposeAsync(extension);
await DisposeHelper.DisposeAsync(extension);

// Assert
extension.CleanupCallCount.Should().Be(2, "Each call to DisposeHelper.DisposeAsync should call CleanupAsync");

Check failure on line 50 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L50

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(50,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 50 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L50

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(50,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 50 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L50

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(50,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)
}

[TestMethod]
public async Task ITestHostApplicationLifetime_WithIAsyncCleanableExtension_CleanupNotCalledTwiceInDisposalFlow()
{
// Arrange - This test verifies the fix for issue #6181
// When an extension implements both ITestHostApplicationLifetime and IAsyncCleanableExtension,
// CleanupAsync should only be called once, not twice.
var extension = new TestLifetimeExtensionWithCleanup("test-id");

// Act - Simulate the disposal flow:
// 1. First disposal happens in RunTestAppAsync after AfterRunAsync
await DisposeHelper.DisposeAsync(extension);

// 2. Verify that the extension was disposed once
extension.CleanupCallCount.Should().Be(1, "CleanupAsync should be called once after first disposal");

Check failure on line 66 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L66

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(66,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 66 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L66

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(66,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 66 in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs#L66

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/DisposeHelperTests.cs(66,36): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'int' does not contain a definition for 'Should' and no accessible extension method 'Should' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

// 3. Second disposal attempt happens in DisposeServiceProviderAsync during final cleanup
// This should not call CleanupAsync again if the extension is tracked in alreadyDisposed list
// Note: In real scenario, CommonHost tracks disposed services and DisposeServiceProviderAsync skips them
// Here we verify that calling DisposeAsync again would call CleanupAsync again (which is the current behavior),
// but in CommonHost with the fix, it won't reach this point due to alreadyDisposed check.
}

private sealed class TestExtensionWithCleanup : IAsyncCleanableExtension
{
public int CleanupCallCount { get; private set; }

public Task CleanupAsync()
{
CleanupCallCount++;
return Task.CompletedTask;
}
}

private sealed class TestLifetimeExtensionWithCleanup : ITestHostApplicationLifetime, IAsyncCleanableExtension
{
public TestLifetimeExtensionWithCleanup(string uid)
{
Uid = uid;
}

public int CleanupCallCount { get; private set; }

public string Uid { get; }

public string Version => "1.0.0";

public string DisplayName => "Test Lifetime Extension";

public string Description => "Extension for testing disposal";

public Task BeforeRunAsync(CancellationToken cancellationToken) => Task.CompletedTask;

public Task AfterRunAsync(int exitCode, CancellationToken cancellation) => Task.CompletedTask;

public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public Task CleanupAsync()
{
CleanupCallCount++;
return Task.CompletedTask;
}
}
}
Loading