Skip to content

Commit

Permalink
Creating custom AutoBogus for WorldPersistenceTest and PacketsSeriali…
Browse files Browse the repository at this point in the history
…zableTest (#2018)

* Fix potential NRE in AssertHelper

* Remove old Bogus implementation

* Added custom AutoFaker implementation

* Use custom faker for WorldPersistenceTest

* Updating WorldPersistenceTest with data structure changes

* Fixing unnoticed bugs; not anymore with the awesome savedata faker :D

* Implement the new faker in the PacketsSerializableTest

* Downgrade AutoBogus nuget to Bogus

* Address requested changes
  • Loading branch information
Jannify authored Apr 22, 2023
1 parent 1e8b7ae commit f9ac48b
Show file tree
Hide file tree
Showing 27 changed files with 841 additions and 426 deletions.
7 changes: 7 additions & 0 deletions Nitrox.Test/Helper/AssertHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public static class AssertHelper
{
public static void IsListEqual<TSource>(IOrderedEnumerable<TSource> first, IOrderedEnumerable<TSource> second, Action<TSource, TSource> assertComparer)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);

List<TSource> firstList = first.ToList();
List<TSource> secondList = second.ToList();

Expand All @@ -22,6 +25,8 @@ public static void IsListEqual<TSource>(IOrderedEnumerable<TSource> first, IOrde

public static void IsDictionaryEqual<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
Assert.AreEqual(first.Count, second.Count);

for (int index = 0; index < first.Count; index++)
Expand All @@ -34,6 +39,8 @@ public static void IsDictionaryEqual<TKey, TValue>(IDictionary<TKey, TValue> fir

public static void IsDictionaryEqual<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second, Action<KeyValuePair<TKey, TValue>, KeyValuePair<TKey, TValue>> assertComparer)
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
Assert.AreEqual(first.Count, second.Count);

for (int index = 0; index < first.Count; index++)
Expand Down
70 changes: 70 additions & 0 deletions Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NitroxModel_Subnautica.Logger;
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
using NitroxServer;
using NitroxServer_Subnautica;
using NitroxServer.ConsoleCommands.Abstract;

namespace Nitrox.Test.Helper.Faker;

public class NitroxAbstractFaker : NitroxFaker, INitroxFaker
{
private static readonly Dictionary<Type, Type[]> subtypesByBaseType;

static NitroxAbstractFaker()
{
Assembly[] assemblies = { typeof(Packet).Assembly, typeof(SubnauticaInGameLogger).Assembly, typeof(ServerAutoFacRegistrar).Assembly, typeof(SubnauticaServerAutoFacRegistrar).Assembly };
HashSet<Type> blacklistedTypes = new() { typeof(Packet), typeof(CorrelatedPacket), typeof(Command), typeof(PacketProcessor) };

List<Type> types = new();
foreach (Assembly assembly in assemblies)
{
types.AddRange(assembly.GetTypes());
}

subtypesByBaseType = types.Where(type => type.IsAbstract && !type.IsSealed && !blacklistedTypes.Contains(type))
.ToDictionary(type => type, type => types.Where(t => type.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface).ToArray())
.Where(dict => dict.Value.Length > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}

public readonly int AssignableTypesCount;
private readonly Queue<INitroxFaker> assignableFakers = new();

public NitroxAbstractFaker(Type type)
{
if (!type.IsAbstract)
{
throw new ArgumentException("Argument is not abstract", nameof(type));
}

if (!subtypesByBaseType.TryGetValue(type, out Type[] subTypes))
{
throw new ArgumentException($"Argument is not contained in {nameof(subtypesByBaseType)}", nameof(type));
}

OutputType = type;
AssignableTypesCount = subTypes.Length;
FakerByType.Add(type, this);
foreach (Type subType in subTypes)
{
assignableFakers.Enqueue(GetOrCreateFaker(subType));
}
}

public INitroxFaker[] GetSubFakers() => assignableFakers.ToArray();

/// <summary>
/// Selects an implementing type in a round-robin fashion of the abstract type of this faker. Then creates an instance of it.
/// </summary>
public object GenerateUnsafe(HashSet<Type> typeTree)
{
INitroxFaker assignableFaker = assignableFakers.Dequeue();
assignableFakers.Enqueue(assignableFaker);
return assignableFaker.GenerateUnsafe(typeTree);
}
}
19 changes: 19 additions & 0 deletions Nitrox.Test/Helper/Faker/NitroxActionFaker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;

namespace Nitrox.Test.Helper.Faker;

public class NitroxActionFaker : NitroxFaker, INitroxFaker
{
private readonly Func<Bogus.Faker, object> generateAction;

public NitroxActionFaker(Type type, Func<Bogus.Faker, object> action)
{
OutputType = type;
generateAction = action;
}

public INitroxFaker[] GetSubFakers() => Array.Empty<INitroxFaker>();

public object GenerateUnsafe(HashSet<Type> _) => generateAction.Invoke(Faker);
}
218 changes: 218 additions & 0 deletions Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using BinaryPack.Attributes;

namespace Nitrox.Test.Helper.Faker;

public class NitroxAutoFaker<T> : NitroxFaker, INitroxFaker
{
private readonly ConstructorInfo constructor;
private readonly MemberInfo[] memberInfos;
private readonly INitroxFaker[] parameterFakers;

public NitroxAutoFaker()
{
Type type = typeof(T);
if (!IsValidType(type))
{
throw new InvalidOperationException($"{type.Name} is not a valid type for {nameof(NitroxAutoFaker<T>)}");
}

OutputType = type;
FakerByType.Add(type, this);

if (type.GetCustomAttributes(typeof(DataContractAttribute), false).Length > 0)
{
memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(member => member.GetCustomAttributes<DataMemberAttribute>().Any()).ToArray();
}
else
{
memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.Where(member => member.MemberType is MemberTypes.Field or MemberTypes.Property &&
!member.GetCustomAttributes<IgnoredMemberAttribute>().Any())
.ToArray();
}

if (!TryGetConstructorForType(type, memberInfos, out constructor) &&
!TryGetConstructorForType(type, Array.Empty<MemberInfo>(), out constructor))
{
throw new NullReferenceException($"Could not find a constructor with no parameters for {type}");
}

parameterFakers = new INitroxFaker[memberInfos.Length];

Type[] constructorArgumentTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray();
for (int i = 0; i < memberInfos.Length; i++)
{
Type dataMemberType = constructorArgumentTypes.Length == memberInfos.Length ? constructorArgumentTypes[i] : memberInfos[i].GetMemberType();

if (FakerByType.TryGetValue(dataMemberType, out INitroxFaker memberFaker))
{
parameterFakers[i] = memberFaker;
}
else
{
parameterFakers[i] = CreateFaker(dataMemberType);
}
}
}

private void ValidateFakerTree()
{
List<INitroxFaker> fakerTree = new();

void ValidateFaker(INitroxFaker nitroxFaker)
{
if (fakerTree.Contains(nitroxFaker))
{
return;
}

fakerTree.Add(nitroxFaker);

if (nitroxFaker is NitroxAbstractFaker abstractFaker)
{
NitroxCollectionFaker collectionFaker = (NitroxCollectionFaker)fakerTree.LastOrDefault(f => f.GetType() == typeof(NitroxCollectionFaker));
if (collectionFaker != null)
{
collectionFaker.GenerateSize = Math.Max(collectionFaker.GenerateSize, abstractFaker.AssignableTypesCount);
}
}

foreach (INitroxFaker subFaker in nitroxFaker.GetSubFakers())
{
ValidateFaker(subFaker);
}

fakerTree.Remove(nitroxFaker);
}

foreach (INitroxFaker parameterFaker in parameterFakers)
{
ValidateFaker(parameterFaker);
}
}

public INitroxFaker[] GetSubFakers() => parameterFakers;

public T Generate()
{
ValidateFakerTree();
return (T)GenerateUnsafe(new HashSet<Type>());
}

public object GenerateUnsafe(HashSet<Type> typeTree)
{
object[] parameterValues = new object[parameterFakers.Length];

for (int i = 0; i < parameterValues.Length; i++)
{
INitroxFaker parameterFaker = parameterFakers[i];

if (typeTree.Contains(parameterFaker.OutputType))
{
if (parameterFaker is NitroxCollectionFaker collectionFaker)
{
parameterValues[i] = Activator.CreateInstance(collectionFaker.OutputCollectionType);
}
else
{
parameterValues[i] = null;
}
}
else
{
typeTree.Add(parameterFaker.OutputType);
parameterValues[i] = parameterFakers[i].GenerateUnsafe(typeTree);
typeTree.Remove(parameterFaker.OutputType);
}
}

if (constructor.GetParameters().Length == parameterValues.Length)
{
return (T)constructor.Invoke(parameterValues);
}

T obj = (T)constructor.Invoke(Array.Empty<object>());
for (int index = 0; index < memberInfos.Length; index++)
{
MemberInfo memberInfo = memberInfos[index];
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
((FieldInfo)memberInfo).SetValue(obj, parameterValues[index]);
break;
case MemberTypes.Property:
PropertyInfo propertyInfo = (PropertyInfo)memberInfo;

if (!propertyInfo.CanWrite &&
NitroxCollectionFaker.IsCollection(parameterValues[index].GetType(), out NitroxCollectionFaker.CollectionType collectionType))
{
dynamic origColl = propertyInfo.GetValue(obj);

switch (collectionType)
{
case NitroxCollectionFaker.CollectionType.ARRAY:
for (int i = 0; i < ((Array)parameterValues[index]).Length; i++)
{
origColl[i] = ((Array)parameterValues[index]).GetValue(i);
}

break;
case NitroxCollectionFaker.CollectionType.LIST:
case NitroxCollectionFaker.CollectionType.DICTIONARY:
case NitroxCollectionFaker.CollectionType.SET:
foreach (dynamic createdValue in ((IEnumerable)parameterValues[index]))
{
origColl.Add(createdValue);
}

break;
case NitroxCollectionFaker.CollectionType.NONE:
default:
throw new ArgumentOutOfRangeException();
}
}
else
{
propertyInfo.SetValue(obj, parameterValues[index]);
}

break;
default:
throw new ArgumentOutOfRangeException();
}
}

return obj;
}

private static bool TryGetConstructorForType(Type type, MemberInfo[] dataMembers, out ConstructorInfo constructorInfo)
{
foreach (ConstructorInfo constructor in type.GetConstructors())
{
if (constructor.GetParameters().Length != dataMembers.Length)
{
continue;
}

bool parameterValid = constructor.GetParameters()
.All(parameter => dataMembers.Any(d => d.GetMemberType() == parameter.ParameterType &&
d.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase)));

if (parameterValid)
{
constructorInfo = constructor;
return true;
}
}

constructorInfo = null;
return false;
}
}
Loading

0 comments on commit f9ac48b

Please sign in to comment.