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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
//            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.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using OpenTap.Plugins;
 
namespace OpenTap
{
 
    /// <summary> Enables callback from the OpenTAP deserializer after deserialization. </summary>
    public interface IDeserializedCallback
    {
        /// <summary>
        /// Called when the object has been deserialized.
        /// </summary>
        void OnDeserialized();
    }
 
    /// <summary>
    /// Can be used to control the order in which members are deserialized.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class DeserializeOrderAttribute : Attribute
    {
        /// <summary> The order in which the member will be deserialized. Higher order, means it will be deserialized later. Minimum value is 0, which is also the default order of not attributed members.</summary>
        public double Order { get; }
 
        /// <summary>
        /// Can be used to control the order in which members are deserialized.
        /// </summary>
        /// <param name="order">The order in which the member will be deserialized. Higher order, means it will be deserialized later. Minimum value is 0, which is also the default order of not attributed members.</param>
        public DeserializeOrderAttribute(double order)
        {
            Order = order;
        }
    }
 
    /// <summary>
    /// Serializing/deserializing OpenTAP objects. This class mostly just orchestrates a number of serializer plugins. <see cref="MacroString"/>
    /// </summary>
    public class TapSerializer
    {
        /// <summary>
        /// Default settings for XmlWriter.
        /// </summary>
        public static readonly XmlWriterSettings DefaultWriterSettings =
            new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false) };
 
        /// <summary>
        /// Default settings for XmlReaders.
        /// </summary>
        public static readonly XmlReaderSettings DefaultReaderSettings =
            new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true };
 
        /// <summary>
        /// Pushes a message to the list of errors for things that happened during load.
        /// </summary>
        /// <param name="element">The element that generated the error. </param>
        /// <param name="message"></param>
        public void PushError(XElement element, string message)
        {
            messages.Add(new XmlError( element, message));
        }
        
        /// <summary>  Pushes an error to the list of errors for things that happened during load. Includes optional Exception value. </summary>
        public void PushError(XElement element, string message, Exception e)
        {
            messages.Add(new XmlError( element, message, e));
        }
 
        
        internal void HandleError(XElement element, string message, Exception e)
        {
            while (e is TargetInvocationException && e.InnerException != null)
                e = e.InnerException;
            PushError(element, $"{message} {e.Message}", e);
        }
        
        /// <summary> Pushes a message. </summary>
        internal void PushMessage(XElement elem, string s)
        {
            messages.Add(new XmlMessage(elem, s));
        }
 
        void LogMessages()
        {
            foreach (var message in messages)
            {
                if (message is XmlError error)
                {
                    log.Error("{0}", message);
 
                    if (error.Exception != null)
                        log.Debug(error.Exception);
                }
                else
                {
                    log.Info("{0}", message);
                }
            }
        }
 
        /// <summary>
        /// Deserializes an object from a XDocument.
        /// </summary>
        /// <param name="document"></param>
        /// <param name="type"></param>
        /// <param name="autoFlush"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public object Deserialize(XDocument document, ITypeData type = null, bool autoFlush = true, string path = null)
        {
            if (document == null)
                throw new ArgumentNullException(nameof(document));
            var node1 = document.Elements().First();
            object serialized = null;
            this.ReadPath = path;
            var prevSer = currentSerializer.Value;
            currentSerializer.Value = this;
            ClearErrors();
            using (ParameterManager.WithSanityCheckDelayed())
            {
                try
                {
                    try
                    {
 
                        Deserialize(node1, x => serialized = x, type);
                    }
                    finally
                    {
                        currentSerializer.Value = prevSer;
                    }
 
                    if (autoFlush)
                        Flush();
                }
                finally
                {
                    if(ThrowOnErrors){
                        if (messages.Count > 0)
                        {
                            throw new Exception("Error during reading XML: " + string.Join("\n", messages));
                        }
                    }
                    if (IgnoreErrors == false)
                    {
                        LogMessages();
 
                        var rs = GetSerializer<ResourceSerializer>();
                        if (rs.TestPlanChanged)
                        {
                            log.Warning("Test Plan changed due to resources missing from Bench settings.");
                            log.Warning("Please review these changes before saving or running the Test Plan.");
                        }
                    }
                }
            }
 
            return serialized;
        }
 
        /// <summary>
        /// Needed by defered loading. Only required to be called if autoFlush is set to false during deserialization.
        /// </summary>
        public void Flush()
        {
            while (deferredLoads.Count > 0)
            {
                try
                {
                    var item = deferredLoads[0];
                    deferredLoads.RemoveAt(0);
                    item.Action();
                }
                catch (Exception e)
                {
                    PushError(null, $"Caught error while finishing serialization: {e.Message}");
                }
            }
        }
 
        /// <summary>
        /// Deserializes an object from a stream.
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="flush"></param>
        /// <param name="type"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public object Deserialize(Stream stream, bool flush = true, ITypeData type = null, string path = null)
        {
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
 
            return Deserialize(XDocument.Load(stream, LoadOptions.SetLineInfo), type: type, autoFlush: flush, path: path);
        }
 
        /// <summary>
        /// Deserializes an object from an xml text string.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="type"></param>
        /// <param name="flush"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public object DeserializeFromString(string text, ITypeData type = null, bool flush = true, string path = null)
        {
            if (text == null)
                throw new ArgumentNullException(nameof(text));
            using (var reader = new MemoryStream(Encoding.UTF8.GetBytes(text)))
                return Deserialize(reader, flush, type, path);
        }
 
        /// <summary>
        /// Deserializes an object from a XML file.
        /// </summary>
        /// <param name="file"></param>
        /// <param name="type"></param>
        /// <param name="flush"></param>
        /// <returns></returns>
        public object DeserializeFromFile(string file, ITypeData type = null, bool flush = true)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));
            using (var fileStream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read))
                return Deserialize(fileStream, flush, type, file);
        }
 
        ITapSerializerPlugin[] serializers = Array.Empty<ITapSerializerPlugin>();
        readonly Stack<object> activeSerializers = new Stack<object>(32);
        
        /// <summary> Get all the serializers loaded by this TapSerializer. </summary>
        public ITapSerializerPlugin[] GetSerializers() => serializers.ToArray();
        /// <summary>
        /// The stack of serializers. Changes during serialization depending on the order of serializers used.
        /// </summary>
        public IEnumerable<ITapSerializerPlugin> SerializerStack => activeSerializers.OfType<ITapSerializerPlugin>();
 
        /// <summary>
        /// True if errors should be ignored.
        /// </summary>
        public bool IgnoreErrors { get; set; } = false;
 
        /// <summary> The serializer will throw an exception if there are any errors. </summary>
        internal bool ThrowOnErrors { get; set; } = false;
        
        /// <summary>
        /// Gets a serializer from the stack of active serializers. Returns null if there is no serializer of that type on the stack.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T GetSerializer<T>() where T : ITapSerializerPlugin
        {
            foreach (var item in serializers)
            {
                if (item is T found)
                    return found;
            }
            return default;
        }
 
        /// <summary> Adds new serializers to the serializer. Will insert them based on the order property. </summary>
        /// <param name="_serializers"></param>
        public void AddSerializers(IEnumerable<ITapSerializerPlugin> _serializers)
        {
            serializers = serializers.Concat(_serializers).OrderByDescending(x => x.Order).ToArray();
        }
 
        static System.Threading.ThreadLocal<TapSerializer> currentSerializer = new System.Threading.ThreadLocal<TapSerializer>();
        
        /// <summary> The serializer currently serializing/deserializing an object.</summary>
        public static TapSerializer GetCurrentSerializer() => currentSerializer.Value;
        /// <summary>
        /// Creates a new serializer instance.
        /// </summary>
        public TapSerializer()
        {
            var previousValue = currentSerializer.Value;
            currentSerializer.Value = this;
 
            var plugins = PluginManager.GetPlugins<ITapSerializerPlugin>();
            AddSerializers(plugins.Select(x =>
            {
                try
                {
                    return (ITapSerializerPlugin)Activator.CreateInstance(x);
                }
                catch
                {
                    return null;
                }
            }).Where(x => x!=null));
 
            currentSerializer.Value = previousValue;
        }
 
        internal enum DeferredLoadOrder
        {
            Normal,
            // ParameterMemberDataSetter should run after other defers, but before ExternalParameter
            ParameterMemberDataSetter,
            // External parameter should be set last
            ExternalParameter,
        }
 
        [DebuggerDisplay("{Order}")]
        struct DeferredAction : IComparable<DeferredAction>
        {
            public DeferredLoadOrder Order;
            public Action Action;
 
            public int CompareTo(DeferredAction other)
            {
                return Order.CompareTo(other.Order);
            }
        }
        private List<DeferredAction> deferredLoads = [];
 
        /// <summary>
        /// Pushes a deferred load action onto a queue of deferred loads.  
        /// </summary>
        /// <param name="deferred"></param>
        public void DeferLoad(Action deferred)
        {
            DeferLoad(deferred, DeferredLoadOrder.Normal);
        }
 
        internal void DeferLoad(Action deferred, DeferredLoadOrder order)
        {
            if (deferred == null)
                throw new ArgumentNullException(nameof(deferred));
            DeferredAction item;
            item.Action = deferred;
            item.Order = order;
 
            // We want to ensure that a new item is inserted at the end of its own priority segment.
            // The linear scan can be rewritten as a custom binary search,
            // but this list is unlikely to grow to an unmanagable size.
            int i = deferredLoads.BinarySearch(item);
            if (i < 0) i = ~i;
            for (; i < deferredLoads.Count; i++)
                if (deferredLoads[i].Order > order)
                    break;
            deferredLoads.Insert(i, item);
        }
 
        readonly List<XmlMessage> messages = new List<XmlMessage>();
 
        /// <summary> Get the errors associated with deserialization. The errors only persists between calls to Serialize/Deserialize. See XmlErrors for more detailed information. </summary>
        public IEnumerable<string> Errors => XmlErrors.Select(x => x.ToString());
 
        /// <summary> Gets a list of exceptions tha occured while loading the test plan.</summary>
        public IEnumerable<XmlError> XmlErrors => messages.OfType<XmlError>();
 
        internal IEnumerable<XmlMessage> XmlMessages => messages.Select(x => x);
 
        /// <summary> Clears the errors accumulated in the serializer. </summary>
        void ClearErrors()
        {
            messages.Clear();
        }
 
        static readonly TraceSource log = Log.CreateSource("Serializer");
 
        /// <summary>
        /// Deserializes an object from an XElement. Calls the setter action with the result. returns true on success. Optionally, the type can be added.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="setter"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public bool Deserialize(XElement element, Action<object> setter, Type t = null)
        {
            return Deserialize(element, setter, t != null ? TypeData.FromType(t) : null);
        }
 
        internal static readonly XName typeName = "type";
        
        /// <summary>
        /// Deserializes an object from XML.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="setter"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        public bool Deserialize(XElement element, Action<object> setter, ITypeData t)
        {
            var typeattribute = element.Attribute(typeName);
            if (typeattribute != null)
            {   
                // if a specific type is given by the element use that.
                // If it cannot be found fall back on previous value.
                // This can happen if LocateType cannot find it, eg. private type.
                if (t is TypeData td && td.Name == typeattribute.Value)
                {
                    // no reason to search for the type if 't' matches it exactly.
                }
                else
                {
                    var t2 = TypeData.GetTypeData(typeattribute.Value);
 
                    if (t2 != null)
                    {
                        t = t2;
                    }
                    else
                    {
                        PushError(element, $"Unable to locate type '{typeattribute.Value}'. Are you missing a plugin?");
                        if (t == null)
                            return false;
                    }
                }
            }
 
            if (t == null)
                throw new Exception("Unable to determine type of XML element.");
            foreach (var serializer in serializers)
            {
                try
                {
                    activeSerializers.Push(serializer);
                    if (serializer is ITapSerializerPlugin ser2)
                    {
                        if (ser2.Deserialize(element, t, setter))
                            return true;
                    }
                }
                finally
                {
                    activeSerializers.Pop();
                }
            }
            return false;
        }
 
        /// <summary>
        /// Serialize an object to a stream.
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="obj"></param>
        public void Serialize(Stream stream, object obj)
        {
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
            
            using (var writer = XmlWriter.Create(stream, DefaultWriterSettings))
                this.Serialize(writer, obj);
        }
 
        static readonly XName rootName = "root";
        
        /// <summary> If set to true, Serialize will write a section of XML instead of an entire document. In other words, it will skip writing the start of the document. </summary>
        public bool WriteFragments { get; set; }
        /// <summary>
        /// Serializes an object to a XML writer.
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="obj"></param>
        public void Serialize(XmlWriter writer, object obj)
        {
            if (writer == null)
                throw new ArgumentNullException(nameof(writer));
            
            XElement elem = new XElement(rootName);
            if(obj != null)
                elem.Name = TypeToXmlString(obj.GetType());
            ClearErrors();
            using(TypeData.WithTypeDataCache())
            using(ParameterManager.WithSanityCheckDelayed(true))
                Serialize(elem, obj);
            if (!WriteFragments)
                writer.WriteStartDocument();
            elem.WriteTo(writer);
            
            if (IgnoreErrors == false)
                LogMessages();
        }
        
        /// <summary>
        /// Serializes an object to a string.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns>The serialized object as an XML string.</returns>
        public string SerializeToString(object obj)
        {
            using (var memoryStream = new MemoryStream())
            {
                this.Serialize(memoryStream, obj);
                memoryStream.Position = 0;
                return DefaultWriterSettings.Encoding.GetString(memoryStream.ToArray());
            }
        }
 
        /// <summary> Serializes an object to XML. </summary>
        public bool Serialize(XElement elem, object obj, ITypeData expectedType = null) => 
            Serialize(elem, obj, expectedType, true);
 
        /// <summary> Serializes an object to XML. Includes an argument whether the serializer should be notified about the type being used.</summary>
        internal bool Serialize(XElement elem, object obj, ITypeData expectedType, bool notifyTypeUsed)
        {
            ITypeData type = null;
            if(obj != null)
            {
                type = TypeData.GetTypeData(obj);
                if(notifyTypeUsed)
                    NotifyTypeUsed(type);
            }
            if (Object.Equals(type, expectedType) == false && type != null)
                elem.SetAttributeValue("type", type.Name);
            else if (expectedType != null && notifyTypeUsed)
                NotifyTypeUsed(expectedType);
 
            foreach (var serializer in serializers)
            {
                try
                {
                    activeSerializers.Push(serializer);
                    if(serializer is ITapSerializerPlugin ser)
                    {
                        if (ser.Serialize(elem, obj, type))
                        {
                            if (ser is ITapSerializerPluginDependencyMarker marker)
                            {
                                if (marker.NeededForDeserialization)
                                {
                                    NotifyTypeUsed(TypeData.GetTypeData(ser));
                                }
                                // else  serializer is specifically not a dependency.
                            }
                            else
                            {    
                                // mark the serializer plugin types as having been used during serialization.
                                NotifyTypeUsed(TypeData.GetTypeData(ser));
                            }
                            return true;
                        }
                    }
                }
                finally
                {
                    activeSerializers.Pop();
                }
            }
            return false;
 
        }
 
        internal static string MakeValidXmlName(string name)
        {
            if (name == null) throw new ArgumentNullException("name");
            if (name.Length == 0) throw new ArgumentException("name length cannot be 0.", "name");
 
            if (XmlConvert.IsStartNCNameChar(name[0]) && name.All(XmlConvert.IsNCNameChar))
                return name;
 
            StringBuilder sb = new StringBuilder(name.Length);
            sb.Append(XmlConvert.IsStartNCNameChar(name[0]) ? name[0] : '_');
            for(int i = 1; i < name.Length; i++)
            {
                var c = name[i];
                if (XmlConvert.IsNCNameChar(c))
                    sb.Append(c);
                else
                    sb.Append('_');
            }
            
            System.Diagnostics.Debug.Assert(sb.Length > 0);
            return sb.ToString();
        }
        
        /// <summary>
        /// Convert a type to a string supported by XML.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static string TypeToXmlString(Type type)
        {
            var attr = type.GetCustomAttributes<XmlTypeAttribute>().FirstOrDefault();
            if(attr != null)
            {
                return attr.TypeName;
            }
 
            var sb = new StringBuilder();
            loopNext:
            if (type.IsArray)
            {
                sb.Append("ArrayOf");
                type = type.GetElementType();
                goto loopNext;
            }
            else if (type.IsGenericType)
            {
                sb.Append(type.GetGenericTypeDefinition().Name.Split('`').First() + "Of");
                type = type.GetGenericArguments()[0];
                goto loopNext;
            }
            
            sb.Append(type.Name);
            
            return MakeValidXmlName(sb.ToString());
        }
        
        /// <summary>
        /// Clones an object using the serializer. Skips generating and parsing XML text, so it is faster than a full serialize/deserialize.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public object Clone(object obj)
        {
            if (obj == null) return null;
            ClearErrors();
            XDocument doc = new XDocument();
            XElement elem = new XElement(rootName);
            Serialize(elem, obj);
            doc.Add(elem);
            return Deserialize(doc);
        }
 
        internal T Clone<T>(T obj) => (T)Clone((object)obj);
 
        /// <summary> for mapping object to serializer. </summary>
        static System.Runtime.CompilerServices.ConditionalWeakTable<object, TapSerializer> serializerSteps = 
            new System.Runtime.CompilerServices.ConditionalWeakTable<object, TapSerializer>();
        internal void Register(object step)
        {
            serializerSteps.Add(step, this);
        }
 
        /// <summary> Returns the serializer for a given object. null if the object is or has not been deserialized.</summary>
        public static TapSerializer GetObjectDeserializer(object @object)
        {
            serializerSteps.TryGetValue(@object, out var serializer);
            return serializer ?? GetCurrentSerializer();
        }
 
        readonly HashSet<ITypeData> registeredTypes = new HashSet<ITypeData>();
        readonly HashSet<string> registeredFiles = new HashSet<string>();
 
        /// <summary> This is used to keep track of which types has been used by the serializer. </summary>
        /// <param name="type"></param>
        internal void NotifyTypeUsed(ITypeData type)
        {
            registeredTypes.Add(type);
        }
 
        internal void NotifyFileUsed(string file)
        {
            registeredFiles.Add(file);
        }
 
        /// <summary> Gets the types this TapSerializer instance has encountered until now. </summary>
        public IEnumerable<ITypeData> GetUsedTypes() => registeredTypes;
 
        /// <summary> Gets the FilePath strings this instance has encountered until now. </summary>
        public IEnumerable<string> GetUsedFiles() => registeredFiles;
 
        /// <summary> The path where the current file is being loaded from. This might be null in cases where it's being loaded from a stream.</summary>
        public string ReadPath { get; private set; }
 
        /// <summary>  Manually push a serializer on the active serializers stack. </summary>
        /// <param name="objectSerializer"></param>
        internal void PushActiveSerializer(ITapSerializerPlugin objectSerializer)
        {
            activeSerializers.Push(objectSerializer);
        }
 
        /// <summary> Manually pop a serializer from the active serializers. </summary>
        internal void PopActiveSerializer()
        {
            activeSerializers.Pop();
        }
 
        readonly Dictionary<string, XName> xmlPropertyNames = new Dictionary<string, XName>();
        internal XName PropertyXmlName(string subPropName) => xmlPropertyNames.GetOrCreateValue(subPropName, name => XmlConvert.EncodeLocalName(name));
 
 
    }
}