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
Prev Previous commit
Next Next commit
Added custom AutoFaker implementation
Merge me
  • Loading branch information
Jannify committed Apr 8, 2023
commit 3be5f7f509ff9d1a12ae22dc1e193e6076c99fac
66 changes: 66 additions & 0 deletions Nitrox.Test/Helper/Faker/NitroxAbstractFaker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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
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);
}
212 changes: 212 additions & 0 deletions Nitrox.Test/Helper/Faker/NitroxAutoFaker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

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);

memberInfos = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(member => member.GetCustomAttributes<DataMemberAttribute>().Any()).ToArray();

constructor = GetConstructorForType(type, memberInfos);

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.Last(f => f.GetType() == typeof(NitroxCollectionFaker));
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 ConstructorInfo GetConstructorForType(Type type, MemberInfo[] dataMembers)
{
ConstructorInfo constructorToUse = null;
ConstructorInfo[] constructors = type.GetConstructors();

foreach (ConstructorInfo constructor in constructors)
{
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)
{
constructorToUse = constructor;
break;
}
}

if (constructorToUse == null)
{
constructorToUse = type.GetConstructor(Array.Empty<Type>());

if (constructorToUse == null)
{
throw new NullReferenceException($"Could not find a constructor with no parameters for {type}");
}
}

return constructorToUse;
}
}
Loading