using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Threading; namespace OpenTap.Plugins.BasicSteps { [Browsable(false)] [AllowAnyChild] [Display("Sweep Parameter", "Table based loop that sweeps the value of its parameters based on a set of values.", "Flow Control")] public class SweepParameterStep : SweepParameterStepBase { public bool SweepValuesEnabled => SelectedParameters.Count > 0; [DeserializeOrder(1)] // this should be deserialized as the last thing. [Display("Sweep Values", "A table of values to be swept for the selected parameters.", "Sweep")] [HideOnMultiSelect] // todo: In the future support multi-selecting this. [EnabledIf(nameof(SweepValuesEnabled), true)] [Unsweepable, Unmergable] [ElementFactory(nameof(NewElement))] [Factory(nameof(NewSweepRowCollection))] public SweepRowCollection SweepValues { get; set; } /// /// This property declares to the Resource Manager which resources are declared by this test step. /// [AnnotationIgnore] [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] public IEnumerable Resources { get { foreach (var row in SweepValues) { if (row.Enabled == false) continue; foreach (var value in row.Values.Values) { if (value is IResource res) yield return res; } } } } public SweepParameterStep() { SweepValues = new SweepRowCollection(this); Name = "Sweep {Parameters}"; Rules.Add(() => string.IsNullOrWhiteSpace(Validate()), Validate, nameof(SweepValues)); } private string lastError = ""; private string Validate() { if (!validateSweepMutex.WaitOne(0)) return lastError; try { if (isRunning) return lastError; if (SelectedParameters.Count <= 0) { SweepValues = new SweepRowCollection(this); } { // Remove sweep values not in the selected parameters. This can be needed when the user has removed some. var rows = SweepValues.Select(x => x.Values); var selectedNames = SelectedParameters.Select(x => x.Name); var namesToRemove = rows.SelectMany(x => x.Keys).ToHashSet(); namesToRemove.ExceptWith(selectedNames); foreach (var removeName in namesToRemove) foreach (var row in rows) row.Remove(removeName); } var errors = ValidateSweepValues(); lastError = errors; return errors; } finally { validateSweepMutex.ReleaseMutex(); } } int iteration; [Output(OutputAvailability.BeforeRun)] [Display("Iteration", "Shows the iteration of the sweep that is currently running or about to run.", "Sweep", Order: 3)] public string IterationInfo => $"{iteration} of {SweepValues.Count(x => x.Enabled)}"; bool isRunning => GetParent()?.IsRunning ?? false; /// /// Validation requires setting the values of parameterized members. /// This mutex will be locked while the test plan is running to prevent validation requests from interfering with test plan runs. /// Mutex validateSweepMutex = new Mutex(); private string ValidateSweepValues() { const int maxErrors = 10; var sets = SelectedMembers.ToArray(); if (SweepValues.Count == 0) return "No rows selected to sweep."; var rowType = SweepValues.Select(TypeData.GetTypeData).FirstOrDefault(); var numErrors = 0; string FormatError(string rowDescriptor, string error) { return $"{rowDescriptor}: {error}"; } List allRowErrors = new List(); List<(int row, string error)> rowErrors = new List<(int row, string error)>(); foreach (var set in sets) { var mem = rowType.GetMember(set.Name); // If an error is reported on all rows, only show one validation error var allRowsHaveErrors = true; var errorTuple = new List<(int row, string error)>(); foreach (var (index, sweepValue) in SweepValues.WithIndex()) { if (sweepValue.Enabled == false) continue; var stepsToValidate = new HashSet(); var rowNumber = index + 1; var value = mem.GetValue(sweepValue); try { set.SetValue(this, value); // Don't try to validate a step if SetValue failed set.ParameterizedMembers.ForEach(m => { if (m.Source is ITestStep step) stepsToValidate.Add(step); }); } catch (TargetInvocationException ex) { var reason = ex?.InnerException?.Message; string valueString; if (value == null) valueString = ""; else if (false == StringConvertProvider.TryGetString(value, out valueString, CultureInfo.InvariantCulture)) valueString = value.ToString(); var error = $"Unable to set '{set.GetDisplayAttribute().Name}' to value '{valueString}'"; if (reason == null) error += "."; else error += $": {reason}"; rowErrors.Add((rowNumber, error)); continue; } var hasErrors = false; foreach (ITestStep step in stepsToValidate) { var errors = step.Error; if (string.IsNullOrWhiteSpace(errors) == false) { rowErrors.Add((rowNumber, $"{step.GetFormattedName()} - {errors}")); hasErrors = true; numErrors += 1; } } if (hasErrors == false) allRowsHaveErrors = false; } if (allRowsHaveErrors && errorTuple.Count > 1 && errorTuple.Select(t => t.error).Distinct().Count() == 1) { var error = errorTuple.First(); allRowErrors.Add(error.error); numErrors += 1; } if (numErrors >= maxErrors) break; } var sb = new StringBuilder(); foreach (var error in allRowErrors.OrderBy(x => x)) sb.AppendLine(FormatError($"All rows", error)); foreach (var error in rowErrors.OrderBy(x => x.error).ToArray().OrderBy(x =>x.row)) sb.AppendLine(FormatError($"Row {error.row}", error.error)); return sb.ToString(); } public override void PrePlanRun() { validateSweepMutex.WaitOne(); base.PrePlanRun(); iteration = 0; if (SelectedParameters.Count <= 0) throw new InvalidOperationException("No parameters selected to sweep"); if (SweepValues.Count <= 0 || SweepValues.All(x => x.Enabled == false)) throw new InvalidOperationException("No values selected to sweep"); } public override void PostPlanRun() { validateSweepMutex.ReleaseMutex(); base.PostPlanRun(); } public override void Run() { base.Run(); iteration = 0; var sets = SelectedMembers.ToArray(); var originalValues = sets.Select(set => set.GetValue(this)).ToArray(); var rowType = SweepValues.Select(TypeData.GetTypeData).FirstOrDefault(); for (int i = 0; i < SweepValues.Count; i++) { SweepRow Value = SweepValues[i]; if (Value.Enabled == false) continue; var AdditionalParams = new ResultParameters(); AdditionalParams.Add("Sweep", "Iteration", iteration + 1, null); foreach (var set in sets) { var mem = rowType.GetMember(set.Name); var value = mem.GetValue(Value); string valueString; if (value == null) valueString = ""; else if (false == StringConvertProvider.TryGetString(value, out valueString, CultureInfo.InvariantCulture)) valueString = value.ToString(); var disp = mem.GetDisplayAttribute(); AdditionalParams.Add(new ResultParameter(disp.Group.FirstOrDefault() ?? "", disp.Name, valueString)); try { set.SetValue(this, value); } catch (TargetInvocationException ex) { throw new ArgumentException($"Unable to set '{set.GetDisplayAttribute()}' to '{valueString}' on row {i}: {ex.InnerException.Message}", ex.InnerException); } } iteration += 1; // Notify that values might have changes OnPropertyChanged(""); Log.Info("Running child steps with {0}", Value.GetIterationString()); 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.Length; i++) sets[i].SetValue(this, originalValues[i]); } SweepRow NewElement() { return new SweepRow(this); } SweepRowCollection NewSweepRowCollection() { return new SweepRowCollection(this); } } }