Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating custom AutoBogus for WorldPersistenceTest and PacketsSerializableTest #2018

Merged
merged 10 commits into from
Apr 22, 2023
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();

Jannify marked this conversation as resolved.
Show resolved Hide resolved
/// <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