// 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 OpenTap.Addin; 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))); } } public ObservableCollection Variables { get; set; } = new ObservableCollection(); /// /// 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); } } }