From 00000000c1805a81bade28ba5f0f2ad31758ba6f Mon Sep 17 00:00:00 2001 From: Konstantin Safonov Date: Sat, 18 Dec 2021 16:00:13 +0300 Subject: [PATCH] Fix nullref, add tests, bump package version to 1.0.3 --- src/Directory.Build.props | 2 +- .../NotEmptyExtensionsBase.cs | 36 +++++++++---- src/kasthack.NotEmpty.Tests/GenericTests.cs | 34 ++++++++++++ .../NotEmptyTestBase.cs | 53 +++++++++++++++++++ .../kasthack.NotEmpty.Tests.csproj | 30 +++++++++++ src/kasthack.NotEmpty.sln | 6 +++ 6 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 src/kasthack.NotEmpty.Tests/GenericTests.cs create mode 100644 src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs create mode 100644 src/kasthack.NotEmpty.Tests/kasthack.NotEmpty.Tests.csproj diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8b9d199..196c0fb 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,7 +11,7 @@ true true true - 1.0.2 + 1.0.3 .NotEmpty<T>() test extension false kasthack nunit xunit mstest test empty null notempty emptinness nullability diff --git a/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs b/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs index 3b5961d..73539d6 100644 --- a/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs +++ b/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs @@ -7,13 +7,22 @@ public abstract class NotEmptyExtensionsBase { - public void NotEmpty(T? value) => this.NotEmptyInternal(value); + public void NotEmpty(T? value) + { + // workaround for boxed structs + if (value is not null && typeof(T) == typeof(object) && value.GetType() != typeof(object)) + { + this.NotEmptyBoxed(value, null!); + } + + this.NotEmptyInternal(value); + } protected abstract void Assert(bool value, string message); private void NotEmptyInternal(T? value, string? path = null) { - string message = $"value{path} is empty"; + string message = GetEmptyMessage(path); this.Assert(!EqualityComparer.Default.Equals(default!, value!), message); switch (value) { @@ -24,7 +33,7 @@ private void NotEmptyInternal(T? value, string? path = null) var index = 0; foreach (var item in e) { - this.NotEmptyBox(item, $"{path}[{index++}]"); + this.NotEmptyBoxed(item, $"{path}[{index++}]"); } this.Assert(index != 0, message); @@ -32,25 +41,33 @@ private void NotEmptyInternal(T? value, string? path = null) default: foreach (var pathValue in CachedPropertyExtractor.GetProperties(value)) { - this.NotEmptyBox(pathValue.Value, $"{path}.{pathValue.Path}"); + this.NotEmptyBoxed(pathValue.Value, $"{path}.{pathValue.Path}"); } break; } } - private void NotEmptyBox(object? value, string path) + private static string GetEmptyMessage(string? path) { - this.Assert(value is not null, $"value{path} is empty"); - CachedEmptyDelegate.GetDelegate(value!.GetType())(value, path); + return $"value{path} is empty"; + } + + private void NotEmptyBoxed(object? value, string? path) + { + this.Assert(value is not null, GetEmptyMessage(path)); + CachedEmptyDelegate.GetDelegate(this, value!.GetType())(value, path); } private static class CachedEmptyDelegate { - private static readonly MethodInfo NotEmptyMethod = typeof(NotEmptyExtensionsBase).GetMethod(nameof(NotEmptyExtensionsBase.NotEmptyInternal), BindingFlags.NonPublic | BindingFlags.Static)!.GetGenericMethodDefinition(); + private static readonly MethodInfo NotEmptyMethod = typeof(NotEmptyExtensionsBase) + .GetMethod(nameof(NotEmptyExtensionsBase.NotEmptyInternal), BindingFlags.NonPublic | BindingFlags.Instance)! + .GetGenericMethodDefinition(); + private static readonly Dictionary> Delegates = new(); - public static Action GetDelegate(Type type) + public static Action GetDelegate(NotEmptyExtensionsBase @this, Type type) { if (!Delegates.TryGetValue(type, out var result)) { @@ -63,6 +80,7 @@ private static class CachedEmptyDelegate result = (Action)Expression .Lambda( Expression.Call( + Expression.Constant(@this), NotEmptyMethod.MakeGenericMethod(type), Expression.Convert( valueParam, diff --git a/src/kasthack.NotEmpty.Tests/GenericTests.cs b/src/kasthack.NotEmpty.Tests/GenericTests.cs new file mode 100644 index 0000000000..d476d47 --- /dev/null +++ b/src/kasthack.NotEmpty.Tests/GenericTests.cs @@ -0,0 +1,34 @@ +namespace kasthack.NotEmpty.Tests +{ + public class XunitNotEmptyTest : NotEmptyTestBase + { + public XunitNotEmptyTest() + : base(x => kasthack.NotEmpty.Xunit.NotEmptyExtensions.NotEmpty(x)) + { + } + } + + public class NunitNotEmptyTest : NotEmptyTestBase + { + public NunitNotEmptyTest() + : base(x => kasthack.NotEmpty.Nunit.NotEmptyExtensions.NotEmpty(x)) + { + } + } + + public class MsTestNotEmptyTest : NotEmptyTestBase + { + public MsTestNotEmptyTest() + : base(x => kasthack.NotEmpty.MsTest.NotEmptyExtensions.NotEmpty(x)) + { + } + } + + public class RawNotEmptyTest : NotEmptyTestBase + { + public RawNotEmptyTest() + : base(x => kasthack.NotEmpty.Raw.NotEmptyExtensions.NotEmpty(x)) + { + } + } +} \ No newline at end of file diff --git a/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs b/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs new file mode 100644 index 0000000000..70a127b --- /dev/null +++ b/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs @@ -0,0 +1,53 @@ +namespace kasthack.NotEmpty.Tests +{ + using System; + using System.Collections.Generic; + + using global::Xunit; + + public abstract class NotEmptyTestBase + { + private readonly Action action; + + public NotEmptyTestBase(Action action) => this.action = action; + + [Fact] + public void NullThrows() => Assert.ThrowsAny(() => this.action(null)); + + [Fact] + public void ObjectWorks() => this.action(new object()); + + [Fact] + public void ObjectWithPropsWorks() => this.action(new { Value = 1 }); + + [Fact] + public void ObjectWithDefaultThrows() => Assert.ThrowsAny(() => this.action(new { Value = 0 })); + + [Fact] + public void NestedObjectWithDefaultThrows() => Assert.ThrowsAny(() => this.action(new { Property = new { Value = 0, } })); + + [Fact] + public void PrimitiveWorks() => this.action(1); + + [Fact] + public void ZeroThrows() => Assert.ThrowsAny(() => this.action(0)); + + [Fact] + public void EmptyArrayThrows() => Assert.ThrowsAny(() => this.action(new object[] { })); + + [Fact] + public void EmptyListThrows() => Assert.ThrowsAny(() => this.action(new List())); + + [Fact] + public void ListWorks() => this.action(new List { new object() }); + + [Fact] + public void ArrayWorks() => this.action(new object[] { new object() }); + + [Fact] + public void ArrayWithDefaultThrows() => Assert.ThrowsAny(() => this.action(new object[] { 1, 0 })); + + [Fact] + public void ArrayWithNullThrows() => Assert.ThrowsAny(() => this.action(new object?[] { null, new object() })); + } +} \ No newline at end of file diff --git a/src/kasthack.NotEmpty.Tests/kasthack.NotEmpty.Tests.csproj b/src/kasthack.NotEmpty.Tests/kasthack.NotEmpty.Tests.csproj new file mode 100644 index 0000000000..8b5212d --- /dev/null +++ b/src/kasthack.NotEmpty.Tests/kasthack.NotEmpty.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/src/kasthack.NotEmpty.sln b/src/kasthack.NotEmpty.sln index b8aafa7..4f52d3b 100644 --- a/src/kasthack.NotEmpty.sln +++ b/src/kasthack.NotEmpty.sln @@ -33,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CI", "CI", "{FA63E98E-9B05- ..\.github\workflows\push.yml = ..\.github\workflows\push.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kasthack.NotEmpty.Tests", "kasthack.NotEmpty.Tests\kasthack.NotEmpty.Tests.csproj", "{49386A26-B16E-45B3-93D7-5846A721C2EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {78A06400-338F-4494-9BC9-A414080B7824}.Debug|Any CPU.Build.0 = Debug|Any CPU {78A06400-338F-4494-9BC9-A414080B7824}.Release|Any CPU.ActiveCfg = Release|Any CPU {78A06400-338F-4494-9BC9-A414080B7824}.Release|Any CPU.Build.0 = Release|Any CPU + {49386A26-B16E-45B3-93D7-5846A721C2EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49386A26-B16E-45B3-93D7-5846A721C2EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49386A26-B16E-45B3-93D7-5846A721C2EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49386A26-B16E-45B3-93D7-5846A721C2EC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE