// 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.Text; using System.ComponentModel; using System.Reflection; using System.Xml.Serialization; using System.Threading; namespace OpenTap.Plugins.BasicSteps { [Browsable(false)] [Display("Sweep Loop", Groups: new [] { "Flow Control", "Legacy" }, Description: "Loops its child steps while sweeping specified parameters/settings on the child steps.", Collapsed:true)] [AllowAnyChild] public class SweepLoop : LoopTestStep, IDeserializedCallback { public enum SweepBehaviour { [Display("Within Run")] Within_Run, [Display("Across Runs")] Across_Runs } private SweepBehaviour _CrossPlan; int crossPlanSweepIndex = 0; int currentIteration = 0; [Output] [Display("Iteration", "Shows the iteration of the loop that is currently running or about to run.", Order: 3)] public string IterationInfo => string.Format("{0} of {1}", Iteration + 1, EnabledRows.Length); [Browsable(false)] public bool[] EnabledRows { get; set; } = Array.Empty(); internal int Iteration { get => currentIteration; set { if (currentIteration != value) { currentIteration = value; OnPropertyChanged(nameof(IterationInfo)); } } } #region Settings public bool SweepParametersEnabled => sweepParameters.Count > 0; List sweepParameters = new List(); [Unparameterizable] [Unsweepable] [Display("Sweep Values", Order: 2, Description: "Select the ranges of values to sweep.")] [EnabledIf(nameof(SweepParametersEnabled), true)] [DeserializeOrder(1)] public List SweepParameters { get => sweepParameters; set { crossPlanSweepIndex = 0; Iteration = 0; sweepParameters = value; } } int changedid = 0; internal void parametersChanged() { changedid += 1; } /// Quickly check if things has changed. /// bool popChanged() { bool ischanged = false; foreach(var param in SweepParameters) { if(param.Step != this) { param.Step = this; ischanged = true; } } if(changedid > 0) { ischanged = true; } if(ischanged) changedid = 0; return ischanged; } [XmlIgnore] [Browsable(true)] [Unsweepable] [Display("Sweep Parameters", Order: 1, Description: "Select which child step settings to sweep.")] public IEnumerable SweepMembers { get => SweepParameters.Select(x => x.Member); set { } } /// /// To ensure that OpenTAP opens the referenced resources. /// [Browsable(false)] public IEnumerable ReferencedResources { get { if (CrossPlan == SweepBehaviour.Across_Runs) return SweepParameters // crossPlanSweepIndex is 0 by default. Even if the number of rows is also 0. .Select(param => param.Values.ElementAtOrDefault(crossPlanSweepIndex)) .OfType(); return SweepParameters .SelectMany(param => param.Values) .OfType(); } } [Display("Sweep Mode", Description: "Loop through the sweep values in a single TestPlan run or change values between runs.", Order:0)] [Unsweepable] public SweepBehaviour CrossPlan { get => _CrossPlan; set { if (_CrossPlan == value) return; _CrossPlan = value; crossPlanSweepIndex = 0; Iteration = 0; } } #endregion public SweepLoop() { SweepParameters = new List(); Rules.Add(() => SweepMembers.Count() != 0, "No parameters selected to sweep", nameof(SweepMembers)); Rules.Add(() => string.IsNullOrWhiteSpace(validateSweep()), validateSweep, nameof(SweepParameters)); ChildTestSteps.ChildStepsChanged += childStepsChanged; PropertyChanged += SweepLoop_PropertyChanged; } void SweepLoop_PropertyChanged(object sender, PropertyChangedEventArgs e) { if(e.PropertyName == nameof(ChildTestSteps)) ChildTestSteps.ChildStepsChanged += childStepsChanged; } void childStepsChanged(TestStepList sender, TestStepList.ChildStepsChangedAction Action, ITestStep Object, int Index) { // Make a copy to stop collection modified exception in TapSerializer. var localSweepParameters = sweepParameters.ToList(); localSweepParameters.RemoveIf(s => ValidateChildSteps(s, ChildTestSteps) == null); sweepParameters = localSweepParameters; } static List setParameterOnChildren(SweepParam sweepParameter, object val, TestStepList childTestSteps) { List ex = null; foreach (ITestStep childTestStep in childTestSteps) { ITypeData childType = TypeData.GetTypeData(childTestStep); foreach (var prop in sweepParameter.Members) { IMemberData paramProp = childType.GetMember(prop.Name); if (paramProp != null) { try { // Previously only the member name and type was used for this check // this can however give some issue since the GUI groups things with // the same name and group, hence this GetDisplayAttribute has been added // Actually probably direct member comparison should have been done instead, // but lots of test plans code depends on the functionality here, so just // adding a comparison for display attribute is probably the most gentle solution. var d1 = prop.GetDisplayAttribute(); var d2 = paramProp.GetDisplayAttribute(); if (d1.Name != d2.Name || d1.Group.SequenceEqual(d2.Group) == false) continue; if (Equals(paramProp.TypeDescriptor, sweepParameter.Member.TypeDescriptor) == false) continue; var tp = TypeData.GetTypeData(val); if ((tp.DescendsTo(paramProp.TypeDescriptor) || val == null)) paramProp.SetValue(childTestStep, val); } catch (TargetInvocationException e) { if (ex == null) ex = new List(); ex.Add(e.InnerException); } } if (false == childType.DescendsTo(typeof(TestPlanReference))) { var e = setParameterOnChildren(sweepParameter, val, childTestStep.ChildTestSteps); if(e != null) { if (ex == null) ex = e; else ex.AddRange(e); } } } } return ex; } Dictionary>> getSweepParameterValues(SweepParam sweepParameter, TestStepList childTestSteps) { var values = new Dictionary>>(); populateChildParameters(values, sweepParameter, childTestSteps); return values; } static void populateChildParameters(Dictionary>> values, SweepParam sweepParameter, TestStepList childTestSteps) { foreach (ITestStep childTestStep in childTestSteps) { ITypeData stepType = TypeData.GetTypeData(childTestStep); foreach (IMemberData property in sweepParameter.Members) { IMemberData paramProp = stepType.GetMember(property.Name); if (object.Equals(paramProp, property)) { if (!values.TryGetValue(childTestStep, out _)) values[childTestStep] = new List>(); values[childTestStep].Add(new Tuple(paramProp, paramProp.GetValue(childTestStep))); } } // TODO: do we want this hack?? if (false == stepType.DescendsTo(typeof(TestPlanReference))) populateChildParameters(values, sweepParameter, childTestStep.ChildTestSteps); } } void RunForSweepParamWithIndex(int index, StringBuilder logMessage) { List ex = null; foreach (SweepParam sweepParameter in SweepParameters) { if (sweepParameter == null) continue; object paramValue = sweepParameter.Values.GetValue(index); TestStepList childTestSteps = ChildTestSteps; var e = setParameterOnChildren(sweepParameter, paramValue, childTestSteps); if(e != null) { if (ex == null) ex = new List(); ex.AddRange(e); } if (logMessage == null) continue; logMessage.Append(sweepParameter.Member.Name); logMessage.Append(" = "); logMessage.Append(StringConvertProvider.GetString(sweepParameter.Values.GetValue(index))); if (sweepParameter != SweepParameters.Last()) logMessage.Append(", "); } if (ex != null) throw new AggregateException(ex); } private ResultParameters RegisterAdditionalParams(int Index) { ResultParameters parameters = new ResultParameters(); foreach (SweepParam sweepParameter in SweepParameters) { if (sweepParameter == null) continue; if (!(sweepParameter.Values.GetValue(Index) is IConvertible)) continue; string DisplayName = sweepParameter.Name; string group; DisplayName = ReflectionHelper.ParseDisplayname(DisplayName, out group); ResultParameter p = new ResultParameter(group, DisplayName, (IConvertible)sweepParameter.Values.GetValue(Index)); parameters.Add(p); } return parameters; } // 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() { // Mostly copied from Run if (SweepParameters.Count <= 0) return ""; if (sweepParameters.All(p => p.Values.Length == 0)) return "No values selected to sweep"; if (isRunning) return ""; // Avoid changing the value during run when the gui asks for validation errors. if (!validateSweepMutex.WaitOne(0)) return ""; sanitizeSweepParams(); var oldParams = SweepParameters.Select(param => getSweepParameterValues(param, ChildTestSteps)).ToList(); try { var affectedSteps = oldParams.SelectMany(d => d.Keys).Distinct().ToList(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < SweepParameters[0].Values.Length; i++) { if (false == EnabledRows[i]) continue; RunForSweepParamWithIndex(i, null); if (i > 0) sb.AppendLine(); bool first = true; foreach (var step in affectedSteps) { if (string.IsNullOrWhiteSpace(step.Error)) continue; if (first) first = false; else sb.AppendLine(); sb.AppendFormat("Row {2}, Step '{0}' : {1}", step.GetFormattedName(), step.Error, i + 1); } } return sb.ToString(); } catch(AggregateException e) { return string.Join("\n", e.InnerExceptions.Select(x => x.Message)); } finally { for (int i = 0; i < SweepParameters.Count; i++) foreach (var kvp in oldParams[i]) { foreach(var i2 in kvp.Value) i2.Item1.SetValue(kvp.Key, i2.Item2); } validateSweepMutex.ReleaseMutex(); } } public string ValidateChildSteps(SweepParam sweepParameter, TestStepList childTestSteps) { if (sweepParameter.Members == null) return ""; string iterate_child_steps(TestStepList stepList) { foreach (ITestStep childTestStep in stepList) { if (childTestStep.IsReadOnly == false) { ITypeData stepType = TypeData.GetTypeData(childTestStep); foreach (IMemberData property in sweepParameter.Members) { IMemberData paramProp = stepType.GetMember(property.Name); if (object.Equals(paramProp, property)) return paramProp.ToString(); } } var param = ValidateChildSteps(sweepParameter, childTestStep.ChildTestSteps); if (param != null) return param; } return null; } return iterate_child_steps(childTestSteps); } public override void PrePlanRun() { validateSweepMutex.WaitOne(); validateSweepMutex.ReleaseMutex(); if (sweepParameters == null) sweepParameters = new List(); sanitizeSweepParams(); if (sweepParameters.Count == 0) throw new InvalidOperationException("No parameters selected to sweep"); if(EnabledRows.Count(x => x) == 0) throw new InvalidOperationException("No values selected to sweep"); } void printException(AggregateException ex) { foreach (var e in ex.InnerExceptions) { Log.Error(e.Message); Log.Debug(e); } } void acrossRunsGotoEnabledSweepIndex() { if (crossPlanSweepIndex >= EnabledRows.Length) crossPlanSweepIndex = 0; for(int i = 0; i < EnabledRows.Length; i++) { if (EnabledRows[crossPlanSweepIndex]) return; crossPlanSweepIndex++; if (crossPlanSweepIndex >= EnabledRows.Length) crossPlanSweepIndex = 0; } throw new InvalidOperationException("No rows enabled!"); } public override void Run() { base.Run(); if (SweepParameters.Count == 0) return; if (EnabledRows.Count(v => v) == 0) return; if (CrossPlan == SweepBehaviour.Across_Runs) { var AdditionalParams = RegisterAdditionalParams(crossPlanSweepIndex); // loop until the parameters are enabled. acrossRunsGotoEnabledSweepIndex(); StringBuilder logMessage = new StringBuilder("Setting sweep parameters for next run ("); try { RunForSweepParamWithIndex(crossPlanSweepIndex, logMessage); } catch (AggregateException ex) { printException(ex); } logMessage.Append(")"); Log.Info(logMessage.ToString()); //Fix issue 687: Ensure the latest Sweep params are applied before running any child steps RunChildSteps(AdditionalParams, BreakLoopRequested, throwOnBreak: false); crossPlanSweepIndex++; acrossRunsGotoEnabledSweepIndex(); Iteration = crossPlanSweepIndex; } else { var oldParams = SweepParameters.Select(param => getSweepParameterValues(param, ChildTestSteps)).ToList(); var affectedSteps = oldParams.SelectMany(d => d.Keys).Distinct().ToList(); for (int i = 0; i < SweepParameters[0].Values.Length; i++) { if (false == EnabledRows[i]) continue; Iteration = i; StringBuilder logMessage = new StringBuilder("Running child steps with "); try { RunForSweepParamWithIndex(i, logMessage); } catch (AggregateException ex) { printException(ex); } affectedSteps.ForEach(step => step.OnPropertyChanged("")); var AdditionalParams = RegisterAdditionalParams(i); Log.Info(logMessage.ToString()); var runs = RunChildSteps(AdditionalParams, BreakLoopRequested, throwOnBreak: false).ToArray(); if (BreakLoopRequested.IsCancellationRequested) break; runs.ForEach(r => r.WaitForCompletion()); if (runs.LastOrDefault()?.BreakConditionsSatisfied() == true) break; } Iteration = 0; // Restore values from before the run for (int i = 0; i < SweepParameters.Count; i++) foreach (var kvp in oldParams[i]) { foreach (var i2 in kvp.Value) i2.Item1.SetValue(kvp.Key, i2.Item2); } affectedSteps.ForEach(step => step.OnPropertyChanged("")); } } /// /// Ensures that the sweep parameters exists within the child test steps and that SweepParam.Property is set. /// internal void sanitizeSweepParams(bool log = true) { if (popChanged() == false) { return; } var steps = Utils.FlattenHeirarchy(ChildTestSteps, step => step is TestPlanReference ? null : step.ChildTestSteps); var superLookup = steps.Select(x => TypeData.GetTypeData(x)).Distinct() .SelectMany(tp => tp.GetMembers()).Distinct() .ToLookup(x => SweepParam.GetMemberName(x), x => x); foreach (var param in SweepParameters) { List members = new List(); foreach (var name in param.MemberNames) { var super = superLookup[name]; members.AddRange(super); } param.Members = members.Distinct().ToArray(); var prop = param.Members.FirstOrDefault(); if (prop != null) { var step = steps.FirstOrDefault(x => TypeData.GetTypeData(x).GetMember(prop.Name) == prop); if (step != null) param.DefaultValue = prop.GetValue(step); } else { Log.Error("Members {0} not found", param.MemberNames); } } bool warn = false; int len = sweepParameters.Select(x => x.Values.Length).DefaultIfEmpty(0).Max(); foreach (var sweepParam in sweepParameters) { // fix IsEnabled if it is null or have different number of elements than sweepParam.Values. if(sweepParam.Values.Length != len) { sweepParam.Resize(len); warn = true; } } if(EnabledRows.Length != len) { int start = EnabledRows.Length; bool[] enabledrows = EnabledRows; Array.Resize(ref enabledrows, len); for (int i = start; i < len; i++) enabledrows[i] = true; EnabledRows = enabledrows; warn = true; } if (warn && log) { Log.Error("Sweep Loop: Invalid settings detected. Please ensure that the sweep data is correct."); } } public void OnDeserialized() { sanitizeSweepParams(); } } }