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
// Sometimes you need to introduce a breaking change in a plugin library.
// Other times you find that two steps does the same, and you want to consolidate the two.
// In any case, you might prefer that the old test plans are still able to load.
// this example shows how to use a ITapSerializerPlugin to migrate and load old test plans in with new plugins.
 
using System;
using System.Xml.Linq;
using OpenTap;
 
namespace PluginDevelopment.Advanced_Examples
{
    [Display("Old Delay Step", "Demonstrates an old version of test step which is wanted to migrate to a new version.",
        Groups: new[] { "Examples", "Plugin Development", "Advanced Examples" })]
    public class OldDelayStep : TestStep
    {
        public int DelayMs { get; set; }
        public override void Run()
        {
            TapThread.Sleep(DelayMs);
        }
    }
 
    [Display("New Delay Step", "Demonstrates an new version of test step which is wanted to migrate." +
                               " To test this, try creating an 'Old Delay Step', save and load. The New Delay Step should have been created.",
        Groups: new[] { "Examples", "Plugin Development", "Advanced Examples" })]
    public class NewDelayStep : TestStep
    {
        public double DelaySec { get; set; }
        public override void Run()
        {
            TapThread.Sleep(TimeSpan.FromSeconds(DelaySec));
        }
    }
     
    public class BreakingChangeFixupSerializer : ITapSerializerPlugin
    {
        static XName steps = "Steps";
        static XName typeattr = "type";
        static XName parameterattr = "Parameter";
        static XName scopeAttr = "Scope";
        static XName delayMsElem = "DelayMs";
        static XName delaySecElem = "DelaySec";
 
        static XName postScaleAttr = "post-scale";
        // the type we want to replace.
        static string searchType = "PluginDevelopment.Advanced_Examples.OldDelayStep";
        static string replaceType = "PluginDevelopment.Advanced_Examples.NewDelayStep";
 
        static readonly TraceSource log = Log.CreateSource("Fix Serializer");
        void iterateAndReplaceTypesInXml(XElement node, XElement testPlanNode)
        {
            if (node.Attribute(typeattr) is XAttribute typeAttribute && typeAttribute.Value == searchType)
            {
                // we discovered the type
                typeAttribute.Value = replaceType;
                var valueElem = node.Element(delayMsElem);
                valueElem.Name = delaySecElem; // change the name of the node to DelaySec
                valueElem.Add(new XAttribute(postScaleAttr, 0.001));
 
                if (valueElem.Attribute(parameterattr) is XAttribute parameterAttribute &&
                    valueElem.Attribute(scopeAttr) == null)
                { 
                    // the property is a test plan parameter. This will give issues with pre-scaling.
                    // in this case it could simply be removed from the test plan node.
                    // but it will give issues if other test steps has properties  that are merged with the same parameter.
                    
                    testPlanNode.Element(parameterAttribute.Value)?.Remove();
                    // but also print a warning.
                    log.Warning("fixing member that is being used in the test plan parameter '{0}'. Please verify the value of this parameter.", parameterAttribute.Value);
                }
                
                
            }
            foreach(var subnode in node.Elements())
                iterateAndReplaceTypesInXml(subnode, testPlanNode);
        }
        
        public bool Deserialize(XElement node, ITypeData t, Action<object> setter)
        {
            // 1. 'Iterate and Replace' if the node is the test plan node, we want to find all test step sub-nodes and replace the types.
            if(t.DescendsTo(typeof(TestPlan)))
                iterateAndReplaceTypesInXml(node, node);
            else if (node.Attribute(postScaleAttr) is XAttribute attribute)
            {
                // 2. do post-scaling.
                // when the old step was detected the property was marked with this post-scale attribute to show that it should scale it after deserialization.
                // this is because the old step used milliseconds, but the new one uses seconds.
                
                attribute.Remove(); // remove the attribute to avoid hitting this again.
                
                // this new setter applies the post-scaling that was added to the attributes during 'iterate and replace'.
                Action<object> newSetter = x => setter(double.Parse(x.ToString()) * double.Parse(attribute.Value));
                
                // call the deserializer to actually deserialize the property, but direct it to use the new setter instead of the original.
                return TapSerializer.GetCurrentSerializer().Deserialize(node, newSetter, t);
            }
 
            return false;
        }
 
        // Do nothing during serialization.
        public bool Serialize(XElement node, object obj, ITypeData expectedType) => false; 
 
        // The right order can be hard to determine, but many different values works!.
        // the best is probably to run some code to check which other serializers is already installed (see below).
        // this specific serializer should be run early in the chain.
        // if a serializer has high order it will get called early.
        public double Order => 5;
 
        // consider calling this code during debugging to list the existing serializers.
        void printExistingSerializerPlugins()
        {
            log.Info("Deserializers:");
            foreach (var type in TypeData.GetDerivedTypes<ITapSerializerPlugin>())
            {
                if (type.CanCreateInstance == false) continue;
                try
                {
                    var plugin = (ITapSerializerPlugin)type.CreateInstance(Array.Empty<object>());
                    log.Info("{0} Order: {1}", type.Name, plugin.Order);
                }
                catch
                {
                    
                }
            }
        }
    }
}