diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.md b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.md index 5992b23..6ebc893 100644 --- a/.docfx/api/namespaces/Codebelt.Extensions.Xunit.md +++ b/.docfx/api/namespaces/Codebelt.Extensions.Xunit.md @@ -13,3 +13,4 @@ Complements: [xUnit: Capturing Output](https://xunit.net/docs/capturing-output) |Type|Ext|Methods| |--:|:-:|---| |ITestOutputHelper|⬇️|`WriteLines`| +|String|⬇️|`ReplaceLineEndings` (TFM netstandard2.0)| diff --git a/Directory.Build.props b/Directory.Build.props index 51c0b55..edd3089 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -70,8 +70,8 @@ - - + + all diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Codebelt.Extensions.Xunit.Hosting.AspNetCore.csproj b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Codebelt.Extensions.Xunit.Hosting.AspNetCore.csproj index 02f5b10..0d0173e 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Codebelt.Extensions.Xunit.Hosting.AspNetCore.csproj +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Codebelt.Extensions.Xunit.Hosting.AspNetCore.csproj @@ -24,8 +24,8 @@ - - + + diff --git a/src/Codebelt.Extensions.Xunit.Hosting/Codebelt.Extensions.Xunit.Hosting.csproj b/src/Codebelt.Extensions.Xunit.Hosting/Codebelt.Extensions.Xunit.Hosting.csproj index d9ac137..d8b4cf5 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/Codebelt.Extensions.Xunit.Hosting.csproj +++ b/src/Codebelt.Extensions.Xunit.Hosting/Codebelt.Extensions.Xunit.Hosting.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Codebelt.Extensions.Xunit/Codebelt.Extensions.Xunit.csproj b/src/Codebelt.Extensions.Xunit/Codebelt.Extensions.Xunit.csproj index ef274f3..f09b8d9 100644 --- a/src/Codebelt.Extensions.Xunit/Codebelt.Extensions.Xunit.csproj +++ b/src/Codebelt.Extensions.Xunit/Codebelt.Extensions.Xunit.csproj @@ -5,13 +5,13 @@ - The Cuemon.Extensions.Xunit namespace contains types that provides a uniform way of doing unit testing. The namespace relates to the Xunit.Abstractions namespace. + The Codebelt.Extensions.Xunit namespace contains types that provides a uniform way of doing unit testing. The namespace relates to the Xunit.Abstractions namespace. test test-output test-disposable test-cleanup - - + + diff --git a/src/Codebelt.Extensions.Xunit/GlobalSuppressions.cs b/src/Codebelt.Extensions.Xunit/GlobalSuppressions.cs new file mode 100644 index 0000000..fbdf1b5 --- /dev/null +++ b/src/Codebelt.Extensions.Xunit/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Major Code Smell", "S3881:\"IDisposable\" should be implemented correctly", Justification = "This is a base class implementation of the IDisposable interface tailored to avoid wrong implementations.", Scope = "type", Target = "~T:Codebelt.Extensions.Xunit.Test")] diff --git a/src/Codebelt.Extensions.Xunit/Test.cs b/src/Codebelt.Extensions.Xunit/Test.cs index 9ec94f3..02172cb 100644 --- a/src/Codebelt.Extensions.Xunit/Test.cs +++ b/src/Codebelt.Extensions.Xunit/Test.cs @@ -9,9 +9,8 @@ namespace Codebelt.Extensions.Xunit /// /// Represents the base class from which all implementations of unit testing should derive. /// - /// /// - public abstract class Test : Disposable, ITest + public abstract class Test : ITest { /// /// Provides a way, with wildcard support, to determine if matches . @@ -71,10 +70,47 @@ protected Test(ITestOutputHelper output = null, Type callerType = null) protected bool HasTestOutput => TestOutput != null; /// - /// Called when this object is being disposed by either or having disposing set to true and is false. + /// Gets a value indicating whether this object is disposed. /// - protected override void OnDisposeManagedResources() + /// true if this object is disposed; otherwise, false. + public bool Disposed { get; private set; } + + /// + /// Called when this object is being disposed by either or having disposing set to true and is false. + /// + protected virtual void OnDisposeManagedResources() + { + } + + /// + /// Called when this object is being disposed by either or and is false. + /// + protected virtual void OnDisposeUnmanagedResources() + { + } + + /// + /// Releases all resources used by the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected void Dispose(bool disposing) { + if (Disposed) { return; } + if (disposing) + { + OnDisposeManagedResources(); + } + OnDisposeUnmanagedResources(); + Disposed = true; } } } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj index 22e9295..f87662d 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj @@ -6,9 +6,9 @@ - - - + + + diff --git a/test/Codebelt.Extensions.Xunit.Tests/Assets/ManagedDisposable.cs b/test/Codebelt.Extensions.Xunit.Tests/Assets/ManagedDisposable.cs new file mode 100644 index 0000000..9720eeb --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Tests/Assets/ManagedDisposable.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace Codebelt.Extensions.Xunit.Assets +{ + public class ManagedDisposable : Test + { + public ManagedDisposable() + { + Stream = new MemoryStream(); + } + + public MemoryStream Stream { get; private set; } + + protected override void OnDisposeManagedResources() + { + try + { + Stream?.Dispose(); + } + finally + { + Stream = null; + } + } + } +} diff --git a/test/Codebelt.Extensions.Xunit.Tests/Assets/UnmanagedDisposable.cs b/test/Codebelt.Extensions.Xunit.Tests/Assets/UnmanagedDisposable.cs new file mode 100644 index 0000000..fb07024 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Tests/Assets/UnmanagedDisposable.cs @@ -0,0 +1,128 @@ +using System; +using System.Runtime.InteropServices; +#if NET48_OR_GREATER +using NativeLibraryLoader; +#endif +namespace Codebelt.Extensions.Xunit.Assets +{ + public class UnmanagedDisposable : Test + { + internal IntPtr _handle = IntPtr.Zero; + internal IntPtr _libHandle = IntPtr.Zero; + + public delegate bool CloseHandle(IntPtr hObject); + + public delegate IntPtr CreateFileDelegate(string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + public delegate IntPtr PtSname(int fd); + +#if NET48_OR_GREATER + internal NativeLibrary _nativeLibrary; +#endif + + public UnmanagedDisposable() + { +#if NET6_0_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (NativeLibrary.TryLoad("kernel32.dll", GetType().Assembly, DllImportSearchPath.System32, out _libHandle)) + { + if (NativeLibrary.TryGetExport(_libHandle, "CreateFileW", out var functionHandle)) + { + var createFileFunc = Marshal.GetDelegateForFunctionPointer(functionHandle); + _handle = createFileFunc(@"C:\TestFile.txt", + 0x80000000, //access read-only + 1, //share-read + IntPtr.Zero, + 3, //open existing + 0, + IntPtr.Zero); + } + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (NativeLibrary.TryLoad("libc.so.6", GetType().Assembly, DllImportSearchPath.SafeDirectories, out _libHandle)) + { + _handle = _libHandle; // i don't know of any native methods on unix + } + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _nativeLibrary = new NativeLibrary("kernel32.dll"); + _libHandle = _nativeLibrary.Handle; + var functionHandle = _nativeLibrary.LoadFunction("CreateFileW"); + var createFileFunc = Marshal.GetDelegateForFunctionPointer(functionHandle); + _handle = createFileFunc(@"C:\TestFile.txt", + 0x80000000, //access read-only + 1, //share-read + IntPtr.Zero, + 3, //open existing + 0, + IntPtr.Zero); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _nativeLibrary = new NativeLibrary("libc.so.6"); + _libHandle = _nativeLibrary.Handle; + _handle = _libHandle; // i don't know of any native methods on unix + } +#endif + } + + ~UnmanagedDisposable() + { + Dispose(false); + } + + + protected override void OnDisposeManagedResources() + { + + } + + protected override void OnDisposeUnmanagedResources() + { +#if NET6_0_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (_handle != IntPtr.Zero) + { + if (NativeLibrary.TryGetExport(_libHandle, "CloseHandle", out var closeHandle)) + { + var closeHandleAction = Marshal.GetDelegateForFunctionPointer(closeHandle); + closeHandleAction(_handle); + } + } + NativeLibrary.Free(_libHandle); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + NativeLibrary.Free(_libHandle); + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (_handle != IntPtr.Zero) + { + var closeHandle = _nativeLibrary.LoadFunction("CloseHandle"); + var closeHandleAction = Marshal.GetDelegateForFunctionPointer(closeHandle); + closeHandleAction(_handle); + } + _nativeLibrary.Dispose(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _nativeLibrary.Dispose(); + } +#endif + } + } +} diff --git a/test/Codebelt.Extensions.Xunit.Tests/Codebelt.Extensions.Xunit.Tests.csproj b/test/Codebelt.Extensions.Xunit.Tests/Codebelt.Extensions.Xunit.Tests.csproj index 421731e..9fe983e 100644 --- a/test/Codebelt.Extensions.Xunit.Tests/Codebelt.Extensions.Xunit.Tests.csproj +++ b/test/Codebelt.Extensions.Xunit.Tests/Codebelt.Extensions.Xunit.Tests.csproj @@ -4,4 +4,8 @@ Codebelt.Extensions.Xunit + + + + diff --git a/test/Codebelt.Extensions.Xunit.Tests/DisposableTest.cs b/test/Codebelt.Extensions.Xunit.Tests/DisposableTest.cs new file mode 100644 index 0000000..e0646e9 --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Tests/DisposableTest.cs @@ -0,0 +1,61 @@ +using System; +using Codebelt.Extensions.Xunit.Assets; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Extensions.Xunit +{ + public class DisposableTest : Test + { + public DisposableTest(ITestOutputHelper output) : base(output) + { + } + + + + [Fact] + public void ManagedDisposable_VerifyThatAssetIsBeingDisposed() + { + ManagedDisposable mdRef = null; + using (var md = new ManagedDisposable()) + { + mdRef = md; + Assert.NotNull(md.Stream); + Assert.Equal(0, md.Stream.Length); + Assert.False(mdRef.Disposed); + } + Assert.NotNull(mdRef); + Assert.Null(mdRef.Stream); + Assert.True(mdRef.Disposed); + } + + private WeakReference unmanaged = null; + + [Fact] + public void UnmanagedDisposable_VerifyThatAssetIsBeingDisposedOnFinalize() + { + Action body = () => + { + var o = new UnmanagedDisposable(); + Assert.NotEqual(IntPtr.Zero, o._libHandle); + Assert.NotEqual(IntPtr.Zero, o._handle); + unmanaged = new WeakReference(o, true); + }; + + try + { + body(); + } + finally + { + GC.Collect(0, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); + } + + if (unmanaged.TryGetTarget(out var ud2)) + { + Assert.True(ud2.Disposed); + } + } + } +}