Skip to content

Commit

Permalink
Merge pull request #4530 from stevenaw/3043-async-dispose
Browse files Browse the repository at this point in the history
Async disposal of test fixtures
  • Loading branch information
stevenaw committed Oct 31, 2023
2 parents 158d21f + ec1f4d4 commit bd4c676
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 5 deletions.
Expand Up @@ -27,8 +27,7 @@ public DisposeFixtureCommand(TestCommand innerCommand)
{
try
{
if (context.TestObject is IDisposable disposable)
disposable.Dispose();
DisposeHelper.EnsureDisposed(context.TestObject);
}
catch (Exception ex)
{
Expand Down
62 changes: 62 additions & 0 deletions src/NUnitFramework/framework/Internal/DisposeHelper.cs
@@ -0,0 +1,62 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

using System;
#if NETFRAMEWORK
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
#endif

namespace NUnit.Framework.Internal
{
internal static class DisposeHelper
{
public static bool IsDisposable(Type type)
{
if (typeof(IDisposable).IsAssignableFrom(type))
{
return true;
}
#if NETFRAMEWORK
return TryGetAsyncDispose(type, out _);
#else
return typeof(IAsyncDisposable).IsAssignableFrom(type);
#endif
}

public static void EnsureDisposed(object? value)
{
if (value is not null)
{
#if NETFRAMEWORK
if (TryGetAsyncDispose(value.GetType(), out var method))
{
AsyncToSyncAdapter.Await(() => method.Invoke(value, null));
}
#else
if (value is IAsyncDisposable asyncDisposable)
{
AsyncToSyncAdapter.Await(() => asyncDisposable.DisposeAsync());
}
#endif
else if (value is IDisposable disposable)
{
disposable.Dispose();
}
}
}

#if NETFRAMEWORK
private static bool TryGetAsyncDispose(Type type, [NotNullWhen(true)] out MethodInfo? method)
{
method = null;

var asyncDisposable = type.GetInterface("System.IAsyncDisposable");
if (asyncDisposable is null)
return false;

method = asyncDisposable.GetMethod("DisposeAsync", Type.EmptyTypes);
return method is not null;
}
#endif
}
}
Expand Up @@ -231,7 +231,7 @@ private TestCommand MakeOneTimeTearDownCommand(List<SetUpTearDownItem> setUpTear
command = new OneTimeTearDownCommand(command, item);

// Dispose of fixture if necessary
if (Test is IDisposableFixture && Test.TypeInfo is not null && typeof(IDisposable).IsAssignableFrom(Test.TypeInfo.Type) && !Test.HasLifeCycle(LifeCycle.InstancePerTestCase))
if (Test is IDisposableFixture && Test.TypeInfo is not null && DisposeHelper.IsDisposable(Test.TypeInfo.Type) && !Test.HasLifeCycle(LifeCycle.InstancePerTestCase))
command = new DisposeFixtureCommand(command);

return command;
Expand Down
Expand Up @@ -114,7 +114,7 @@ internal TestCommand MakeTestCommand()

// Dispose of fixture if necessary
var isInstancePerTestCase = Test.HasLifeCycle(LifeCycle.InstancePerTestCase);
if (isInstancePerTestCase && parentFixture is IDisposableFixture && typeof(IDisposable).IsAssignableFrom(parentFixture.TypeInfo.Type))
if (isInstancePerTestCase && parentFixture is IDisposableFixture && DisposeHelper.IsDisposable(parentFixture.TypeInfo.Type))
command = new DisposeFixtureCommand(command);

// In the current implementation, upstream actions only apply to tests. If that should change in the future,
Expand Down
22 changes: 22 additions & 0 deletions src/NUnitFramework/testdata/LifeCycleFixture.cs
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace NUnit.TestData.LifeCycleTests
Expand Down Expand Up @@ -432,5 +433,26 @@ public void Dispose()
public void VerifyDisposed() => Assert.That(DisposeCount, Is.EqualTo(1));
}

[TestFixture]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class InstancePerTestCaseWithAsyncDisposeTestCase : IAsyncDisposable
{
public static int DisposeCount;

public ValueTask DisposeAsync()
{
Interlocked.Increment(ref DisposeCount);
return new ValueTask(Task.CompletedTask);
}

[Test]
[Order(1)]
public void Test() => Assert.Pass();

[Test]
[Order(2)]
public void VerifyDisposed() => Assert.That(DisposeCount, Is.EqualTo(1));
}

#endregion
}
73 changes: 73 additions & 0 deletions src/NUnitFramework/testdata/OneTimeSetUpTearDownData.cs
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;

namespace NUnit.TestData.OneTimeSetUpTearDownData
Expand Down Expand Up @@ -482,6 +483,78 @@ public void Dispose()
}
}

[TestFixture]
public class AsyncDisposableFixture : IAsyncDisposable
{
public int DisposeCalled = 0;
public List<string> Actions = new();

[OneTimeSetUp]
public void OneTimeSetUp()
{
Actions.Add(nameof(OneTimeSetUp));
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
Actions.Add(nameof(OneTimeTearDown));
}

[Test]
public void OneTest()
{
}

public ValueTask DisposeAsync()
{
Actions.Add(nameof(DisposeAsync));
DisposeCalled++;
return new ValueTask(Task.CompletedTask);
}
}

