Skip to content

Commit

Permalink
Add test options, add an option to allow empty strings, bump package …
Browse files Browse the repository at this point in the history
…version to 1.0.5
  • Loading branch information
kasthack committed Dec 25, 2021
1 parent 0000000 commit 0000000
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 48 deletions.
4 changes: 2 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0;net6.0</TargetFrameworks>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<NoWarn>SA1633;SA1600;SA1601;CS1591</NoWarn>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
Expand All @@ -11,7 +11,7 @@
<PublishDocumentationFile>true</PublishDocumentationFile>
<PublishDocumentationFiles>true</PublishDocumentationFiles>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageVersion>1.0.4</PackageVersion>
<PackageVersion>1.0.5</PackageVersion>
<PackageDescription>.NotEmpty&lt;T&gt;() test extension</PackageDescription>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>kasthack nunit xunit mstest test empty null notempty emptinness nullability</PackageTags>
Expand Down
84 changes: 64 additions & 20 deletions src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,32 @@

public abstract class NotEmptyExtensionsBase
{
public void NotEmpty<T>(T? value)
[Obsolete($"Use {nameof(NotEmptyExtensionsBase.NotEmpty)}<T>(T, {nameof(AssertOptions)}) instead.")]
public void NotEmpty<T>(T? value) => this.NotEmpty(value, null);

public void NotEmpty<T>(T? value, AssertOptions? assertOptions)
{
assertOptions ??= AssertOptions.Default;

// workaround for boxed structs passed as objects
if (value is not null && typeof(T) == typeof(object) && value.GetType() != typeof(object))
{
this.NotEmptyBoxed(value, null!);
this.NotEmptyBoxed(value, assertOptions, null!);
}

this.NotEmptyInternal(value);
this.NotEmptyInternal(value, assertOptions, null);
}

protected abstract void Assert(bool value, string message);

private static string GetEmptyMessage(string? path) => $"value{path} is empty";

private void NotEmptyInternal<T>(T? value, string? path = null)
private void NotEmptyInternal<T>(T? value, AssertOptions assertOptions, string? path = null)
{
string message = GetEmptyMessage(path);
this.Assert(value is not null, message); // fast lane
this.Assert(!EqualityComparer<T>.Default.Equals(default!, value!), message);

switch (value)
{
// TODO: add max-depth instead of doing this
Expand All @@ -40,64 +46,80 @@ private void NotEmptyInternal<T>(T? value, string? path = null)
#endif
break;
case string s:
this.Assert(!string.IsNullOrEmpty(s), message);
this.Assert(
assertOptions.AllowEmptyStrings
? s != null
: !string.IsNullOrEmpty(s),
message);
break;
case System.Collections.IEnumerable e:
var index = 0;
foreach (var item in e)
{
this.NotEmptyBoxed(item, $"{path}[{index++}]");
this.NotEmptyBoxed(item, assertOptions, $"{path}[{index++}]");
}

this.Assert(index != 0, message);
break;
default:
foreach (var pathValue in CachedPropertyExtractor<T>.GetProperties(value))
{
this.NotEmptyBoxed(pathValue.Value, $"{path}.{pathValue.Path}");
this.NotEmptyBoxed(pathValue.Value, assertOptions, $"{path}.{pathValue.Path}");
}

break;
}
}

private void NotEmptyBoxed(object? value, string? path)
private void NotEmptyBoxed(object? value, AssertOptions assertOptions, string? path)
{
this.Assert(value is not null, GetEmptyMessage(path));
CachedEmptyDelegate.GetDelegate(this, value!.GetType())(value, path);
CachedEmptyDelegate.GetDelegate(value!.GetType())(this, value, assertOptions, path);
}

// Creates NotEmptyInternal<T> wrapper:
// (object value, string path) => this.NotEmptyInternal<ACTUAL_TYPE_OF_VALUE>((ACTUAL_TYPE_OF_VALUE)value, path)
// (object value, AssertOptions options, string path) => this.NotEmptyInternal<ACTUAL_TYPE_OF_VALUE>((ACTUAL_TYPE_OF_VALUE)value, options, path)
private static class CachedEmptyDelegate
{
private static readonly MethodInfo NotEmptyMethod = typeof(NotEmptyExtensionsBase)
.GetMethod(nameof(NotEmptyExtensionsBase.NotEmptyInternal), BindingFlags.NonPublic | BindingFlags.Instance)!
.GetGenericMethodDefinition();

private static readonly Dictionary<Type, Action<object?, string?>> Delegates = new();
private static readonly Dictionary<Type, Action<NotEmptyExtensionsBase, object?, AssertOptions, string?>> Delegates = new();

public static Action<object?, string?> GetDelegate(NotEmptyExtensionsBase @this, Type type)
public static Action<NotEmptyExtensionsBase, object?, AssertOptions, string?> GetDelegate(Type type)
{
if (!Delegates.TryGetValue(type, out var result))
{
lock (Delegates)
{
if (!Delegates.TryGetValue(type, out result))
{
var thisParam = Expression.Parameter(typeof(NotEmptyExtensionsBase));
var valueParam = Expression.Parameter(typeof(object));
var optionsParam = Expression.Parameter(typeof(AssertOptions));
var pathParam = Expression.Parameter(typeof(string));
result = (Action<object?, string?>)Expression
var parameters = new[]
{
thisParam,
valueParam,
optionsParam,
pathParam,
};
result = (Action<NotEmptyExtensionsBase, object?, AssertOptions, string?>)Expression
.Lambda(
Expression.Call(
Expression.Constant(@this),
thisParam,
NotEmptyMethod.MakeGenericMethod(type),
Expression.Convert(
valueParam,
type),
pathParam),
valueParam,
pathParam)
arguments: new Expression[]
{
Expression.Convert(
valueParam,
type),
optionsParam,
pathParam,
}),
parameters)
.Compile();
Delegates[type] = result;
}
Expand Down Expand Up @@ -143,4 +165,26 @@ public PathValue(string path, object? value)
public object? Value { get; }
}
}

public class AssertOptions
{
internal static AssertOptions Default { get; } = new();

/*
///// <summary>
///// Maximum assert depth. Useful for preventing stack overflows for objects with generated properties / complex graphs.
///// </summary>
//public int? MaxDepth { get; set; } = 100;
///// <summary>
///// Allow zeros in number arrays. Useful when you have binary data as a byte array.
///// </summary>
//public bool AllowZerosInNumberArrays { get; set; } = false;
*/

/// <summary>
/// Allows empty strings but not nulls.
/// </summary>
public bool AllowEmptyStrings { get; set; } = false;
}
}
12 changes: 11 additions & 1 deletion src/kasthack.NotEmpty.MsTest/NotEmptyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace kasthack.NotEmpty.MsTest
{
using System;
using kasthack.NotEmpty.Core;

/// <summary>
Expand All @@ -14,7 +15,16 @@ public static class NotEmptyExtensions
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value) => Instance.NotEmpty(value);
[Obsolete]
public static void NotEmpty<T>(this T? value) => NotEmpty<T>(value, default);

