Commit 310888a6 by David Neumaier

add assertions

parents
using System.Runtime.CompilerServices;
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 class Assert
{
public static Assertion That(params object?[] parameters)
=> new Assertion().SetActual(parameters);
public class Assertion
{
public delegate bool Function(object? actual, object? expected = null);
private readonly Assertion? _previous;
private bool _sealed = false;
private string? _name;
private object?[] _expected = [];
private object?[] _actual = [];
private Function? _callback = null;
private Assertion? _next;
public string FullName
{
get
{
Assertion assertion = Last();
string name = assertion._name ?? "";
while (assertion._previous != null)
{
assertion = assertion._previous;
name = assertion._name + name;
}
return name;
}
}
public Assertion(Assertion? previous = null)
{
previous?.AssertNotSealed();
_previous = previous;
}
internal void Execute()
{
object?[] actual = GetActual();
object?[] 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++)
{
object? currentActual = actual[i];
object? currentExpected = singleExpected ? expected[0] : i < expected.Length ? expected[i] : null;
AssertAndThrow(initiator, currentActual, currentExpected, FullName);
}
}
internal Assertion First()
{
Assertion assertion = this;
while (assertion._previous != null)
assertion = assertion._previous;
return assertion;
}
internal Assertion Last()
{
Assertion 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 assertion = First();
Function? callback = assertion._callback;
while (callback == null && assertion._next != null)
{
assertion = assertion._next;
callback = assertion._callback;
}
return callback;
}
private object?[] GetExpected()
{
Assertion assertion = Last();
object?[] expected = assertion._expected;
while (expected.Length == 0 && assertion._previous != null)
{
assertion = assertion._previous;
expected = assertion._expected;
}
return expected;
}
private object?[] GetActual()
{
Assertion assertion = First();
object?[] actual = assertion._actual;
while (actual.Length == 0 && assertion._next != null)
{
assertion = assertion._next;
actual = assertion._actual;
}
return actual;
}
internal Assertion SetActual(params object?[] parameters)
{
_actual = (object?[])parameters.Clone();
return this;
}
#region Private Flow-Pattern Methods
private Assertion Finalize()
{
Seal();
Execute();
return this;
}
private Assertion Seal()
{
_sealed = true;
return this;
}
private Assertion Init(string? customName = null, [CallerMemberName] string? name = null)
{
AssertNotSealed();
return SetName(customName ?? name);
}
private Assertion SetName(string? name)
{
_name = name;
return this;
}
private Assertion SetExpected(params object?[] parameters)
{
_expected = (object?[])parameters.Clone();
return this;
}
private Assertion SetCallback(Function callback)
{
_callback = callback;
return this;
}
private Assertion Next()
{
_next ??= new Assertion(this);
return _next;
}
#endregion
#region Public Flow-Pattern Methods
public Assertion Positive()
=> Init().SetCallback((a, e) => (int)a! > 0).Finalize();
public Assertion Equal(params object?[] expected)
=> Init().SetExpected(expected).SetCallback(Equals).Finalize();
public Assertion Null()
=> Init().SetExpected([null]).SetCallback(ReferenceEquals).Finalize();
public Assertion Not
=> Init().SetCallback((a, e) => !_next._callback(a, e)).Next();
public Assertion Is
=> Init().Next();
#endregion
}
private static void AssertAndThrow(
Assertion.Function function,
object? actual,
object? expected,
string name = "")
{
if (!function.Invoke(actual, expected))
{
throw new AssertionException(name, actual, expected);
}
}
public class AssertionException : Exception
{
public readonly string Assertion;
public readonly object?[] Parameters;
public AssertionException(string assertion, params object?[] parameters)
: base($"Assertion {assertion} failed for parameters:\n{string.Join('\n', parameters.Select(p => $" - <{p?.GetType().Name ?? "null"}>{p?.ToString() ?? "null"}"))}")
{
Assertion = assertion;
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