using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Versioning; using Mono.Cecil; using Mono.Cecil.Cil; using NUnit.Framework; namespace OpenTap.Package.UnitTests; [TestFixture] public class UninstallContextTest { [Test] public void TestDeleteFile() { var uninstallContext = UninstallContext.Create(Installation.Current); // initially verify that all the files exist. foreach (var file in Installation.Current.GetOpenTapPackage().Files) { Assert.That(File.Exists(file.RelativeDestinationPath)); } foreach (var file in Installation.Current.GetOpenTapPackage().Files) { uninstallContext.Delete(file); } // After deleting the files verify that all files are deleted. bool verifyAllFilesDeleted = true; foreach (var file in Installation.Current.GetOpenTapPackage().Files) { if (File.Exists(file.RelativeDestinationPath)) { verifyAllFilesDeleted = false; } } uninstallContext.UndoAllDeletions(); Assert.That(verifyAllFilesDeleted, Is.True); // finally after undo, verify that all files are back. foreach (var file in Installation.Current.GetOpenTapPackage().Files) { Assert.That(File.Exists(file.RelativeDestinationPath)); } Assert.That(File.Exists(".uninstall/.OpenTapIgnore"), Is.True); } class TestAsmDef(string assemblyName, string @namespace, string className, string displayName, int majorVersion, string helpLink = null) { public readonly string assemblyName = assemblyName; public readonly string className = className; public readonly string @namespace = @namespace; public readonly string displayName = displayName; public int majorVersion = majorVersion; public readonly string helpLink = helpLink; } static string CreateNewAssemblyWithTestStep(TestAsmDef def) { var asmName = new AssemblyNameDefinition(def.assemblyName, new Version(def.majorVersion, 0)); string moduleName = "TestModule"; 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(def.@namespace, def.className, 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); } // Add display attribute { Type attrType = typeof(DisplayAttribute); object[] arguments = [def.displayName, "Runtime Generated Step", "Cecil", 1.0, false, Array.Empty()]; Type[] signature = [ .. arguments.Select(x => x.GetType()) ]; var tCtor = t.Module.ImportReference(attrType.GetConstructor(signature)); var attr = new CustomAttribute(tCtor); var attrArguments = arguments.Select(x => new CustomAttributeArgument(t.Module.ImportReference(x.GetType()), x)); foreach (var arg in attrArguments) attr.ConstructorArguments.Add(arg); t.CustomAttributes.Add(attr); } // Add HelpLink attribute if (def.helpLink != null) { Type attrType = typeof(HelpLinkAttribute); object[] arguments = [def.helpLink]; Type[] signature = [ .. arguments.Select(x => x.GetType()) ]; var tCtor = t.Module.ImportReference(attrType.GetConstructor(signature)); var attr = new CustomAttribute(tCtor); var attrArguments = arguments.Select(x => new CustomAttributeArgument(t.Module.ImportReference(x.GetType()), x)); foreach (var arg in attrArguments) attr.ConstructorArguments.Add(arg); t.CustomAttributes.Add(attr); } } var fp = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".dll"); asm.Write(fp); return fp; } [Test] public void TestLoadDifferentPluginVersions() { const string testAssemblyName = nameof(TestLoadDifferentPluginVersions); var asmLocation = Path.Combine(Installation.Current.Directory, testAssemblyName + ".dll"); if (File.Exists(asmLocation)) File.Delete(asmLocation); using var _ = Utils.WithDisposable(() => File.Delete(asmLocation)); var uninstall = UninstallContext.Create(Installation.Current); var def1 = new TestAsmDef(testAssemblyName, "ns1", "stepclass", "First Name", 1); var def2 = new TestAsmDef(testAssemblyName, "ns1", "stepclass", "Second Name", 2); // Create two different versions of the same assembly with minor changes. var asm1 = CreateNewAssemblyWithTestStep(def1); var asm2 = CreateNewAssemblyWithTestStep(def2); TypeData initialTd = null; { // Copy the assembly into the installation File.Copy(asm1, asmLocation); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); var disp1 = td.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ First Name")); // load the plugin. By loading the plugin, we prevent the Searcher from invalidating it. td.AsTypeData().Load(); initialTd = td.AsTypeData(); } // Verify the type still exists after deletion { uninstall.Delete(new PackageFile() { FileName = testAssemblyName + ".dll", RelativeDestinationPath = testAssemblyName + ".dll" }); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); Assert.That(td.AsTypeData().Type, Is.Not.Null); } // Uninstall the type uninstall.Delete(new PackageFile() { FileName = testAssemblyName + ".dll", RelativeDestinationPath = testAssemblyName + ".dll" }); // Verify that the type now remains even if it was deleted { PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); Type tp = td.AsTypeData().Type; Assert.That(tp, Is.Not.Null); Assert.That(tp.Assembly.Location, Does.Not.Exist); } // Verify that the old type name is still used after updating the dll { File.Copy(asm2, asmLocation); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); var disp1 = td.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ First Name")); } // Verify that the first loaded typedata is still valid { Assert.That(initialTd, Is.Not.Null); var disp1 = initialTd.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ First Name")); var step = (TestStep)initialTd.CreateInstance(); } // Verify that the actual file on disk is the 2nd variant { var newSearcher = new PluginSearcher(); newSearcher.Search([asmLocation]); Dictionary allTypes = (Dictionary)newSearcher.GetType() .GetField("AllTypes", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .GetValue(newSearcher); Assert.That(allTypes, Is.Not.Null); Assert.That(allTypes.Count, Is.EqualTo(1)); Assert.That(allTypes[$"{def1.@namespace}.{def1.className}"].GetDisplayAttribute().GetFullName(), Is.EqualTo("Cecil \\ Second Name")); } } [Test] public void TestUpgradeUnloadedPluginTypes() { const string testAssemblyName = nameof(TestUpgradeUnloadedPluginTypes); // This test is very similar to the first variant. // The difference is that this variant does not load the plugin initially. // This verifies the functionality that we should be able to remove / update a plugin if it has not been loaded previously. var asmLocation = Path.Combine(Installation.Current.Directory, testAssemblyName + ".dll"); if (File.Exists(asmLocation)) File.Delete(asmLocation); using var _ = Utils.WithDisposable(() => File.Delete(asmLocation)); var uninstall = UninstallContext.Create(Installation.Current); var def1 = new TestAsmDef(testAssemblyName, "ns2", "stepclass", "First Name", 1); var def2 = new TestAsmDef(testAssemblyName, "ns2", "stepclass", "Second Name", 2); // Create two different versions of the same assembly with minor changes. var asm1 = CreateNewAssemblyWithTestStep(def1); var asm2 = CreateNewAssemblyWithTestStep(def2); { // Copy the assembly into the installation File.Copy(asm1, asmLocation); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); var disp1 = td.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ First Name")); bool loaded = td.AsTypeData().IsAssemblyLoaded(); } // Verify that the type is gone after uninstalling it { uninstall.Delete(new PackageFile() { FileName = testAssemblyName + ".dll", RelativeDestinationPath = testAssemblyName + ".dll" }); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Null); } // Verify that we get the new version by updating the dll { File.Copy(asm2, asmLocation); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}"); Assert.That(td, Is.Not.Null); var disp1 = td.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ Second Name")); } } [Test] public void TestAttributeScanning() { const string testAssemblyName = nameof(TestAttributeScanning); var asmLocation = Path.Combine(Installation.Current.Directory, testAssemblyName + ".dll"); if (File.Exists(asmLocation)) File.Delete(asmLocation); var def1 = new TestAsmDef(testAssemblyName, "AttributeTest", "MyStepClass", "My Type", 1, helpLink: "example helplink string"); var asm1 = CreateNewAssemblyWithTestStep(def1); File.Copy(asm1, asmLocation); using var _ = Utils.WithDisposable(() => File.Delete(asmLocation)); PluginManager.Search(); var td = TypeData.GetDerivedTypes().FirstOrDefault(s => s.Name == $"{def1.@namespace}.{def1.className}").AsTypeData(); { Assert.That(td, Is.Not.Null); Assert.That(td.IsAssemblyLoaded(), Is.False); } { var disp1 = td.GetDisplayAttribute(); Assert.That(disp1.GetFullName(), Is.EqualTo("Cecil \\ My Type")); Assert.That(td.IsAssemblyLoaded(), Is.False); } { var disp2 = td.GetAttribute(); Assert.That(disp2.GetFullName(), Is.EqualTo("Cecil \\ My Type")); Assert.That(td.IsAssemblyLoaded(), Is.False); } { Assert.That(td.IsBrowsable, Is.True); Assert.That(td.IsAssemblyLoaded(), Is.False); } { var help = td.GetAttribute(); Assert.That(help, Is.Not.Null); Assert.That(help.HelpLink, Is.EqualTo("example helplink string")); Assert.That(td.IsAssemblyLoaded(), Is.False); } } [SupportedOSPlatform("ios")] public class UnsupportedStep : TestStep { public override void Run() { } } [SupportedOSPlatform("osx")] [SupportedOSPlatform("macos")] [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] public class SupportedStep : TestStep { public override void Run() { } } [TestCase(typeof(UnsupportedStep), false)] [TestCase(typeof(SupportedStep), true)] public void TestSupportedOSPlatformScanned(Type t, bool supported) { var td = TypeData.FromType(t); var scanned = TypeData.GetDerivedTypes().FirstOrDefault(x => x.Name == td.Name); TypeData expected = supported ? td : null; Assert.That(scanned, Is.EqualTo(expected)); } }