// 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.ComponentModel; using System.Reflection; using System.Xml.Serialization; using System.Text; using System.Xml.Linq; using System.Threading; namespace OpenTap.Plugins.BasicSteps { /// Sweep behavior for linear sweeps. public enum SweepBehavior { /// Linear growth function. [Display("Linear", Description: "Linear growth function.")] Linear = 0, /// Exponential growth function. [Display("Exponential", Description: "Exponential growth function.")] Exponential = 1 } [Browsable(false)] [Display("Sweep Loop (Range)", Groups: new [] { "Flow Control", "Legacy" }, Description: "Loops all of its child steps while sweeping a specified parameter/setting over a range.", Collapsed:true)] [AllowAnyChild] public class SweepLoopRange : LoopTestStep { [Display("Sweep Parameters", Order: -4, Description: "Test step parameters that should be swept. The variable must be a numeric type.")] [XmlIgnore] [Browsable(true)] [Unsweepable] public List SweepProperties { get; set; } [Display("Start", Order: -2, Description: "The parameter value where the sweep will start.")] public decimal SweepStart { get; set; } [Display("Stop", Order: -1, Description: "The parameter value where the sweep will stop.")] public decimal SweepStop { get; set; } [Browsable(false)] public decimal SweepEnd { get { return SweepStop; } set { SweepStop = value; } } [Display("Step Size", Order: 1, Description: "The value to be increased or decreased between every iteration of the sweep.")] [EnabledIf("SweepBehavior", SweepBehavior.Linear, HideIfDisabled = true)] [XmlIgnore] // this is inferred from the other properties and should not be set by the serializer [Browsable(true)] public decimal SweepStep { get { if (SweepPoints == 0) return 0; if (SweepPoints == 1) return 0; return (SweepStop - SweepStart) / (SweepPoints - 1); } set { if (decimal.Zero == value) return; var newv = (uint)Math.Round((SweepStop - SweepStart) / value) + 1; SweepPoints = newv; } } [Display("Points", Order: 1, Description: "The number of points to sweep.")] public uint SweepPoints { get; set; } [Display("Behavior", Order: -3, Description: "Linear or exponential growth.")] public SweepBehavior SweepBehavior { get; set; } [Output] [Display("Current Value", "Shows the current value of the loop.", Order: 4)] public decimal Current { get; private set; } [Browsable(false)] [DeserializeOrder(1.0)] public string SweepPropertyName { get => propertyInfosToString(SweepProperties); set => SweepProperties = parseInfosFromString(value).ToList(); } static string propertyInfosToString(IEnumerable infos) { return string.Join("|", infos.Select(x => String.Format("{0};{1}", x.Name, x.DeclaringType.Name))); } List parseInfosFromString(string str) { var cs = this.RecursivelyGetChildSteps(TestStepSearch.All).Select( x => TypeData.GetTypeData(x)).ToArray(); var allMembers = cs.SelectMany(x => x.GetMembers().ToArray()); Dictionary dict = new Dictionary(); foreach(var x in allMembers) { var key = String.Format("{0};{1}", x.Name, x.DeclaringType.Name); dict[key] = x; } var items = str.Split('|'); List outlist = new List(); foreach (var item in items) { if (dict.TryGetValue(item, out IMemberData member)) outlist.Add(member); } return outlist; } public override void PrePlanRun() { if (SweepProperties.Count == 0) throw new InvalidOperationException("No parameters selected to sweep"); validateSweepMutex.WaitOne(); validateSweepMutex.ReleaseMutex(); } ITypeData SweepType => SweepProperties.FirstOrDefault()?.TypeDescriptor; // Check if the test plan is running before validating sweeps. // the validateSweep might have been started before the plan started. // hence we use the validateSweeepMutex to ensure that validation is done before // the plan starts. bool isRunning => GetParent()?.IsRunning ?? false; Mutex validateSweepMutex = new Mutex(); string validateSweep(decimal Value) { // Mostly copied from Run if (SweepProperties.Count == 0) return ""; if (isRunning) return ""; // Avoid changing the value during run when the gui asks for validation errors. if (!validateSweepMutex.WaitOne(0)) return ""; StringBuilder sb = new StringBuilder(); var sets = GetPropertySets(ChildTestSteps).ToList(); var originalValues = sets.Select(set => set.GetValue()).ToArray(); try { foreach (var set in sets) set.SetValue(Value, false); bool first = true; foreach (var step in sets) { if (string.IsNullOrWhiteSpace(step.Obj.Error)) continue; if (first) first = false; else sb.AppendLine(); ITestStep step2 = step.Obj as ITestStep; if (step2 != null) sb.AppendFormat("Step '{0}' : {1}", step2.Name, step2.Error); else sb.AppendFormat("Object '{0}' : {1}", step.Obj, step.Obj.Error); } return sb.ToString(); } catch (TargetInvocationException e) { return e.InnerException.Message; } finally { for (int i = 0; i < sets.Count; i++) sets[i].SetValue(originalValues[i], false); validateSweepMutex.ReleaseMutex(); } } public SweepLoopRange() { SweepProperties = new List(); SweepStart = 1; SweepStop = 100; SweepPoints = 100; Rules.Add(() => SweepProperties.Count != 0, "No parameters selected to sweep", nameof(SweepProperties)); Rules.Add(() => string.IsNullOrWhiteSpace(validateSweep(SweepStart)), () => validateSweep(SweepStart), "SweepStart"); Rules.Add(() => string.IsNullOrWhiteSpace(validateSweep(SweepStop)), () => validateSweep(SweepStop), "SweepEnd"); Rules.Add(() => ((SweepBehavior != SweepBehavior.Exponential)) || (SweepStart != 0), "Sweep start value must be non-zero.", "SweepStart"); Rules.Add(() => ((SweepBehavior != SweepBehavior.Exponential)) || (SweepStop != 0), "Sweep end value must be non-zero.", "SweepEnd"); Rules.Add(() => SweepPoints > 1, "Sweep points must be bigger than 1.", "SweepPoints"); Rules.Add(() => ((SweepBehavior != SweepBehavior.Exponential)) || (Math.Sign(SweepStop) == Math.Sign(SweepStart)), "Sweep start and end value must have the same sign.", "SweepEnd", "SweepStart"); ChildTestSteps.ChildStepsChanged += childStepsChanged; PropertyChanged += SweepLoopRange_PropertyChanged; } void SweepLoopRange_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ChildTestSteps)) ChildTestSteps.ChildStepsChanged += childStepsChanged; } readonly Dictionary membersCache = new Dictionary(); void childStepsChanged(TestStepList sender, TestStepList.ChildStepsChangedAction Action, ITestStep Object, int Index) { // Make a copy to stop collection modified exception in TapSerializer. membersCache.Clear(); getPropertiesForItems(this, membersCache); SweepProperties.RemoveIf(x => membersCache.ContainsKey(x) == false); } static bool memberCanSweep(IMemberData mem) => false == mem.HasAttribute(); static void getPropertiesForItems(ITestStep step, Dictionary members) { foreach (ITestStep cs in step.ChildTestSteps) { foreach (var member in TypeData.GetTypeData(cs).GetMembers()) { if (member.TypeDescriptor is TypeData t) { if (t.Type.IsNumeric() && members.ContainsKey(member) == false && memberCanSweep(member)) { members.Add(member, member.GetValue(cs)); } } } getPropertiesForItems(cs, members); } } internal static Dictionary GetPropertiesForItems(ITestStep baseStep) { Dictionary members = new Dictionary(); getPropertiesForItems(baseStep, members); return members; } /// Obj/Property mapping struct ObjPropSet { public IValidatingObject Obj; public IMemberData Prop; public void SetValue(object value, bool invokePropertyChanged = true) { if (Prop.TypeDescriptor.DescendsTo(value.GetType())) { Prop.SetValue(Obj, value); } else if(Prop.TypeDescriptor is TypeData cst) { Prop.SetValue(Obj, Convert.ChangeType(value, cst.Type)); } else { return; } if (invokePropertyChanged) Obj.OnPropertyChanged(Prop.Name); } public object GetValue() { return Prop.GetValue(Obj); } } IEnumerable GetPropertySets(TestStepList childTestStep) { foreach (var step in childTestStep) { foreach (var prop in SweepProperties) { IMemberData paramProp = TypeData.GetTypeData(step).GetMember(prop.Name); if (paramProp != null && (paramProp.TypeDescriptor == prop.TypeDescriptor && paramProp.GetDisplayAttribute().GetFullName() == prop.GetDisplayAttribute().GetFullName())) yield return new ObjPropSet { Obj = step, Prop = paramProp }; if (step is TestPlanReference) continue; //TODO: keep this hack? foreach (var set in GetPropertySets(step.ChildTestSteps)) { yield return set; } } } } public static IEnumerable LinearRange(decimal start, decimal end, int points) { if (points == 0) yield break; decimal Value = start; for(int i = 0; i < points - 1; i++) { yield return (start * (points - 1- i) + end * (i)) / (points - 1); } yield return end; } public static IEnumerable ExponentialRange(decimal start, decimal end, int points) { if (start == 0) throw new ArgumentException("Start value must be different than zero."); if (end == 0) throw new ArgumentException("End value must be different than zero."); if (Math.Sign(start) != Math.Sign(end)) throw new ArgumentException("Start and end value must have the same sign."); var logs = Math.Log10((double) start); var loge = Math.Log10((double) end); return LinearRange((decimal)logs, (decimal)loge, points).Select(x => (decimal)Math.Pow(10, (double)x)); } public override void Run() { base.Run(); var sets = GetPropertySets(ChildTestSteps).ToList(); var originalValues = sets.Select(set => set.GetValue()).ToArray(); IEnumerable range = LinearRange(SweepStart, SweepStop, (int)SweepPoints); if (SweepBehavior == SweepBehavior.Exponential) range = ExponentialRange(SweepStart, SweepStop, (int)SweepPoints); var disps = SweepProperties.Select(x => x.GetDisplayAttribute()).ToList(); string names = string.Join(", ", disps.Select(x => x.Name)); if (disps.Count > 1) names = string.Format("{{{0}}}", names); foreach (var Value in range) { Current = Value; OnPropertyChanged("Current"); foreach (var set in sets) { try { set.SetValue(Value); } catch (TargetInvocationException ex) { Log.Error("Unable to set '{0}' to value '{2}': {1}", set.Prop.GetDisplayAttribute().Name, ex.InnerException.Message, Value); Log.Debug(ex.InnerException); } } var AdditionalParams = new ResultParameters(); foreach (var disp in disps) AdditionalParams.Add(new ResultParameter(disp.Group.FirstOrDefault() ?? "", disp.Name, Value)); Log.Info("Running child steps with {0} = {1} ", names, Value); var runs = RunChildSteps(AdditionalParams, BreakLoopRequested, throwOnBreak: false).ToArray(); if (BreakLoopRequested.IsCancellationRequested) break; runs.ForEach(r => r.WaitForCompletion()); if (runs.LastOrDefault()?.BreakConditionsSatisfied() == true) break; } for (int i = 0; i < sets.Count; i++) sets[i].SetValue(originalValues[i]); } } public class LegacySweepLoader : TapSerializerPlugin { public override double Order { get { return 4; } } HashSet proccessingNodes = new HashSet(); static readonly XName Type = "type"; public override bool Deserialize(XElement node, ITypeData t, Action setter) { var typeAttribute = node.Attribute(Type); if(typeAttribute != null && (typeAttribute.Value == "OpenTap.Plugins.BasicSteps.LinearSweepLoop" || typeAttribute.Value == "OpenTap.Plugins.BasicSteps.SweepLoopRange") ) { if (!proccessingNodes.Add(node)) return false; try { typeAttribute.SetValue(typeof(SweepLoopRange).FullName); //Rename old SweepEnd setting to its new (8x) name "SweepStop" var newN = node.Element("SweepStop"); var oldN = node.Element("SweepEnd"); if (oldN != null) { oldN.Name = "SweepStop"; if (newN != null) { if (oldN.Attribute("external") != null && newN.Attribute("external") == null) newN.SetAttributeValue("external", oldN.Attribute("external").Value); // move the attribute from the old to the new oldN.Remove(); } } return Serializer.Deserialize(node, setter, typeof(SweepLoopRange)); } finally { proccessingNodes.Remove(node); } } return false; } public override bool Serialize(XElement node, object obj, ITypeData expectedType) { return false; } } }