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);
}
}
}