first commit
This commit is contained in:
87
Nitrox.Test/Patcher/PatchTestHelper.cs
Normal file
87
Nitrox.Test/Patcher/PatchTestHelper.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using FluentAssertions;
|
||||
using HarmonyLib;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxPatcher.PatternMatching;
|
||||
|
||||
namespace NitroxTest.Patcher
|
||||
{
|
||||
public static class PatchTestHelper
|
||||
{
|
||||
public static List<CodeInstruction> GenerateDummyInstructions(int count)
|
||||
{
|
||||
List<CodeInstruction> instructions = new List<CodeInstruction>();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
instructions.Add(new CodeInstruction(OpCodes.Nop));
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
public static ReadOnlyCollection<CodeInstruction> GetInstructionsFromMethod(DynamicMethod targetMethod)
|
||||
{
|
||||
Validate.NotNull(targetMethod);
|
||||
return GetInstructionsFromIL(GetILInstructions(targetMethod));
|
||||
}
|
||||
|
||||
public static ReadOnlyCollection<CodeInstruction> GetInstructionsFromMethod(MethodInfo targetMethod)
|
||||
{
|
||||
Validate.NotNull(targetMethod);
|
||||
return GetInstructionsFromIL(GetILInstructions(targetMethod));
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<OpCode, object>> GetILInstructions(MethodInfo method)
|
||||
{
|
||||
return PatchProcessor.ReadMethodBody(method, method.GetILGenerator());
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<OpCode, object>> GetILInstructions(DynamicMethod method)
|
||||
{
|
||||
return PatchProcessor.ReadMethodBody(method, method.GetILGenerator());
|
||||
}
|
||||
|
||||
public static ILGenerator GetILGenerator(this MethodInfo method)
|
||||
{
|
||||
return new DynamicMethod(method.Name, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
|
||||
}
|
||||
|
||||
public static void TestPattern(MethodInfo targetMethod, InstructionsPattern pattern, out IEnumerable<CodeInstruction> originalIl, out IEnumerable<CodeInstruction> transformedIl)
|
||||
{
|
||||
bool shouldHappen = false;
|
||||
originalIl = PatchProcessor.GetCurrentInstructions(targetMethod);
|
||||
transformedIl = originalIl
|
||||
.Transform(pattern, (_, _) =>
|
||||
{
|
||||
shouldHappen = true;
|
||||
})
|
||||
.ToArray(); // Required, otherwise nothing happens.
|
||||
|
||||
shouldHappen.Should().BeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the instructions so that the returned instructions are not the same reference.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Useful for testing code differences before and after a Harmony transpiler.
|
||||
/// </remarks>
|
||||
public static List<CodeInstruction> Clone(this IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
return new List<CodeInstruction>(instructions.Select(il => new CodeInstruction(il)));
|
||||
}
|
||||
|
||||
private static ReadOnlyCollection<CodeInstruction> GetInstructionsFromIL(IEnumerable<KeyValuePair<OpCode, object>> il)
|
||||
{
|
||||
List<CodeInstruction> result = new List<CodeInstruction>();
|
||||
foreach (KeyValuePair<OpCode, object> instruction in il)
|
||||
{
|
||||
result.Add(new CodeInstruction(instruction.Key, instruction.Value));
|
||||
}
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
218
Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs
Normal file
218
Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using System.Reflection.Emit;
|
||||
using HarmonyLib;
|
||||
using NitroxPatcher.Patches;
|
||||
using NitroxPatcher.Patches.Dynamic;
|
||||
using NitroxPatcher.Patches.Persistent;
|
||||
using NitroxPatcher.PatternMatching;
|
||||
using NitroxTest.Patcher;
|
||||
|
||||
namespace Nitrox.Test.Patcher.Patches;
|
||||
|
||||
[TestClass]
|
||||
public class PatchesTranspilerTest
|
||||
{
|
||||
// Add "true" to any of those elements to have its transformed IL printed.
|
||||
public static IEnumerable<object[]> TranspilerPatchClasses =>
|
||||
[
|
||||
[typeof(AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch), 3],
|
||||
[typeof(AttackCyclops_OnCollisionEnter_Patch), -17],
|
||||
[typeof(AttackCyclops_UpdateAggression_Patch), -23],
|
||||
[typeof(Bullet_Update_Patch), 3],
|
||||
[typeof(BaseDeconstructable_Deconstruct_Patch), BaseDeconstructable_Deconstruct_Patch.InstructionsToAdd(true).Count() * 2],
|
||||
[typeof(BaseHullStrength_CrushDamageUpdate_Patch), 3],
|
||||
[typeof(BreakableResource_SpawnResourceFromPrefab_Patch), 2],
|
||||
[typeof(Builder_TryPlace_Patch), Builder_TryPlace_Patch.InstructionsToAdd1.Count + Builder_TryPlace_Patch.InstructionsToAdd2.Count],
|
||||
[typeof(CellManager_TryLoadCacheBatchCells_Patch), 4],
|
||||
[typeof(Constructable_Construct_Patch), Constructable_Construct_Patch.InstructionsToAdd.Count],
|
||||
[typeof(Constructable_DeconstructAsync_Patch), Constructable_DeconstructAsync_Patch.InstructionsToAdd.Count],
|
||||
[typeof(ConstructableBase_SetState_Patch), ConstructableBase_SetState_Patch.InstructionsToAdd.Count],
|
||||
[typeof(ConstructorInput_OnCraftingBegin_Patch), 7],
|
||||
[typeof(CrafterLogic_TryPickupSingleAsync_Patch), 4],
|
||||
[typeof(CrashHome_Spawn_Patch), 2],
|
||||
[typeof(CrashHome_Update_Patch), -5],
|
||||
[typeof(CreatureDeath_OnKillAsync_Patch), 9],
|
||||
[typeof(CreatureDeath_SpawnRespawner_Patch), 2],
|
||||
[typeof(CyclopsDestructionEvent_DestroyCyclops_Patch), 3],
|
||||
[typeof(CyclopsDestructionEvent_SpawnLootAsync_Patch), 7],
|
||||
[typeof(CyclopsShieldButton_OnClick_Patch), -6],
|
||||
[typeof(CyclopsSonarButton_Update_Patch), 3],
|
||||
[typeof(CyclopsSonarDisplay_NewEntityOnSonar_Patch), 3],
|
||||
[typeof(DevConsole_Update_Patch), 0],
|
||||
[typeof(Eatable_IterateDespawn_Patch), 2],
|
||||
[typeof(EnergyMixin_SpawnDefaultAsync_Patch), -64],
|
||||
[typeof(EntityCell_AwakeAsync_Patch), 2],
|
||||
[typeof(EntityCell_SleepAsync_Patch), 2],
|
||||
[typeof(Equipment_RemoveItem_Patch), 7],
|
||||
[typeof(EscapePod_Start_Patch), 43],
|
||||
[typeof(FireExtinguisherHolder_TakeTankAsync_Patch), 2],
|
||||
[typeof(FireExtinguisherHolder_TryStoreTank_Patch), 3],
|
||||
[typeof(Flare_Update_Patch), 0],
|
||||
[typeof(FootstepSounds_OnStep_Patch), 6],
|
||||
[typeof(GameInput_Initialize_Patch), 5],
|
||||
[typeof(GrowingPlant_SpawnGrownModelAsync_Patch), -1],
|
||||
[typeof(Player_TriggerInfectionRevealAsync_Patch), 1],
|
||||
[typeof(IngameMenu_OnSelect_Patch), -2],
|
||||
[typeof(IngameMenu_QuitGameAsync_Patch), 2],
|
||||
[typeof(IngameMenu_QuitSubscreen_Patch), -24],
|
||||
[typeof(ItemsContainer_DestroyItem_Patch), 2],
|
||||
[typeof(Knife_OnToolUseAnim_Patch), 0],
|
||||
[typeof(LargeRoomWaterPark_OnDeconstructionStart_Patch), 3],
|
||||
[typeof(LargeWorldEntity_UpdateCell_Patch), 1],
|
||||
[typeof(LaunchRocket_OnHandClick_Patch), -9],
|
||||
[typeof(LeakingRadiation_Update_Patch), 0],
|
||||
[typeof(MainGameController_StartGame_Patch), 1],
|
||||
[typeof(MeleeAttack_CanDealDamageTo_Patch), 4],
|
||||
[typeof(PDAScanner_Scan_Patch), 3],
|
||||
[typeof(PickPrefab_AddToContainerAsync_Patch), 4],
|
||||
[typeof(Player_OnKill_Patch), 0],
|
||||
[typeof(Respawn_Start_Patch), 3],
|
||||
[typeof(RocketConstructor_StartRocketConstruction_Patch), 3],
|
||||
[typeof(SpawnConsoleCommand_SpawnAsync_Patch), 2],
|
||||
[typeof(SpawnOnKill_OnKill_Patch), 3],
|
||||
[typeof(SubConsoleCommand_OnConsoleCommand_sub_Patch), 0],
|
||||
[typeof(SubRoot_OnPlayerEntered_Patch), 5],
|
||||
[typeof(Trashcan_Update_Patch), 4],
|
||||
[typeof(uGUI_OptionsPanel_AddAccessibilityTab_Patch), -10],
|
||||
[typeof(uGUI_PDA_Initialize_Patch), 2],
|
||||
[typeof(uGUI_PDA_SetTabs_Patch), 3],
|
||||
[typeof(uGUI_Pings_IsVisibleNow_Patch), 0],
|
||||
[typeof(uGUI_SceneIntro_HandleInput_Patch), -2],
|
||||
[typeof(uGUI_SceneIntro_IntroSequence_Patch), 8],
|
||||
[typeof(uSkyManager_SetVaryingMaterialProperties_Patch), 0],
|
||||
[typeof(Welder_Weld_Patch), 1],
|
||||
[typeof(Poop_Perform_Patch), 1],
|
||||
[typeof(SeaDragonMeleeAttack_OnTouchFront_Patch), 9],
|
||||
[typeof(SeaDragonMeleeAttack_SwatAttack_Patch), 4],
|
||||
[typeof(SeaTreaderSounds_SpawnChunks_Patch), 3],
|
||||
[typeof(Vehicle_TorpedoShot_Patch), 3],
|
||||
[typeof(SeamothTorpedo_Update_Patch), 0],
|
||||
[typeof(SeaTreader_UpdatePath_Patch), 0],
|
||||
[typeof(SeaTreader_UpdateTurning_Patch), 0],
|
||||
[typeof(SeaTreader_Update_Patch), 0],
|
||||
[typeof(StasisSphere_LateUpdate_Patch), 0],
|
||||
[typeof(WaterParkCreature_BornAsync_Patch), 6],
|
||||
[typeof(WaterParkCreature_ManagedUpdate_Patch), 2],
|
||||
];
|
||||
|
||||
[TestMethod]
|
||||
public void AllTranspilerPatchesHaveSanityTest()
|
||||
{
|
||||
Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(NitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray();
|
||||
|
||||
foreach (Type patch in allPatchesWithTranspiler)
|
||||
{
|
||||
if (TranspilerPatchClasses.All(x => (Type)x[0] != patch))
|
||||
{
|
||||
Assert.Fail($"{patch.Name} has an Transpiler but is not included in the test-suit and {nameof(TranspilerPatchClasses)}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(TranspilerPatchClasses))]
|
||||
public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bool logInstructions = false)
|
||||
{
|
||||
FieldInfo targetMethodInfo = patchClassType.GetRuntimeFields().FirstOrDefault(x => string.Equals(x.Name.Replace("_", ""), "targetMethod", StringComparison.OrdinalIgnoreCase));
|
||||
if (targetMethodInfo == null)
|
||||
{
|
||||
Assert.Fail($"Could not find either \"TARGET_METHOD\" nor \"targetMethod\" inside {patchClassType.Name}");
|
||||
}
|
||||
|
||||
MethodInfo targetMethod = targetMethodInfo.GetValue(null) as MethodInfo;
|
||||
List<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList();
|
||||
List<CodeInstruction> originalIlCopy = PatchTestHelper.GetInstructionsFromMethod(targetMethod).ToList(); // Our custom pattern matching replaces OpCode/Operand in place, therefor we need a copy to compare if changes are present
|
||||
|
||||
MethodInfo transpilerMethod = patchClassType.GetMethod("Transpiler");
|
||||
if (transpilerMethod == null)
|
||||
{
|
||||
Assert.Fail($"Could not find \"Transpiler\" inside {patchClassType.Name}");
|
||||
}
|
||||
|
||||
List<object> injectionParameters = [];
|
||||
foreach (ParameterInfo parameterInfo in transpilerMethod.GetParameters())
|
||||
{
|
||||
if (parameterInfo.ParameterType == typeof(MethodBase))
|
||||
{
|
||||
injectionParameters.Add(targetMethod);
|
||||
}
|
||||
else if (parameterInfo.ParameterType == typeof(IEnumerable<CodeInstruction>))
|
||||
{
|
||||
injectionParameters.Add(originalIl);
|
||||
}
|
||||
else if (parameterInfo.ParameterType == typeof(ILGenerator))
|
||||
{
|
||||
injectionParameters.Add(GetILGenerator(targetMethod, patchClassType));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail($"Unexpected parameter type: {parameterInfo.ParameterType} inside Transpiler method of {patchClassType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
List<CodeInstruction> transformedIl = (transpilerMethod.Invoke(null, injectionParameters.ToArray()) as IEnumerable<CodeInstruction>)?.ToList();
|
||||
|
||||
if (logInstructions)
|
||||
{
|
||||
Console.WriteLine(transformedIl.ToPrettyString());
|
||||
}
|
||||
|
||||
if (transformedIl == null || transformedIl.Count == 0)
|
||||
{
|
||||
Assert.Fail($"Calling {patchClassType.Name}.Transpiler() through reflection returned null or an empty list.");
|
||||
}
|
||||
|
||||
originalIlCopy.Count.Should().Be(transformedIl.Count - ilDifference);
|
||||
Assert.IsFalse(originalIlCopy.SequenceEqual(transformedIl, new CodeInstructionComparer()), $"The transpiler patch of {patchClassType.Name} did not change the IL");
|
||||
}
|
||||
|
||||
private static readonly ModuleBuilder patchTestModule;
|
||||
|
||||
static PatchesTranspilerTest()
|
||||
{
|
||||
AssemblyName asmName = new();
|
||||
asmName.Name = "PatchTestAssembly";
|
||||
|
||||
PersistedAssemblyBuilder myAsmBuilder = new(asmName, typeof(object).Assembly);
|
||||
patchTestModule = myAsmBuilder.DefineDynamicModule(asmName.Name);
|
||||
}
|
||||
|
||||
/// This complicated generation is required for ILGenerator.DeclareLocal to work
|
||||
private static ILGenerator GetILGenerator(MethodInfo method, Type generatingType)
|
||||
{
|
||||
TypeBuilder myTypeBld = patchTestModule.DefineType($"{generatingType}_PatchTestType", TypeAttributes.Public);
|
||||
|
||||
return myTypeBld.DefineMethod(method.Name, MethodAttributes.Public, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
|
||||
}
|
||||
}
|
||||
|
||||
public class CodeInstructionComparer : IEqualityComparer<CodeInstruction>
|
||||
{
|
||||
public bool Equals(CodeInstruction x, CodeInstruction y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (x is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (x.GetType() != y.GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return x.opcode.Equals(y.opcode) && Equals(x.operand, y.operand);
|
||||
}
|
||||
|
||||
public int GetHashCode(CodeInstruction obj)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (obj.opcode.GetHashCode() * 397) ^ (obj.operand != null ? obj.operand.GetHashCode() : 0);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
using LitJson;
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace Nitrox.Test.Patcher.Patches.Persistent;
|
||||
|
||||
[TestClass]
|
||||
public class Language_LoadLanguageFile_PatchTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void DefaultLanguageSanity()
|
||||
{
|
||||
string languageFolder = Path.Combine(NitroxUser.AssetsPath, "LanguageFiles");
|
||||
Assert.IsTrue(Directory.Exists(languageFolder), $"The language files folder does not exist at {languageFolder}.");
|
||||
|
||||
string defaultLanguageFilePath = Path.Combine(languageFolder, "en.json");
|
||||
Assert.IsTrue(File.Exists(defaultLanguageFilePath), $"The english language file does not exist at {defaultLanguageFilePath}.");
|
||||
|
||||
using StreamReader streamReader = new(defaultLanguageFilePath);
|
||||
try
|
||||
{
|
||||
JsonData defaultLanguage = JsonMapper.ToObject(streamReader);
|
||||
Assert.IsTrue(defaultLanguage.Keys.Count > 0, "The english language file has no entries.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Unable to map default json file : {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user