/// <summary>
/// Tests objects for emptinness(being null, default(T), empty collection or string) recursively.
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <param name="assertOptions">Test options.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value, AssertOptions? assertOptions = null) => Instance.NotEmpty(value, assertOptions);

private class NotEmptyExtensionsBaseXunit : NotEmptyExtensionsBase
{
Expand Down
12 changes: 11 additions & 1 deletion src/kasthack.NotEmpty.Nunit/NotEmptyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace kasthack.NotEmpty.Nunit
{
using System;
using kasthack.NotEmpty.Core;

/// <summary>
Expand All @@ -14,7 +15,16 @@ public static class NotEmptyExtensions
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value) => Instance.NotEmpty(value);
[Obsolete]
public static void NotEmpty<T>(this T? value) => NotEmpty<T>(value, default);

/// <summary>
/// Tests objects for emptinness(being null, default(T), empty collection or string) recursively.
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <param name="assertOptions">Test options.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value, AssertOptions? assertOptions = null) => Instance.NotEmpty(value, assertOptions);

private class NotEmptyExtensionsBaseXunit : NotEmptyExtensionsBase
{
Expand Down
12 changes: 11 additions & 1 deletion src/kasthack.NotEmpty.Raw/NotEmptyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace kasthack.NotEmpty.Raw
{
using System;
using kasthack.NotEmpty.Core;

/// <summary>
Expand All @@ -14,7 +15,16 @@ public static class NotEmptyExtensions
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value) => Instance.NotEmpty(value);
[Obsolete]
public static void NotEmpty<T>(this T? value) => NotEmpty<T>(value, default);

/// <summary>
/// Tests objects for emptinness(being null, default(T), empty collection or string) recursively.
/// </summary>
/// <param name="value">Value to test for emptinness.</param>
/// <param name="assertOptions">Test options.</param>
/// <typeparam name="T">Type of value(inferred by the compiler).</typeparam>
public static void NotEmpty<T>(this T? value, AssertOptions? assertOptions = null) => Instance.NotEmpty(value, assertOptions);

private class NotEmptyExtensionsBaseXunit : NotEmptyExtensionsBase
{
Expand Down
8 changes: 4 additions & 4 deletions src/kasthack.NotEmpty.Tests/GenericTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@ namespace kasthack.NotEmpty.Tests
public class XunitNotEmptyTest : NotEmptyTestBase
{
public XunitNotEmptyTest()
: base(x => kasthack.NotEmpty.Xunit.NotEmptyExtensions.NotEmpty(x))
: base((x, o) => kasthack.NotEmpty.Xunit.NotEmptyExtensions.NotEmpty(x, o))
{
}
}

public class NunitNotEmptyTest : NotEmptyTestBase
{
public NunitNotEmptyTest()
: base(x => kasthack.NotEmpty.Nunit.NotEmptyExtensions.NotEmpty(x))
: base((x, o) => kasthack.NotEmpty.Nunit.NotEmptyExtensions.NotEmpty(x, o))
{
}
}

public class MsTestNotEmptyTest : NotEmptyTestBase
{
public MsTestNotEmptyTest()
: base(x => kasthack.NotEmpty.MsTest.NotEmptyExtensions.NotEmpty(x))
: base((x, o) => kasthack.NotEmpty.MsTest.NotEmptyExtensions.NotEmpty(x, o))
{
}
}

public class RawNotEmptyTest : NotEmptyTestBase
{
public RawNotEmptyTest()
: base(x => kasthack.NotEmpty.Raw.NotEmptyExtensions.NotEmpty(x))
: base((x, o) => kasthack.NotEmpty.Raw.NotEmptyExtensions.NotEmpty(x, o))
{
}
}
Expand Down
43 changes: 25 additions & 18 deletions src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,65 @@

using global::Xunit;

using kasthack.NotEmpty.Core;

public abstract class NotEmptyTestBase
{
private readonly Action<object?> action;
private readonly Action<object?, AssertOptions?> action;

public NotEmptyTestBase(Action<object?, AssertOptions?> action) => this.action = action;

public NotEmptyTestBase(Action<object?> action) => this.action = action;
[Fact]
public void NullThrows() => Assert.ThrowsAny<Exception>(() => this.Action(null));

[Fact]
public void NullThrows() => Assert.ThrowsAny<Exception>(() => this.action(null));
public void ObjectWorks() => this.Action(new object());

[Fact]
public void ObjectWorks() => this.action(new object());
public void ObjectWithPropsWorks() => this.Action(new { Value = 1 });

[Fact]
public void ObjectWithPropsWorks() => this.action(new { Value = 1 });
public void ObjectWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new { Value = 0 }));

[Fact]
public void ObjectWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.action(new { Value = 0 }));
public void NestedObjectWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new { Property = new { Value = 0, } }));

