< Summary - pva.SuperV

Information
Class: pva.Helpers.Extensions.ShouldlyExtensions
Assembly: pva.Helpers
File(s): /home/runner/work/pva.SuperV/pva.SuperV/pva.Helpers/Extensions/ShouldlyExtensions.cs
Tag: dotnet-ubuntu_22190969454
Line coverage
73%
Covered lines: 76
Uncovered lines: 28
Coverable lines: 104
Total lines: 216
Line coverage: 73%
Branch coverage
59%
Covered branches: 55
Total branches: 92
Branch coverage: 59.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ShouldBeEquivalentTo(...)50%4471.42%
ShouldBeEquivalentTo(...)0%620%
ValidateAndSetupOptions(...)25%321660%
CompareObjects(...)56.66%403077.77%
IsOfType(...)100%11100%
ComparePropertyValues(...)88.46%342676.92%
CompareCollections(...)100%22100%
IsCollectionType(...)100%22100%
IsComplexType(...)50%1010100%

File(s)

/home/runner/work/pva.SuperV/pva.SuperV/pva.Helpers/Extensions/ShouldlyExtensions.cs

#LineLine coverage
 1using Shouldly;
 2using System.Collections;
 3using System.Linq.Expressions;
 4
 5namespace pva.Helpers.Extensions;
 6
 7public class EquivalencyOptions<T>
 8{
 9    public HashSet<string> ExcludedProperties { get; } = [];
 10    public HashSet<Type> ExcludedTypes { get; } = [];
 11    public TimeSpan? DateTimeTolerance { get; private set; }
 12
 13    public EquivalencyOptions<T> Exclude(Expression<Func<T, object>> propertyExpression)
 14    {
 15        ArgumentNullException.ThrowIfNull(propertyExpression);
 16
 17        ExcludedProperties.Add(GetPropertyPath(propertyExpression));
 18        return this;
 19    }
 20
 21    public EquivalencyOptions<T> ExcludeType(Type type)
 22    {
 23        ArgumentNullException.ThrowIfNull(type);
 24
 25        ExcludedTypes.Add(type);
 26        return this;
 27    }
 28
 29    public EquivalencyOptions<T> UsingDateTimeTolerance(TimeSpan tolerance)
 30    {
 31        DateTimeTolerance = tolerance;
 32        return this;
 33    }
 34
 35    private static string GetPropertyPath(Expression expression)
 36    {
 37        var propertyNames = new List<string>();
 38        ExtractPropertyPath(expression, propertyNames);
 39        propertyNames.Reverse();
 40        return string.Join(".", propertyNames);
 41    }
 42
 43    private static void ExtractPropertyPath(Expression expression, List<string> propertyNames)
 44    {
 45        switch (expression)
 46        {
 47            case MemberExpression memberExpression:
 48                propertyNames.Add(memberExpression.Member.Name);
 49                ExtractPropertyPath(memberExpression.Expression!, propertyNames);
 50                break;
 51            case MethodCallExpression methodCallExpression:
 52                foreach (var argument in methodCallExpression.Arguments.Reverse())
 53                {
 54                    ExtractPropertyPath(argument, propertyNames);
 55                }
 56                break;
 57            case UnaryExpression unaryExpression:
 58                ExtractPropertyPath(unaryExpression.Operand, propertyNames);
 59                break;
 60            case LambdaExpression lambdaExpression:
 61                ExtractPropertyPath(lambdaExpression.Body, propertyNames);
 62                break;
 63            case ParameterExpression:
 64                break;
 65            default:
 66                throw new ArgumentException("Invalid property expression");
 67        }
 68    }
 69}
 70
 71
 72[ShouldlyMethods]
 73public static class ShouldlyExtensions
 74{
 75    public static void ShouldBeEquivalentTo<T>(this T? actual, T? expected, Action<EquivalencyOptions<T>> options) where
 1576    {
 1577        if (typeof(IEnumerable).IsAssignableFrom(typeof(T)) && typeof(T) != typeof(string))
 078        {
 079            throw new InvalidOperationException("This overload is not intended for collection types.");
 80        }
 81
 1582        var equivalencyOptions = ValidateAndSetupOptions(actual, expected, options);
 83
 1584        CompareObjects(actual, expected, equivalencyOptions, new HashSet<(object, object)>());
 1585    }
 86
 87    public static void ShouldBeEquivalentTo<T>(this IEnumerable<T?> actual, IEnumerable<T?> expected, Action<Equivalency
 088    {
 089        var equivalencyOptions = ValidateAndSetupOptions(actual, expected, options);
 090        var actualList = actual.ToList();
 091        var expectedList = expected.ToList();
 92
 093        actualList.Count.ShouldBe(expectedList.Count, "Collection counts do not match.");
 94
 095        for (int i = 0; i < actualList.Count; i++)
 096        {
 097            CompareObjects(actualList[i], expectedList[i], equivalencyOptions, new HashSet<(object, object)>());
 098        }
 099    }
 100
 101    private static EquivalencyOptions<T> ValidateAndSetupOptions<T>(object? actual, object? expected, Action<Equivalency
 15102    {
 15103        if (actual == null && expected != null || actual != null && expected == null)
 0104        {
 0105            throw new ShouldAssertException(@$"Comparing object equivalence:
 0106actual: {actual?.GetType().ToString() ?? "null"}
 0107expected: {expected?.GetType().ToString() ?? "null"}");
 108        }
 109
 15110        var equivalencyOptions = new EquivalencyOptions<T>();
 15111        options(equivalencyOptions);
 112
 15113        return equivalencyOptions;
 15114    }
 115
 116    private static void CompareObjects<T>(object? actual, object? expected, EquivalencyOptions<T> options, HashSet<(obje
 165117    {
 165118        if (actual == null && expected == null)
 0119        {
 0120            return;
 121        }
 122
 165123        if (actual == null || expected == null)
 0124        {
 0125            throw new ShouldAssertException(@$"Comparing object equivalence, at path '{parentPath}':
 0126actual: {actual?.GetType().ToString() ?? "null"}
 0127expected: {expected?.GetType().ToString() ?? "null"}");
 128        }
 129
 165130        if (visitedObjects.Contains((actual, expected)))
 53131        {
 53132            return;
 133        }
 112134        visitedObjects.Add((actual, expected));
 135
 1216136        foreach (var property in actual.GetType().GetProperties())
 440137        {
 440138            var propertyPath = string.IsNullOrEmpty(parentPath) ? property.Name : $"{parentPath}.{property.Name}";
 440139            if (property.GetIndexParameters().Length > 0)
 70140            {
 70141                continue; // Skip indexers
 142            }
 370143            object? actualPropertyValue = property.GetValue(actual, null);
 370144            if (options.ExcludedProperties.Contains(propertyPath)
 370145                || (actualPropertyValue != null && IsOfType(options, actualPropertyValue)))
 2146            {
 2147                continue;
 148            }
 149
 368150            ComparePropertyValues(property.GetValue(actual, null), property.GetValue(expected, null), property, options,
 368151        }
 165152    }
 153
 154    private static bool IsOfType<T>(EquivalencyOptions<T> options, object actualPropertyValue)
 305155    {
 610156        return options.ExcludedTypes.Any(type => actualPropertyValue.GetType().IsSubclassOf(type));
 305157    }
 158
 159    private static void ComparePropertyValues<T>(object? actualValue, object? expectedValue, System.Reflection.PropertyI
 368160    {
 368161        var hasNull = actualValue == null || expectedValue == null;
 368162        if (!hasNull && property.PropertyType == typeof(DateTime) && options.DateTimeTolerance.HasValue)
 0163        {
 0164            ((DateTime)actualValue!).ShouldBe((DateTime)expectedValue!, options.DateTimeTolerance.Value, $"Property {pro
 0165        }
 368166        else if (!hasNull && property.PropertyType == typeof(DateTimeOffset) && options.DateTimeTolerance.HasValue)
 0167        {
 0168            ((DateTimeOffset)actualValue!).ShouldBe((DateTimeOffset)expectedValue!, options.DateTimeTolerance.Value, $"P
 0169        }
 368170        else if (!hasNull && IsCollectionType(property.PropertyType))
 57171        {
 57172            CompareCollections(actualValue!, expectedValue!, options, visitedObjects, propertyPath);
 57173        }
 311174        else if (!hasNull && actualValue.GetType().IsTypeDefinition)
 219175        {
 219176            actualValue.ShouldBeEquivalentTo(expectedValue, $"Property {propertyPath} does not match.");
 219177        }
 92178        else if (!hasNull && IsComplexType(actualValue!))
 27179        {
 27180            CompareObjects(actualValue, expectedValue, options, visitedObjects, propertyPath);
 27181        }
 182        else
 65183        {
 65184            actualValue.ShouldBe(expectedValue, $"Property {propertyPath} does not match.");
 65185        }
 368186    }
 187
 188    private static void CompareCollections<T>(object actual, object expected, EquivalencyOptions<T> options, HashSet<(ob
 57189    {
 57190        var actualList = ((IEnumerable<object>)actual).ToList();
 57191        var expectedList = ((IEnumerable<object>)expected).ToList();
 192
 57193        actualList.Count.ShouldBe(expectedList.Count, "Collection counts do not match.");
 194
 360195        for (int i = 0; i < actualList.Count; i++)
 123196        {
 123197            CompareObjects(actualList[i], expectedList[i], options, visitedObjects, parentPath);
 123198        }
 57199    }
 200
 201    private static bool IsCollectionType(Type type)
 303202    {
 303203        return typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);
 303204    }
 205
 206    private static bool IsComplexType(object obj)
 27207    {
 27208        var type = obj.GetType();
 27209        return !type.IsPrimitive
 27210            && !type.IsEnum
 27211            && type != typeof(string)
 27212            && type != typeof(decimal)
 27213            && type != typeof(DateTime)
 27214            && type != typeof(DateTimeOffset);
 27215    }
 216}