chr
2026-04-08 53e656200368a983e563550e2cc1acbc6d86b729
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
//            Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
 
namespace OpenTap.Plugins
{
    /// <summary> Serializer implementation for TestStep. </summary>
    public class TestStepSerializer : TapSerializerPlugin
    {
        /// <summary> The order of this serializer. </summary>
        public override double Order { get { return 1; } }
 
        readonly Dictionary<Guid, ITestStep> stepLookup = new Dictionary<Guid, ITestStep>();
 
        /// <summary> Tries to find a step based on ID. </summary>
        internal ITestStep FindStep(Guid id)
        {
            stepLookup.TryGetValue(id, out var step);
            return step;
        }
        
        /// <summary>
        /// Guids where duplicate guids should be ignored. Useful when pasting to test plan.
        /// </summary>
        readonly HashSet<Guid> ignoredGuids = new HashSet<Guid>();
 
        /// <summary>
        /// Adds known steps to the list of tests used for finding references in deserialization.
        /// </summary>
        /// <param name="stepParent"></param>
        public void AddKnownStepHeirarchy(ITestStepParent stepParent)
        {
            var step = stepParent as ITestStep; // could also be TestPlan.
 
            if (step != null)
            {
                stepLookup[step.Id] = step;
                ignoredGuids.Add(step.Id);
            }
            
            foreach (var step2 in stepParent.ChildTestSteps)
                AddKnownStepHeirarchy(step2);
        }
 
        /// <summary>
        /// Ensures that duplicate step IDs are not present in the test plan and updates an ID->step mapping.
        /// </summary>
        /// <param name="step">the step to fix.</param>
        /// <param name="recurse"> true if child steps should also be 'fixed'.</param>
        public void FixupStep(ITestStep step, bool recurse)
        {
            if (stepLookup.TryGetValue(step.Id, out ITestStep currentStep) && currentStep != step && !ignoredGuids.Contains(step.Id))
            {
                bool anyParentReadonly(ITestStep s) => s.GetParents().Any(p => p.ChildTestSteps.IsReadOnly);
                step.Id = Guid.NewGuid();
                // If any parent of the step's child steps are readonly, this step was likely generated from some other source.
                // This could happen e.g. when loading a TestPlanReference to the same test plan twice. In this case,
                // we don't want to emit any warnings about duplicate IDs. Just assign the step a new ID and continue.
                if (anyParentReadonly(step) == false)
                {
                    if (step is IDynamicStep)
                    {
                        // if newStep is an IDynamicStep, we just print in debug.
                        Log.Debug(
                            "Duplicate test step ID found in dynamic step. The duplicate ID has been changed for step '{0}'.",
                            step.Name);
                    }
                    else
                    {
                        Log.Warning("Duplicate test step ID found. The duplicate ID has been changed for step '{0}'.",
                            step.Name);
                    }
                }
            }
            stepLookup[step.Id] = step;
 
            if (recurse == false) return;
            foreach(var step2 in step.ChildTestSteps)
            {
                FixupStep(step2, true);
            }
        }
 
        /// <summary> Deserialization implementation. </summary>
        public override bool Deserialize( XElement elem, ITypeData t, Action<object> setResult)
        {
            if(t.DescendsTo(typeof(ITestStep)))
            {
                
                if (elem.HasElements == false)
                {
                    Guid stepGuid;
                    if (Guid.TryParse(elem.Value, out stepGuid))
                    {
                        Serializer.DeferLoad(() =>
                        {
                            if (stepLookup.ContainsKey(stepGuid))
                            {
                                setResult(stepLookup[stepGuid]);
                            }
                            else
                            {
                                if(Serializer.IgnoreErrors == false)
                                    Log.Warning("Unable to find referenced step {0}", stepGuid);
                            }
                                
                        });
                        return true;
                    }
                }
                else
                {
                    if (currentNode.Contains(elem)) return false;
                    ITestStep step = null;
                    currentNode.Add(elem);
                    try
                    {
                        if (Serializer.Deserialize(elem, x => step = (ITestStep)x, t))
                        {
                            setResult(step);
                            FixupStep(step, true);
                        }
                        // return true even tough the deserialization failed. 
                        // since this is a test step being deserialized
                        // it should always be handled here.
                        // otherwise the errors will show up twice.
                        return true;
                    }finally
                    {
                        currentNode.Remove(elem);
                    }
                }
            }
            return false;
        }
        HashSet<XElement> currentNode = new HashSet<XElement>();
        
        /// <summary> Serialization implementation. </summary>
        public override bool Serialize( XElement elem, object obj, ITypeData expectedType)
        {
            if (false == obj is ITestStep) return false;
            
            // if we are currently serializing a test step, then if that points to another test step,
            // that should be serialized as a reference.
            
            var currentlySerializing = Serializer.SerializerStack.OfType<ObjectSerializer>().FirstOrDefault();
 
            if(currentlySerializing?.Object != null)
            {
                if (currentlySerializing.CurrentMember.TypeDescriptor.DescendsTo(typeof(ITestStep)))
                {
                    elem.Attributes("type")?.Remove();
                    elem.Value = ((ITestStep)obj).Id.ToString();
                    return true;
                }
                if (currentlySerializing.CurrentMember.TypeDescriptor is TypeData tp)
                {
                    // serialize references in list<ITestStep>, only when they are declared by a test step and not a TestStepList.
                    if (tp.Type != typeof(TestStepList) && (tp.ElementType?.DescendsTo(typeof(ITestStep)) ?? false) && currentlySerializing.CurrentMember.DeclaringType.DescendsTo(typeof(ITestStep)))
                    {
                        elem.Attributes("type")?.Remove();
                        elem.Value = ((ITestStep)obj).Id.ToString();
                        return true;
                    }
                }
            }
            return false;
        }
    }
 
}