chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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<string>()];
                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<ITestStep>().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<ITestStep>().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<ITestStep>().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<ITestStep>().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<string, TypeData> allTypes = (Dictionary<string, TypeData>)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<ITestStep>().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<ITestStep>().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<ITestStep>().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<ITestStep>().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<DisplayAttribute>();
            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<HelpLinkAttribute>();
            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<ITestStep>().FirstOrDefault(x => x.Name == td.Name);
        TypeData expected = supported ? td : null;
        Assert.That(scanned, Is.EqualTo(expected));
    }
}