// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Xml.Serialization;
namespace OpenTap
{
///
/// Used to specify that a TestStep class allows any class of step to be added as a child.
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class AllowAnyChildAttribute : Attribute
{
}
///
/// Identifies which types that allow this TestStep as a child.
/// Parent type can be TestPlan.
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AllowAsChildInAttribute : Attribute
{
///
/// Type of that can be a parent for this TestStep.
///
public Type ParentStepType { get; private set; }
///
/// Identifies which types that allow this TestStep as a child.
/// Parent type can be TestPlan.
///
/// Type of that can be a parent for this TestStep.
public AllowAsChildInAttribute(Type parentStepType)
{
ParentStepType = parentStepType;
}
}
///
/// Specifies that a TestStep class allows any child TestStep of a given type.
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AllowChildrenOfTypeAttribute : Attribute
{
///
/// Which child type is allowed.
///
public Type RequiredInterface { get; private set; }
///
/// Specifies that a TestStep class allows any child TestStep of a given type.
///
/// Which child type is allowed.
public AllowChildrenOfTypeAttribute(Type requiredInterface)
{
RequiredInterface = requiredInterface;
}
}
///
/// This class holds a list of TestSteps and is used for the Children property of TestStepBase.
/// It is responsible for making sure that all TestSteps added to the list are supported/allowed
/// as children of the TestStep in the TestStepList.Parent field.
///
[DebuggerDisplay("TestStepList {Count}")]
public class TestStepList : ObservableCollection
{
private string sequenceName;
[XmlAttribute("SequenceName")]
public string SequenceName
{
get => sequenceName;
set
{
sequenceName = value;
OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(nameof(SequenceName)));
}
}
///
/// When true, the nesting rules defined by and
/// are checked when trying to insert a step into
/// this list. If the rules are not fulfilled the TestStep is not inserted and a warning
/// is written to the log.
///
public bool EnforceNestingRulesOnInsert = true;
/// Determines if the TestStepList is read only.
public bool IsReadOnly { get; set; }
private ITestStepParent _Parent;
///
/// Parent item of type to which this list belongs.
/// TestSteps in this list (including TestSteps that are added later) will have this item set as their .
///
public ITestStepParent Parent
{
get { return _Parent; }
set
{
_Parent = value;
foreach (ITestStep step in this)
{
step.Parent = value;
}
onContentChanged(this, ChildStepsChangedAction.ListReplaced, null, -1);
if (Parent is TestPlan)
{
rebuildIdLookup();
}
else
{
idLookup = null;
}
}
}
///
/// Removes all the items in the list.
///
protected override void ClearItems()
{
var steps = this.ToList();
base.ClearItems();
foreach (var step in steps)
{
step.Parent = null;
onContentChanged(this, ChildStepsChangedAction.RemovedStep, step, 0);
}
}
/// Constructor for the TestStepList.
public TestStepList()
{
IsReadOnly = false;
}
void rebuildIdLookup()
{
if (idLookup == null)
idLookup = new Dictionary();
idLookup.Clear();
foreach (var thing in RecursivelyGetAllTestSteps(TestStepSearch.All))
{
idLookup.Add(thing.Id, thing);
}
if (Parent is ITestStep step)
idLookup.Add(step.Id, step);
}
Dictionary idLookup = null;
private void TestStepList_ChildStepsChanged(TestStepList senderList, ChildStepsChangedAction Action, ITestStep Object, int Index)
{
if (idLookup == null) rebuildIdLookup();
if (Action == ChildStepsChangedAction.RemovedStep)
{
foreach (var step in Utils.FlattenHeirarchy(new[] { Object }, subStep => subStep.ChildTestSteps))
{
idLookup.Remove(step.Id);
}
}
else if (Action == ChildStepsChangedAction.AddedStep)
{
foreach (var step in Utils.FlattenHeirarchy(new[] { Object }, subStep => subStep.ChildTestSteps))
{
// in some cases a thing can be added twice.
// happens for TestPlanReference. Hence we should use [] and not Add
idLookup[step.Id] = step;
}
}
else if (Action == ChildStepsChangedAction.SetStep)
{
if (idLookup.TryGetValue(Object.Id, out var previous))
{
foreach (var step in Utils.FlattenHeirarchy(new[] { previous }, subStep => subStep.ChildTestSteps))
{
idLookup.Remove(step.Id);
}
}
foreach (var step in Utils.FlattenHeirarchy(new[] { Object }, subStep => subStep.ChildTestSteps))
{
idLookup[step.Id] = step;
}
}
}
///
/// Determines whether a TestStep of a specified type is allowed as child step to a parent of a specified type.
///
public static bool AllowChild(Type parentType, Type childType)
{
if (parentType == null)
throw new ArgumentNullException(nameof(parentType));
if (childType == null)
throw new ArgumentNullException(nameof(childType));
return AllowChild(TypeData.FromType(parentType), TypeData.FromType(childType));
}
///
/// Determines whether a TestStep of a specified type is allowed as child step to a parent of a specified type.
///
public static bool AllowChild(ITypeData parentType, ITypeData childType)
{
if (parentType == null)
throw new ArgumentNullException(nameof(parentType));
if (childType == null)
throw new ArgumentNullException(nameof(childType));
// if the parent is a TestPlan or the parent specifies "AllowAnyChild", then OK
bool parentAllowsAnyChild = parentType.HasAttributeInherited();
bool isChildIn = childType.HasAttributeInherited();
if (parentAllowsAnyChild)
{
if (!isChildIn)
{
return true;
}
}
if (isChildIn)
{
// if the child specifies the parent type or the parent ancestor type in a "AllowAsChildIn" attribute, then OK
var childIn = childType.GetAttributesInherited();
foreach (AllowAsChildInAttribute attribute in childIn)
{
if (parentType.DescendsTo(attribute.ParentStepType))
return true;
}
}
// if the parent specifies the childType or a base type of childType in an "AllowChildrenOfType" attribute, then OK
var child = parentType.GetAttributesInherited();
foreach (AllowChildrenOfTypeAttribute attribute in child)
{
if (childType.DescendsTo(attribute.RequiredInterface))
return true;
}
// otherwise not OK
return false;
}
private ITestStep findStepWithGuid(Guid guid)
{
TestStepList toplst;
if (Parent != null)
{
ITestStepParent topitem = Parent;
while (topitem.Parent != null) topitem = topitem.Parent;
toplst = topitem.ChildTestSteps;
}
else
{
toplst = this;
}
return toplst.GetStep(guid);
}
///
/// Inserts an item into the collection at a specified index.
///
/// Location in list.
/// To be inserted.
protected override void InsertItem(int index, ITestStep item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
throwIfRunning();
var childsteps = Utils.FlattenHeirarchy([item], static step => step.ChildTestSteps);
foreach (var step in childsteps)
{
var existingstep = findStepWithGuid(step.Id);
if (existingstep != null)
{
if (existingstep == step)
throw new InvalidOperationException("Test step already exists in the test plan");
step.Id = Guid.NewGuid();
}
}
// Parent can be null if the list is being loaded from XML, in that case we
// set the parent property of the children in the setter of the Parent property
if (Parent != null)
item.Parent = Parent;
if (EnforceNestingRulesOnInsert && Parent != null)
{
Type parentType = Parent.GetType();
if (!AllowChild(parentType, item.GetType()))
{
if (Parent is TestPlan)
throw new ArgumentException(String.Format("Cannot add test step of type {0} as a root test step (must be nested).", item.GetType().Name));
else
throw new ArgumentException(String.Format("Cannot add test step of type {0} to {1}", item.GetType().Name, Parent.GetType().Name));
}
}
base.InsertItem(index, item);
onContentChanged(this, ChildStepsChangedAction.AddedStep, item, index);
}
///
/// Invoked when an item is set.
///
///
///
///
///
protected override void SetItem(int index, ITestStep item)
{
throwIfRunning();
// Parent can be null if the list is being loaded from XML, in that case we
// set the parent property of the children in the setter of the Parent property
if (Parent != null)
item.Parent = Parent;
if (EnforceNestingRulesOnInsert && Parent != null)
{
Type parentType = Parent.GetType();
if (!AllowChild(parentType, item.GetType()))
{
if (Parent is TestPlan)
throw new ArgumentException(String.Format("Cannot add test step of type {0} as a root test step (must be nested).", item.GetType().Name));
else
throw new ArgumentException(String.Format("Cannot add test step of type {0} to {1}", item.GetType().Name, Parent.GetType().Name));
}
}
base.SetItem(index, item);
onContentChanged(this, ChildStepsChangedAction.SetStep, item, index);
}
///
/// Invoked when an item has been moved
///
///
///
protected override void MoveItem(int oldIndex, int newIndex)
{
base.MoveItem(oldIndex, newIndex);
onContentChanged(this, ChildStepsChangedAction.MovedStep, this[newIndex], newIndex);
}
///
/// Returns true if a TestStep of type stepType can be inserted as a child step.
///
///
///
public bool CanInsertType(Type stepType)
{
if (IsReadOnly) return false;
if (Parent != null)
{
Type parentType = Parent.GetType();
if (!AllowChild(parentType, stepType))
return false;
}
// If no parent, anything can be inserted.
return true;
}
///
/// Defines the callback interface that can get invoked when a child step lists changes.
///
/// The list that changed
/// How the list changed
/// Which object changed in the list (might be null if Reset)
/// The index of the item changed.
public delegate void ChildStepsChangedDelegate(TestStepList senderList, ChildStepsChangedAction Action, ITestStep Object, int Index);
///
/// Invoked when changes for this TestStepList and child TestStepLists.
///
public event ChildStepsChangedDelegate ChildStepsChanged;
///
/// Specifies what has changed.
///
public enum ChildStepsChangedAction
{
///
/// Specifies that a step has been added to a list.
///
AddedStep,
///
/// Specifies that a step has been removed from the list.
///
RemovedStep,
///
/// Specifies that the TestStepList has been replaced. The sender is in this case the new object. Object and Index will be null.
///
ListReplaced,
///
/// Specifies that a step has been set.
///
SetStep,
///
/// Specifies that a step has been moved.
///
MovedStep
}
static readonly Random changeIdRandomState = new Random();
// generate random IDs to try avoiding collisions with other cached states.
// changeid is often used for caching.
internal int ChangeId { get; set; } = changeIdRandomState.Next();
void onContentChanged(TestStepList sender, ChildStepsChangedAction Action, ITestStep Object, int Index)
{
ChangeId += 1;
if (Parent is TestPlan)
{
TestStepList_ChildStepsChanged(sender, Action, Object, Index);
}
else if (Parent is ITestStepParent parent && parent.Parent is ITestStepParent parent2 && parent2.ChildTestSteps != null)
Parent.Parent.ChildTestSteps.onContentChanged(sender, Action, Object, Index);
if (ChildStepsChanged != null)
ChildStepsChanged(sender, Action, Object, Index);
}
TestPlan getTestPlanParent()
{
var parent = Parent;
while (parent is ITestStep)
parent = parent.Parent;
return parent as TestPlan;
}
void throwIfRunning()
{
var plan = getTestPlanParent();
if (plan != null && plan.IsRunning)
throw new InvalidOperationException("Test plans cannot be modified while running.");
}
///
/// Removes the item at the specified index of the collection.
///
protected override void RemoveItem(int index)
{
throwIfRunning();
var item = this[index];
base.RemoveItem(index);
item.Parent = null;
onContentChanged(this, ChildStepsChangedAction.RemovedStep, item, index);
}
/// Removed a number of steps from the test plan.
/// The steps to remove.
public void RemoveItems(IEnumerable steps)
{
// find the topmost parent (usually the test plan itself).
ITestStepParent parent = Parent;
while (parent.Parent != null)
parent = parent.Parent;
HashSet allRemovedSteps = Utils.FlattenHeirarchy(steps, s => s.ChildTestSteps).Distinct().ToHashSet();
List filteredsteps = steps.Where(s => allRemovedSteps.Contains(s.Parent as ITestStep) == false).ToList();
if (filteredsteps.Any(step => step.Parent.ChildTestSteps.IsReadOnly))
throw new InvalidOperationException("Cannot remove a step from a read-only list.");
foreach (var step in filteredsteps)
step.Parent.ChildTestSteps.Remove(step);
{
// For all remaining steps, check if they have any TestStep properties
// If so, check that that property still exists in the plan. Otherwise set it to null.
var AllSteps = Utils.FlattenHeirarchy(parent.ChildTestSteps, x => x.ChildTestSteps).ToHashSet();
Dictionary propLookup = new Dictionary();
foreach (var step in AllSteps)
{
var t = step.GetType();
if (propLookup.ContainsKey(t)) continue;
var props = t.GetPropertiesTap().Where(p => p.PropertyType.DescendsTo(typeof(ITestStep)) && p.GetSetMethod() != null).ToArray();
propLookup[t] = props;
}
foreach (var step in AllSteps)
{
var t = step.GetType();
if (propLookup.ContainsKey(t) == false) continue;
foreach (var prop in propLookup[t])
{
ITestStep value = (ITestStep)prop.GetValue(step);
if (AllSteps.Contains(value) == false)
{
try
{
prop.SetValue(step, null);
}
catch
{
// catch all usercode errors here.
}
}
}
}
}
}
///
/// Recursively iterates steps and child steps to collect all steps in the list.
///
/// Search pattern.
///
public IEnumerable RecursivelyGetAllTestSteps(TestStepSearch stepSearch)
{
return Utils.FlattenHeirarchy(GetSteps(stepSearch), x => x.ChildTestSteps.GetSteps(stepSearch));
}
///
/// Gets steps based on the search pattern. Ignores child steps. Returns null if not found.
///
/// Search pattern.
///
public IEnumerable GetSteps(TestStepSearch stepSearch)
{
if (stepSearch == TestStepSearch.All) return this;
return this.Where(step => stepSearch == TestStepSearch.EnabledOnly == step.Enabled);
}
///
/// Returns the test step that matches the . Returns null if not found.
///
///
///
public ITestStep GetStep(Guid id)
{
if (Parent == null && idLookup == null)
{
rebuildIdLookup();
}
if (idLookup != null)
{
if (idLookup.TryGetValue(id, out ITestStep result))
return result;
return null;
}
if (this._Parent is ITestStep thisStep && thisStep.Id == id)
return thisStep;
foreach (ITestStep step in this)
{
if (step.Id == id)
return step;
if (step.ChildTestSteps.Count > 0)
{
ITestStep ret = step.ChildTestSteps.GetStep(id);
if (ret != null)
return ret;
}
}
return null;
}
internal void AddRange(IEnumerable steps)
{
foreach (var step in steps)
Add(step);
}
}
}