public class InheritedAsyncDisposableFixture : AsyncDisposableFixture
{
}

[TestFixture]
public class AsyncAndSyncDisposableFixture : IAsyncDisposable, IDisposable
{
public int DisposeCalled = 0;
public List<string> Actions = new();

[OneTimeSetUp]
public void OneTimeSetUp()
{
Actions.Add(nameof(OneTimeSetUp));
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
Actions.Add(nameof(OneTimeTearDown));
}

[Test]
public void OneTest()
{
}

public ValueTask DisposeAsync()
{
Actions.Add(nameof(DisposeAsync));
DisposeCalled++;
return new ValueTask(Task.CompletedTask);
}

public void Dispose()
{
Actions.Add(nameof(Dispose));
DisposeCalled++;
}
}

[TestFixture]
public class DisposableFixtureWithTestCases : IDisposable
{
Expand Down
4 changes: 4 additions & 0 deletions src/NUnitFramework/testdata/nunit.testdata.csproj
Expand Up @@ -11,6 +11,10 @@
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\framework\nunit.framework.csproj" />
<ProjectReference Include="..\nunit.framework.legacy\nunit.framework.legacy.csproj" />
Expand Down
13 changes: 12 additions & 1 deletion src/NUnitFramework/tests/Attributes/LifeCycleAttributeTests.cs
Expand Up @@ -209,9 +209,20 @@ public void InstancePerTestCaseWithDispose()
var fixture = TestBuilder.MakeFixture(typeof(InstancePerTestCaseWithDisposeTestCase));

ITestResult result = TestBuilder.RunTest(fixture);
Assert.That(InstancePerTestCaseWithDisposeTestCase.DisposeCount, Is.EqualTo(2));
Assert.That(InstancePerTestCaseWithDisposeTestCase.DisposeCount, Is.EqualTo(fixture.TestCaseCount));
Assert.That(result.ResultState.Status, Is.EqualTo(TestStatus.Passed));
}

[Test]
public void InstancePerTestCaseWithAsyncDispose()
{
var fixture = TestBuilder.MakeFixture(typeof(InstancePerTestCaseWithAsyncDisposeTestCase));

ITestResult result = TestBuilder.RunTest(fixture);
Assert.That(InstancePerTestCaseWithAsyncDisposeTestCase.DisposeCount, Is.EqualTo(fixture.TestCaseCount));
Assert.That(result.ResultState.Status, Is.EqualTo(TestStatus.Passed));
}

#endregion

#region Assembly level InstancePerTestCase
Expand Down
34 changes: 34 additions & 0 deletions src/NUnitFramework/tests/Attributes/OneTimeSetUpTearDownTests.cs
Expand Up @@ -372,6 +372,40 @@ public void DisposeCalledOnceWhenFixtureImplementsIDisposableAndHasTestCases()
TestBuilder.RunTestFixture(fixture);
Assert.That(fixture.DisposeCalled, Is.EqualTo(1));
}

[Test]
public void AsyncDisposeCalledOnceWhenFixtureImplementsIAsyncDisposable()
{
var fixture = new AsyncDisposableFixture();
TestBuilder.RunTestFixture(fixture);

var expected = new[] { nameof(AsyncDisposableFixture.OneTimeSetUp), nameof(AsyncDisposableFixture.OneTimeTearDown), nameof(AsyncDisposableFixture.DisposeAsync) };

Assert.That(fixture.DisposeCalled, Is.EqualTo(1));
Assert.That(fixture.Actions, Is.EqualTo(expected));
}

[Test]
public void AsyncDisposeCalledOnceWhenFixtureImplementsIAsyncDisposableThroughInheritance()
{
var fixture = new InheritedAsyncDisposableFixture();
TestBuilder.RunTestFixture(fixture);

var expected = new[] { nameof(AsyncDisposableFixture.OneTimeSetUp), nameof(AsyncDisposableFixture.OneTimeTearDown), nameof(AsyncDisposableFixture.DisposeAsync) };

Assert.That(fixture.DisposeCalled, Is.EqualTo(1));
Assert.That(fixture.Actions, Is.EqualTo(expected));
}

[Test]
public void AsyncDisposePrioritizedWhenSyncAndAsyncDispose()
{
var fixture = new AsyncAndSyncDisposableFixture();
TestBuilder.RunTestFixture(fixture);
Assert.That(fixture.DisposeCalled, Is.EqualTo(1));
Assert.That(fixture.Actions, Does.Contain(nameof(IAsyncDisposable.DisposeAsync)));
Assert.That(fixture.Actions, Does.Not.Contain(nameof(IDisposable.Dispose)));
}
}

[TestFixture]
Expand Down
4 changes: 4 additions & 0 deletions src/NUnitFramework/tests/nunit.framework.tests.csproj
Expand Up @@ -12,6 +12,10 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
Expand Down

0 comments on commit bd4c676

Please sign in to comment.