[Fact]
public void NestedObjectWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.action(new { Property = new { Value = 0, } }));
public void PrimitiveWorks() => this.Action(1);

[Fact]
public void PrimitiveWorks() => this.action(1);
public void EmptyStringThrows() => Assert.ThrowsAny<Exception>(() => this.Action(string.Empty));

[Fact]
public void EmptyStringThrows() => Assert.ThrowsAny<Exception>(() => this.action(string.Empty));
public void DefaultThrows() => Assert.ThrowsAny<Exception>(() => this.Action(0));

[Fact]
public void DefaultThrows() => Assert.ThrowsAny<Exception>(() => this.action(0));
public void EmptyArrayThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new object[] { }));

[Fact]
public void EmptyArrayThrows() => Assert.ThrowsAny<Exception>(() => this.action(new object[] { }));
public void EmptyListThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new List<object>()));

[Fact]
public void EmptyListThrows() => Assert.ThrowsAny<Exception>(() => this.action(new List<object>()));
public void ArrayWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new object[] { 1, 0 }));

[Fact]
public void ArrayWithDefaultThrows() => Assert.ThrowsAny<Exception>(() => this.action(new object[] { 1, 0 }));
public void ArrayWithNullThrows() => Assert.ThrowsAny<Exception>(() => this.Action(new object?[] { null, new object() }));

[Fact]
public void ArrayWithNullThrows() => Assert.ThrowsAny<Exception>(() => this.action(new object?[] { null, new object() }));
public void ListWorks() => this.Action(new List<object> { new object() });

[Fact]
public void ListWorks() => this.action(new List<object> { new object() });
public void ArrayWorks() => this.Action(new object[] { new object() });

[Fact]
public void ArrayWorks() => this.action(new object[] { new object() });
public void EmptyDateTimeThrows() => Assert.ThrowsAny<Exception>(() => this.Action(default(DateTime)));

[Fact]
public void EmptyDateTimeThrows() => Assert.ThrowsAny<Exception>(() => this.action(default(DateTime)));
public void KnownTypeWithInfiniteRecursionDoesntThrow() => this.Action(new DateTime(2000, 1, 1, 0, 0, 0));

[Fact]
public void KnownTypeWithInfiniteRecursionDoesntThrow() => this.action(new DateTime(2000, 1, 1, 0, 0, 0));
public void AllowsEmptyStringsWithConfiguredOption() => this.Action("", new AssertOptions { AllowEmptyStrings = true, });

private void Action(object? value, AssertOptions? options = null) => this.action(value, options);
}
}
Loading

0 comments on commit 0000000

Please sign in to comment.