Commit 53344f76 by David Neumaier

generic assert

parent ec831ede
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
namespace Qrakhen.Assertions
{
/// <summary>
/// Usage:
/// Assert.That(actual).Equal(expected);
///
/// "Is" may be chained it for readability:
/// Assert.That(1).Is.Equal(1);
///
/// Multiple actual values allowed:
/// Assert.That(a, b, c).Not.Null();
///
/// Multiple expected values allowed:
/// Assert.That(3, 4, 5).Equal(3, 4, 5);
///
/// If one expected value but multiple actual values are given, #
/// they are all matched against that single expected value:
/// Assert.That(5, 5, 5).Equal(5);
/// </summary>
public static partial class Assert
{
public static Assertion<T> That<T>(params T?[] parameters)
=> new Assertion<T>().SetActual(parameters);
internal delegate bool Function(object? actual, object? expected = null);
public class Assertion<T>
{
private readonly Assertion<T>? _previous;
private bool _sealed;
private string? _name;
private T?[] _expected = [];
private T?[] _actual = [];
private Function? _callback;
private Assertion<T>? _next;
/// <summary>
/// Full name of this assertion chain, e.g. IsNotNull
/// </summary>
public string FullName
{
get
{
Assertion<T> assertion = Last();
string name = assertion._name ?? "";
while (assertion._previous != null)
{
assertion = assertion._previous;
name = assertion._name + name;
}
return name;
}
}
internal Assertion(Assertion<T>? previous = null)
{
previous?.AssertNotSealed();
_previous = previous;
}
/// <summary>
/// Executes this assertion chain by finding the actual value collection, the expected value collection - if provided - and the initiator function.
/// </summary>
internal void Execute()
{
T?[] actual = GetActual();
T?[] expected = GetExpected();
Function? initiator = GetInitiator();
if (actual.Length == 0)
throw new InvalidOperationException($"Could not find actual parameters for assertion, did you forget to add them using Assert.That(actual)?");
if (initiator == null)
throw new InvalidOperationException($"Could not find initiator callback, did you forget a finalizing Assertion like Equals(expected) or Not.Null()?");
bool singleExpected = false;
if (actual.Length != expected.Length)
{
if (expected.Length == 1)
singleExpected = true;
else if (expected.Length > 1)
throw new InvalidOperationException(
$"Actual parameter count ({actual.Length}) not matching expected parameter count ({expected.Length}).\n"
+ "Either provide a singular expected value that all actual values get compared against, or a matching amount for element-wise checks."
);
}
for (int i = 0; i < actual.Length; i++)
{
T? currentActual = actual[i];
T? currentExpected = singleExpected ? expected[0] : i < expected.Length ? expected[i] : default;
AssertAndThrow(initiator, currentActual, currentExpected, FullName);
}
}
/// <returns>First Assertion in the chain.</returns>
internal Assertion<T> First()
{
Assertion<T> assertion = this;
while (assertion._previous != null)
assertion = assertion._previous;
return assertion;
}
/// <returns>Last Assertion in the chain.</returns>
internal Assertion<T> Last()
{
Assertion<T> assertion = this;
while (assertion._next != null)
assertion = assertion._next;
return assertion;
}
private void AssertNotSealed()
{
if (_sealed)
throw new InvalidOperationException($"Can not chain another assertion after sealed assertion {_name}!");
}
private Function? GetInitiator()
{
Assertion<T> assertion = First();
Function? callback = assertion._callback;
while (callback == null && assertion._next != null)
{
assertion = assertion._next;
callback = assertion._callback;
}
return callback;
}
private T?[] GetExpected()
{
Assertion<T> assertion = Last();
T?[] expected = assertion._expected;
while (expected.Length == 0 && assertion._previous != null)
{
assertion = assertion._previous;
expected = assertion._expected;
}
return expected;
}
private T?[] GetActual()
{
Assertion<T> assertion = First();
T?[] actual = assertion._actual;
while (actual.Length == 0 && assertion._next != null)
{
assertion = assertion._next;
actual = assertion._actual;
}
return actual;
}
internal Assertion<T> SetActual(params T?[] parameters)
{
_actual = (T?[])parameters.Clone();
return this;
}
#region Private Flow-Pattern Methods
private Assertion<T> Fire()
{
AssertNotSealed();
Execute();
return Seal();
}
private Assertion<T> Seal()
{
_sealed = true;
return this;
}
private Assertion<T> Init(string? customName = null, [CallerMemberName] string? name = null)
{
AssertNotSealed();
return SetName(customName ?? name);
}
private Assertion<T> SetName(string? name)
{
_name = name;
return this;
}
private Assertion<T> SetExpected(params T?[] parameters)
{
_expected = (T?[])parameters.Clone();
return this;
}
private Assertion<T> SetCallback(Function callback)
{
_callback = callback;
return this;
}
private Assertion<T> Next()
{
_next ??= new Assertion<T>(this);
return _next;
}
#endregion
#region Public Flow-Pattern Methods
/// <summary>
/// Asserts whether values are larger than 0
/// </summary>
public Assertion<T> Positive()
=> Init().SetCallback((a, e) => (int)a! > 0).Fire();
/// <summary>
/// Asserts whether values are equal to expected
/// </summary>
public Assertion<T> Equal(params T?[] expected)
=> Init().SetExpected(expected).SetCallback(Equals).Fire();
/// <summary>
/// Asserts whether values are null references
/// </summary>
public Assertion<T> Null()
=> Init().SetCallback((a, e) => ReferenceEquals(a, null)).Fire();
/// <summary>
/// Asserts whether values are default (0, null,)
/// </summary>
public Assertion<T> Default()
=> Init().SetExpected([default]).SetCallback(Equals).Fire();
/// <summary>
/// Asserts whether the next assertion fails
/// </summary>
[IgnoreDataMember]
public Assertion<T> Not
=> Init().SetCallback((a, e) => !_next._callback(a, e)).Next();
/// <summary>
/// Readability sugar for grammar-correct coding (e.g. Assert.That(...).Is.Not.Positive();
/// </summary>
[IgnoreDataMember]
public Assertion<T> Is
=> Init().Next();
#endregion
}
private static void AssertAndThrow(
Function function,
object? actual,
object? expected,
string name = "",
string message = "")
{
if (!function.Invoke(actual, expected))
{
throw new AssertionException(name, message, actual, expected);
}
}
public class AssertionException : Exception
{
public readonly string Name;
public readonly object?[] Parameters;
public AssertionException(string name, string message, params object?[] parameters)
: base($"Assertion {name} failed: {message}\nParameters:\n{string.Join('\n', parameters.Select(p => $" - <{p?.GetType().Name ?? "null"}>{p?.ToString() ?? "null"}"))}")
{
Name = name;
Parameters = parameters;
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment