using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using NUnit.Framework; using OpenTap; using OpenTap.Package; namespace Package.UnitTests; [TestFixture] public class TestConcurrentTypeDataLoad { static System.Reflection.Assembly CreateNewAssemblyWithTestStep(string stepName) { var asmName = new AssemblyNameDefinition(Guid.NewGuid().ToString(), new Version(1, 0)); string moduleName = Guid.NewGuid().ToString(); using var asm = AssemblyDefinition.CreateAssembly(asmName, moduleName, ModuleKind.Dll); var teststep = asm.MainModule.ImportReference(typeof(TestStep)); var runMethod = asm.MainModule.ImportReference(typeof(TestStep).GetMethod("Run", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)).Resolve(); var ctorMethod = asm.MainModule.ImportReference(typeof(TestStep).GetConstructor([])).Resolve(); // create dummy plugin { // Create new test step var t = new TypeDefinition(Guid.NewGuid().ToString(), stepName, TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.Public, teststep); asm.MainModule.Types.Add(t); // Create default constructor { var ctor = new MethodDefinition(".ctor", ctorMethod.Attributes, asm.MainModule.TypeSystem.Void); var il = ctor.Body.GetILProcessor(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, t.Module.ImportReference(ctorMethod)); il.Emit(OpCodes.Ret); t.Methods.Add(ctor); } // build Run() method { var newRunMethod = new MethodDefinition("Run", MethodAttributes.Public, runMethod.ReturnType) { IsHideBySig = true, IsVirtual = true }; var il = newRunMethod.Body.GetILProcessor(); il.Emit(OpCodes.Ret); t.Methods.Add(newRunMethod); } } var path = Path.Combine(Installation.Current.Directory, "TestTypeDataLoadAssemblies", asm.Name.Name + ".dll"); asm.Write(path); return System.Reflection.Assembly.LoadFrom(path); } [TestCase(1)] [TestCase(10)] [TestCase(100)] [TestCase(500)] public void TestTypeDataLoad(int count) { static void VerifyAssemblyData(int index, System.Reflection.Assembly asm) { var step = asm.GetExportedTypes().First(); var td = TypeData.FromType(step); TestStep instance = (TestStep)td.CreateInstance(); Assert.That(instance, Is.Not.Null); Assert.That(td.GetDisplayAttribute().Name, Is.EqualTo($"Step_{index}")); } var ctx = UninstallContext.Create(Installation.Current); var directory = Path.Combine(Installation.Current.Directory, "TestTypeDataLoadAssemblies"); Directory.CreateDirectory(directory); // Ensure searching is done before we start creating assemblies PluginManager.GetSearcher(); // Create a ton of distinct assemblies and load them var assemblies = Enumerable.Range(0, count).Select(i => CreateNewAssemblyWithTestStep($"Step_{i}")).ToArray(); // Simultaneously attempt to load typedata from each plugin Task[] tasks = [.. Enumerable.Range(0, count).Select(i => StartAwaitable(() => VerifyAssemblyData(i, assemblies[i])))]; Task.WaitAll(tasks); // Delete all assemblies and try again -- it should still work foreach (var asm in assemblies) { var rel = Path.GetRelativePath(Installation.Current.Directory, asm.Location); ctx.Delete(rel); } PluginManager.Search(); tasks = [.. Enumerable.Range(0, count).Select(i => StartAwaitable(() => VerifyAssemblyData(i, assemblies[i])))]; Task.WaitAll(tasks); // This should succeed since all the dlls were uninstalled Directory.Delete(directory, false); } internal static Task StartAwaitable(Action action) { var result = new TaskCompletionSource(); TapThread.Start(() => { try { action(); result.SetResult(true); } catch (Exception inner) { result.SetException(inner); } }); return result.Task; } }