// 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 { /// Serializer implementation for TestStep. public class TestStepSerializer : TapSerializerPlugin { /// The order of this serializer. public override double Order { get { return 1; } } readonly Dictionary stepLookup = new Dictionary(); /// Tries to find a step based on ID. internal ITestStep FindStep(Guid id) { stepLookup.TryGetValue(id, out var step); return step; } /// /// Guids where duplicate guids should be ignored. Useful when pasting to test plan. /// readonly HashSet ignoredGuids = new HashSet(); /// /// Adds known steps to the list of tests used for finding references in deserialization. /// /// 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); } /// /// Ensures that duplicate step IDs are not present in the test plan and updates an ID->step mapping. /// /// the step to fix. /// true if child steps should also be 'fixed'. 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); } } /// Deserialization implementation. public override bool Deserialize( XElement elem, ITypeData t, Action 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 currentNode = new HashSet(); /// Serialization implementation. 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().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, 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; } } }